Repository: r2dbc/r2dbc-mssql Branch: main Commit: dfb2476a8ec3 Files: 329 Total size: 1.6 MB Directory structure: gitextract_rm82x0ys/ ├── .github/ │ └── workflows/ │ ├── ci.yml │ ├── pullrequests.yml │ └── release.yml ├── .gitignore ├── .mvn/ │ └── wrapper/ │ ├── MavenWrapperDownloader.java │ └── maven-wrapper.properties ├── .travis.yml ├── CHANGELOG ├── LICENSE ├── NOTICE ├── README.md ├── ci/ │ └── build-and-deploy-to-maven-central.sh ├── intellij-style.xml ├── mvnw ├── mvnw.cmd ├── pom.xml ├── settings.xml └── src/ ├── jmh/ │ └── java/ │ └── io/ │ └── r2dbc/ │ └── mssql/ │ ├── BenchmarkSettings.java │ ├── ParametrizedMssqlStatementBenchmarks.java │ ├── PooledBenchmarks.java │ ├── StagedResultSizeBenchmarks.java │ ├── StatementBenchmarks.java │ └── codec/ │ ├── BinaryCodecBenchmarks.java │ ├── BooleanCodecBenchmarks.java │ ├── ByteCodecBenchmarks.java │ ├── CodecBenchmarkSupport.java │ ├── DecimalCodecBenchmarks.java │ ├── DoubleCodecBenchmarks.java │ ├── IntegerCodecBenchmarks.java │ ├── LocalDateCodecBenchmarks.java │ ├── LocalDateTimeCodecBenchmarks.java │ ├── LocalTimeCodecBenchmarks.java │ ├── LongCodecBenchmarks.java │ ├── MoneyCodecBenchmarks.java │ ├── ShortCodecBenchmarks.java │ ├── StringCodecBenchmarks.java │ └── UuidCodecBenchmarks.java ├── main/ │ ├── java/ │ │ └── io/ │ │ └── r2dbc/ │ │ └── mssql/ │ │ ├── AbstractMssqlException.java │ │ ├── Binding.java │ │ ├── ConnectionOptions.java │ │ ├── DefaultMssqlResult.java │ │ ├── ErrorDetails.java │ │ ├── EscapeAwareNameMatcher.java │ │ ├── ExceptionFactory.java │ │ ├── GeneratedValues.java │ │ ├── IndefinitePreparedStatementCache.java │ │ ├── LoginConfiguration.java │ │ ├── LoginFlow.java │ │ ├── MssqlBatch.java │ │ ├── MssqlColumnMetadata.java │ │ ├── MssqlConnection.java │ │ ├── MssqlConnectionConfiguration.java │ │ ├── MssqlConnectionFactory.java │ │ ├── MssqlConnectionFactoryMetadata.java │ │ ├── MssqlConnectionFactoryProvider.java │ │ ├── MssqlConnectionMetadata.java │ │ ├── MssqlException.java │ │ ├── MssqlIsolationLevel.java │ │ ├── MssqlResult.java │ │ ├── MssqlReturnValues.java │ │ ├── MssqlReturnValuesMetadata.java │ │ ├── MssqlRow.java │ │ ├── MssqlRowMetadata.java │ │ ├── MssqlSegmentResult.java │ │ ├── MssqlStatement.java │ │ ├── MssqlStatementSupport.java │ │ ├── NamedCollectionSupport.java │ │ ├── OptionMapper.java │ │ ├── ParametrizedMssqlStatement.java │ │ ├── PreparedStatementCache.java │ │ ├── QueryLogger.java │ │ ├── QueryMessageFlow.java │ │ ├── RpcQueryMessageFlow.java │ │ ├── SimpleMssqlStatement.java │ │ ├── api/ │ │ │ ├── MssqlTransactionDefinition.java │ │ │ ├── SimpleTransactionDefinition.java │ │ │ └── package-info.java │ │ ├── client/ │ │ │ ├── Client.java │ │ │ ├── ClientConfiguration.java │ │ │ ├── ConnectionContext.java │ │ │ ├── ConnectionState.java │ │ │ ├── DisabledSslTunnel.java │ │ │ ├── EnvironmentChangeEvent.java │ │ │ ├── EnvironmentChangeListener.java │ │ │ ├── MessageDecoder.java │ │ │ ├── ReactorNettyClient.java │ │ │ ├── StreamDecoder.java │ │ │ ├── TdsEncoder.java │ │ │ ├── TransactionStatus.java │ │ │ ├── package-info.java │ │ │ └── ssl/ │ │ │ ├── ContextProxy.java │ │ │ ├── ExpectedHostnameX509TrustManager.java │ │ │ ├── HostNamePredicate.java │ │ │ ├── SslConfiguration.java │ │ │ ├── SslEventHandler.java │ │ │ ├── SslState.java │ │ │ ├── TdsSslHandler.java │ │ │ ├── TrustAllTrustManager.java │ │ │ ├── X509CertificateUtil.java │ │ │ └── package-info.java │ │ ├── codec/ │ │ │ ├── AbstractCodec.java │ │ │ ├── AbstractNumericCodec.java │ │ │ ├── BigIntegerCodec.java │ │ │ ├── BinaryCodec.java │ │ │ ├── BlobCodec.java │ │ │ ├── BooleanCodec.java │ │ │ ├── ByteArray.java │ │ │ ├── ByteCodec.java │ │ │ ├── CharacterEncoder.java │ │ │ ├── ClobCodec.java │ │ │ ├── Codec.java │ │ │ ├── Codecs.java │ │ │ ├── DecimalCodec.java │ │ │ ├── Decodable.java │ │ │ ├── DefaultCodecs.java │ │ │ ├── DoubleCodec.java │ │ │ ├── Encoded.java │ │ │ ├── FloatCodec.java │ │ │ ├── IntegerCodec.java │ │ │ ├── LocalDateCodec.java │ │ │ ├── LocalDateTimeCodec.java │ │ │ ├── LocalTimeCodec.java │ │ │ ├── LongCodec.java │ │ │ ├── MoneyCodec.java │ │ │ ├── OffsetDateTimeCodec.java │ │ │ ├── PlpEncoded.java │ │ │ ├── PlpEncodedCharacters.java │ │ │ ├── RpcDirection.java │ │ │ ├── RpcEncoding.java │ │ │ ├── RpcParameterContext.java │ │ │ ├── ShortCodec.java │ │ │ ├── StringCodec.java │ │ │ ├── TimestampCodec.java │ │ │ ├── UuidCodec.java │ │ │ ├── ZonedDateTimeCodec.java │ │ │ └── package-info.java │ │ ├── message/ │ │ │ ├── ClientMessage.java │ │ │ ├── Message.java │ │ │ ├── TDSVersion.java │ │ │ ├── TransactionDescriptor.java │ │ │ ├── header/ │ │ │ │ ├── DefaultHeaderOptions.java │ │ │ │ ├── Header.java │ │ │ │ ├── HeaderOptions.java │ │ │ │ ├── PacketIdProvider.java │ │ │ │ ├── Status.java │ │ │ │ └── Type.java │ │ │ ├── package-info.java │ │ │ ├── tds/ │ │ │ │ ├── ContextualTdsFragment.java │ │ │ │ ├── Decode.java │ │ │ │ ├── Encode.java │ │ │ │ ├── FirstTdsFragment.java │ │ │ │ ├── LastTdsFragment.java │ │ │ │ ├── ProtocolException.java │ │ │ │ ├── Redirect.java │ │ │ │ ├── ServerCharset.java │ │ │ │ ├── TdsFragment.java │ │ │ │ ├── TdsPacket.java │ │ │ │ ├── TdsPackets.java │ │ │ │ └── package-info.java │ │ │ ├── token/ │ │ │ │ ├── AbstractDataToken.java │ │ │ │ ├── AbstractDoneToken.java │ │ │ │ ├── AbstractInfoToken.java │ │ │ │ ├── AllHeaders.java │ │ │ │ ├── Attention.java │ │ │ │ ├── ColInfoToken.java │ │ │ │ ├── Column.java │ │ │ │ ├── ColumnMetadataToken.java │ │ │ │ ├── DataToken.java │ │ │ │ ├── DoneInProcToken.java │ │ │ │ ├── DoneProcToken.java │ │ │ │ ├── DoneToken.java │ │ │ │ ├── EnvChangeToken.java │ │ │ │ ├── ErrorToken.java │ │ │ │ ├── FeatureExtAckToken.java │ │ │ │ ├── Identifier.java │ │ │ │ ├── InfoToken.java │ │ │ │ ├── Login7.java │ │ │ │ ├── LoginAckToken.java │ │ │ │ ├── NbcRowToken.java │ │ │ │ ├── OrderToken.java │ │ │ │ ├── Prelogin.java │ │ │ │ ├── ReturnStatus.java │ │ │ │ ├── ReturnValue.java │ │ │ │ ├── RowToken.java │ │ │ │ ├── RpcRequest.java │ │ │ │ ├── SqlBatch.java │ │ │ │ ├── TabnameToken.java │ │ │ │ ├── Tabular.java │ │ │ │ ├── TokenStream.java │ │ │ │ └── package-info.java │ │ │ └── type/ │ │ │ ├── AbstractTypeDecoderStrategy.java │ │ │ ├── Collation.java │ │ │ ├── Length.java │ │ │ ├── LengthStrategy.java │ │ │ ├── MutableTypeInformation.java │ │ │ ├── PlpLength.java │ │ │ ├── SqlServerType.java │ │ │ ├── TdsDataType.java │ │ │ ├── TypeBuilder.java │ │ │ ├── TypeDecoderStrategies.java │ │ │ ├── TypeDecoderStrategy.java │ │ │ ├── TypeInformation.java │ │ │ ├── TypeUtils.java │ │ │ └── package-info.java │ │ ├── package-info.java │ │ └── util/ │ │ ├── Assert.java │ │ ├── DriverVersion.java │ │ ├── FluxDiscardOnCancel.java │ │ ├── Operators.java │ │ ├── PredicateUtils.java │ │ ├── ReferenceCountUtil.java │ │ ├── StringUtils.java │ │ ├── Version.java │ │ └── package-info.java │ └── resources/ │ └── META-INF/ │ └── services/ │ └── io.r2dbc.spi.ConnectionFactoryProvider └── test/ ├── java/ │ └── io/ │ └── r2dbc/ │ └── mssql/ │ ├── BindingUnitTests.java │ ├── CodecIntegrationTests.java │ ├── ColumnMetadataIntegrationTests.java │ ├── ConcurrentAccessIntegrationTests.java │ ├── EscapeAwareNameMatcherUnitTests.java │ ├── ExceptionFactoryUnitTests.java │ ├── GeneratedValuesUnitTests.java │ ├── IndefinitePreparedStatementCacheUnitTests.java │ ├── JsonIntegrationTests.java │ ├── LobIntegrationTests.java │ ├── LoginFlowUnitTests.java │ ├── MssqlBatchIntegrationTests.java │ ├── MssqlBatchUnitTests.java │ ├── MssqlCancelIntegrationTests.java │ ├── MssqlConnectionConfigurationUnitTests.java │ ├── MssqlConnectionFactoryMetadataUnitTests.java │ ├── MssqlConnectionFactoryProviderTest.java │ ├── MssqlConnectionFactoryUnitTests.java │ ├── MssqlConnectionIntegrationTests.java │ ├── MssqlConnectionMetadataUnitTests.java │ ├── MssqlConnectionUnitTests.java │ ├── MssqlResultUnitTests.java │ ├── MssqlReturnValuesUnitTests.java │ ├── MssqlRowMetadataUnitTests.java │ ├── MssqlRowUnitTests.java │ ├── MssqlSegmentResultUnitTests.java │ ├── MssqlTestKit.java │ ├── ParametrizedMssqlStatementIntegrationTests.java │ ├── ParametrizedMssqlStatementStoredProcedureIntegrationTests.java │ ├── ParametrizedMssqlStatementUnitTests.java │ ├── QueryMessageFlowUnitTests.java │ ├── ReturnGeneratedValuesIntegrationTests.java │ ├── RpcBlobUnitTests.java │ ├── RpcQueryMessageFlowUnitTests.java │ ├── SimpleMssqlStatementIntegrationTests.java │ ├── SimpleMssqlStatementUnitTests.java │ ├── SqlVariantIntegrationTests.java │ ├── TestConnectionOptions.java │ ├── TransactionIntegrationTests.java │ ├── XmlIntegrationTests.java │ ├── client/ │ │ ├── ConnectionStateUnitTests.java │ │ ├── ReactorNettyClientIntegrationTests.java │ │ ├── StreamDecoderUnitTests.java │ │ ├── TdsEncoderUnitTests.java │ │ ├── TestClient.java │ │ └── ssl/ │ │ ├── HostNamePredicateUnitTests.java │ │ ├── TdsSslHandlerUnitTests.java │ │ └── X509CertificateUtilUnitTests.java │ ├── codec/ │ │ ├── BigIntegerCodecUnitTests.java │ │ ├── BinaryCodecUnitTests.java │ │ ├── BlobCodecUnitTests.java │ │ ├── BooleanCodecUnitTests.java │ │ ├── ByteCodecUnitTests.java │ │ ├── ClobCodecUnitTests.java │ │ ├── ColumnUtil.java │ │ ├── DecimalCodecUnitTests.java │ │ ├── DoubleCodecUnitTests.java │ │ ├── EncodedUnitTests.java │ │ ├── FloatCodecUnitTests.java │ │ ├── IntegerCodecUnitTests.java │ │ ├── LocalDateCodecUnitTests.java │ │ ├── LocalDateTimeCodecUnitTests.java │ │ ├── LocalTimeCodecUnitTests.java │ │ ├── LongCodecUnitTests.java │ │ ├── MoneyCodecUnitTests.java │ │ ├── OffsetDateTimeCodecUnitTests.java │ │ ├── PlpEncodedUnitTests.java │ │ ├── RpcEncodingUnitTests.java │ │ ├── ShortCodecUnitTests.java │ │ ├── StringCodecUnitTests.java │ │ ├── TimestampCodecUnitTests.java │ │ ├── UuidCodecUnitTests.java │ │ └── ZonedDateTimeCodecUnitTests.java │ ├── message/ │ │ ├── TDSVersionUnitTests.java │ │ ├── header/ │ │ │ ├── HeaderUnitTests.java │ │ │ ├── StatusUnitTests.java │ │ │ └── TypeUnitTests.java │ │ ├── token/ │ │ │ ├── AllHeadersUnitTests.java │ │ │ ├── CanDecodeTestSupport.java │ │ │ ├── ColInfoTokenUnitTests.java │ │ │ ├── ColumnMetadataTokenUnitTests.java │ │ │ ├── DoneInProcUnitTests.java │ │ │ ├── DoneProcUnitTests.java │ │ │ ├── DoneTokenUnitTests.java │ │ │ ├── EnvChangeTokenUnitTests.java │ │ │ ├── ErrorTokenUnitTests.java │ │ │ ├── FeatureExtAckTokenUnitTests.java │ │ │ ├── IdentifierUnitTests.java │ │ │ ├── InfoTokenUnitTests.java │ │ │ ├── Login7UnitTests.java │ │ │ ├── LoginAckTokenUnitTests.java │ │ │ ├── NbcRowTokenUnitTests.java │ │ │ ├── OrderTokenUnitTests.java │ │ │ ├── PreloginUnitTests.java │ │ │ ├── ReturnValueUnitTests.java │ │ │ ├── RowTokenFactory.java │ │ │ ├── RowTokenUnitTests.java │ │ │ ├── RpcRequestUnitTests.java │ │ │ ├── SqlBatchUnitTests.java │ │ │ ├── TabnameTokenUnitTests.java │ │ │ └── TabularUnitTests.java │ │ └── type/ │ │ ├── CollationUnitTests.java │ │ └── TypeBuilderUnitTests.java │ └── util/ │ ├── ClientMessageAssert.java │ ├── EmbeddedChannelAssert.java │ ├── EncodedAssert.java │ ├── FluxDiscardOnCancelUnitTests.java │ ├── HexUtils.java │ ├── IntegrationTestSupport.java │ ├── MsSqlServerExtension.java │ ├── TestByteBufAllocator.java │ └── Types.java └── resources/ ├── int-varcharmax-data.txt └── logback-test.xml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/ci.yml ================================================ # This workflow will build a Java project with Maven # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven name: Java CI with Maven on: push: branches: [ main, 1.0.x, 0.9.x, 0.8.x ] jobs: build: if: github.repository == 'r2dbc/r2dbc-mssql' runs-on: ${{ matrix.os }} strategy: matrix: include: - os: ubuntu-latest mvn: ./mvnw - os: windows-latest mvn: mvn - os: macos-latest mvn: ./mvnw steps: - uses: actions/checkout@v5 - name: Set up Java uses: actions/setup-java@v5 with: java-version: 24 distribution: temurin cache: maven - name: Build with Maven env: CENTRAL_TOKEN_USERNAME: ${{ secrets.CENTRAL_TOKEN_USERNAME }} CENTRAL_TOKEN_PASSWORD: ${{ secrets.CENTRAL_TOKEN_PASSWORD }} run: ${{ matrix.mvn }} -B deploy -D skipITs -P snapshot -s settings.xml ================================================ FILE: .github/workflows/pullrequests.yml ================================================ # This workflow will build a Java project with Maven # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven name: Build Pull request with Maven on: [pull_request] jobs: pr-build: runs-on: ${{ matrix.os }} strategy: matrix: include: - os: ubuntu-latest mvn: ./mvnw - os: windows-latest mvn: mvn - os: macos-latest mvn: ./mvnw steps: - uses: actions/checkout@v5 - name: Set up Java uses: actions/setup-java@v5 with: java-version: 24 distribution: temurin cache: maven - name: Build with Maven run: ${{ matrix.mvn }} -B verify -D skipITs ================================================ FILE: .github/workflows/release.yml ================================================ # This workflow will build a Java project with Maven # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven name: Stage release to Maven Central on: push: branches: [ release ] jobs: release: if: github.repository == 'r2dbc/r2dbc-mssql' runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Set up Java uses: actions/setup-java@v5 with: java-version: 24 distribution: temurin - name: Initialize Maven Version run: ./mvnw -q org.apache.maven.plugins:maven-help-plugin:2.1.1:evaluate -Dexpression=project.version - name: GPG Check run: gpg -k - name: Release with Maven env: CENTRAL_TOKEN_USERNAME: ${{ secrets.CENTRAL_TOKEN_USERNAME }} CENTRAL_TOKEN_PASSWORD: ${{ secrets.CENTRAL_TOKEN_PASSWORD }} GPG_KEY_BASE64: ${{ secrets.GPG_KEY_BASE64 }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} run: ci/build-and-deploy-to-maven-central.sh ================================================ FILE: .gitignore ================================================ ### Maven template target/ pom.xml.tag pom.xml.releaseBackup pom.xml.versionsBackup pom.xml.next release.properties dependency-reduced-pom.xml buildNumber.properties .mvn/timing.properties # IntelliJ out/ build/ .idea/ *.iml ### Java template # Compiled class file *.class # Log file *.log # Package Files # *.jar *.war *.nar *.ear *.zip *.tar.gz *.rar # virtual machine crash logs, see https://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* .flattened-pom.xml ================================================ FILE: .mvn/wrapper/MavenWrapperDownloader.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.net.*; import java.io.*; import java.nio.channels.*; import java.util.Properties; public class MavenWrapperDownloader { /** * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. */ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"; /** * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to * use instead of the default one. */ private static final String MAVEN_WRAPPER_PROPERTIES_PATH = ".mvn/wrapper/maven-wrapper.properties"; /** * Path where the maven-wrapper.jar will be saved to. */ private static final String MAVEN_WRAPPER_JAR_PATH = ".mvn/wrapper/maven-wrapper.jar"; /** * Name of the property which should be used to override the default download url for the wrapper. */ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; public static void main(String args[]) { System.out.println("- Downloader started"); File baseDirectory = new File(args[0]); System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); // If the maven-wrapper.properties exists, read it and check if it contains a custom // wrapperUrl parameter. File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); String url = DEFAULT_DOWNLOAD_URL; if(mavenWrapperPropertyFile.exists()) { FileInputStream mavenWrapperPropertyFileInputStream = null; try { mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); Properties mavenWrapperProperties = new Properties(); mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); } catch (IOException e) { System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); } finally { try { if(mavenWrapperPropertyFileInputStream != null) { mavenWrapperPropertyFileInputStream.close(); } } catch (IOException e) { // Ignore ... } } } System.out.println("- Downloading from: : " + url); File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); if(!outputFile.getParentFile().exists()) { if(!outputFile.getParentFile().mkdirs()) { System.out.println( "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); } } System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); try { downloadFileFromURL(url, outputFile); System.out.println("Done"); System.exit(0); } catch (Throwable e) { System.out.println("- Error downloading"); e.printStackTrace(); System.exit(1); } } private static void downloadFileFromURL(String urlString, File destination) throws Exception { URL website = new URL(urlString); ReadableByteChannel rbc; rbc = Channels.newChannel(website.openStream()); FileOutputStream fos = new FileOutputStream(destination); fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); fos.close(); rbc.close(); } } ================================================ FILE: .mvn/wrapper/maven-wrapper.properties ================================================ distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip ================================================ FILE: .travis.yml ================================================ language: java jdk: - openjdk8 cache: directories: - $HOME/.m2 notifications: slack: secure: JMisDlbY76AZeIQn/IUw0TyCR7A6x0FoazJ5BR5Fz0S+AU0peSgU9J3IaqYWzp7rv7eXOOCExLVwzXFNz9cXCREhbbg5yoaKIfV0nQvu03Rt1cwsFqAHP/3M7zwSS2cdw++hoX3ErNcJVt1rhZCQkbcc33LDwx8tJnfDOgZ5wAg5Z3iempp4Zig+IgtqoccI2hmefgoNY68hApfRDhHTpJHdQeCLeYtEA8UpNpLhKGBzTONVhjwyeJxCBVuY/9GIW8p6TceWbIPpPWE1hRtQgLLfTtz3LUAU1aMbTYFEEYnNfeAMdSqnyPqgehpUaoX4M/ep/cvb5NGtDz4ksQrIBdc578YAv/YXP9Q1cTk8A2DT7e7yYQWrgH2M+KyEPi/HYRE8fT2BjzecA9wbJhkeNGS/Zt02mzUb8WmitgstnFLac+ibyUtXwK27RN3zdbaIKjr2FtDHU2qbQ3m8TRJXi7UgeAFc5HAnJ0TyqGYw/FbCRDgEuW3gz3a8MZSJmxy4/JtrAxdLdsI3UyZtj0wXCqxuaJOyOOI11UCqlD16fzB0S0A12lqq6sAIkNbGPB7shJ5iMK5rtip6S1ssuRJ8XgTATT/XiNIBXYaRMXAzv0QCYTRrR2dQnIDT4pzsEsTS4T150Jl5SgD88QfXUyree2Qg7bRTvQwe9ruPsBWDUBs= ================================================ FILE: CHANGELOG ================================================ R2DBC MSSQL Changelog ============================= 1.0.0.RELEASE ------------------ * Upgrade to Project Reactor 2022.0.0 #260 * Upgrade to R2DBC Pool 1.0.0.RELEASE #261 1.0.0.RC1 ------------------ * Expand CI testing to other platforms. #250 * MssqlConnectionConfigurationUnitTests.configureKeyStore fails Windows test environment. #251 * Upgrade to R2DBC 1.0 #253 * Upgrade to Reactor 2022.0.0-M4 #255 0.9.0.RELEASE ------------------ * `SSL` configuration option enables SSL handshaking regardless of the configuration value #240 * Extend license years in copyright header to 2022 #242 0.9.0.RC1 ------------------ * Upgrade to R2DBC 0.9.0.RELEASE #237 * Upgrade to R2DBC SPI 0.9 RC1 #223 * Adopt renamed TCK column name #236 * Move off deprecated Reactor Netty API #231 * Let `Statement.add()` always create a new binding set #230 * `BigDecimal` with negative scale incorrectly encoded #228 * Use sequential processing in `Result.flatMap(…)` #225 * Propagate offending SQL into R2DBC exceptions #224 * Upgrade to R2DBC SPI 0.9 RC1 #223 * Align `Statement.bind(…)` and `Readable.get(…)` exceptions with specification #222 * Upgrade dependencies #221 * Replace `EmitterProcessor` with `Sinks` API #219 * The FOR XML clause is not allowed in a CURSOR statement #209 0.9.0.M2 ------------------ * Statements hang up on reading `nvarchar(max)` columns #216 * Add support for Attention token (cancelling running queries) #215 * Add support for lock wait timeout #214 * Add support for statement timeout #213 * Upgrade to R2DBC SPI 0.9 M2 #212 * The FOR XML clause is not allowed in a CURSOR statement #209 * `OffsetDateTimeCodec` does not properly decode negative timezone offsets #208 * Upgrade to Testcontainers 1.15.3 #203 * ClassCastException when calling RowMetadata.getColumnNames().toArray(T[]) #200 * Add support to consume return values from stored procedures #199 * Exclude transitive SLF4J pulled from HikariCP #198 * Can't combine bind variables with T-SQL local variables #197 * Statement batch doesn't produce the correct number of update counts #196 * Eager buffer allocation in `TdsEncoder.writeChunkedMessage(…)` can lead to memory leaks #195 * Add support for trustServerCertificate flag #184 * Exception is not thrown when do SQL insert through ReactiveCrudRepository's save method. #180 0.9.0.M1 ------------------ * Upgrade to Reactor 2020.0.4 #193 * Upgrade to R2DBC Pool 0.9.0.M1 #192 * Upgrade to R2DBC SPI 0.9.0.M1 #190 * Strip ROWSTAT column from MssqlRowMetadata #188 * Add support for extended transaction definitions #183 * Add support for SPI Parameters #182 * Enable Developer Certificate of Origin #181 * Restrict CI and release task to r2dbc/r2dbc-mssql repo #179 * Upgrade dependencies #178 * Add config options for TCP KeepAlive and NoDelay #177 * Add support for SSL tunnels #176 * Use GitHub actions to deploy to OSS Sonatype/Maven Central #173 * Clob decoding is prone to in-character decoding splits #172 * StringCodec fix for characters split between PLP chunks #171 * Upgrade to R2DBC SPI 0.8.3 #170 * Ensure no snapshots get referenced in release builds #165 * Add integration tests for null decode #163 * Failed statement with returnGeneratedValues enabled causes onErrorDropped #162 * Rename master branch to main #159 * Upgrade to Reactor Dysprosium SR9 #158 * Upgrade to Reactor Dysprosium SR8 #157 * Upgrade dependencies #155 * Add sslContextBuilderCustomizer(Function) #152 * Issue inserting byte objects when size is greater than 8000 and and less than 65535 bytes #151 * Add support for configuring a custom trust store #150 * Add BlockHound to integration tests #149 * Allow custom trust store for server certificate verification. #148 * Upgrade build and test dependencies #146 * Upgrade to Reactor Dysprosium-SR6 #145 * Stage releases directly on maven central #143 * Multiple TDS chunks in a single buffer cause connection reset on Azure SQL #142 * Protocol errors get swallowed in RPC message flow for direct queries #141 * Query String bigger than 4000 characters result in java.lang.UnsupportedOperationException #140 * Provide additional configuration options for hostNameInCertificiate #138 * Update dependencies #134 * Upgrade to Testcontainers 1.12.5 #132 * Fix infinite loop when clearing bindings #131 * Support casting of BIGINT to BigDecimal #130 * Enable Travis for pull requests #125 * Migrate to Jenkins CI #124 * ENVCHANGE Token is not decoded properly for Routing type #116 0.8.5.RELEASE ------------------ * Upgrade dependencies #178 * Add config options for TCP KeepAlive and NoDelay #177 * Add support for SSL tunnels #176 * Use GitHub actions to deploy to OSS Sonatype/Maven Central #173 * Clob decoding is prone to in-character decoding splits #172 * StringCodec fix for characters split between PLP chunks #171 * Upgrade to R2DBC SPI 0.8.3 #170 0.8.4.RELEASE ------------------ * Ensure no snapshots get referenced in release builds #165 * Add integration tests for null decode #163 * Failed statement with returnGeneratedValues enabled causes onErrorDropped #162 * Rename master branch to main #159 * Upgrade to Reactor Dysprosium SR9 #158 * Upgrade to Reactor Dysprosium SR8 #157 0.8.3.RELEASE ------------------ * Upgrade dependencies #155 * Add sslContextBuilderCustomizer(Function) #152 * Issue inserting byte objects when size is greater than 8000 and and less than 65535 bytes #151 * Add support for configuring a custom trust store #150 * Add BlockHound to integration tests #149 * Allow custom trust store for server certificate verification. #148 * Multiple TDS chunks in a single buffer cause connection reset on Azure SQL #142 0.8.2.RELEASE ------------------ * Upgrade build and test dependencies #146 * Upgrade to Reactor Dysprosium-SR6 #145 * Stage releases directly on maven central #144 * Protocol errors get swallowed in RPC message flow for direct queries #141 * Query String bigger than 4000 characters result in java.lang.UnsupportedOperationException #140 * Provide additional configuration options for hostNameInCertificiate #138 * ENVCHANGE Token is not decoded properly for Routing type #116 0.8.1.RELEASE ------------------ * Update dependencies #134 * Fix infinite loop when clearing bindings #131 * Support casting of BIGINT to BigDecimal #130 * Backport Travis support to 0.8.x #128 * Backport Jenkins to 0.8.x #127 0.8.0.RELEASE ------------------ * Upgrade to Reactor Dysprosium SR2 #123 * Upgrade to R2DBC SPI 0.8.0.RELEASE #121 * Remove SLF4J in favor of Reactor Core Loggers #120 * Clob codec should support UNICODE #117 * Default to scalar values for LOB column retrieval according to spec changes #115 * Upgrade to Testcontainers 1.12.3 #114 * SELECT (NEXT VALUE FOR TestSeq) with RPC Flow and fast-forward scroll option skips sequence items #113 * Add support for sendStringParametersAsUnicode property #112 * Add hints to ByteBufs #110 * Statement execution gets stuck when connection gets disconnected #109 0.8.0.RC2 ------------------ * Revert reactor netty exclusions #107 0.8.0.RC1 ------------------ * Fix malformed Javadoc #104 * Add automatic module name #103 * Upgrade to Reactor Dysprosium GA #102 * Upgrade dependencies #100 * BinaryCodec uses varbinary which limits the byteArray to 65kb #99 * Use ByteBuffer as default type for binary payloads #98 * Remove repositories declaration from published pom #97 * Move jitpack repository declaration to JMH profile #96 * Adapt to Statement.bind and Row.get by name #95 * Report ConnectionMetadata from SERVERPROPERTY and @@VERSION #94 * Upgrade to Reactor Dysprosium RC1 #93 * Rename MssqlExample to MssqlTestKit #92 * IllegalArgumentException Invalid TDS type is 0 on SQL Server 2014 #90 * Connection reset by peer #89 * Replace RuleBasedCollector with simple string matcher in MssqlRowMetadata #87 * Add implementation for Connection.validate(…) #86 * Expose ConnectionMetadata #85 * QueryMessageFlow terminates without final DONE token #84 * Improve debugging experience #83 * Allow control of AutoCommit and retrieval of the IsolationLevel #82 * Optimize operator allocation #81 * Introduce literals for NULL values #80 * Buffer refCnt = 0 reported when encoding large lob #79 * Add support for expected hostname configuration #78 * NotSslRecordException thrown when connecting to Azure SQL Server #77 * Exclude not-required netty dependencies #75 * Add benchmark suites #68 * Consider large chunks in StreamDecoder #63 * Add FluxDiscardOnCancel operator #6 0.8.0.M8 ------------------ * Upgrade to AssertJ 3.12.0 #72 * Upgrade to Reactor Dysprosium M1 #71 * Adapt to IsolationLevel changes (switch from enum to constant class) #70 * Implement RowMetadata.getColumnNames() #64 * Readme mentions mysql as driver identifier #62 * Example Tests #60 * Completion in GeneratedValues.reduceToSingleCountDoneToken(…) leaves non-consumed protocol messages on the wire #59 * Add support for BLOB/CLOB types #58 * Use R2DBC Exception hierarchy for driver exceptions #57 * Reduce dependencies #56 * Add configurable fetch size to MssqlStatement #55 * Executing a parametrized Statement twice fails #54 * Introduce cache for parsed SQL statements #53 * Fix memory leak in cursored RPC flow #52 * Reduce object allocations #51 * SimpleMssqlStatement creates eagerly QueryMessageFlow #50 * Defer error signal emission in MssqlResult until done token is processed #49 * Introduce direct/cursored preference Predicate to prefer direct/cursored execution #48 * Add support for SP_EXECUTESQL for simple parametrized statements #47 * Query-Subscribers of Client.exchange(…) remain subscribed #46 * Getting java.lang.IllegalStateException: Collation not available when querying the database. #37 * Add ConnectionFactoryProvider.getDriver() implementation #31 * Git ignore enhancement #30 * Add support for varchar(max) and nvarchar(max) #28 * Support SQL Server-specific transaction isolation levels by adding setTransactionIsolationLevel(MssqlIsolationLevel) to MssqlConnection #19 * Document supported data types #18 * Add support for binary types #3 1.0.0.M7 ------------------ * Update changelog for M7 #25 * Fix ConnectionFactories usage example in readme #24 * Tabular decode function retains previous column metadata #23 * Introduce caching for RowMetadata instead creating an instance per row #22 * Enhanced ColumnMetadata #21 * Upgrade to TestContainers 1.10.6 #20 * Add Statement.returnGeneratedValues(String...) #17 * Remove Recursive Generics #16 * Add configuration support connect timeout #15 * Implement ConnectionFactory Discovery #14 * Null values should return IllegalArgumentException #10 * Parametrized INSERT … SELECT select SCOPE_IDENTITY() returns wrong affected rows count #7 * Add support for transport-level encryption to allow Azure usage #5 * Add support for OffsetDateTime #4 1.0.0.M6 ------------------ * Inception ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: NOTICE ================================================ Reactive Relational Database Connectivity Copyright 2017-2022 the original author or authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # Reactive Relational Database Connectivity Microsoft SQL Server Implementation [![Java CI with Maven](https://github.com/r2dbc/r2dbc-mssql/actions/workflows/ci.yml/badge.svg)](https://github.com/r2dbc/r2dbc-mssql/actions/workflows/ci.yml) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.r2dbc/r2dbc-mssql/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.r2dbc/r2dbc-mssql) This project contains the [Microsoft SQL Server][m] implementation of the [R2DBC SPI][r]. This implementation is not intended to be used directly, but rather to be used as the backing implementation for a humane client library to delegate to [m]: https://microsoft.com/sqlserver [r]: https://github.com/r2dbc/r2dbc-spi This driver provides the following features: * Complies with R2DBC 1.0 * Login with username/password with temporary SSL encryption * Full SSL encryption support (for e.g. Azure usage). * Transaction Control * Simple execution of SQL batches (direct and cursored execution) * Execution of parametrized statements (direct and cursored execution) * Extensive type support (including `TEXT`, `VARCHAR(MAX)`, `IMAGE`, `VARBINARY(MAX)` and national variants, see below for exceptions) * Execution of stored procedures Next steps: * Add support for TVP and UDTs ## Code of Conduct This project is governed by the [R2DBC Code of Conduct](https://github.com/r2dbc/.github/blob/main/CODE_OF_CONDUCT.adoc). By participating, you are expected to uphold this code of conduct. Please report unacceptable behavior to [info@r2dbc.io](mailto:info@r2dbc.io). ## Getting Started Here is a quick teaser of how to use R2DBC MSSQL in Java: **URL Connection Factory Discovery** ```java ConnectionFactory connectionFactory = ConnectionFactories.get("r2dbc:mssql://:1433/"); Publisher connectionPublisher = connectionFactory.create(); ``` **Programmatic Connection Factory Discovery** ```java ConnectionFactoryOptions options = builder() .option(DRIVER, "sqlserver") .option(HOST, "…") .option(PORT, …) // optional, defaults to 1433 .option(USER, "…") .option(PASSWORD, "…") .option(DATABASE, "…") // optional .option(SSL, true) // optional, defaults to false .option(Option.valueOf("applicationName"), "…") // optional .option(Option.valueOf("preferCursoredExecution"), true/false) // optional .option(Option.valueOf("connectionId"), new UUID(…)) // optional .build(); ConnectionFactory connectionFactory = ConnectionFactories.get(options); Publisher connectionPublisher = connectionFactory.create(); // Alternative: Creating a Mono using Project Reactor Mono connectionMono = Mono.from(connectionFactory.create()); ``` **Supported ConnectionFactory Discovery Options** | Option | Description |---------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | `ssl` | Whether to use transport-level encryption for the entire SQL server traffic. | `driver` | Must be `sqlserver`. | `host` | Server hostname to connect to. | `port` | Server port to connect to. Defaults to `1433`. _(Optional)_ | `username` | Login username. | `password` | Login password. | `database` | Initial database to select. Defaults to SQL Server user profile settings. _(Optional)_ | `applicationName` | Name of the application. Defaults to driver name and version. _(Optional)_ | `connectionId` | Connection Id for tracing purposes. Defaults to a random Id. _(Optional)_ | `connectionProvider` | Set the `reactor.netty.resources.ConnectionProvider` to be used when creating the connection. Defaults to `ConnectionProvider.newConnection()`. _(Optional)_ | `connectTimeout` | Connection Id for tracing purposes. Defaults to 30 seconds. _(Optional)_ | `hostNameInCertificate` | Expected hostname in SSL certificate. Supports wildcards (e.g. `*.database.windows.net`). _(Optional)_ | `lockWaitTimeout` | Lock wait timeout using `SET LOCK_TIMEOUT …`. _(Optional)_ | `preferCursoredExecution` | Whether to prefer cursors or direct execution for queries. Uses by default direct. Cursors require more round-trips but are more backpressure-friendly. Defaults to direct execution. Can be `boolean` or a `Predicate` accepting the SQL query. _(Optional)_ | `sendStringParametersAsUnicode` | Configure whether to send character data as unicode (NVARCHAR, NCHAR, NTEXT) or whether to use the database encoding, defaults to `true`. If disabled, `CharSequence` data is sent using the database-specific collation such as ASCII/MBCS instead of Unicode. | `sslTunnel` | Enables SSL tunnel usage when using a SSL tunnel or SSL terminator in front of SQL Server. Accepts `Function` to customize the SSL tunnel settings. SSL tunneling is not related to SQL Server's built-in SSL support. _(Optional)_ | `sslContextBuilderCustomizer` | SSL Context customizer to configure SQL Server's built-in SSL support (`Function`) _(Optional)_ | `tcpKeepAlive` | Enable/disable TCP KeepAlive. Disabled by default. _(Optional)_ | `tcpNoDelay` | Enable/disable TCP NoDelay. Enabled by default. _(Optional)_ | `trustServerCertificate` | Fully trust the server certificate bypassing X.509 certificate validation. Disabled by default. _(Optional)_ | `trustStoreType` | Type of the TrustStore. Defaults to `KeyStore.getDefaultType()`. _(Optional)_ | `trustStore` | Path to the certificate TrustStore file. _(Optional)_ | `trustStorePassword` | Password used to check the integrity of the TrustStore data. _(Optional)_ **Programmatic Configuration** ```java MssqlConnectionConfiguration configuration = MssqlConnectionConfiguration.builder() .host("…") .username("…") .password("…") .database("…") .preferCursoredExecution(…) .build(); MssqlConnectionFactory factory = new MssqlConnectionFactory(configuration); Mono connectionMono = factory.create(); ``` Microsoft SQL Server uses named parameters that are prefixed with `@`. The following SQL statement makes use of parameters: ```sql INSERT INTO person (id, first_name, last_name) VALUES(@id, @firstname, @lastname) ``` Parameters are referenced without the `@` prefix when binding these: ```java connection.createStatement("INSERT INTO person (id, first_name, last_name) VALUES(@id, @firstname, @lastname)") .bind("id", 1) .bind("firstname", "Walter") .bind("lastname", "White") .execute() ``` Binding also allows positional index (zero-based) references. The parameter index is derived from the parameter discovery order when parsing the query. ### Maven configuration Artifacts can be found on [Maven Central](https://central.sonatype.com/search?q=r2dbc-mssql). ```xml io.r2dbc r2dbc-mssql ${version} ``` If you'd rather like the latest snapshots of the upcoming major version, use our Maven snapshot repository and declare the appropriate dependency version. ```xml io.r2dbc r2dbc-mssql ${version}.BUILD-SNAPSHOT central-portal-snapshots Central Portal Snapshots https://central.sonatype.com/repository/maven-snapshots/ ``` ## Transaction Definitions SQL Server supports additional options when starting a transaction. In particular, the following options can be specified: * Isolation Level (`isolationLevel`) (reset after the transaction to previous value) * Transaction Name (`name`) * Transaction Log Mark (`mark`) * Lock Wait Timeout (`lockWaitTimeout`) (reset after the transaction to `-1`) These options can be specified upon transaction begin to start the transaction and apply options in a single command roundtrip: ```java MssqlConnection connection= …; connection.beginTransaction(MssqlTransactionDefinition.from(IsolationLevel.READ_UNCOMMITTED) .name("my-transaction").mark("tx-log-mark") .lockTimeout(Duration.ofMinutes(1))); ``` See also: https://docs.microsoft.com/en-us/sql/t-sql/language-elements/begin-transaction-transact-sql ### Data Type Mapping This reference table shows the type mapping between [Microsoft SQL Server][m] and Java data types: | Microsoft SQL Server Type | Java Data Type | |:------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------| | [`bit`][sql-bit-ref] | [**`Boolean`**][java-boolean-ref], [`Byte`][java-byte-ref], [`Short`][java-short-ref], [`Integer`][java-integer-ref], [`Long`][java-long-ref], [`BigDecimal`][java-bigdecimal-ref], [`BigInteger`][java-biginteger-ref] | | [`tinyint`][sql-all-int-ref] | [**`Byte`**][java-byte-ref], [`Boolean`][java-boolean-ref], [`Short`][java-short-ref], [`Integer`][java-integer-ref], [`Long`][java-long-ref], [`BigDecimal`][java-bigdecimal-ref], [`BigInteger`][java-biginteger-ref] | | [`smallint`][sql-all-int-ref] | [**`Short`**][java-short-ref], [`Boolean`][java-boolean-ref], [`Byte`][java-byte-ref], [`Integer`][java-integer-ref], [`Long`][java-long-ref], [`BigDecimal`][java-bigdecimal-ref], [`BigInteger`][java-biginteger-ref] | | [`int`][sql-all-int-ref] | [**`Integer`**][java-integer-ref], [`Boolean`][java-boolean-ref], [`Byte`][java-byte-ref], [`Short`][java-short-ref], [`Long`][java-long-ref], [`BigDecimal`][java-bigdecimal-ref], [`BigInteger`][java-biginteger-ref] | | [`bigint`][sql-all-int-ref] | [**`Long`**][java-long-ref], [`Boolean`][java-boolean-ref], [`Byte`][java-byte-ref], [`Short`][java-short-ref], [`Integer`][java-integer-ref], [`BigDecimal`][java-bigdecimal-ref], [`BigInteger`][java-biginteger-ref] | | [`real`][sql-float-real-ref] | [**`Float`**][java-float-ref], [`Double`][java-double-ref] | [`float`][sql-float-real-ref] | [**`Double`**][java-double-ref], [`Float`][java-float-ref] | [`decimal`][sql-decimal-ref] | [**`BigDecimal`**][java-bigdecimal-ref], [`BigInteger`][java-biginteger-ref] | [`numeric`][sql-decimal-ref] | [**`BigDecimal`**][java-bigdecimal-ref], [`BigInteger`][java-biginteger-ref] | [`uniqueidentifier`][sql-uid-ref] | [**`UUID`**][java-uuid-ref], [`String`][java-string-ref] | [`smalldatetime`][sql-smalldatetime-ref] | [`LocalDateTime`][java-ldt-ref] | [`datetime`][sql-datetime-ref] | [`LocalDateTime`][java-ldt-ref] | [`datetime2`][sql-datetime2-ref] | [`LocalDateTime`][java-ldt-ref] | [`date`][sql-date-ref] | [`LocalDate`][java-ld-ref] | [`time`][sql-time-ref] | [`LocalTime`][java-lt-ref] | [`datetimeoffset`][sql-dtof-ref] | [**`OffsetDateTime`**][java-odt-ref], [`ZonedDateTime`][java-zdt-ref] | [`timestamp`][sql-timestamp-ref] | [`byte[]`][java-byte-ref] | [`smallmoney`][sql-money-ref] | [`BigDecimal`][java-bigdecimal-ref] | [`money`][sql-money-ref] | [`BigDecimal`][java-bigdecimal-ref] | [`char`][sql-(var)char-ref] | [`String`][java-string-ref], [`Clob`][r2dbc-clob-ref] | [`varchar`][sql-(var)char-ref] | [`String`][java-string-ref], [`Clob`][r2dbc-clob-ref] | [`varcharmax`][sql-(var)char-ref] | [`String`][java-string-ref], [`Clob`][r2dbc-clob-ref] | [`nchar`][sql-n(var)char-ref] | [`String`][java-string-ref], [`Clob`][r2dbc-clob-ref] | [`nvarchar`][sql-n(var)char-ref] | [`String`][java-string-ref], [`Clob`][r2dbc-clob-ref] | [`nvarcharmax`][sql-n(var)char-ref] | [`String`][java-string-ref], [`Clob`][r2dbc-clob-ref] | [`text`][sql-(n)text-ref] | [`String`][java-string-ref], [`Clob`][r2dbc-clob-ref] | [`ntext`][sql-(n)text-ref] | [`String`][java-string-ref], [`Clob`][r2dbc-clob-ref] | [`image`][sql-(n)text-ref] | [**`ByteBuffer`**][java-ByteBuffer-ref], [`byte[]`][java-byte-ref], [`Blob`][r2dbc-blob-ref] | [`binary`][sql-binary-ref] | [**`ByteBuffer`**][java-ByteBuffer-ref], [`byte[]`][java-byte-ref], [`Blob`][r2dbc-blob-ref] | [`varbinary`][sql-binary-ref] | [**`ByteBuffer`**][java-ByteBuffer-ref], [`byte[]`][java-byte-ref], [`Blob`][r2dbc-blob-ref] | [`varbinarymax`][sql-binary-ref] | [**`ByteBuffer`**][java-ByteBuffer-ref], [`byte[]`][java-byte-ref], [`Blob`][r2dbc-blob-ref] | [`sql_variant`][sql-sql-variant-ref] | Not yet supported. | [`xml`][sql-xml-ref] | Not yet supported. | [`udt`][sql-udt-ref] | Not yet supported. | [`geometry`][sql-geometry-ref] | Not yet supported. | [`geography`][sql-geography-ref] | Not yet supported. Types in **bold** indicate the native (default) Java type. **Note:** BLOB (`image`, `binary`, `varbinary` and `varbinary(max)`) and CLOB (`text`, `ntext`, `varchar(max)` and `nvarchar(max)`) values are fully materialized in the client before decoding. Make sure to account for proper memory sizing. [sql-bit-ref]: https://docs.microsoft.com/en-us/sql/t-sql/data-types/bit-transact-sql?view=sql-server-2017 [sql-all-int-ref]: https://docs.microsoft.com/en-us/sql/t-sql/data-types/int-bigint-smallint-and-tinyint-transact-sql?view=sql-server-2017 [sql-float-real-ref]: https://docs.microsoft.com/en-us/sql/t-sql/data-types/float-and-real-transact-sql?view=sql-server-2017 [sql-decimal-ref]: https://docs.microsoft.com/en-us/sql/t-sql/data-types/decimal-and-numeric-transact-sql?view=sql-server-2017 [sql-smalldatetime-ref]: https://docs.microsoft.com/en-us/sql/t-sql/data-types/smalldatetime-transact-sql?view=sql-server-2017 [sql-datetime-ref]: https://docs.microsoft.com/en-us/sql/t-sql/data-types/datetime-transact-sql?view=sql-server-2017 [sql-datetime2-ref]: https://docs.microsoft.com/en-us/sql/t-sql/data-types/datetime2-transact-sql?view=sql-server-2017 [sql-date-ref]: https://docs.microsoft.com/en-us/sql/t-sql/data-types/date-transact-sql?view=sql-server-2017 [sql-time-ref]: https://docs.microsoft.com/en-us/sql/t-sql/data-types/time-transact-sql?view=sql-server-2017 [sql-timestamp-ref]: https://docs.microsoft.com/en-us/sql/t-sql/data-types/rowversion-transact-sql?view=sql-server-2017 [sql-uid-ref]: https://docs.microsoft.com/en-us/sql/t-sql/data-types/uniqueidentifier-transact-sql?view=sql-server-2017 [sql-dtof-ref]: https://docs.microsoft.com/en-us/sql/t-sql/data-types/datetimeoffset-transact-sql?view=sql-server-2017 [sql-money-ref]: https://docs.microsoft.com/en-us/sql/t-sql/data-types/money-and-smallmoney-transact-sql?view=sql-server-2017 [sql-(var)char-ref]: https://docs.microsoft.com/en-us/sql/t-sql/data-types/char-and-varchar-transact-sql?view=sql-server-2017 [sql-n(var)char-ref]: https://docs.microsoft.com/en-us/sql/t-sql/data-types/nchar-and-nvarchar-transact-sql?view=sql-server-2017 [sql-(n)text-ref]: https://docs.microsoft.com/en-us/sql/t-sql/data-types/ntext-text-and-image-transact-sql?view=sql-server-2017 [sql-binary-ref]: https://docs.microsoft.com/en-us/sql/t-sql/data-types/binary-and-varbinary-transact-sql?view=sql-server-2017 [sql-sql-variant-ref]: https://docs.microsoft.com/en-us/sql/t-sql/data-types/sql-variant-transact-sql?view=sql-server-2017 [sql-xml-ref]: https://docs.microsoft.com/en-us/sql/t-sql/xml/xml-transact-sql?view=sql-server-2017 [sql-udt-ref]: https://docs.microsoft.com/en-us/sql/relational-databases/clr-integration-database-objects-user-defined-types/clr-user-defined-types?view=sql-server-2017 [sql-geometry-ref]: https://docs.microsoft.com/en-us/sql/t-sql/spatial-geometry/spatial-types-geometry-transact-sql?view=sql-server-2017 [sql-geography-ref]: https://docs.microsoft.com/en-us/sql/t-sql/spatial-geography/spatial-types-geography?view=sql-server-2017 [r2dbc-blob-ref]: https://r2dbc.io/spec/1.0.0.RELEASE/api/io/r2dbc/spi/Blob.html [r2dbc-clob-ref]: https://r2dbc.io/spec/1.0.0.RELEASE/api/io/r2dbc/spi/Clob.html [java-bigdecimal-ref]: https://docs.oracle.com/javase/8/docs/api/java/math/BigDecimal.html [java-biginteger-ref]: https://docs.oracle.com/javase/8/docs/api/java/math/BigInteger.html [java-boolean-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/Boolean.html [java-byte-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/Byte.html [java-ByteBuffer-ref]: https://docs.oracle.com/javase/8/docs/api/java/nio/ByteBuffer.html [java-double-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/Double.html [java-float-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/Float.html [java-integer-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/Integer.html [java-long-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/Long.html [java-ldt-ref]: https://docs.oracle.com/javase/8/docs/api/java/time/LocalDateTime.html [java-ld-ref]: https://docs.oracle.com/javase/8/docs/api/java/time/LocalDate.html [java-lt-ref]: https://docs.oracle.com/javase/8/docs/api/java/time/LocalTime.html [java-odt-ref]: https://docs.oracle.com/javase/8/docs/api/java/time/OffsetDateTime.html [java-short-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/Short.html [java-string-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/String.html [java-uuid-ref]: https://docs.oracle.com/javase/8/docs/api/java/util/UUID.html [java-zdt-ref]: https://docs.oracle.com/javase/8/docs/api/java/time/ZonedDateTime.html ## Logging If SL4J is on the classpath, it will be used. Otherwise, there are two possible fallbacks: Console or `java.util.logging.Logger`). By default, the Console fallback is used. To use the JDK loggers, set the `reactor.logging.fallback` System property to `JDK`. Logging facilities: * Driver Logging (`io.r2dbc.mssql`) * Query Logging (`io.r2dbc.mssql.QUERY` on `DEBUG` level) * Transport Logging (`io.r2dbc.mssql.client`) * `DEBUG` enables `Message` exchange logging * `TRACE` enables traffic logging ## Getting Help Having trouble with R2DBC? We'd love to help! * Check the [spec documentation](https://r2dbc.io/spec/1.0.0.RELEASE/spec/html/), and [Javadoc](https://r2dbc.io/spec/1.0.0.RELEASE/api/). * If you are upgrading, check out the [changelog](https://r2dbc.io/spec/1.0.0.RELEASE/CHANGELOG.txt) for "new and noteworthy" features. * Ask a question - we monitor [stackoverflow.com](https://stackoverflow.com) for questions tagged with [`r2dbc`](https://stackoverflow.com/tags/r2dbc). You can also chat with the community on [Gitter](https://gitter.im/r2dbc/r2dbc). * Report bugs with R2DBC MSSQL at [github.com/r2dbc/r2dbc-mssql/issues](https://github.com/r2dbc/r2dbc-mssql/issues). ## Reporting Issues R2DBC uses GitHub as issue tracking system to record bugs and feature requests. If you want to raise an issue, please follow the recommendations below: * Before you log a bug, please search the [issue tracker](https://github.com/r2dbc/r2dbc-mssql/issues) to see if someone has already reported the problem. * If the issue doesn't already exist, [create a new issue](https://github.com/r2dbc/r2dbc-mssql/issues/new). * Please provide as much information as possible with the issue report, we like to know the version of R2DBC MSSQL that you are using and JVM version. * If you need to paste code, or include a stack trace use Markdown ``` escapes before and after your text. * If possible try to create a test-case or project that replicates the issue. Attach a link to your code or a compressed file containing your code. ## Building from Source You don't need to build from source to use R2DBC MSSQL (binaries in Maven Central), but if you want to try out the latest and greatest, R2DBC MSSQL can be easily built with the [maven wrapper](https://github.com/takari/maven-wrapper). You also need JDK 1.8 and Docker to run integration tests. ```bash $ ./mvnw clean install ``` If you want to build with the regular `mvn` command, you will need [Maven v3.6.0 or above](https://maven.apache.org/run-maven/index.html). _Also see [CONTRIBUTING.adoc](https://github.com/r2dbc/.github/blob/main/CONTRIBUTING.adoc) if you wish to submit pull requests. Commits require `Signed-off-by` (`git commit -s`) to ensure [Developer Certificate of Origin](https://developercertificate.org/)._ ### Running JMH Benchmarks Running the JMH benchmarks builds and runs the benchmarks without running tests. ```bash $ ./mvnw clean install -Pjmh ``` ## Staging to Maven Central To stage a release to Maven Central, you need to create a release tag (release version) that contains the desired state and version numbers ( `mvn versions:set versions:commit -q -o -DgenerateBackupPoms=false -DnewVersion=x.y.z.(RELEASE|Mnnn|RCnnn`) and force-push it to the `release` branch. This push will trigger a Maven staging build. ## License R2DBC MSSQL is Open Source software released under the [Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0.html). ================================================ FILE: ci/build-and-deploy-to-maven-central.sh ================================================ #!/bin/bash set -euo pipefail VERSION=$(./mvnw org.apache.maven.plugins:maven-help-plugin:2.1.1:evaluate -Dexpression=project.version -o | grep -v INFO) if [[ $VERSION =~ [^.*-SNAPSHOT$] ]] ; then echo "Cannot deploy a snapshot: $VERSION" exit 1 fi if [[ $VERSION =~ [^(\d+\.)+(RC(\d+)|M(\d+)|RELEASE)$] ]] ; then # # Prepare GPG Key is expected to be in base64 # Exported with gpg -a --export-secret-keys "your@email" | base64 > gpg.base64 # printf "$GPG_KEY_BASE64" | base64 --decode > gpg.asc echo ${GPG_PASSPHRASE} | gpg --batch --yes --passphrase-fd 0 --import gpg.asc gpg -k # # Stage on Maven Central # echo "Staging $VERSION to Central Portal" ./mvnw \ -s settings.xml \ -Pcentral \ -Dmaven.test.skip=true \ -Dgpg.passphrase=${GPG_PASSPHRASE} \ clean deploy -B -D skipITs else echo "Not a release: $VERSION" exit 1 fi ================================================ FILE: intellij-style.xml ================================================ ================================================ FILE: mvnw ================================================ #!/bin/sh # ---------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- # Maven2 Start Up Batch script # # Required ENV vars: # ------------------ # JAVA_HOME - location of a JDK home dir # # Optional ENV vars # ----------------- # M2_HOME - location of maven2's installed home dir # MAVEN_OPTS - parameters passed to the Java VM when running Maven # e.g. to debug Maven itself, use # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 # MAVEN_SKIP_RC - flag to disable loading of mavenrc files # ---------------------------------------------------------------------------- if [ -z "$MAVEN_SKIP_RC" ] ; then if [ -f /etc/mavenrc ] ; then . /etc/mavenrc fi if [ -f "$HOME/.mavenrc" ] ; then . "$HOME/.mavenrc" fi fi # OS specific support. $var _must_ be set to either true or false. cygwin=false; darwin=false; mingw=false case "`uname`" in CYGWIN*) cygwin=true ;; MINGW*) mingw=true;; Darwin*) darwin=true # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home # See https://developer.apple.com/library/mac/qa/qa1170/_index.html if [ -z "$JAVA_HOME" ]; then if [ -x "/usr/libexec/java_home" ]; then export JAVA_HOME="`/usr/libexec/java_home`" else export JAVA_HOME="/Library/Java/Home" fi fi ;; esac if [ -z "$JAVA_HOME" ] ; then if [ -r /etc/gentoo-release ] ; then JAVA_HOME=`java-config --jre-home` fi fi if [ -z "$M2_HOME" ] ; then ## resolve links - $0 may be a link to maven's home PRG="$0" # need this for relative symlinks while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG="`dirname "$PRG"`/$link" fi done saveddir=`pwd` M2_HOME=`dirname "$PRG"`/.. # make it fully qualified M2_HOME=`cd "$M2_HOME" && pwd` cd "$saveddir" # echo Using m2 at $M2_HOME fi # For Cygwin, ensure paths are in UNIX format before anything is touched if $cygwin ; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --unix "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --unix "$CLASSPATH"` fi # For Mingw, ensure paths are in UNIX format before anything is touched if $mingw ; then [ -n "$M2_HOME" ] && M2_HOME="`(cd "$M2_HOME"; pwd)`" [ -n "$JAVA_HOME" ] && JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" # TODO classpath? fi if [ -z "$JAVA_HOME" ]; then javaExecutable="`which javac`" if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then # readlink(1) is not available as standard on Solaris 10. readLink=`which readlink` if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then if $darwin ; then javaHome="`dirname \"$javaExecutable\"`" javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" else javaExecutable="`readlink -f \"$javaExecutable\"`" fi javaHome="`dirname \"$javaExecutable\"`" javaHome=`expr "$javaHome" : '\(.*\)/bin'` JAVA_HOME="$javaHome" export JAVA_HOME fi fi fi if [ -z "$JAVACMD" ] ; then if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi else JAVACMD="`which java`" fi fi if [ ! -x "$JAVACMD" ] ; then echo "Error: JAVA_HOME is not defined correctly." >&2 echo " We cannot execute $JAVACMD" >&2 exit 1 fi if [ -z "$JAVA_HOME" ] ; then echo "Warning: JAVA_HOME environment variable is not set." fi CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher # traverses directory structure from process work directory to filesystem root # first directory with .mvn subdirectory is considered project base directory find_maven_basedir() { if [ -z "$1" ] then echo "Path not specified to find_maven_basedir" return 1 fi basedir="$1" wdir="$1" while [ "$wdir" != '/' ] ; do if [ -d "$wdir"/.mvn ] ; then basedir=$wdir break fi # workaround for JBEAP-8937 (on Solaris 10/Sparc) if [ -d "${wdir}" ]; then wdir=`cd "$wdir/.."; pwd` fi # end of workaround done echo "${basedir}" } # concatenates all lines of a file concat_lines() { if [ -f "$1" ]; then echo "$(tr -s '\n' ' ' < "$1")" fi } BASE_DIR=`find_maven_basedir "$(pwd)"` if [ -z "$BASE_DIR" ]; then exit 1; fi ########################################################################################## # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central # This allows using the maven wrapper in projects that prohibit checking in binary data. ########################################################################################## if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found .mvn/wrapper/maven-wrapper.jar" fi else if [ "$MVNW_VERBOSE" = true ]; then echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." fi jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" while IFS="=" read key value; do case "$key" in (wrapperUrl) jarUrl="$value"; break ;; esac done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" if [ "$MVNW_VERBOSE" = true ]; then echo "Downloading from: $jarUrl" fi wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" if command -v wget > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found wget ... using wget" fi wget "$jarUrl" -O "$wrapperJarPath" elif command -v curl > /dev/null; then if [ "$MVNW_VERBOSE" = true ]; then echo "Found curl ... using curl" fi curl -o "$wrapperJarPath" "$jarUrl" else if [ "$MVNW_VERBOSE" = true ]; then echo "Falling back to using Java to download" fi javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" if [ -e "$javaClass" ]; then if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then if [ "$MVNW_VERBOSE" = true ]; then echo " - Compiling MavenWrapperDownloader.java ..." fi # Compiling the Java class ("$JAVA_HOME/bin/javac" "$javaClass") fi if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then # Running the downloader if [ "$MVNW_VERBOSE" = true ]; then echo " - Running MavenWrapperDownloader.java ..." fi ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") fi fi fi fi ########################################################################################## # End of extension ########################################################################################## export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} if [ "$MVNW_VERBOSE" = true ]; then echo $MAVEN_PROJECTBASEDIR fi MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" # For Cygwin, switch paths to Windows format before running java if $cygwin; then [ -n "$M2_HOME" ] && M2_HOME=`cygpath --path --windows "$M2_HOME"` [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` [ -n "$CLASSPATH" ] && CLASSPATH=`cygpath --path --windows "$CLASSPATH"` [ -n "$MAVEN_PROJECTBASEDIR" ] && MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` fi WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain exec "$JAVACMD" \ $MAVEN_OPTS \ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" ================================================ FILE: mvnw.cmd ================================================ @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @REM distributed with this work for additional information @REM regarding copyright ownership. The ASF licenses this file @REM to you under the Apache License, Version 2.0 (the @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM @REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @REM KIND, either express or implied. See the License for the @REM specific language governing permissions and limitations @REM under the License. @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- @REM Maven2 Start Up Batch script @REM @REM Required ENV vars: @REM JAVA_HOME - location of a JDK home dir @REM @REM Optional ENV vars @REM M2_HOME - location of maven2's installed home dir @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM e.g. to debug Maven itself, use @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files @REM ---------------------------------------------------------------------------- @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' @echo off @REM set title of command window title %0 @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% @REM set %HOME% to equivalent of $HOME if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") @REM Execute a user defined script before this one if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre @REM check for pre script, once with legacy .bat ending and once with .cmd ending if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" :skipRcPre @setlocal set ERROR_CODE=0 @REM To isolate internal variables from possible post scripts, we use another setlocal @setlocal @REM ==== START VALIDATION ==== if not "%JAVA_HOME%" == "" goto OkJHome echo. echo Error: JAVA_HOME not found in your environment. >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error :OkJHome if exist "%JAVA_HOME%\bin\java.exe" goto init echo. echo Error: JAVA_HOME is set to an invalid directory. >&2 echo JAVA_HOME = "%JAVA_HOME%" >&2 echo Please set the JAVA_HOME variable in your environment to match the >&2 echo location of your Java installation. >&2 echo. goto error @REM ==== END VALIDATION ==== :init @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". @REM Fallback to current working directory if not found. set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir set EXEC_DIR=%CD% set WDIR=%EXEC_DIR% :findBaseDir IF EXIST "%WDIR%"\.mvn goto baseDirFound cd .. IF "%WDIR%"=="%CD%" goto baseDirNotFound set WDIR=%CD% goto findBaseDir :baseDirFound set MAVEN_PROJECTBASEDIR=%WDIR% cd "%EXEC_DIR%" goto endDetectBaseDir :baseDirNotFound set MAVEN_PROJECTBASEDIR=%EXEC_DIR% cd "%EXEC_DIR%" :endDetectBaseDir IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig @setlocal EnableExtensions EnableDelayedExpansion for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% :endReadAdditionalConfig SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B ) @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central @REM This allows using the maven wrapper in projects that prohibit checking in binary data. if exist %WRAPPER_JAR% ( echo Found %WRAPPER_JAR% ) else ( echo Couldn't find %WRAPPER_JAR%, downloading it ... echo Downloading from: %DOWNLOAD_URL% powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" echo Finished downloading %WRAPPER_JAR% ) @REM End of extension %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* if ERRORLEVEL 1 goto error goto end :error set ERROR_CODE=1 :end @endlocal & set ERROR_CODE=%ERROR_CODE% if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost @REM check for post script, once with legacy .bat ending and once with .cmd ending if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" :skipRcPost @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' if "%MAVEN_BATCH_PAUSE%" == "on" pause if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% exit /B %ERROR_CODE% ================================================ FILE: pom.xml ================================================ 4.0.0 io.r2dbc r2dbc-mssql 1.1.0.BUILD-SNAPSHOT jar Reactive Relational Database Connectivity - Microsoft SQL Server Reactive Relational Database Connectivity Driver Implementation for Microsoft SQL Server https://github.com/r2dbc/r2dbc-mssql 3.23.1 4.0.3 1.8 3.0.2 5.13.4 1.37 0.6.0.RELEASE 1.5.24 4.11.0 13.2.1.jre8 UTF-8 UTF-8 1.0.0.RELEASE 1.0.2.RELEASE 2022.0.22 5.3.39 1.21.4 Apache License 2.0 https://www.apache.org/licenses/LICENSE-2.0.txt repo scm:git:https://github.com/r2dbc/r2dbc-mssql https://github.com/r2dbc/r2dbc-mssql Ben Hale bhale@pivotal.io Mark Paluch mpaluch@pivotal.io io.projectreactor reactor-bom ${reactor.version} pom import org.junit junit-bom ${junit.version} pom import org.testcontainers testcontainers-bom ${testcontainers.version} pom import io.r2dbc r2dbc-spi ${r2dbc-spi.version} io.projectreactor reactor-core io.projectreactor.netty reactor-netty-core com.google.code.findbugs jsr305 ${jsr305.version} provided io.r2dbc r2dbc-pool ${r2dbc-pool.version} test io.projectreactor reactor-test test io.r2dbc r2dbc-spi-test ${r2dbc-spi.version} test org.assertj assertj-core ${assertj.version} test org.junit.jupiter junit-jupiter-api test org.junit.jupiter junit-jupiter-engine test org.junit.jupiter junit-jupiter-params test org.mockito mockito-core ${mockito.version} test org.testcontainers mssqlserver test com.microsoft.sqlserver mssql-jdbc ${mssql-jdbc.version} test com.zaxxer HikariCP ${hikari-cp.version} test org.slf4j slf4j-api org.springframework spring-jdbc ${spring-framework.version} test ch.qos.logback logback-classic ${logback.version} test org.apache.maven.plugins maven-compiler-plugin 3.11.0 true 8 org.apache.maven.plugins maven-jar-plugin 3.3.0 true true ${r2dbc-spi.version} r2dbc.mssql org.apache.maven.plugins maven-deploy-plugin 3.1.1 org.apache.maven.plugins maven-enforcer-plugin 3.3.0 enforce-no-snapshots enforce true No Snapshots in releases allowed! org.apache.maven.plugins maven-javadoc-plugin 3.5.0 io.r2dbc.mssql.authentication,io.r2dbc.mssql.client,io.r2dbc.mssql.codec,io.r2dbc.mssql.message,io.r2dbc.mssql.util https://r2dbc.io/spec/${r2dbc-spi.version}/api/ https://projectreactor.io/docs/core/release/api/ https://www.reactive-streams.org/reactive-streams-1.0.4-javadoc/ -missing 1.8 attach-javadocs jar org.apache.maven.plugins maven-source-plugin 3.3.0 attach-javadocs jar org.apache.maven.plugins maven-surefire-plugin 3.1.2 random **/**IntegrationTests.java **/*Tests.java org.apache.maven.plugins maven-failsafe-plugin 3.1.2 integration-test verify random **/*TestKit.java **/**IntegrationTests.java true org.codehaus.mojo flatten-maven-plugin 1.5.0 flatten process-resources flatten true oss remove expand remove remove flatten-clean clean clean org.sonatype.central central-publishing-maven-plugin 0.10.0 true true R2DBC MSSQL ${project.version} central ${project.basedir} LICENSE NOTICE CHANGELOG META-INF src/main/resources jmh com.github.mp911de.microbenchmark-runner microbenchmark-runner-junit5 ${mbr.version} test org.openjdk.jmh jmh-core ${jmh.version} test org.openjdk.jmh jmh-generator-annprocess ${jmh.version} test org.codehaus.mojo build-helper-maven-plugin 3.3.0 add-source generate-sources add-test-source src/jmh/java org.apache.maven.plugins maven-surefire-plugin true org.apache.maven.plugins maven-failsafe-plugin true org.codehaus.mojo exec-maven-plugin 3.1.0 run-benchmarks pre-integration-test exec test java -classpath org.openjdk.jmh.Main .* jitpack.io https://jitpack.io snapshot org.sonatype.central central-publishing-maven-plugin central org.apache.maven.plugins maven-gpg-plugin 3.1.0 sign-artifacts verify sign --pinentry-mode loopback org.apache.maven.plugins maven-gpg-plugin org.sonatype.central central-publishing-maven-plugin ================================================ FILE: settings.xml ================================================ central ${env.CENTRAL_TOKEN_USERNAME} ${env.CENTRAL_TOKEN_PASSWORD} ================================================ FILE: src/jmh/java/io/r2dbc/mssql/BenchmarkSettings.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.OutputTimeUnit; import org.openjdk.jmh.annotations.Warmup; import java.util.concurrent.TimeUnit; /** * Global benchmark settings. * * @author Mark Paluch */ @Warmup(iterations = 5, time = 2000, timeUnit = TimeUnit.MILLISECONDS) @Measurement(iterations = 5, time = 1000, timeUnit = TimeUnit.MILLISECONDS) @Fork(value = 1, warmups = 0) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) public abstract class BenchmarkSettings { } ================================================ FILE: src/jmh/java/io/r2dbc/mssql/ParametrizedMssqlStatementBenchmarks.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.client.Client; import io.r2dbc.mssql.codec.DefaultCodecs; import org.junit.platform.commons.annotation.Testable; import org.mockito.Mockito; import org.openjdk.jmh.annotations.Benchmark; import java.util.function.Function; /** * Benchmarks for {@link ParametrizedMssqlStatement}, specifically query parsing. * * @author Mark Paluch */ @Testable public class ParametrizedMssqlStatementBenchmarks extends BenchmarkSettings { private static final ConnectionOptions cached = new ConnectionOptions(s -> false, new DefaultCodecs(), new IndefinitePreparedStatementCache(), true); private static final ConnectionOptions uncached = new ConnectionOptions(s -> false, new DefaultCodecs(), new PreparedStatementCache() { @Override public int getHandle(String sql, Binding binding) { return 0; } @Override public void putHandle(int handle, String sql, Binding binding) { } @Override public T getParsedSql(String sql, Function parseFunction) { return parseFunction.apply(sql); } @Override public int size() { return 0; } }, true); private static final Client client = Mockito.mock(Client.class); @Benchmark public Object parseSqlCached1Param() { return new ParametrizedMssqlStatement(client, cached, "SELECT * from FOO where firstname = @firstname"); } @Benchmark public Object parseSqlCached5Param() { return new ParametrizedMssqlStatement(client, cached, "SELECT * from FOO where firstname = @firstname and firstname = @firstname and p2 = @p2 and p3 = @p3 and p4 = @p4 and p5 = @p5"); } @Benchmark public Object parseSqlNonCached1Param() { return new ParametrizedMssqlStatement(client, uncached, "SELECT * from FOO where firstname = @firstname"); } @Benchmark public Object parseSqlNonCached5Param() { return new ParametrizedMssqlStatement(client, uncached, "SELECT * from FOO where firstname = @firstname and firstname = @firstname and p2 = @p2 and p3 = @p3 and p4 = @p4 and p5 = @p5"); } } ================================================ FILE: src/jmh/java/io/r2dbc/mssql/PooledBenchmarks.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import com.zaxxer.hikari.HikariDataSource; import io.r2dbc.mssql.util.MsSqlServerExtension; import io.r2dbc.pool.ConnectionPool; import io.r2dbc.pool.ConnectionPoolConfiguration; import io.r2dbc.spi.ConnectionFactory; import org.junit.platform.commons.annotation.Testable; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.OutputTimeUnit; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.infra.Blackhole; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.concurrent.TimeUnit; /** * Benchmarks for direct Statement execution mode using connection pooling. * * @author Mark Paluch */ @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) public class PooledBenchmarks extends BenchmarkSettings { @State(Scope.Benchmark) public static class ConnectionHolder { final HikariDataSource jdbc; final ConnectionFactory r2dbc; public ConnectionHolder() { MsSqlServerExtension extension = new MsSqlServerExtension(); extension.initialize(); this.jdbc = extension.getDataSource(); MssqlConnectionConfiguration configuration = extension.configBuilder().build(); MssqlConnectionFactory mssqlConnectionFactory = new MssqlConnectionFactory(configuration); ConnectionPoolConfiguration poolConfiguration = ConnectionPoolConfiguration.builder(mssqlConnectionFactory).maxSize(4).build(); this.r2dbc = new ConnectionPool(poolConfiguration); } } @Benchmark @Testable public void simpleDirectJdbc(ConnectionHolder connectionHolder, Blackhole voodoo) throws SQLException { Connection connection = connectionHolder.jdbc.getConnection(); Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery("SELECT * FROM MSreplication_options"); while (resultSet.next()) { voodoo.consume(resultSet.getString("optname")); } resultSet.close(); statement.close(); connection.close(); } @Benchmark @Testable public void simpleDirectR2dbc(ConnectionHolder connectionHolder, Blackhole voodoo) { String optname = Mono.usingWhen(connectionHolder.r2dbc.create(), connection -> { MssqlStatement statement = (MssqlStatement) connection.createStatement("SELECT * FROM MSreplication_options"); statement.fetchSize(0); return Flux.from(statement.execute()).flatMap(it -> it.map((row, rowMetadata) -> row.get("optname", String.class))).last(); }, io.r2dbc.spi.Connection::close, (conn, err) -> conn.close(), io.r2dbc.spi.Connection::close ).block(); voodoo.consume(optname); } } ================================================ FILE: src/jmh/java/io/r2dbc/mssql/StagedResultSizeBenchmarks.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.util.MsSqlServerExtension; import org.junit.platform.commons.annotation.Testable; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.OutputTimeUnit; import org.openjdk.jmh.annotations.Param; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.infra.Blackhole; import reactor.core.publisher.Flux; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.UUID; import java.util.concurrent.TimeUnit; /** * Benchmarks for Statement execution modes across various result sizes. Contains the following execution methods: * *
    *
  • SQLBATCH (Direct, Statements without parameters)
  • *
  • SP_EXECUTESQL (Direct, Statements with parameters)
  • *
  • SP_CURSOROPEN (Cursors, Statements without parameters)
  • *
  • SP_CURSORPREPEXEC (Cursors, Prepared statements)
  • *
* * @author Mark Paluch */ @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) public class StagedResultSizeBenchmarks extends BenchmarkSettings { @State(Scope.Benchmark) public static class ConnectionHolder { final Connection jdbc; final io.r2dbc.spi.Connection r2dbc; @Param({"1", "10", "100", "200"}) int resultSize; public ConnectionHolder() { try { MsSqlServerExtension extension = new MsSqlServerExtension(); extension.initialize(); this.jdbc = extension.getDataSource().getConnection(); MssqlConnectionConfiguration configuration = extension.configBuilder().preferCursoredExecution(sql -> sql.contains(" /* cursored */")).build(); this.r2dbc = new MssqlConnectionFactory(configuration).create().block(); } catch (SQLException e) { throw new RuntimeException(e); } } @Setup public void setup() { try { Statement statement = this.jdbc.createStatement(); try { statement.execute("DROP TABLE result_sizes"); } catch (SQLException e) { } statement.execute("CREATE TABLE result_sizes (id int, name VARCHAR(255))"); for (int i = 0; i < this.resultSize; i++) { statement.execute(String.format("INSERT INTO result_sizes VALUES(%d, '%s')", i, UUID.randomUUID())); } } catch (SQLException e) { throw new RuntimeException(e); } } } @Benchmark @Testable public void simpleDirectJdbc(ConnectionHolder connectionHolder, Blackhole voodoo) throws SQLException { Statement statement = connectionHolder.jdbc.createStatement(); ResultSet resultSet = statement.executeQuery("SELECT * FROM result_sizes"); while (resultSet.next()) { voodoo.consume(resultSet.getString("name")); } resultSet.close(); statement.close(); } @Benchmark @Testable public void simpleDirectR2dbc(ConnectionHolder connectionHolder, Blackhole voodoo) { io.r2dbc.spi.Statement statement = connectionHolder.r2dbc.createStatement("SELECT * FROM result_sizes"); ((MssqlStatement) statement).fetchSize(0); String name = Flux.from(statement.execute()).flatMap(it -> it.map((row, rowMetadata) -> row.get("name", String.class))).blockLast(); voodoo.consume(name); } @Benchmark public void simpleCursoredJdbc(ConnectionHolder connectionHolder, Blackhole voodoo) throws SQLException { Statement statement = connectionHolder.jdbc.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); statement.setFetchSize(128); ResultSet resultSet = statement.executeQuery("SELECT * FROM result_sizes /* cursored */"); while (resultSet.next()) { voodoo.consume(resultSet.getString("name")); } resultSet.close(); statement.close(); } @Benchmark public void simpleCursoredR2dbc(ConnectionHolder connectionHolder, Blackhole voodoo) { io.r2dbc.spi.Statement statement = connectionHolder.r2dbc.createStatement("SELECT * FROM result_sizes /* cursored */"); String name = Flux.from(statement.execute()).flatMap(it -> it.map((row, rowMetadata) -> row.get("name", String.class))).blockLast(); voodoo.consume(name); } @Benchmark public void parametrizedDirectJdbc(ConnectionHolder connectionHolder, Blackhole voodoo) throws SQLException { PreparedStatement statement = connectionHolder.jdbc.prepareStatement("SELECT * FROM result_sizes WHERE name != ?"); statement.setString(1, "foo"); ResultSet resultSet = statement.executeQuery(); while (resultSet.next()) { voodoo.consume(resultSet.getString("name")); } resultSet.close(); statement.close(); } @Benchmark public void parametrizedDirectR2dbc(ConnectionHolder connectionHolder, Blackhole voodoo) throws SQLException { io.r2dbc.spi.Statement statement = connectionHolder.r2dbc.createStatement("SELECT * FROM result_sizes WHERE name != @P0").bind("P0", "foo"); ((MssqlStatement) statement).fetchSize(0); String name = Flux.from(statement.execute()).flatMap(it -> it.map((row, rowMetadata) -> row.get("name", String.class))).blockLast(); voodoo.consume(name); } @Benchmark public void preparedCursoredJdbc(ConnectionHolder connectionHolder, Blackhole voodoo) throws SQLException { PreparedStatement statement = connectionHolder.jdbc.prepareStatement("SELECT * FROM result_sizes WHERE name != ?", ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); statement.setFetchSize(128); statement.setString(1, "foo"); ResultSet resultSet = statement.executeQuery(); while (resultSet.next()) { voodoo.consume(resultSet.getString("name")); } resultSet.close(); statement.close(); } @Benchmark public void preparedCursoredR2dbc(ConnectionHolder connectionHolder, Blackhole voodoo) { io.r2dbc.spi.Statement statement = connectionHolder.r2dbc.createStatement("SELECT * FROM result_sizes WHERE name != @P0 /* cursored */").bind("P0", "foo"); String name = Flux.from(statement.execute()).flatMap(it -> it.map((row, rowMetadata) -> row.get("name", String.class))).blockLast(); voodoo.consume(name); } } ================================================ FILE: src/jmh/java/io/r2dbc/mssql/StatementBenchmarks.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.util.MsSqlServerExtension; import org.junit.platform.commons.annotation.Testable; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.OutputTimeUnit; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.infra.Blackhole; import reactor.core.publisher.Flux; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.concurrent.TimeUnit; /** * Benchmarks for Statement execution modes. Contains the following execution methods: * *
    *
  • SQLBATCH (Direct, Statements without parameters)
  • *
  • SP_EXECUTESQL (Direct, Statements with parameters)
  • *
  • SP_CURSOROPEN (Cursors, Statements without parameters)
  • *
  • SP_CURSORPREPEXEC (Cursors, Prepared statements)
  • *
* * @author Mark Paluch */ @BenchmarkMode(Mode.Throughput) @OutputTimeUnit(TimeUnit.SECONDS) public class StatementBenchmarks extends BenchmarkSettings { @State(Scope.Benchmark) public static class ConnectionHolder { final Connection jdbc; final MssqlConnection r2dbc; public ConnectionHolder() { try { MsSqlServerExtension extension = new MsSqlServerExtension(); extension.initialize(); this.jdbc = extension.getDataSource().getConnection(); Statement statement = this.jdbc.createStatement(); try { statement.execute("DROP TABLE simple_test"); } catch (SQLException e) { } statement.execute("CREATE TABLE simple_test (name VARCHAR(255))"); statement.execute("INSERT INTO simple_test VALUES('foo')"); statement.execute("INSERT INTO simple_test VALUES('bar')"); statement.execute("INSERT INTO simple_test VALUES('baz')"); MssqlConnectionConfiguration configuration = extension.configBuilder().preferCursoredExecution(sql -> sql.contains(" /* cursored */")).build(); this.r2dbc = new MssqlConnectionFactory(configuration).create().block(); } catch (SQLException e) { throw new RuntimeException(e); } } } @Benchmark @Testable public void simpleDirectJdbc(ConnectionHolder connectionHolder, Blackhole voodoo) throws SQLException { Statement statement = connectionHolder.jdbc.createStatement(); ResultSet resultSet = statement.executeQuery("SELECT * FROM simple_test"); while (resultSet.next()) { voodoo.consume(resultSet.getString("name")); } resultSet.close(); statement.close(); } @Benchmark @Testable public void simpleDirectR2dbc(ConnectionHolder connectionHolder, Blackhole voodoo) { MssqlStatement statement = connectionHolder.r2dbc.createStatement("SELECT * FROM simple_test"); statement.fetchSize(0); String name = Flux.from(statement.execute()).flatMap(it -> it.map((row, rowMetadata) -> row.get("name", String.class))).blockLast(); voodoo.consume(name); } @Benchmark @Testable public void simpleCursoredJdbc(ConnectionHolder connectionHolder, Blackhole voodoo) throws SQLException { Statement statement = connectionHolder.jdbc.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); ResultSet resultSet = statement.executeQuery("SELECT * FROM simple_test /* cursored */"); while (resultSet.next()) { voodoo.consume(resultSet.getString("name")); } resultSet.close(); statement.close(); } @Benchmark public void simpleCursoredR2dbc(ConnectionHolder connectionHolder, Blackhole voodoo) { io.r2dbc.spi.Statement statement = connectionHolder.r2dbc.createStatement("SELECT * FROM simple_test /* cursored */"); String name = Flux.from(statement.execute()).flatMap(it -> it.map((row, rowMetadata) -> row.get("name", String.class))).blockLast(); voodoo.consume(name); } @Benchmark public void parametrizedDirectJdbc(ConnectionHolder connectionHolder, Blackhole voodoo) throws SQLException { PreparedStatement statement = connectionHolder.jdbc.prepareStatement("SELECT * FROM simple_test WHERE name = ?"); statement.setString(1, "foo"); ResultSet resultSet = statement.executeQuery(); while (resultSet.next()) { voodoo.consume(resultSet.getString("name")); } resultSet.close(); statement.close(); } @Benchmark @Testable public void parametrizedDirectR2dbc(ConnectionHolder connectionHolder, Blackhole voodoo) { io.r2dbc.spi.Statement statement = connectionHolder.r2dbc.createStatement("SELECT * FROM simple_test WHERE name = @P0").bind("P0", "foo"); String name = Flux.from(statement.execute()).flatMap(it -> it.map((row, rowMetadata) -> row.get("name", String.class))).blockLast(); voodoo.consume(name); } @Benchmark public void preparedCursoredJdbc(ConnectionHolder connectionHolder, Blackhole voodoo) throws SQLException { PreparedStatement statement = connectionHolder.jdbc.prepareStatement("SELECT * FROM simple_test WHERE name = ?", ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); statement.setString(1, "foo"); ResultSet resultSet = statement.executeQuery(); while (resultSet.next()) { voodoo.consume(resultSet.getString("name")); } resultSet.close(); statement.close(); } @Benchmark public void preparedCursoredR2dbc(ConnectionHolder connectionHolder, Blackhole voodoo) { io.r2dbc.spi.Statement statement = connectionHolder.r2dbc.createStatement("SELECT * FROM simple_test WHERE name = @P0 /* cursored */").bind("P0", "foo"); String name = Flux.from(statement.execute()).flatMap(it -> it.map((row, rowMetadata) -> row.get("name", String.class))).blockLast(); voodoo.consume(name); } } ================================================ FILE: src/jmh/java/io/r2dbc/mssql/codec/BinaryCodecBenchmarks.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.token.Column; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.HexUtils; import org.junit.platform.commons.annotation.Testable; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; import java.nio.ByteBuffer; /** * Benchmarks for {@code BinaryCodec}. * * @author Mark Paluch */ @State(Scope.Thread) @Testable public class BinaryCodecBenchmarks extends CodecBenchmarkSupport { private static final DefaultCodecs codecs = new DefaultCodecs(); private static final Column binary = new Column(0, "", TypeInformation.builder().withLengthStrategy(LengthStrategy.USHORTLENTYPE).withServerType(SqlServerType.VARBINARY).build()); private final ByteBuf binaryBuffer = HexUtils.decodeToByteBuf("03 00 62 61 72"); private final byte[] toEncode = "foo".getBytes(); private final ByteBuffer bufferToEncode = ByteBuffer.wrap(this.toEncode); @Benchmark public Object decodeToByteArray() { this.binaryBuffer.readerIndex(0); return codecs.decode(this.binaryBuffer, binary, byte[].class); } @Benchmark public Object decodeToByteBuffer() { this.binaryBuffer.readerIndex(0); return codecs.decode(this.binaryBuffer, binary, ByteBuffer.class); } @Benchmark public Encoded encodeByteArray() { return doEncode(this.toEncode); } @Benchmark public Encoded encodeByteBuffer() { this.bufferToEncode.rewind(); return doEncode(this.bufferToEncode); } @Benchmark public Encoded encodeNull() { Encoded encoded = codecs.encodeNull(alloc, byte[].class); encoded.dispose(); return encoded; } private Encoded doEncode(Object value) { Encoded encoded = codecs.encode(alloc, RpcParameterContext.in(), value); encoded.dispose(); return encoded; } } ================================================ FILE: src/jmh/java/io/r2dbc/mssql/codec/BooleanCodecBenchmarks.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.token.Column; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.HexUtils; import org.junit.platform.commons.annotation.Testable; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; /** * Benchmarks for {@code BooleanCodec}. * * @author Mark Paluch */ @State(Scope.Thread) @Testable public class BooleanCodecBenchmarks extends CodecBenchmarkSupport { private static final DefaultCodecs codecs = new DefaultCodecs(); private static final Column column = new Column(0, "", TypeInformation.builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withServerType(SqlServerType.TINYINT).build()); private final ByteBuf buffer = HexUtils.decodeToByteBuf("01 01"); @Benchmark public Object decode() { this.buffer.readerIndex(0); return codecs.decode(this.buffer, column, Boolean.class); } @Benchmark public Encoded encode() { return doEncode(true); } @Benchmark public Encoded encodeNull() { Encoded encoded = codecs.encodeNull(alloc, Boolean.class); encoded.dispose(); return encoded; } private Encoded doEncode(Object value) { Encoded encoded = codecs.encode(alloc, RpcParameterContext.in(), value); encoded.dispose(); return encoded; } } ================================================ FILE: src/jmh/java/io/r2dbc/mssql/codec/ByteCodecBenchmarks.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.token.Column; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.HexUtils; import org.junit.platform.commons.annotation.Testable; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; /** * Benchmarks for {@code ByteCodec}. * * @author Mark Paluch */ @State(Scope.Thread) @Testable public class ByteCodecBenchmarks extends CodecBenchmarkSupport { private static final DefaultCodecs codecs = new DefaultCodecs(); private static final Column column = new Column(0, "", TypeInformation.builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withServerType(SqlServerType.TINYINT).build()); private final ByteBuf buffer = HexUtils.decodeToByteBuf("01 01"); @Benchmark public Object decode() { this.buffer.readerIndex(0); return codecs.decode(this.buffer, column, Byte.class); } @Benchmark public Encoded encode() { return doEncode(Byte.MIN_VALUE); } @Benchmark public Encoded encodeNull() { Encoded encoded = codecs.encodeNull(alloc, Byte.class); encoded.dispose(); return encoded; } private Encoded doEncode(Object value) { Encoded encoded = codecs.encode(alloc, RpcParameterContext.in(), value); encoded.dispose(); return encoded; } } ================================================ FILE: src/jmh/java/io/r2dbc/mssql/codec/CodecBenchmarkSupport.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.BenchmarkSettings; /** * Settings for codec benchmarks. * * @author Mark Paluch */ public abstract class CodecBenchmarkSupport extends BenchmarkSettings { static final ByteBufAllocator alloc = ByteBufAllocator.DEFAULT; } ================================================ FILE: src/jmh/java/io/r2dbc/mssql/codec/DecimalCodecBenchmarks.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.token.Column; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.HexUtils; import org.junit.platform.commons.annotation.Testable; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; import java.math.BigDecimal; /** * Benchmarks for {@code DecimalCodec}. * * @author Mark Paluch */ @State(Scope.Thread) @Testable public class DecimalCodecBenchmarks extends CodecBenchmarkSupport { private static final DefaultCodecs codecs = new DefaultCodecs(); private static final Column column = new Column(0, "", TypeInformation.builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withServerType(SqlServerType.NUMERIC).withScale(2).withPrecision(5).build()); private final ByteBuf buffer = HexUtils.decodeToByteBuf("08 08 01 00 00 00 00 00 00 01"); private final BigDecimal toEncode = new BigDecimal("36.89"); @Benchmark public Object decode() { this.buffer.readerIndex(0); return codecs.decode(this.buffer, column, BigDecimal.class); } @Benchmark public Encoded encode() { return doEncode(this.toEncode); } @Benchmark public Encoded encodeNull() { Encoded encoded = codecs.encodeNull(alloc, BigDecimal.class); encoded.dispose(); return encoded; } private Encoded doEncode(Object value) { Encoded encoded = codecs.encode(alloc, RpcParameterContext.in(), value); encoded.dispose(); return encoded; } } ================================================ FILE: src/jmh/java/io/r2dbc/mssql/codec/DoubleCodecBenchmarks.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.token.Column; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.HexUtils; import org.junit.platform.commons.annotation.Testable; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; /** * Benchmarks for {@code DoubleCodec}. * * @author Mark Paluch */ @State(Scope.Thread) @Testable public class DoubleCodecBenchmarks extends CodecBenchmarkSupport { private static final DefaultCodecs codecs = new DefaultCodecs(); private static final Column column = new Column(0, "", TypeInformation.builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withServerType(SqlServerType.FLOAT).withMaxLength(8).build()); private final ByteBuf doubleBuffer = HexUtils.decodeToByteBuf("08 FE D4 78 E9 46 28 C6 40"); private final ByteBuf floatBuffer = HexUtils.decodeToByteBuf("04 04 37 42 31 46"); @Benchmark public Object decodeDouble() { this.doubleBuffer.readerIndex(0); return codecs.decode(this.doubleBuffer, column, Double.class); } @Benchmark public Object decodeFloat() { this.floatBuffer.readerIndex(0); return codecs.decode(this.floatBuffer, column, Double.class); } @Benchmark public Encoded encode() { return doEncode(Double.MIN_VALUE); } @Benchmark public Encoded encodeNull() { Encoded encoded = codecs.encodeNull(alloc, Double.class); encoded.dispose(); return encoded; } private Encoded doEncode(Object value) { Encoded encoded = codecs.encode(alloc, RpcParameterContext.in(), value); encoded.dispose(); return encoded; } } ================================================ FILE: src/jmh/java/io/r2dbc/mssql/codec/IntegerCodecBenchmarks.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.token.Column; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.HexUtils; import org.junit.platform.commons.annotation.Testable; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; /** * Benchmarks for {@code IntegerCodec}. * * @author Mark Paluch */ @State(Scope.Thread) @Testable public class IntegerCodecBenchmarks extends CodecBenchmarkSupport { private static final DefaultCodecs codecs = new DefaultCodecs(); private static final Column column = new Column(0, "", TypeInformation.builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withServerType(SqlServerType.INTEGER).build()); private final ByteBuf buffer = HexUtils.decodeToByteBuf("04 04 01 00 00 01"); @Benchmark public Object decode() { this.buffer.readerIndex(0); return codecs.decode(this.buffer, column, Integer.class); } @Benchmark public Encoded encode() { return doEncode(Integer.MIN_VALUE); } @Benchmark public Encoded encodeNull() { Encoded encoded = codecs.encodeNull(alloc, Integer.class); encoded.dispose(); return encoded; } private Encoded doEncode(Object value) { Encoded encoded = codecs.encode(alloc, RpcParameterContext.in(), value); encoded.dispose(); return encoded; } } ================================================ FILE: src/jmh/java/io/r2dbc/mssql/codec/LocalDateCodecBenchmarks.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.token.Column; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.HexUtils; import org.junit.platform.commons.annotation.Testable; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; import java.time.LocalDate; /** * Benchmarks for {@code LocalDateCodec}. * * @author Mark Paluch */ @State(Scope.Thread) @Testable public class LocalDateCodecBenchmarks extends CodecBenchmarkSupport { private static final DefaultCodecs codecs = new DefaultCodecs(); private static final Column column = new Column(0, "", TypeInformation.builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withServerType(SqlServerType.DATE).build()); private final ByteBuf buffer = HexUtils.decodeToByteBuf("03 DD 3E 0B"); private final LocalDate toEncode = LocalDate.parse("2018-10-23"); @Benchmark public Object decode() { this.buffer.readerIndex(0); return codecs.decode(this.buffer, column, LocalDate.class); } @Benchmark public Encoded encode() { return doEncode(this.toEncode); } @Benchmark public Encoded encodeNull() { Encoded encoded = codecs.encodeNull(alloc, LocalDate.class); encoded.dispose(); return encoded; } private Encoded doEncode(Object value) { Encoded encoded = codecs.encode(alloc, RpcParameterContext.in(), value); encoded.dispose(); return encoded; } } ================================================ FILE: src/jmh/java/io/r2dbc/mssql/codec/LocalDateTimeCodecBenchmarks.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.token.Column; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.HexUtils; import org.junit.platform.commons.annotation.Testable; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; import java.time.LocalDateTime; /** * Benchmarks for {@code LocalDateTimeCodec}. * * @author Mark Paluch */ @State(Scope.Thread) @Testable public class LocalDateTimeCodecBenchmarks extends CodecBenchmarkSupport { private static final DefaultCodecs codecs = new DefaultCodecs(); private static final Column smallDateTimeColumn = new Column(0, "", TypeInformation.builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withServerType(SqlServerType.SMALLDATETIME).build()); private final ByteBuf smallDateTimeBuffer = HexUtils.decodeToByteBuf("04 F5 A8 3E 00"); private static final Column dateTimeColumn = new Column(0, "", TypeInformation.builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withServerType(SqlServerType.DATETIME).build()); private final ByteBuf dateTimeBuffer = HexUtils.decodeToByteBuf("08 86 A9 00 00 AA 70 02 01"); private static final Column dateTime2Column = new Column(0, "", TypeInformation.builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withServerType(SqlServerType.DATETIME2).withScale(7).build()); private final ByteBuf dateTime2Buffer = HexUtils.decodeToByteBuf("082006E17483E13E0B"); private final LocalDateTime toEncode = LocalDateTime.parse("2018-06-04T01:02"); @Benchmark public Object decodeSmallDateTime() { this.smallDateTimeBuffer.readerIndex(0); return codecs.decode(this.smallDateTimeBuffer, smallDateTimeColumn, LocalDateTime.class); } @Benchmark public Object decodeDateTime() { this.dateTimeBuffer.readerIndex(0); return codecs.decode(this.dateTimeBuffer, dateTimeColumn, LocalDateTime.class); } @Benchmark public Object decodeDateTime2() { this.dateTime2Buffer.readerIndex(0); return codecs.decode(this.dateTime2Buffer, dateTime2Column, LocalDateTime.class); } @Benchmark public Encoded encode() { return doEncode(this.toEncode); } @Benchmark public Encoded encodeNull() { Encoded encoded = codecs.encodeNull(alloc, LocalDateTime.class); encoded.dispose(); return encoded; } private Encoded doEncode(Object value) { Encoded encoded = codecs.encode(alloc, RpcParameterContext.in(), value); encoded.dispose(); return encoded; } } ================================================ FILE: src/jmh/java/io/r2dbc/mssql/codec/LocalTimeCodecBenchmarks.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.token.Column; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.HexUtils; import org.junit.platform.commons.annotation.Testable; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; import java.time.LocalTime; /** * Benchmarks for {@code LocalTimeCodec}. * * @author Mark Paluch */ @State(Scope.Thread) @Testable public class LocalTimeCodecBenchmarks extends CodecBenchmarkSupport { private static final DefaultCodecs codecs = new DefaultCodecs(); private static final Column column = new Column(0, "", TypeInformation.builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withServerType(SqlServerType.TIME).withScale(7).build()); private final ByteBuf buffer = HexUtils.decodeToByteBuf("05 C0 C9 B1 61 5D"); private final LocalTime toEncode = LocalTime.parse("18:13:14"); @Benchmark public Object decode() { this.buffer.readerIndex(0); return codecs.decode(this.buffer, column, LocalTime.class); } @Benchmark public Encoded encode() { return doEncode(this.toEncode); } @Benchmark public Encoded encodeNull() { Encoded encoded = codecs.encodeNull(alloc, LocalTime.class); encoded.dispose(); return encoded; } private Encoded doEncode(Object value) { Encoded encoded = codecs.encode(alloc, RpcParameterContext.in(), value); encoded.dispose(); return encoded; } } ================================================ FILE: src/jmh/java/io/r2dbc/mssql/codec/LongCodecBenchmarks.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.token.Column; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.HexUtils; import org.junit.platform.commons.annotation.Testable; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; /** * Benchmarks for {@code LongCodec}. * * @author Mark Paluch */ @State(Scope.Thread) @Testable public class LongCodecBenchmarks extends CodecBenchmarkSupport { private static final DefaultCodecs codecs = new DefaultCodecs(); private static final Column column = new Column(0, "", TypeInformation.builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withServerType(SqlServerType.BIGINT).build()); private final ByteBuf buffer = HexUtils.decodeToByteBuf("08 08 01 00 00 00 00 00 00 01"); @Benchmark public Object decode() { this.buffer.readerIndex(0); return codecs.decode(this.buffer, column, Long.class); } @Benchmark public Encoded encode() { return doEncode(Long.MIN_VALUE); } @Benchmark public Encoded encodeNull() { Encoded encoded = codecs.encodeNull(alloc, Long.class); encoded.dispose(); return encoded; } private Encoded doEncode(Object value) { Encoded encoded = codecs.encode(alloc, RpcParameterContext.in(), value); encoded.dispose(); return encoded; } } ================================================ FILE: src/jmh/java/io/r2dbc/mssql/codec/MoneyCodecBenchmarks.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.token.Column; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.HexUtils; import org.junit.platform.commons.annotation.Testable; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; import java.math.BigDecimal; /** * Benchmarks for {@code MoneyCodec}. * * @author Mark Paluch */ @State(Scope.Thread) @Testable public class MoneyCodecBenchmarks extends CodecBenchmarkSupport { private static final DefaultCodecs codecs = new DefaultCodecs(); private static final Column column = new Column(0, "", TypeInformation.builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withServerType(SqlServerType.MONEY).withMaxLength(8).build()); private final ByteBuf buffer = HexUtils.decodeToByteBuf("08 08 11 00 00 00 20 a1 07 00"); private final BigDecimal toEncode = new BigDecimal("7301494.4032"); @Benchmark public Object decode() { this.buffer.readerIndex(0); return codecs.decode(this.buffer, column, BigDecimal.class); } } ================================================ FILE: src/jmh/java/io/r2dbc/mssql/codec/ShortCodecBenchmarks.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.token.Column; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.HexUtils; import org.junit.platform.commons.annotation.Testable; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; /** * Benchmarks for {@code ShortCodec}. * * @author Mark Paluch */ @State(Scope.Thread) @Testable public class ShortCodecBenchmarks extends CodecBenchmarkSupport { private static final DefaultCodecs codecs = new DefaultCodecs(); private static final Column column = new Column(0, "", TypeInformation.builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withServerType(SqlServerType.SMALLINT).build()); private final ByteBuf buffer = HexUtils.decodeToByteBuf("02 01 01"); @Benchmark public Object decode() { this.buffer.readerIndex(0); return codecs.decode(this.buffer, column, Short.class); } @Benchmark public Encoded encode() { return doEncode(Short.MIN_VALUE); } @Benchmark public Encoded encodeNull() { Encoded encoded = codecs.encodeNull(alloc, Short.class); encoded.dispose(); return encoded; } private Encoded doEncode(Object value) { Encoded encoded = codecs.encode(alloc, RpcParameterContext.in(), value); encoded.dispose(); return encoded; } } ================================================ FILE: src/jmh/java/io/r2dbc/mssql/codec/StringCodecBenchmarks.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.codec.RpcParameterContext.ValueContext; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.message.tds.ServerCharset; import io.r2dbc.mssql.message.token.Column; import io.r2dbc.mssql.message.type.Collation; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.TestByteBufAllocator; import org.junit.platform.commons.annotation.Testable; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; /** * Benchmarks for {@code StringCodec}. * * @author Mark Paluch */ @State(Scope.Thread) @Testable public class StringCodecBenchmarks extends CodecBenchmarkSupport { private static final DefaultCodecs codecs = new DefaultCodecs(); private static final Collation cp1252 = Collation.from(13632521, 52); private static final Column varchar = new Column(0, "", TypeInformation.builder().withCharset(ServerCharset.CP1252.charset()).withLengthStrategy(LengthStrategy.USHORTLENTYPE).withServerType(SqlServerType.VARCHAR).build()); private final ByteBuf varcharBuffer; private static final Collation unicode = Collation.from(0x0445, 0); private static final Column nvarchar = new Column(0, "", TypeInformation.builder().withCharset(ServerCharset.UNICODE.charset()).withLengthStrategy(LengthStrategy.USHORTLENTYPE).withServerType(SqlServerType.NVARCHAR).build()); private final ByteBuf nvarcharBuffer; private static final Column text = new Column(0, "", TypeInformation.builder().withMaxLength(2147483647).withLengthStrategy(LengthStrategy.LONGLENTYPE).withServerType(SqlServerType.TEXT).withCharset(ServerCharset.CP1252.charset()).build()); private final ByteBuf textBuffer; private static final Column uuid = new Column(0, "", TypeInformation.builder().withMaxLength(16).withLengthStrategy(LengthStrategy.FIXEDLENTYPE).withServerType(SqlServerType.GUID).build()); private final ByteBuf uuidBuffer; public StringCodecBenchmarks() { this.varcharBuffer = alloc.buffer(); Encode.uShort(this.varcharBuffer, 6); this.varcharBuffer.writeCharSequence("foobar", ServerCharset.CP1252.charset()); this.nvarcharBuffer = alloc.buffer(); Encode.uShort(this.nvarcharBuffer, 104); this.nvarcharBuffer.writeCharSequence("Γαζέες καὶ μυρτιὲς δὲν θὰ βρῶ πιὰ στὸ χρυσαφὶ ξέφωτο", ServerCharset.UNICODE.charset()); this.textBuffer = HexUtils.decodeToByteBuf("10 64" + "75 6D 6D 79 20 74 65 78 74 70 74 72 00 00 00 64" + "75 6D 6D 79 54 53 00 0B 00 00 00 6D 79 74 65 78" + "74 76 61 6C 75 65"); this.uuidBuffer = HexUtils.decodeToByteBuf("F17B0DC7C7E5C54098C7A12F7E686724FD"); } @Benchmark public String decodeVarchar() { this.varcharBuffer.readerIndex(0); return codecs.decode(this.varcharBuffer, varchar, String.class); } @Benchmark public Encoded encodeVarchar() { return doEncode(cp1252, "foobar"); } @Benchmark public String decodeNVarchar() { this.nvarcharBuffer.readerIndex(0); return codecs.decode(this.nvarcharBuffer, nvarchar, String.class); } @Benchmark public Encoded encodeNvarchar() { return doEncode(unicode, "Γαζέες καὶ μυρτιὲς δὲν θὰ βρῶ πιὰ στὸ χρυσαφὶ ξέφωτο"); } @Benchmark public String decodeText() { this.textBuffer.readerIndex(0); return codecs.decode(this.textBuffer, text, String.class); } @Benchmark public String decodeUuid() { this.uuidBuffer.readerIndex(0); return codecs.decode(this.uuidBuffer, uuid, String.class); } @Benchmark public Encoded encodeNull() { Encoded encoded = codecs.encodeNull(TestByteBufAllocator.TEST, String.class); encoded.dispose(); return encoded; } private Encoded doEncode(Collation collation, Object value) { Encoded encoded = codecs.encode(TestByteBufAllocator.TEST, RpcParameterContext.in(ValueContext.character(collation, true)), value); encoded.dispose(); return encoded; } } ================================================ FILE: src/jmh/java/io/r2dbc/mssql/codec/UuidCodecBenchmarks.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.token.Column; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.HexUtils; import org.junit.platform.commons.annotation.Testable; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.State; import java.util.UUID; /** * Benchmarks for {@code UuidCodec}. * * @author Mark Paluch */ @State(Scope.Thread) @Testable public class UuidCodecBenchmarks extends CodecBenchmarkSupport { private static final DefaultCodecs codecs = new DefaultCodecs(); private static final Column column = new Column(0, "", TypeInformation.builder().withMaxLength(16).withLengthStrategy(LengthStrategy.FIXEDLENTYPE).withPrecision(16).withServerType(SqlServerType.GUID).build()); private final ByteBuf buffer = HexUtils.decodeToByteBuf("F17B0DC7C7E5C54098C7A12F7E686724"); private final UUID toEncode = UUID.fromString("C70D7BF1-E5C7-40C5-98C7-A12F7E686724"); @Benchmark public Object decode() { this.buffer.readerIndex(0); return codecs.decode(this.buffer, column, UUID.class); } @Benchmark public Encoded encode() { return doEncode(this.toEncode); } @Benchmark public Encoded encodeNull() { Encoded encoded = codecs.encodeNull(alloc, UUID.class); encoded.dispose(); return encoded; } private Encoded doEncode(Object value) { Encoded encoded = codecs.encode(alloc, RpcParameterContext.in(), value); encoded.dispose(); return encoded; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/AbstractMssqlException.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.spi.R2dbcException; import reactor.util.annotation.Nullable; /** * Microsoft SQL Server-specific exception class. * * @author Mark Paluch */ public class AbstractMssqlException extends R2dbcException { public static final int DRIVER_ERROR_NONE = 0; public static final int DRIVER_ERROR_FROM_DATABASE = 2; public static final int DRIVER_ERROR_IO_FAILED = 3; public static final int DRIVER_ERROR_INVALID_TDS = 4; public static final int DRIVER_ERROR_SSL_FAILED = 5; public static final int DRIVER_ERROR_UNSUPPORTED_CONFIG = 6; public static final int DRIVER_ERROR_INTERMITTENT_TLS_FAILED = 7; public static final int ERROR_SOCKET_TIMEOUT = 8; public static final int ERROR_QUERY_TIMEOUT = 9; /** * Creates a new exception. */ public AbstractMssqlException() { this((String) null); } /** * Creates a new exception. * * @param reason the reason for the error. Set as the exception's message and retrieved with {@link #getMessage()}. */ public AbstractMssqlException(@reactor.util.annotation.Nullable String reason) { this(reason, (String) null); } /** * Creates a new exception. * * @param reason the reason for the error. Set as the exception's message and retrieved with {@link #getMessage()}. * @param sqlState the "SQLstate" string, which follows either the XOPEN SQLstate conventions or the SQL:2003 conventions */ public AbstractMssqlException(@Nullable String reason, @Nullable String sqlState) { this(reason, sqlState, 0); } /** * Creates a new exception. * * @param reason the reason for the error. Set as the exception's message and retrieved with {@link #getMessage()}. * @param sqlState the "SQLstate" string, which follows either the XOPEN SQLstate conventions or the SQL:2003 conventions * @param errorCode a vendor-specific error code representing this failure */ public AbstractMssqlException(@Nullable String reason, @Nullable String sqlState, int errorCode) { this(reason, sqlState, errorCode, null); } /** * Creates a new exception. * * @param reason the reason for the error. Set as the exception's message and retrieved with {@link #getMessage()}. * @param sqlState the "SQLstate" string, which follows either the XOPEN SQLstate conventions or the SQL:2003 conventions * @param errorCode a vendor-specific error code representing this failure * @param cause the cause */ public AbstractMssqlException(@Nullable String reason, @Nullable String sqlState, int errorCode, @Nullable Throwable cause) { super(reason, sqlState, errorCode, cause); } /** * Creates a new exception. * * @param reason the reason for the error. Set as the exception's message and retrieved with {@link #getMessage()}. * @param sqlState the "SQLstate" string, which follows either the XOPEN SQLstate conventions or the SQL:2003 conventions * @param cause the cause */ public AbstractMssqlException(@Nullable String reason, @Nullable String sqlState, @Nullable Throwable cause) { this(reason, sqlState, 0, cause); } /** * Creates a new exception. * * @param reason the reason for the error. Set as the exception's message and retrieved with {@link #getMessage()}. * @param cause the cause */ public AbstractMssqlException(@Nullable String reason, @Nullable Throwable cause) { this(reason, null, cause); } /** * Creates a new exception. * * @param cause the cause */ public AbstractMssqlException(@Nullable Throwable cause) { this(null, cause); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/Binding.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.codec.Encoded; import io.r2dbc.mssql.codec.RpcDirection; import io.r2dbc.mssql.util.Assert; import reactor.util.annotation.Nullable; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.BiConsumer; /** * A collection of {@link Encoded encoded parameters} for a single bind invocation of a prepared statement. * Bindings for Microsoft SQL Server are name-based and names are handled without the prefixing at-sign. The at sign is added during encoding. * * @author Mark Paluch */ class Binding { private final Map parameters = new LinkedHashMap<>(); private boolean hasOutParameters = false; @Nullable private volatile String formalRepresentation; /** * Add a {@link Encoded encoded parameter} to the binding. * * @param name the name of the {@link Encoded encoded parameter} * @param direction the direction of the encoded parameter * @param parameter the {@link Encoded encoded parameter} * @return this {@link Binding} */ public Binding add(String name, RpcDirection direction, Encoded parameter) { Assert.requireNonNull(name, "Name must not be null"); Assert.requireNonNull(direction, "RpcDirection must not be null"); Assert.requireNonNull(parameter, "Parameter must not be null"); this.formalRepresentation = null; this.parameters.put(name, new RpcParameter(direction, parameter)); if (direction == RpcDirection.OUT) { this.hasOutParameters = true; } return this; } /** * Returns parameter names of the return values. * * @return */ boolean hasOutParameters() { return this.hasOutParameters; } /** * Clear/release binding values. */ void clear() { this.parameters.forEach((s, parameter) -> { if (!parameter.encoded.isDisposed()) { parameter.encoded.dispose(); } }); this.parameters.clear(); } /** * Returns a formal representation of the bound parameters such as {@literal @P0 VARCHAR(8000), @P1 DECIMAL(12,6)} * * @return a formal representation of the bound parameters. */ public String getFormalParameters() { String formalRepresentation = this.formalRepresentation; if (formalRepresentation != null) { return formalRepresentation; } StringBuilder builder = new StringBuilder(this.parameters.size() * 16); Set> entries = this.parameters.entrySet(); for (Map.Entry entry : entries) { if (builder.length() != 0) { builder.append(','); } builder.append('@').append(entry.getKey()).append(' ').append(entry.getValue().encoded.getFormalType()); if (entry.getValue().rpcDirection == RpcDirection.OUT) { builder.append(" OUTPUT"); } } formalRepresentation = builder.toString(); this.formalRepresentation = formalRepresentation; return formalRepresentation; } /** * Performs the given action for each entry in this binding until all bound parameters * have been processed or the action throws an exception. Unless * otherwise specified by the implementing class, actions are performed in * the order of entry set iteration (if an iteration order is specified.) * * @param action The action to be performed for each bound parameter. */ public void forEach(BiConsumer action) { Assert.requireNonNull(action, "Action must not be null"); this.parameters.forEach(action); } Map getParameters() { return this.parameters; } /** * Returns whether this {@link Binding} is empty (i.e. no parameters bound). * * @return {@code true} if no parameters were bound. */ public boolean isEmpty() { return this.parameters.isEmpty(); } /** * Returns the number of bound parameters. * * @return the number of bound parameters. */ int size() { return this.parameters.size(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Binding that = (Binding) o; return Objects.equals(this.parameters, that.parameters); } @Override public int hashCode() { return Objects.hash(this.parameters); } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [parameters=").append(this.parameters); sb.append(']'); return sb.toString(); } public static class RpcParameter { final RpcDirection rpcDirection; final Encoded encoded; public RpcParameter(RpcDirection rpcDirection, Encoded encoded) { this.rpcDirection = rpcDirection; this.encoded = encoded; } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/ConnectionOptions.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.codec.Codecs; import reactor.util.annotation.Nullable; import java.time.Duration; import java.util.function.Predicate; /** * @author Mark Paluch */ class ConnectionOptions { private final Predicate preferCursoredExecution; private final Codecs codecs; private final PreparedStatementCache preparedStatementCache; private final boolean sendStringParametersAsUnicode; private volatile Duration statementTimeout = Duration.ZERO; ConnectionOptions(Predicate preferCursoredExecution, Codecs codecs, PreparedStatementCache preparedStatementCache, boolean sendStringParametersAsUnicode) { this.preferCursoredExecution = preferCursoredExecution; this.codecs = codecs; this.preparedStatementCache = preparedStatementCache; this.sendStringParametersAsUnicode = sendStringParametersAsUnicode; } public Codecs getCodecs() { return this.codecs; } public PreparedStatementCache getPreparedStatementCache() { return this.preparedStatementCache; } public boolean prefersCursors(String sql) { return this.preferCursoredExecution.test(sql); } public boolean isSendStringParametersAsUnicode() { return this.sendStringParametersAsUnicode; } public Duration getStatementTimeout() { return this.statementTimeout; } public void setStatementTimeout(@Nullable Duration statementTimeout) { this.statementTimeout = statementTimeout; } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [preferCursoredExecution=").append(this.preferCursoredExecution); sb.append(", codecs=").append(this.codecs); sb.append(", preparedStatementCache=").append(this.preparedStatementCache); sb.append(", sendStringParametersAsUnicode=").append(this.sendStringParametersAsUnicode); sb.append(", statementTimeout=").append(this.statementTimeout); sb.append(']'); return sb.toString(); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/DefaultMssqlResult.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.netty.util.ReferenceCountUtil; import io.r2dbc.mssql.client.ConnectionContext; import io.r2dbc.mssql.codec.Codecs; import io.r2dbc.mssql.message.token.AbstractDoneToken; import io.r2dbc.mssql.message.token.ColumnMetadataToken; import io.r2dbc.mssql.message.token.ErrorToken; import io.r2dbc.mssql.message.token.NbcRowToken; import io.r2dbc.mssql.message.token.ReturnValue; import io.r2dbc.mssql.message.token.RowToken; import io.r2dbc.mssql.util.Assert; import io.r2dbc.spi.Readable; import io.r2dbc.spi.Result; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.Logger; import reactor.util.Loggers; import java.util.ArrayList; import java.util.List; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; /** * {@link Result} of query results. * * @author Mark Paluch */ final class DefaultMssqlResult implements MssqlResult { private static final Logger LOGGER = Loggers.getLogger(DefaultMssqlResult.class); public static final boolean DEBUG_ENABLED = LOGGER.isDebugEnabled(); private final String sql; private final ConnectionContext context; private final Codecs codecs; private final Flux messages; private final boolean expectReturnValues; private volatile MssqlRowMetadata rowMetadata; private DefaultMssqlResult(String sql, ConnectionContext context, Codecs codecs, Flux messages, boolean expectReturnValues) { this.sql = sql; this.context = context; this.codecs = codecs; this.messages = messages; this.expectReturnValues = expectReturnValues; } /** * Create a {@link DefaultMssqlResult}. * * @param sql the underlying SQL statement. * @param codecs the codecs to use. * @param messages message stream. * @param expectReturnValues {@code true} if the result is expected to have result values. * @return {@link Result} object. */ static MssqlResult toResult(String sql, ConnectionContext context, Codecs codecs, Flux messages, boolean expectReturnValues) { Assert.requireNonNull(sql, "SQL must not be null"); Assert.requireNonNull(codecs, "Codecs must not be null"); Assert.requireNonNull(context, "ConnectionContext must not be null"); Assert.requireNonNull(messages, "Messages must not be null"); LOGGER.debug(context.getMessage("Creating new result")); return new DefaultMssqlResult(sql, context, codecs, messages, expectReturnValues); } @Override public Mono getRowsUpdated() { return this.messages .handle((message, sink) -> { if (message instanceof AbstractDoneToken) { AbstractDoneToken doneToken = (AbstractDoneToken) message; if (doneToken.hasCount()) { if (DEBUG_ENABLED) { LOGGER.debug(this.context.getMessage("Incoming row count: {}"), doneToken); } sink.next(doneToken.getRowCount()); } if (doneToken.isAttentionAck()) { sink.error(new ExceptionFactory.MssqlStatementCancelled(this.sql)); return; } } if (message instanceof ErrorToken) { sink.error(ExceptionFactory.createException((ErrorToken) message, this.sql)); return; } ReferenceCountUtil.release(message); }).reduce(Long::sum); } @Override public Flux map(BiFunction mappingFunction) { Assert.requireNonNull(mappingFunction, "Mapping function must not be null"); return doMap(true, false, readable -> { Row row = (Row) readable; return mappingFunction.apply(row, row.getMetadata()); }); } @Override public Publisher map(Function mappingFunction) { Assert.requireNonNull(mappingFunction, "Mapping function must not be null"); return doMap(true, true, mappingFunction); } private Flux doMap(boolean rows, boolean outparameters, Function mappingFunction) { Flux mappedReturnValues = Flux.empty(); Flux messages = this.messages; if (this.expectReturnValues && outparameters) { List returnValues = new ArrayList<>(); messages = messages.doOnNext(message -> { if (message instanceof ReturnValue) { returnValues.add((ReturnValue) message); } }).filter(it -> !(it instanceof ReturnValue)); mappedReturnValues = Flux.defer(() -> { if (returnValues.size() != 0) { MssqlReturnValues mssqlReturnValues = MssqlReturnValues.toReturnValues(this.codecs, returnValues); try { return Flux.just(mappingFunction.apply(mssqlReturnValues)); } finally { mssqlReturnValues.release(); } } return Flux.empty(); }); } Flux mapped = messages .handle((message, sink) -> { if (message instanceof AbstractDoneToken) { AbstractDoneToken doneToken = (AbstractDoneToken) message; if (doneToken.isAttentionAck()) { sink.error(new ExceptionFactory.MssqlStatementCancelled(this.sql)); return; } } if (message.getClass() == ColumnMetadataToken.class) { ColumnMetadataToken token = (ColumnMetadataToken) message; if (!token.hasColumns()) { return; } if (DEBUG_ENABLED) { LOGGER.debug(this.context.getMessage("Result column definition: {}"), message); } this.rowMetadata = MssqlRowMetadata.create(this.codecs, token); return; } if (rows && (message.getClass() == RowToken.class || message.getClass() == NbcRowToken.class)) { MssqlRowMetadata rowMetadata = this.rowMetadata; if (rowMetadata == null) { sink.error(new IllegalStateException("No MssqlRowMetadata available")); return; } MssqlRow row = MssqlRow.toRow(this.codecs, (RowToken) message, rowMetadata); try { sink.next(mappingFunction.apply(row)); } finally { row.release(); } return; } if (message instanceof ErrorToken) { sink.error(ExceptionFactory.createException((ErrorToken) message, this.sql)); return; } if (this.expectReturnValues && message instanceof ReturnValue) { return; } ReferenceCountUtil.release(message); }); if (this.expectReturnValues) { mapped = mapped.concatWith(mappedReturnValues); } return mapped; } @Override public MssqlResult filter(Predicate filter) { return MssqlSegmentResult.toResult(this.sql, this.context, this.codecs, this.messages, this.expectReturnValues).filter(filter); } @Override public Flux flatMap(Function> mappingFunction) { return MssqlSegmentResult.toResult(this.sql, this.context, this.codecs, this.messages, this.expectReturnValues).flatMap(mappingFunction); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/ErrorDetails.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.message.token.ErrorToken; import io.r2dbc.mssql.message.token.InfoToken; import io.r2dbc.mssql.util.StringUtils; import reactor.util.annotation.Nullable; /** * Details for an Error. * * @author Mark Paluch * @see ErrorToken * @see InfoToken */ public final class ErrorDetails { /** * Error message. */ private final String message; /** * Info number. */ private final long number; /** * The error state, used as a modifier to the info Number. */ private final int state; /** * The class (severity) of the error. A class of less than 10 indicates an informational message. */ private final int infoClass; /** * The server name length and server name using B_VARCHAR format. */ @Nullable private final String serverName; /** * The stored procedure name length and stored procedure name using B_VARCHAR format. */ @Nullable private final String procName; /** * The line number in the SQL batch or stored procedure that caused the error. * Line numbers begin at 1; therefore, if the line number is not applicable to the message as determined by the upper * layer, the value of LineNumber will be 0. */ private final long lineNumber; /** * Create {@link ErrorDetails}. * * @param message the exception message. * @param number the error number. * @param state SQL state. * @param infoClass message classification. * @param serverName name of the server. * @param procName procedure name. * @param lineNumber line number in the offending SQL. */ public ErrorDetails(String message, long number, int state, int infoClass, String serverName, String procName, long lineNumber) { this.message = message; this.number = number; this.state = state; this.infoClass = infoClass; this.serverName = StringUtils.hasText(serverName) ? serverName : null; this.procName = procName; this.lineNumber = lineNumber; } /** * Returns the error message. * * @return the error message. */ public String getMessage() { return this.message; } /** * Returns the message number. * * @return the message number. */ public long getNumber() { return this.number; } /** * The error state, used as a modifier to the message number. * * @return the error state. */ public int getState() { return this.state; } /** * The error state code. * * @return the error state. */ public String getStateCode() { return getStateCode((int) getNumber(), getState()); } /** * Returns the severity class of this {@link ErrorDetails}. * * @return severity class of this {@link ErrorDetails}. */ public int getInfoClass() { return this.infoClass; } /** * Returns the server name. * * @return the server name. */ @Nullable public String getServerName() { return this.serverName; } /** * Returns the procedure name. * * @return the procedure name. */ @Nullable public String getProcName() { return this.procName; } /** * The line number in the SQL batch or stored procedure that caused the error. Line numbers begin at 1; therefore, if the line number is not applicable to the message as determined by the upper * layer, the value of LineNumber will be 0. * * @return the line number in the SQL batch. */ public long getLineNumber() { return this.lineNumber; } private String getStateCode(int errNum, int databaseState) { switch (errNum) { // case 18456: return "08001"; //username password wrong at login case 8152: return "22001"; // String data right truncation case 515: // 2.2705 case 547: return "23000"; // Integrity constraint violation case 2601: return "23000"; // Integrity constraint violation case 2714: return "S0001"; // table already exists case 208: return "S0002"; // table not found case 1205: return "40001"; // deadlock detected case 2627: return "23000"; // DPM 4.04. Primary key violation } return "S000" + databaseState; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/EscapeAwareNameMatcher.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import reactor.util.annotation.Nullable; import java.util.Collection; /** * Matcher utility for column name ({@code sysname}) comparison. Uses case-insensitive comparison by default. * Supports name escaping with square brackets ({@code [sysname]}) to enforce case-sensitive comparison rules. * * @author Mark Paluch */ final class EscapeAwareNameMatcher { @Nullable public static String find(String name, Collection names) { for (String s : names) { if (matches(name, s)) { return s; } } return null; } private static boolean matches(String o1, String o2) { boolean exactMatch = false; if (o1.startsWith("[") && o1.endsWith("]")) { exactMatch = true; o1 = o1.substring(1, o1.length() - 1); } if (o2.startsWith("[") && o2.endsWith("]")) { exactMatch = true; o2 = o2.substring(1, o2.length() - 1); } return exactMatch ? o1.equals(o2) : o1.equalsIgnoreCase(o2); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/ExceptionFactory.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.message.Message; import io.r2dbc.mssql.message.tds.ProtocolException; import io.r2dbc.mssql.message.token.AbstractInfoToken; import io.r2dbc.mssql.message.token.ErrorToken; import io.r2dbc.mssql.message.token.InfoToken; import io.r2dbc.spi.R2dbcBadGrammarException; import io.r2dbc.spi.R2dbcDataIntegrityViolationException; import io.r2dbc.spi.R2dbcException; import io.r2dbc.spi.R2dbcNonTransientException; import io.r2dbc.spi.R2dbcNonTransientResourceException; import io.r2dbc.spi.R2dbcPermissionDeniedException; import io.r2dbc.spi.R2dbcRollbackException; import io.r2dbc.spi.R2dbcTimeoutException; import io.r2dbc.spi.R2dbcTransientException; import io.r2dbc.spi.R2dbcTransientResourceException; import reactor.core.publisher.SynchronousSink; import static io.r2dbc.mssql.message.token.AbstractInfoToken.Classification.GENERAL_ERROR; /** * Factory for SQL Server-specific {@link R2dbcException}s. * * @author Mark Paluch */ final class ExceptionFactory { private final String sql; private ExceptionFactory(String sql) { this.sql = sql; } /** * Creates a {@link ExceptionFactory} associated with a SQL query. * * @param sql * @return */ static ExceptionFactory withSql(String sql) { return new ExceptionFactory(sql); } /** * Create a {@link R2dbcException} from {@link InfoToken}. * * @param token * @return */ R2dbcException createException(AbstractInfoToken token) { return createException(token, this.sql); } /** * Creates a {@link R2dbcException} from an {@link AbstractInfoToken}. * * @param token the token that contains the error details. * @param sql underlying SQL. * @return the {@link R2dbcException}. * @see ErrorToken */ static R2dbcException createException(AbstractInfoToken token, String sql) { switch ((int) token.getNumber()) { case 106: // Too many table names in the query. The maximum allowable is %d. case 130: // Cannot perform an aggregate function on an expression containing an aggregate or a subquery. case 206: // Operand type clash: %ls is incompatible with %ls. case 207: // Invalid column name '%.*ls'. case 208: // Invalid object name '%.*ls'. case 209: // Ambiguous column name '%.*ls'. case 213: // Column name or number of supplied values does not match table definition. case 267: // Object '%.*ls' cannot be found. case 565: // A stack overflow occurred in the server while compiling the query. Please simplify the query. case 4408: // Too many tables. The query and the views or functions in it exceed the limit of %d tables. Revise the query to reduce the number of tables. case 2812: // Could not find stored procedure '%.*ls'. return new MssqlBadGrammarException(createErrorDetails(token), sql); case 2601: // Cannot insert duplicate key row in object '%.*ls' with unique index '%.*ls'. The duplicate key value is %ls. case 2627: // Violation of %ls constraint '%.*ls'. Cannot insert duplicate key in object '%.*ls'. The duplicate key value is %ls. case 544: // Cannot insert explicit value for identity column in table '%.*ls' when IDENTITY_INSERT is set to OFF. case 8114: // Error converting data type %ls to %ls. case 8115: // Arithmetic overflow error converting %ls to data type %ls. return new MssqlDataIntegrityViolationException(createErrorDetails(token), sql); case 1222: // Lock request time out period exceeded. return new MssqlTimeoutException(createErrorDetails(token), sql); case 701: // Maximum number of databases used for each query has been exceeded. The maximum allowed is %d. case 1204: // The instance of the SQL Server Database Engine cannot obtain a LOCK resource at this time. Rerun your statement when there are fewer active users. Ask the database // administrator to check the lock and memory configuration for this instance, or to check for return new MssqlTransientException(createErrorDetails(token), sql); case 1203: // Process ID %d attempted to unlock a resource it does not own: %.*ls. Retry the transaction, because this error may be caused by a timing condition. If the problem // persists, contact the database administrator. case 1215: // A conflicting ABORT_AFTER_WAIT = BLOCKERS request is waiting for existing transactions to rollback. This request cannot be executed. Please retry when the previous // request is completed. case 1216: // The DDL statement with ABORT_AFTER_WAIT = BLOCKERS option cannot be completed due to a conflicting system task. The request can abort only user transactions. Please wait // for the system task to complete and retry. case 1221: // The Database Engine is attempting to release a group of locks that are not currently held by the transaction. Retry the transaction. If the problem persists, contact your // support provider. case 1206: // The Microsoft Distributed Transaction Coordinator (MS DTC) has cancelled the distributed transaction. case 3938: // The transaction has been stopped because it conflicted with the execution of a FILESTREAM close operation using the same transaction. The transaction will be rolled back. case 28611: // The request is aborted because the transaction has been aborted by Matrix Transaction Coordination Manager. This is mostly caused by one or more transaction particpant // brick went offline. return new MssqlRollbackException(createErrorDetails(token), sql); case 921: // Database '%.*ls' has not been recovered yet. Wait and try again. case 941: // Database '%.*ls' cannot be opened because it is not started. Retry when the database is started. case 1105: // Could not allocate space for object '%.*ls'%.*ls in database '%.*ls' because the '%.*ls' filegroup is full. Create disk space by deleting unneeded files, dropping objects // in the filegroup, adding additional files to the filegroup, or setting autogrowth on case 1456: // The ALTER DATABASE command could not be sent to the remote server instance '%.*ls'. The database mirroring configuration was not changed. Verify that the server is // connected, and try again. case 5061: // ALTER DATABASE failed because a lock could not be placed on database '%.*ls'. Try again later. case 10930: // The service is currently too busy. Please try again later. case 45168: // The server '%.*ls' has too many active connections. Try again later. case 40642: // The server is currently too busy. Please try again later. case 40675: // The service is currently too busy. Please try again later. case 40825: // Unable to complete request now. Please try again later. return new MssqlTransientResourceException(createErrorDetails(token), sql); } if (token.getClassification() == GENERAL_ERROR && token.getNumber() == 4002) { return new ProtocolException(token.getMessage()); } switch (token.getClassification()) { case OBJECT_DOES_NOT_EXIST: case SYNTAX_ERROR: return new MssqlBadGrammarException(createErrorDetails(token), sql); case INCONSISTENT_NO_LOCK: return new MssqlDataIntegrityViolationException(createErrorDetails(token), sql); case TX_DEADLOCK: return new MssqlRollbackException(createErrorDetails(token), sql); case SECURITY: return new MssqlPermissionDeniedException(createErrorDetails(token), sql); case GENERAL_ERROR: return new MssqlNonTransientException(createErrorDetails(token), sql); case OUT_OF_RESOURCES: return new MssqlTransientResourceException(createErrorDetails(token), sql); default: return new MssqlNonTransientResourceException(createErrorDetails(token), sql); } } /** * Handle {@link Message}s and inspect for {@link ErrorToken} to emit a {@link R2dbcException}. * * @param message the message. * @param sink the outbound sink. */ void handleErrorResponse(Message message, SynchronousSink sink) { if (message instanceof ErrorToken) { sink.error(createException((ErrorToken) message, this.sql)); } else { sink.next(message); } } /** * Create a {@link R2dbcException} for a {@link ErrorToken}. * * @param message the message. */ RuntimeException createException(ErrorToken message) { return createException(message, this.sql); } static ErrorDetails createErrorDetails(AbstractInfoToken token) { return new ErrorDetails(token.getMessage(), token.getNumber(), token.getState(), token.getInfoClass(), token.getServerName(), token.getProcName(), token.getLineNumber()); } /** * SQL Server-specific {@link R2dbcBadGrammarException}. */ static final class MssqlBadGrammarException extends R2dbcBadGrammarException implements MssqlException { private final ErrorDetails errorDetails; MssqlBadGrammarException(ErrorDetails errorDetails, String sql) { super(errorDetails.getMessage(), errorDetails.getStateCode(), (int) errorDetails.getNumber(), sql); this.errorDetails = errorDetails; } @Override public ErrorDetails getErrorDetails() { return this.errorDetails; } } /** * SQL Server-specific {@link R2dbcDataIntegrityViolationException}. */ static final class MssqlDataIntegrityViolationException extends R2dbcDataIntegrityViolationException implements MssqlException { private final ErrorDetails errorDetails; MssqlDataIntegrityViolationException(ErrorDetails errorDetails, String sql) { super(errorDetails.getMessage(), errorDetails.getStateCode(), (int) errorDetails.getNumber(), sql); this.errorDetails = errorDetails; } @Override public ErrorDetails getErrorDetails() { return this.errorDetails; } } /** * SQL Server-specific {@link R2dbcNonTransientException}. */ static final class MssqlNonTransientException extends R2dbcNonTransientException implements MssqlException { private final ErrorDetails errorDetails; MssqlNonTransientException(ErrorDetails errorDetails, String sql) { super(errorDetails.getMessage(), errorDetails.getStateCode(), (int) errorDetails.getNumber(), sql); this.errorDetails = errorDetails; } @Override public ErrorDetails getErrorDetails() { return this.errorDetails; } } /** * SQL Server-specific {@link R2dbcNonTransientResourceException}. */ static final class MssqlNonTransientResourceException extends R2dbcNonTransientResourceException implements MssqlException { private final ErrorDetails errorDetails; MssqlNonTransientResourceException(ErrorDetails errorDetails, String sql) { super(errorDetails.getMessage(), errorDetails.getStateCode(), (int) errorDetails.getNumber(), sql); this.errorDetails = errorDetails; } @Override public ErrorDetails getErrorDetails() { return this.errorDetails; } } /** * SQL Server-specific {@link R2dbcPermissionDeniedException}. */ static final class MssqlPermissionDeniedException extends R2dbcPermissionDeniedException implements MssqlException { private final ErrorDetails errorDetails; MssqlPermissionDeniedException(ErrorDetails errorDetails, String sql) { super(errorDetails.getMessage(), errorDetails.getStateCode(), (int) errorDetails.getNumber(), sql); this.errorDetails = errorDetails; } @Override public ErrorDetails getErrorDetails() { return this.errorDetails; } } /** * SQL Server-specific {@link R2dbcRollbackException}. */ static final class MssqlRollbackException extends R2dbcRollbackException implements MssqlException { private final ErrorDetails errorDetails; MssqlRollbackException(ErrorDetails errorDetails, String sql) { super(errorDetails.getMessage(), errorDetails.getStateCode(), (int) errorDetails.getNumber(), sql); this.errorDetails = errorDetails; } @Override public ErrorDetails getErrorDetails() { return this.errorDetails; } } /** * SQL Server-specific {@link R2dbcTimeoutException}. */ static final class MssqlTimeoutException extends R2dbcTimeoutException implements MssqlException { private final ErrorDetails errorDetails; MssqlTimeoutException(ErrorDetails errorDetails, String sql) { super(errorDetails.getMessage(), errorDetails.getStateCode(), (int) errorDetails.getNumber(), sql); this.errorDetails = errorDetails; } @Override public ErrorDetails getErrorDetails() { return this.errorDetails; } } /** * SQL Server-specific {@link R2dbcTimeoutException}. */ static final class MssqlStatementTimeoutException extends R2dbcTimeoutException { public MssqlStatementTimeoutException(String reason, String sql) { super(reason, sql); } } /** * SQL Server-specific {@link R2dbcTransientException}. */ static final class MssqlTransientException extends R2dbcTransientException implements MssqlException { private final ErrorDetails errorDetails; public MssqlTransientException(ErrorDetails errorDetails, String sql) { super(errorDetails.getMessage(), errorDetails.getStateCode(), (int) errorDetails.getNumber(), sql); this.errorDetails = errorDetails; } @Override public ErrorDetails getErrorDetails() { return this.errorDetails; } } /** * SQL Server-specific {@link R2dbcTransientException} upon statement cancellation due to an attention acknowledgement. */ static final class MssqlStatementCancelled extends R2dbcTransientException { public MssqlStatementCancelled(String sql) { super("Statement cancelled", null, 0, sql); } } /** * SQL Server-specific {@link R2dbcTransientResourceException}. */ private static final class MssqlTransientResourceException extends R2dbcTransientResourceException implements MssqlException { private final ErrorDetails errorDetails; MssqlTransientResourceException(ErrorDetails errorDetails, String sql) { super(errorDetails.getMessage(), errorDetails.getStateCode(), (int) errorDetails.getNumber(), sql); this.errorDetails = errorDetails; } @Override public ErrorDetails getErrorDetails() { return this.errorDetails; } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/GeneratedValues.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.message.Message; import io.r2dbc.mssql.message.token.AbstractDoneToken; import io.r2dbc.mssql.util.Assert; import io.r2dbc.spi.Result; import reactor.core.publisher.Flux; import reactor.util.annotation.Nullable; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; /** * Utility to generated the generated values clause using {@code SCOPE_IDENTITY()}. * * @author Mark Paluch */ final class GeneratedValues { private static final String GENERATED_KEYS_QUERY = "SELECT SCOPE_IDENTITY() AS "; private static final String DEFAULT_GENERATED_KEY_NAME = "GENERATED_KEYS"; /** * Adjust the message flow to emit the first received count frame at a later time, when a second {@link AbstractDoneToken} is emitted. This reordering prevents multi-result creation (i.e. * emitting only a single {@link Result}) and merges the count into the {@link Result} by replacing the second {@link AbstractDoneToken}. * * @param messages the message flow. * @return the transformed message flow. */ static Flux reduceToSingleCountDoneToken(Flux messages) { return Flux.defer(() -> { AtomicReference countToken = new AtomicReference<>(); AtomicBoolean rerouteCountFrame = new AtomicBoolean(true); return messages.handle((message, sink) -> { if (rerouteCountFrame.get() && AbstractDoneToken.hasCount(message)) { if (countToken.get() == null) { countToken.set((AbstractDoneToken) message); return; } if (countToken.get() != null) { rerouteCountFrame.set(false); sink.next(countToken.get()); return; } } sink.next(message); }); }); } /** * Augment query for generated keys retrieval. Prior to augmenting, use {@link #shouldExpectGeneratedKeys(String[])} to check whether augmentation should apply. * * @param sql the query to to augment. * @param generatedColumns column name for the generated keys. Can be {@code null}. * @return the potentially augmented query. * @see #shouldExpectGeneratedKeys(String[]) */ static String augmentQuery(String sql, @Nullable String[] generatedColumns) { if (shouldExpectGeneratedKeys(generatedColumns)) { return sql + " " + getGeneratedKeysClause(generatedColumns); } return sql; } /** * Returns {@code true} whether {@code generatedColumns} indicate the caller to expect generated keys. * * @param generatedColumns column name for the generated keys. Can be {@code null}. * @return {@code true} whether {@code generatedColumns} indicates that keys should be generated. */ static boolean shouldExpectGeneratedKeys(@Nullable String[] generatedColumns) { return generatedColumns != null; } /** * Return the key generation clause. Allows column name customization by passing a single column name. Multiple column names are not supported. * * @param columns column names. Defaults to {@link #DEFAULT_GENERATED_KEY_NAME} if no column name specified. * @return the generated keys clause. * @throws IllegalArgumentException if {@code columns} is {@code null}. * @throws UnsupportedOperationException if {@code columns} contains more than one column. */ static String getGeneratedKeysClause(String... columns) { Assert.requireNonNull(columns, "Columns must not be null"); if (columns.length == 0) { return GENERATED_KEYS_QUERY + DEFAULT_GENERATED_KEY_NAME; } if (columns.length == 1) { Assert.requireNonNull(columns[0], "Column must not be null"); return GENERATED_KEYS_QUERY + columns[0]; } throw new UnsupportedOperationException("SQL Server does not support multiple generated keys"); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/IndefinitePreparedStatementCache.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.util.Assert; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; /** * Cache that stores prepared statement handles eternally. * * @author Mark Paluch */ class IndefinitePreparedStatementCache implements PreparedStatementCache { private final Map preparedStatements = new ConcurrentHashMap<>(); private final Map parsedSql = new ConcurrentHashMap<>(); @Override public int getHandle(String sql, Binding binding) { Assert.requireNonNull(sql, "SQL query must not be null"); Assert.requireNonNull(binding, "Binding query must not be null"); return this.preparedStatements.getOrDefault(createKey(sql, binding), UNPREPARED); } @Override public void putHandle(int handle, String sql, Binding binding) { Assert.requireNonNull(sql, "SQL query must not be null"); Assert.requireNonNull(binding, "Binding query must not be null"); this.preparedStatements.put(createKey(sql, binding), handle); } @SuppressWarnings("unchecked") @Override public T getParsedSql(String sql, Function parseFunction) { return (T) this.parsedSql.computeIfAbsent(sql, parseFunction); } @Override public int size() { return this.preparedStatements.size(); } private static String createKey(String sql, Binding binding) { return sql + "-" + binding.getFormalParameters(); } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [preparedStatements=").append(this.preparedStatements); sb.append(", parsedSql=").append(this.parsedSql); sb.append(']'); return sb.toString(); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/LoginConfiguration.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.message.token.Login7; import io.r2dbc.mssql.util.Assert; import io.r2dbc.mssql.util.StringUtils; import reactor.util.annotation.Nullable; import java.util.UUID; /** * Login configuration properties. Used to build a {@link Login7} message. * * @author Mark Paluch */ final class LoginConfiguration { @Nullable private final String applicationName; @Nullable private final UUID connectionId; private final String database; private final String hostname; private final CharSequence password; private final String serverName; private final boolean useSsl; private final String username; LoginConfiguration(@Nullable String applicationName, @Nullable UUID connectionId, String database, String hostname, CharSequence password, String serverName, boolean useSsl, String username) { this.username = Assert.requireNonNull(username, "Username must not be null"); this.password = Assert.requireNonNull(password, "Password must not be null"); this.database = Assert.requireNonNull(database, "Database must not be null"); this.hostname = Assert.requireNonNull(hostname, "Hostname must not be null"); this.applicationName = applicationName; this.serverName = Assert.requireNonNull(serverName, "Server name must not be null"); this.connectionId = connectionId; this.useSsl = useSsl; } @Nullable UUID getConnectionId() { return this.connectionId; } boolean useSsl() { return this.useSsl; } Login7.Builder asBuilder() { Login7.Builder builder = Login7.builder().username(this.username).password(this.password).database(this.database) .hostName(this.hostname).serverName(this.serverName); if (StringUtils.hasText(this.applicationName)) { builder.applicationName(this.applicationName); } return builder; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/LoginFlow.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.client.Client; import io.r2dbc.mssql.client.ssl.SslState; import io.r2dbc.mssql.message.ClientMessage; import io.r2dbc.mssql.message.Message; import io.r2dbc.mssql.message.TDSVersion; import io.r2dbc.mssql.message.tds.ProtocolException; import io.r2dbc.mssql.message.token.DoneToken; import io.r2dbc.mssql.message.token.ErrorToken; import io.r2dbc.mssql.message.token.Login7; import io.r2dbc.mssql.message.token.Prelogin; import io.r2dbc.mssql.util.Assert; import reactor.core.publisher.Flux; import reactor.core.publisher.Sinks; import java.util.concurrent.atomic.AtomicReference; import static io.r2dbc.mssql.util.PredicateUtils.or; /** * A utility class that encapsulates the Login message flow. * * @author Mark Paluch */ final class LoginFlow { private LoginFlow() { } /** * @param client the {@link Client} to exchange messages with * @param login the login configuration for login negotiation * @return the messages received after authentication is complete, in response to this exchange */ static Flux exchange(Client client, LoginConfiguration login) { Assert.requireNonNull(client, "client must not be null"); Assert.requireNonNull(login, "Login must not be null"); Prelogin.Builder builder = Prelogin.builder(); if (login.getConnectionId() != null) { builder.withConnectionId(login.getConnectionId()); } if (login.useSsl()) { builder.withEncryptionEnabled(); } AtomicReference preloginResponse = new AtomicReference<>(); Sinks.Many requests = Sinks.many().unicast().onBackpressureBuffer(); Prelogin request = builder.build(); requests.emitNext(request, Sinks.EmitFailureHandler.FAIL_FAST); return client.exchange(requests.asFlux(), DoneToken::isDone) // .filter(or(Prelogin.class::isInstance, SslState.class::isInstance, DoneToken.class::isInstance, ErrorToken.class::isInstance)) // .handle((message, sink) -> { try { if (message instanceof Prelogin) { Prelogin response = (Prelogin) message; preloginResponse.set(response); Prelogin.Encryption encryption = response.getRequiredToken(Prelogin.Encryption.class); if (!encryption.requiresSslHandshake()) { requests.emitNext(createLoginMessage(login, response), Sinks.EmitFailureHandler.FAIL_FAST); } return; } if (message instanceof SslState && message == SslState.NEGOTIATED) { Prelogin prelogin = preloginResponse.get(); requests.emitNext(createLoginMessage(login, prelogin), Sinks.EmitFailureHandler.FAIL_FAST); return; } if (DoneToken.isDone(message)) { sink.next(message); sink.complete(); return; } if (message instanceof ErrorToken) { sink.error(ExceptionFactory.createException((ErrorToken) message, "")); client.close().subscribe(); return; } throw ProtocolException.unsupported(String.format("Unexpected login flow message: %s", message)); } catch (Exception e) { requests.emitError(e, Sinks.EmitFailureHandler.FAIL_FAST); sink.error(e); } }); } private static Login7 createLoginMessage(LoginConfiguration login, Prelogin prelogin) { Prelogin.Version serverVersion = prelogin.getRequiredToken(Prelogin.Version.class); TDSVersion tdsVersion = getTdsVersion(serverVersion.getVersion()); return login.asBuilder().tdsVersion(tdsVersion).build(); } private static TDSVersion getTdsVersion(int serverVersion) { if (serverVersion >= 11) // Denali --> TDS 7.4 { return TDSVersion.VER_DENALI; } if (serverVersion >= 10) // Katmai (10.0) & later 7.3B { return TDSVersion.VER_KATMAI; } if (serverVersion >= 9) // Yukon (9.0) --> TDS 7.2 // Prelogin disconnects anything older { return TDSVersion.VER_YUKON; } throw ProtocolException.unsupported("Unsupported server version: " + serverVersion); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/MssqlBatch.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.client.Client; import io.r2dbc.mssql.util.Assert; import io.r2dbc.spi.Batch; import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; /** * An implementation of {@link Batch} for executing a collection of statements in a batch against a Microsoft SQL Server database. * * @author Mark Paluch */ public final class MssqlBatch implements Batch { private final Client client; private final ConnectionOptions connectionOptions; private final List statements = new ArrayList<>(); MssqlBatch(Client client, ConnectionOptions connectionOptions) { this.client = Assert.requireNonNull(client, "Client must not be null"); this.connectionOptions = Assert.requireNonNull(connectionOptions, "ConnectionOptions must not be null"); } @Override public MssqlBatch add(String sql) { Assert.requireNonNull(sql, "SQL must not be null"); this.statements.add(sql); return this; } @Override public Flux execute() { return new SimpleMssqlStatement(this.client, this.connectionOptions, String.join("; ", this.statements)) .execute(); } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [client=").append(this.client); sb.append(", connectionOptions=").append(this.connectionOptions); sb.append(", statements=").append(this.statements); sb.append(']'); return sb.toString(); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/MssqlColumnMetadata.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.codec.Codecs; import io.r2dbc.mssql.codec.Decodable; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.Assert; import io.r2dbc.spi.ColumnMetadata; import io.r2dbc.spi.Nullability; import io.r2dbc.spi.OutParameterMetadata; import io.r2dbc.spi.Type; import javax.annotation.Nonnull; /** * Microsoft SQL Server-specific {@link ColumnMetadata} based on {@link Decodable}. * * @author Mark Paluch */ public final class MssqlColumnMetadata implements ColumnMetadata, OutParameterMetadata { private final Decodable decodable; private final Codecs codecs; /** * Creates a new {@link MssqlColumnMetadata}. * * @param decodable the column. * @param codecs the {@link Codecs codec registry} */ MssqlColumnMetadata(Decodable decodable, Codecs codecs) { this.decodable = Assert.requireNonNull(decodable, "Decodable must not be null"); this.codecs = Assert.requireNonNull(codecs, "Codecs must not be null"); } @Override public String getName() { return this.decodable.getName(); } @Override public Integer getPrecision() { return getNativeTypeMetadata().getPrecision(); } @Override public Integer getScale() { return getNativeTypeMetadata().getScale(); } @Override public Nullability getNullability() { return getNativeTypeMetadata().isNullable() ? Nullability.NULLABLE : Nullability.NON_NULL; } @Override public Class getJavaType() { return this.codecs.getJavaType(getNativeTypeMetadata()); } @Override public Type getType() { return getNativeTypeMetadata().getServerType(); } @Override @Nonnull public TypeInformation getNativeTypeMetadata() { return this.decodable.getType(); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/MssqlConnection.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.api.MssqlTransactionDefinition; import io.r2dbc.mssql.client.Client; import io.r2dbc.mssql.client.ConnectionContext; import io.r2dbc.mssql.client.TransactionStatus; import io.r2dbc.mssql.util.Assert; import io.r2dbc.spi.Connection; import io.r2dbc.spi.IsolationLevel; import io.r2dbc.spi.Option; import io.r2dbc.spi.TransactionDefinition; import io.r2dbc.spi.ValidationDepth; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.Logger; import reactor.util.Loggers; import java.time.Duration; import java.util.function.Function; import java.util.regex.Pattern; /** * {@link Connection} to a Microsoft SQL Server. * * @author Mark Paluch * @author Hebert Coelho * @author Nayan Hajratwala * @see MssqlConnection * @see DefaultMssqlResult * @see ErrorDetails */ public final class MssqlConnection implements Connection { private static final Pattern IDENTIFIER_PATTERN = Pattern.compile("[\\d\\w_]{1,32}"); private static final Pattern IDENTIFIER128_PATTERN = Pattern.compile("[\\d\\w_]{1,128}"); private static final Logger logger = Loggers.getLogger(MssqlConnection.class); private final Client client; private final MssqlConnectionMetadata metadata; private final ConnectionContext context; private final ConnectionOptions connectionOptions; private final Flux validationQuery; private volatile boolean autoCommit; private volatile IsolationLevel isolationLevel; private volatile IsolationLevel previousIsolationLevel; private volatile boolean resetLockWaitTime = false; MssqlConnection(Client client, MssqlConnectionMetadata connectionMetadata, ConnectionOptions connectionOptions) { this.client = Assert.requireNonNull(client, "Client must not be null"); this.metadata = connectionMetadata; this.context = client.getContext(); this.connectionOptions = Assert.requireNonNull(connectionOptions, "ConnectionOptions must not be null"); TransactionStatus transactionStatus = client.getTransactionStatus(); this.autoCommit = transactionStatus == TransactionStatus.AUTO_COMMIT; this.isolationLevel = IsolationLevel.READ_COMMITTED; this.validationQuery = new SimpleMssqlStatement(this.client, connectionOptions, "SELECT 1").fetchSize(0).execute().flatMap(MssqlResult::getRowsUpdated); } @Override public Mono beginTransaction() { return beginTransaction(EmptyTransactionDefinition.INSTANCE); } @Override public Mono beginTransaction(TransactionDefinition transactionDefinition) { return useTransactionStatus(tx -> { if (tx == TransactionStatus.STARTED) { logger.debug(this.context.getMessage("Skipping begin transaction because status is [{}]"), tx); return Mono.empty(); } String name = transactionDefinition.getAttribute(MssqlTransactionDefinition.NAME); String mark = transactionDefinition.getAttribute(MssqlTransactionDefinition.MARK); IsolationLevel isolationLevel = transactionDefinition.getAttribute(MssqlTransactionDefinition.ISOLATION_LEVEL); Duration lockWaitTime = transactionDefinition.getAttribute(MssqlTransactionDefinition.LOCK_WAIT_TIMEOUT); StringBuilder builder = new StringBuilder(); builder.append("BEGIN TRANSACTION"); if (name != null) { String nameToUse = sanitize(name, 32); Assert.isTrue(IDENTIFIER_PATTERN.matcher(nameToUse).matches(), "Transaction names must contain only characters and numbers and must not exceed 32 characters"); builder.append(" ").append(nameToUse); if (mark != null) { String markToUse = sanitize(mark, 128); Assert.isTrue(IDENTIFIER128_PATTERN.matcher(markToUse.substring(0, Math.min(128, markToUse.length()))).matches(), "Transaction names must contain only characters and numbers and" + " must not exceed 128 characters"); builder.append(' ').append("WITH MARK '").append(markToUse).append("'"); } } builder.append(';'); if (isolationLevel != null) { builder.append(renderSetIsolationLevel(isolationLevel)).append(';'); } if (lockWaitTime != null) { this.resetLockWaitTime = true; builder.append("SET LOCK_TIMEOUT ").append(lockWaitTime.isNegative() ? "-1" : lockWaitTime.toMillis()).append(';'); } logger.debug(this.context.getMessage("Beginning transaction from status [{}]"), tx); return exchange(builder.toString()).doOnSuccess(unused -> { this.previousIsolationLevel = this.isolationLevel; if (isolationLevel != null) { this.isolationLevel = isolationLevel; } }); }); } /** * Cancel an ongoing request. * * @return */ Mono cancel() { return this.client.attention(); } @Override public Mono close() { return this.client.close(); } @Override public Mono commitTransaction() { return useTransactionStatus(tx -> { if (tx != TransactionStatus.STARTED) { logger.debug(this.context.getMessage("Skipping commit transaction because status is [{}]"), tx); return Mono.empty(); } logger.debug(this.context.getMessage("Committing transaction with status [{}]"), tx); return exchange("IF @@TRANCOUNT > 0 COMMIT TRANSACTION;" + cleanup()).doOnSuccess(v -> { if (this.previousIsolationLevel != null) { this.isolationLevel = this.previousIsolationLevel; this.previousIsolationLevel = null; } this.resetLockWaitTime = false; }); }); } private String cleanup() { String cleanupSql = ""; if (this.previousIsolationLevel != null && this.previousIsolationLevel != this.isolationLevel) { cleanupSql = renderSetIsolationLevel(this.previousIsolationLevel) + ";"; } if (this.resetLockWaitTime) { cleanupSql += "SET LOCK_TIMEOUT -1;"; } return cleanupSql; } @Override public MssqlBatch createBatch() { return new MssqlBatch(this.client, this.connectionOptions); } @Override public Mono createSavepoint(String name) { Assert.requireNonNull(name, "Savepoint name must not be null"); String nameToUse = sanitize(name, 32); Assert.isTrue(IDENTIFIER_PATTERN.matcher(nameToUse).matches(), "Save point names must contain only characters and numbers and must not exceed 32 characters"); return useTransactionStatus(tx -> { logger.debug(this.context.getMessage("Creating savepoint [{}] for transaction with status [{}]"), nameToUse, tx); if (this.autoCommit) { logger.debug(this.context.getMessage("Setting auto-commit mode to [false]")); } return exchange(String.format("SET IMPLICIT_TRANSACTIONS ON; IF @@TRANCOUNT = 0 BEGIN BEGIN TRAN IF @@TRANCOUNT = 2 COMMIT TRAN END SAVE TRAN %s;", nameToUse)).doOnSuccess(ignore -> { this.autoCommit = false; }); }); } @Override public MssqlStatement createStatement(String sql) { Assert.requireNonNull(sql, "SQL must not be null"); logger.debug(this.context.getMessage("Creating statement for SQL: [{}]"), sql); if (ParametrizedMssqlStatement.supports(sql)) { return new ParametrizedMssqlStatement(this.client, this.connectionOptions, sql); } return new SimpleMssqlStatement(this.client, this.connectionOptions, sql); } @Override public Mono releaseSavepoint(String name) { return Mono.empty(); } @Override public Mono rollbackTransaction() { return useTransactionStatus(tx -> { if (tx != TransactionStatus.STARTED && tx != TransactionStatus.EXPLICIT) { logger.debug(this.context.getMessage("Skipping rollback transaction because status is [{}]"), tx); return Mono.empty(); } logger.debug(this.context.getMessage("Rolling back transaction with status [{}]"), tx); return exchange("IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;" + cleanup()).doOnSuccess(v -> { if (this.previousIsolationLevel != null) { this.isolationLevel = this.previousIsolationLevel; this.previousIsolationLevel = null; } this.resetLockWaitTime = false; }).doOnSuccess(v -> { if (this.previousIsolationLevel != null) { this.isolationLevel = this.previousIsolationLevel; this.previousIsolationLevel = null; } this.resetLockWaitTime = false; }); }); } @Override public Mono rollbackTransactionToSavepoint(String name) { Assert.requireNonNull(name, "Savepoint name must not be null"); String nameToUse = sanitize(name, 32); Assert.isTrue(IDENTIFIER_PATTERN.matcher(nameToUse).matches(), "Save point names must contain only characters and numbers and must not exceed 32 characters"); return useTransactionStatus(tx -> { if (tx != TransactionStatus.STARTED) { logger.debug(this.context.getMessage("Skipping rollback transaction to savepoint [{}] because status is [{}]"), nameToUse, tx); return Mono.empty(); } logger.debug(this.context.getMessage("Rolling back transaction to savepoint [{}] with status [{}]"), nameToUse, tx); return exchange(String.format("ROLLBACK TRANSACTION %s", nameToUse)); }); } public boolean isAutoCommit() { return this.autoCommit && this.client.getTransactionStatus() != TransactionStatus.STARTED; } public Mono setAutoCommit(boolean autoCommit) { return Mono.defer(() -> { StringBuilder builder = new StringBuilder(); logger.debug(this.context.getMessage("Setting auto-commit mode to [{}]"), autoCommit); if (this.autoCommit != autoCommit) { logger.debug(this.context.getMessage("Committing pending transactions")); builder.append("IF @@TRANCOUNT > 0 COMMIT TRAN;"); } builder.append(autoCommit ? "SET IMPLICIT_TRANSACTIONS OFF;" : "SET IMPLICIT_TRANSACTIONS ON;"); return exchange(builder.toString()).doOnSuccess(ignore -> this.autoCommit = autoCommit); }); } /** * Configure the lock wait timeout via {@code SET LOCK_TIMEOUT}. {@link Duration#isNegative() Negative values} are translated to {@code -1} meaning infinite wait. * * @param timeout the timeout to apply. * @return a {@link Mono} that indicates that the lock wait timeout has been applied * @since 0.9 */ @Override public Mono setLockWaitTimeout(Duration timeout) { Assert.requireNonNull(timeout, "Timeout must not be null"); return exchange("SET LOCK_TIMEOUT " + (timeout.isNegative() ? "-1" : "" + timeout.toMillis())); } /** * Configure the statement wait timeout. Statements exceeding the timeout are being cancelled. * * @param timeout the timeout to apply. * @return a {@link Mono} that indicates that the statement timeout has been applied * @since 0.9 */ @Override public Mono setStatementTimeout(Duration timeout) { Assert.requireNonNull(timeout, "Timeout must not be null"); return Mono.fromRunnable(() -> this.connectionOptions.setStatementTimeout(timeout)); } @Override public MssqlConnectionMetadata getMetadata() { return this.metadata; } public IsolationLevel getTransactionIsolationLevel() { return this.isolationLevel; } @Override public Mono setTransactionIsolationLevel(IsolationLevel isolationLevel) { Assert.requireNonNull(isolationLevel, "IsolationLevel must not be null"); return exchange(renderSetIsolationLevel(isolationLevel)).doOnSuccess(ignore -> this.isolationLevel = isolationLevel); } @Override public Mono validate(ValidationDepth depth) { if (depth == ValidationDepth.LOCAL) { return Mono.fromSupplier(this.client::isConnected); } return Mono.create(sink -> { if (!this.client.isConnected()) { sink.success(false); return; } this.validationQuery.subscribe(new CoreSubscriber() { @Override public void onSubscribe(Subscription s) { s.request(Integer.MAX_VALUE); } @Override public void onNext(Long integer) { } @Override public void onError(Throwable t) { logger.debug("Validation failed", t); sink.success(false); } @Override public void onComplete() { sink.success(true); } }); }); } private static String renderSetIsolationLevel(IsolationLevel isolationLevel) { return "SET TRANSACTION ISOLATION LEVEL " + isolationLevel.asSql(); } static String sanitize(String identifier, int maxLength) { return identifier .replace('-', '_') .replace('.', '_') .substring(0, Math.min(identifier.length(), maxLength)); } private Mono exchange(String sql) { ExceptionFactory factory = ExceptionFactory.withSql(sql); return QueryMessageFlow.exchange(this.client, sql) .handle(factory::handleErrorResponse) .then(); } private Mono useTransactionStatus(Function> function) { return Flux.defer(() -> function.apply(this.client.getTransactionStatus())) .then(); } enum EmptyTransactionDefinition implements TransactionDefinition { INSTANCE; @Override public T getAttribute(Option option) { return null; } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/MssqlConnectionConfiguration.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.netty.handler.ssl.IdentityCipherSuiteFilter; import io.netty.handler.ssl.OpenSsl; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.r2dbc.mssql.client.ClientConfiguration; import io.r2dbc.mssql.client.ssl.ExpectedHostnameX509TrustManager; import io.r2dbc.mssql.client.ssl.SslConfiguration; import io.r2dbc.mssql.client.ssl.TrustAllTrustManager; import io.r2dbc.mssql.codec.Codecs; import io.r2dbc.mssql.codec.DefaultCodecs; import io.r2dbc.mssql.message.tds.Redirect; import io.r2dbc.mssql.util.Assert; import io.r2dbc.mssql.util.StringUtils; import reactor.netty.resources.ConnectionProvider; import reactor.util.annotation.Nullable; import javax.net.ssl.SSLException; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.net.InetAddress; import java.net.Socket; import java.net.UnknownHostException; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.time.Duration; import java.util.Arrays; import java.util.Locale; import java.util.Optional; import java.util.UUID; import java.util.function.Function; import java.util.function.Predicate; /** * Connection configuration information for connecting to a Microsoft SQL database. * Allows configuration of the connection endpoint, login credentials, database and trace details such as application name and connection Id. * * @author Mark Paluch * @author Alex Stockinger * @author Paul Johe */ public final class MssqlConnectionConfiguration { /** * Default SQL Server port. */ public static final int DEFAULT_PORT = 1433; /** * Default connect timeout. */ public static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(30); @Nullable private final String applicationName; private final ConnectionProvider connectionProvider; @Nullable private final UUID connectionId; private final Duration connectTimeout; private final String database; private final String host; private final String hostNameInCertificate; private final CharSequence password; private final Predicate preferCursoredExecution; @Nullable private final Duration lockWaitTimeout; private final int port; private final boolean sendStringParametersAsUnicode; private final boolean ssl; private final Function sslContextBuilderCustomizer; @Nullable private final Function sslTunnelSslContextBuilderCustomizer; private final boolean tcpKeepAlive; private final boolean tcpNoDelay; private final boolean trustServerCertificate; @Nullable private final File trustStore; @Nullable private final String trustStoreType; @Nullable private final char[] trustStorePassword; private final String username; private MssqlConnectionConfiguration(@Nullable String applicationName, @Nullable UUID connectionId, ConnectionProvider connectionProvider, Duration connectTimeout, @Nullable String database, String host, String hostNameInCertificate, @Nullable Duration lockWaitTimeout, CharSequence password, Predicate preferCursoredExecution, int port, boolean sendStringParametersAsUnicode, boolean ssl, Function sslContextBuilderCustomizer, @Nullable Function sslTunnelSslContextBuilderCustomizer, boolean tcpKeepAlive, boolean tcpNoDelay, boolean trustServerCertificate, @Nullable File trustStore, @Nullable String trustStoreType, @Nullable char[] trustStorePassword, String username) { this.applicationName = applicationName; this.connectionId = connectionId; this.connectionProvider = connectionProvider; this.connectTimeout = Assert.requireNonNull(connectTimeout, "connect timeout must not be null"); this.database = database; this.host = Assert.requireNonNull(host, "host must not be null"); this.hostNameInCertificate = Assert.requireNonNull(hostNameInCertificate, "hostNameInCertificate must not be null"); this.lockWaitTimeout = lockWaitTimeout; this.password = Assert.requireNonNull(password, "password must not be null"); this.preferCursoredExecution = Assert.requireNonNull(preferCursoredExecution, "preferCursoredExecution must not be null"); this.port = port; this.sendStringParametersAsUnicode = sendStringParametersAsUnicode; this.ssl = ssl; this.sslContextBuilderCustomizer = sslContextBuilderCustomizer; this.sslTunnelSslContextBuilderCustomizer = sslTunnelSslContextBuilderCustomizer; this.tcpKeepAlive = tcpKeepAlive; this.tcpNoDelay = tcpNoDelay; this.trustServerCertificate = trustServerCertificate; this.trustStore = trustStore; this.trustStoreType = trustStoreType; this.trustStorePassword = trustStorePassword; this.username = Assert.requireNonNull(username, "username must not be null"); } /** * Returns a new {@link Builder}. * * @return a new {@link Builder} */ public static Builder builder() { return new Builder(); } /** * Create a new configuration instance targeting the redirect. * * @param redirect the redirect * @return a new configuration instance * @since 0.8.2 */ MssqlConnectionConfiguration withRedirect(Redirect redirect) { String redirectServerName = redirect.getServerName(); String hostNameInCertificate = this.hostNameInCertificate; // Same behavior as mssql-jdbc if (this.hostNameInCertificate.startsWith("*") && redirectServerName.indexOf('.') != -1) { // Check if redirectServerName and hostNameInCertificate are from same domain. boolean trustedDomain = redirectServerName.endsWith(hostNameInCertificate.substring(1)); if (trustedDomain) { hostNameInCertificate = String.format("*%s", redirectServerName.substring(redirectServerName.indexOf('.'))); } } return new MssqlConnectionConfiguration(this.applicationName, this.connectionId, this.connectionProvider, this.connectTimeout, this.database, redirectServerName, hostNameInCertificate, this.lockWaitTimeout, this.password, this.preferCursoredExecution, redirect.getPort(), this.sendStringParametersAsUnicode, this.ssl, this.sslContextBuilderCustomizer, this.sslTunnelSslContextBuilderCustomizer, this.tcpKeepAlive, this.tcpNoDelay, this.trustServerCertificate, this.trustStore, this.trustStoreType, this.trustStorePassword, this.username ); } ClientConfiguration toClientConfiguration() { return new DefaultClientConfiguration(this.connectionProvider, this.connectTimeout, this.host, this.hostNameInCertificate, this.port, this.ssl, this.sslContextBuilderCustomizer, this.sslTunnelSslContextBuilderCustomizer, this.tcpKeepAlive, this.tcpNoDelay, this.trustServerCertificate, this.trustStore, this.trustStoreType, this.trustStorePassword ); } ConnectionOptions toConnectionOptions(Codecs codecs) { return new ConnectionOptions(this.preferCursoredExecution, codecs, new IndefinitePreparedStatementCache(), this.sendStringParametersAsUnicode); } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [applicationName=\"").append(this.applicationName).append('\"'); sb.append(", connectionId=").append(this.connectionId); sb.append(", connectTimeout=\"").append(this.connectTimeout).append('\"'); sb.append(", database=\"").append(this.database).append('\"'); sb.append(", host=\"").append(this.host).append('\"'); sb.append(", hostNameInCertificate=\"").append(this.hostNameInCertificate).append('\"'); sb.append(", lockWaitTimeout=\"").append(this.lockWaitTimeout).append('\"'); sb.append(", password=\"").append(repeat(this.password.length(), "*")).append('\"'); sb.append(", preferCursoredExecution=\"").append(this.preferCursoredExecution).append('\"'); sb.append(", port=").append(this.port); sb.append(", sendStringParametersAsUnicode=").append(this.sendStringParametersAsUnicode); sb.append(", ssl=").append(this.ssl); sb.append(", sslContextBuilderCustomizer=").append(this.sslContextBuilderCustomizer); sb.append(", sslTunnelSslContextBuilderCustomizer=").append(this.sslTunnelSslContextBuilderCustomizer); sb.append(", tcpKeepAlive=\"").append(this.tcpKeepAlive).append("\""); sb.append(", tcpNoDelay=\"").append(this.tcpNoDelay).append("\""); sb.append(", trustServerCertificate=").append(this.trustServerCertificate); sb.append(", trustStore=\"").append(this.trustStore).append("\""); sb.append(", trustStorePassword=\"").append(repeat(this.trustStorePassword == null ? 0 : this.trustStorePassword.length, "*")).append('\"'); sb.append(", trustStoreType=\"").append(this.trustStoreType).append("\""); sb.append(", username=\"").append(this.username).append('\"'); sb.append(']'); return sb.toString(); } @Nullable String getApplicationName() { return this.applicationName; } @Nullable UUID getConnectionId() { return this.connectionId; } Duration getConnectTimeout() { return this.connectTimeout; } Optional getDatabase() { return Optional.ofNullable(this.database); } String getHost() { return this.host; } String getHostNameInCertificate() { return this.hostNameInCertificate; } @Nullable Duration getLockWaitTimeout() { return this.lockWaitTimeout; } CharSequence getPassword() { return this.password; } Predicate getPreferCursoredExecution() { return this.preferCursoredExecution; } int getPort() { return this.port; } boolean isSendStringParametersAsUnicode() { return this.sendStringParametersAsUnicode; } boolean useSsl() { return this.ssl; } boolean isTcpKeepAlive() { return this.tcpKeepAlive; } boolean isTcpNoDelay() { return this.tcpNoDelay; } String getUsername() { return this.username; } LoginConfiguration getLoginConfiguration() { return new LoginConfiguration(getApplicationName(), this.connectionId, getDatabase().orElse(""), lookupHostName(), getPassword(), getHost(), useSsl(), getUsername() ); } private static String repeat(int length, String character) { StringBuilder builder = new StringBuilder(); for (int i = 0; i < length; i++) { builder.append(character); } return builder.toString(); } /** * Looks up local hostname of client machine. * * @return hostname string or IP of host if hostname cannot be resolved. If neither hostname or IP found returns an empty string. */ private static String lookupHostName() { try { InetAddress localAddress = InetAddress.getLocalHost(); if (localAddress != null) { String value = localAddress.getHostName(); if (StringUtils.hasText(value)) { return value; } value = localAddress.getHostAddress(); if (StringUtils.hasText(value)) { return value; } } } catch (UnknownHostException e) { return ""; } // If hostname not found, return standard "" string. return ""; } /** * A builder for {@link MssqlConnectionConfiguration} instances. *

* This class is not threadsafe */ public static final class Builder { @Nullable private String applicationName; private ConnectionProvider connectionProvider = ConnectionProvider.newConnection(); private UUID connectionId = UUID.randomUUID(); private Duration connectTimeout = DEFAULT_CONNECT_TIMEOUT; private String database; private String host; private String hostNameInCertificate; @Nullable private Duration lockWaitTimeout; private Predicate preferCursoredExecution = DefaultCursorPreference.INSTANCE; private CharSequence password; private int port = DEFAULT_PORT; private boolean sendStringParametersAsUnicode = true; private boolean ssl; private boolean trustServerCertificate; private Function sslContextBuilderCustomizer = Function.identity(); @Nullable private Function sslTunnelSslContextBuilderCustomizer; private String username; private boolean tcpKeepAlive = false; private boolean tcpNoDelay = true; @Nullable private File trustStore; @Nullable private String trustStoreType; @Nullable private char[] trustStorePassword; private Builder() { } /** * Configure the applicationName. * * @param applicationName the applicationName * @return this {@link Builder} * @throws IllegalArgumentException if {@code applicationName} is {@code null} */ public Builder applicationName(String applicationName) { this.applicationName = Assert.requireNonNull(applicationName, "applicationName must not be null"); return this; } /** * Configure the connectionId. * * @param connectionId the application name * @return this {@link Builder} * @throws IllegalArgumentException when {@link UUID} is {@code null}. */ public Builder connectionId(UUID connectionId) { this.connectionId = Assert.requireNonNull(connectionId, "connectionId must not be null"); return this; } /** * Configure the {@link ConnectionProvider} to be used with Reactor Netty. * Defaults to {@link ConnectionProvider#newConnection()}. * * @param connectionProvider the connection provider * @return this {@link Builder} * @since 1.0.3 */ public Builder connectionProvider(ConnectionProvider connectionProvider) { this.connectionProvider = Assert.requireNonNull(connectionProvider, "connectionProvider must not be null"); return this; } /** * Configure the connect timeout. Defaults to 30 seconds. * * @param connectTimeout the connect timeout * @return this {@link Builder} */ public Builder connectTimeout(Duration connectTimeout) { Assert.requireNonNull(connectTimeout, "connect timeout must not be null"); Assert.isTrue(!connectTimeout.isNegative(), "connect timeout must not be negative"); this.connectTimeout = connectTimeout; return this; } /** * Configure the database. * * @param database the database * @return this {@link Builder} */ public Builder database(@Nullable String database) { this.database = database; return this; } /** * Enable SSL usage. This flag is also known as Use Encryption in other drivers. * * @return this {@link Builder} */ public Builder enableSsl() { this.ssl = true; return this; } /** * Enable SSL tunnel usage to encrypt all traffic right from the connect phase. This option is required when using a SSL tunnel (e.g. stunnel or other SSL terminator) in front of the SQL * server and it is not related to SQL Server's built-in SSL support. * * @return this {@link Builder} * @since 0.8.5 */ public Builder enableSslTunnel() { return enableSslTunnel(Function.identity()); } /** * Enable SSL tunnel usage to encrypt all traffic right from the connect phase. This option is required when using a SSL tunnel (e.g. stunnel or other SSL terminator) in front of the SQL * server and it is not related to SQL Server's built-in SSL support. * The given customizer gets applied on each SSL connection attempt to allow for just-in-time configuration updates. The {@link Function} gets * * called with the prepared {@link SslContextBuilder} that has all configuration options applied. The customizer may return the same builder or return a new builder instance to be used to * * build the SSL context. * * @param sslTunnelSslContextBuilderCustomizer customizer function * @return this {@link Builder} * @throws IllegalArgumentException if {@code sslTunnelSslContextBuilderCustomizer} is {@code null} * @since 0.8.5 */ public Builder enableSslTunnel(Function sslTunnelSslContextBuilderCustomizer) { this.sslTunnelSslContextBuilderCustomizer = Assert.requireNonNull(sslTunnelSslContextBuilderCustomizer, "sslTunnelSslContextBuilderCustomizer must not be null"); return this; } /** * Configure the host. * * @param host the host * @return this {@link Builder} * @throws IllegalArgumentException if {@code host} is {@code null} */ public Builder host(String host) { this.host = Assert.requireNonNull(host, "host must not be null"); return this; } /** * Configure the expected hostname in the SSL certificate. Defaults to {@link #host(String)} if left unconfigured. Accepts wildcards such as {@code *.database.windows.net}. * * @param hostNameInCertificate the hostNameInCertificate * @return this {@link Builder} * @throws IllegalArgumentException if {@code hostNameInCertificate} is {@code null} */ public Builder hostNameInCertificate(String hostNameInCertificate) { this.hostNameInCertificate = Assert.requireNonNull(hostNameInCertificate, "hostNameInCertificate must not be null"); return this; } /** * Configure the lock wait timeout via {@code SET LOCK_TIMEOUT}. {@link Duration#isNegative() Negative values} are translated to {@code -1} meaning infinite wait. * * @param timeout the lock wait timeout * @return this {@link Builder} * @since 0.9 */ public Builder lockWaitTimeout(Duration timeout) { Assert.requireNonNull(timeout, "lock wait timeout must not be null"); this.lockWaitTimeout = timeout; return this; } /** * Configure the password. * * @param password the password * @return this {@link Builder} * @throws IllegalArgumentException if {@code password} is {@code null} */ public Builder password(CharSequence password) { this.password = Assert.requireNonNull(password, "password must not be null"); return this; } /** * Configure whether to prefer cursored execution. * * @param preferCursoredExecution {@code true} prefers cursors, {@code false} prefers direct execution. Defaults to direct execution. * @return this {@link Builder} * @throws IllegalArgumentException if {@code password} is {@code null} */ public Builder preferCursoredExecution(boolean preferCursoredExecution) { return preferCursoredExecution(sql -> preferCursoredExecution); } /** * Configure whether to prefer cursored execution on a statement-by-statement basis. The {@link Predicate} accepts the SQL query string and returns a boolean flag indicating preference. * {@code true} prefers cursors, {@code false} prefers direct execution. Defaults to direct execution. * * @param preference the {@link Predicate}. * @return this {@link Builder} * @throws IllegalArgumentException if {@code password} is {@code null} */ public Builder preferCursoredExecution(Predicate preference) { this.preferCursoredExecution = Assert.requireNonNull(preference, "Predicate must not be null"); return this; } /** * Configure the port. Defaults to {@code 5432}. * * @param port the port * @return this {@link Builder} */ public Builder port(int port) { this.port = port; return this; } /** * Configure whether to send character data as unicode (NVARCHAR, NCHAR, NTEXT) or whether to use the database encoding. Enabled by default. If disabled, {@link CharSequence} data is sent * using the database-specific collation such as ASCII/MBCS instead of Unicode. * * @param sendStringParametersAsUnicode {@literal true} to send character data as unicode (NVARCHAR, NCHAR, NTEXT) or whether to use the database encoding. Enabled by default. * @return this {@link Builder} */ public Builder sendStringParametersAsUnicode(boolean sendStringParametersAsUnicode) { this.sendStringParametersAsUnicode = sendStringParametersAsUnicode; return this; } /** * Configure a {@link SslContextBuilder} customizer. The customizer gets applied on each SSL connection attempt to allow for just-in-time configuration updates. The {@link Function} gets * called with the prepared {@link SslContextBuilder} that has all configuration options applied. The customizer may return the same builder or return a new builder instance to be used to * build the SSL context. * * @param sslContextBuilderCustomizer customizer function * @return this {@link Builder} * @throws IllegalArgumentException if {@code sslContextBuilderCustomizer} is {@code null} * @since 0.8.3 */ public Builder sslContextBuilderCustomizer(Function sslContextBuilderCustomizer) { this.sslContextBuilderCustomizer = Assert.requireNonNull(sslContextBuilderCustomizer, "sslContextBuilderCustomizer must not be null"); return this; } /** * Configure TCP KeepAlive. Disabled by default. * * @param enabled whether to enable/disable TCP KeepAlive * @return this {@link Builder} * @see Socket#setKeepAlive(boolean) * @since 0.8.5 */ public Builder tcpKeepAlive(boolean enabled) { this.tcpKeepAlive = enabled; return this; } /** * Configure TCP NoDelay. Enabled by default. * * @param enabled whether to enable/disable TCP NoDelay * @return this {@link Builder} * @see Socket#setTcpNoDelay(boolean) * @since 0.8.5 */ public Builder tcpNoDelay(boolean enabled) { this.tcpNoDelay = enabled; return this; } /** * Allow using SSL by fully trusting the server certificate. Enabling this option skips certificate verification. * * @return this {@link Builder}. * @see TrustAllTrustManager * @since 0.8.6 */ public Builder trustServerCertificate() { return trustServerCertificate(true); } /** * Allow using SSL by fully trusting the server certificate. Enabling this option skips certificate verification. * * @param trustServerCertificate {@code true} to trust the server certificate without further validation. * @return this {@link Builder}. * @see TrustAllTrustManager * @since 0.8.6 */ public Builder trustServerCertificate(boolean trustServerCertificate) { this.trustServerCertificate = trustServerCertificate; return this; } /** * Configure the trust store type. * * @param trustStoreType the type of the trust store to be used for SSL certificate verification. Defaults to {@link KeyStore#getDefaultType()} if not set. * @return this {@link Builder} * @throws IllegalArgumentException if {@code trustStoreType} is {@code null} * @since 0.8.3 */ public Builder trustStoreType(String trustStoreType) { this.trustStoreType = Assert.requireNonNull(trustStoreType, "trustStoreType must not be null"); return this; } /** * Configure the file path to the trust store. * * @param trustStoreFile the path of the trust store to be used for SSL certificate verification. * @return this {@link Builder} * @throws IllegalArgumentException if {@code trustStore} is {@code null} * @since 0.8.3 */ public Builder trustStore(String trustStoreFile) { return trustStore(new File(Assert.requireNonNull(trustStoreFile, "trustStore must not be null"))); } /** * Configure the path to the trust store. * * @param trustStore the path of the trust store to be used for SSL certificate verification. * @return this {@link Builder} * @throws IllegalArgumentException if {@code trustStore} is {@code null} * @since 0.8.3 */ public Builder trustStore(File trustStore) { this.trustStore = Assert.requireNonNull(trustStore, "trustStore must not be null"); return this; } /** * Configure the password to read the trust store. * * @param trustStorePassword the password to read the trust store. * @return this {@link Builder} * @since 0.8.3 */ public Builder trustStorePassword(char[] trustStorePassword) { this.trustStorePassword = Assert.requireNonNull(Arrays.copyOf(trustStorePassword, trustStorePassword.length), "trustStorePassword must not be null"); return this; } /** * Configure the username. * * @param username the username * @return this {@link Builder} * @throws IllegalArgumentException if {@code username} is {@code null} */ public Builder username(String username) { this.username = Assert.requireNonNull(username, "username must not be null"); return this; } /** * Returns a configured {@link MssqlConnectionConfiguration}. * * @return a configured {@link MssqlConnectionConfiguration}. */ public MssqlConnectionConfiguration build() { if (this.hostNameInCertificate == null) { this.hostNameInCertificate = this.host; } return new MssqlConnectionConfiguration(this.applicationName, this.connectionId, this.connectionProvider, this.connectTimeout, this.database, this.host, this.hostNameInCertificate, this.lockWaitTimeout, this.password, this.preferCursoredExecution, this.port, this.sendStringParametersAsUnicode, this.ssl, this.sslContextBuilderCustomizer, this.sslTunnelSslContextBuilderCustomizer, this.tcpKeepAlive, this.tcpNoDelay, this.trustServerCertificate, this.trustStore, this.trustStoreType, this.trustStorePassword, this.username); } } static class DefaultClientConfiguration implements ClientConfiguration { private final ConnectionProvider connectionProvider; private final Duration connectTimeout; private final String host; private final String hostNameInCertificate; private final int port; private final boolean ssl; private final Function sslContextBuilderCustomizer; @Nullable private final Function sslTunnelSslContextBuilderCustomizer; private final boolean tcpKeepAlive; private final boolean tcpNoDelay; private final boolean trustServerCertificate; @Nullable private final File trustStore; @Nullable private final String trustStoreType; @Nullable private final char[] trustStorePassword; DefaultClientConfiguration(ConnectionProvider connectionProvider, Duration connectTimeout, String host, String hostNameInCertificate, int port, boolean ssl, Function sslContextBuilderCustomizer, @Nullable Function sslTunnelSslContextBuilderCustomizer, boolean tcpKeepAlive, boolean tcpNoDelay, boolean trustServerCertificate, @Nullable File trustStore, @Nullable String trustStoreType, @Nullable char[] trustStorePassword) { this.connectTimeout = connectTimeout; this.host = host; this.hostNameInCertificate = hostNameInCertificate; this.port = port; this.ssl = ssl; this.sslContextBuilderCustomizer = sslContextBuilderCustomizer; this.sslTunnelSslContextBuilderCustomizer = sslTunnelSslContextBuilderCustomizer; this.tcpKeepAlive = tcpKeepAlive; this.tcpNoDelay = tcpNoDelay; this.trustServerCertificate = trustServerCertificate; this.trustStore = trustStore; this.trustStoreType = trustStoreType; this.trustStorePassword = trustStorePassword; this.connectionProvider = connectionProvider; } @Override public String getHost() { return this.host; } @Override public int getPort() { return this.port; } @Override public Duration getConnectTimeout() { return this.connectTimeout; } @Override public boolean isTcpKeepAlive() { return this.tcpKeepAlive; } @Override public boolean isTcpNoDelay() { return this.tcpNoDelay; } @Override public ConnectionProvider getConnectionProvider() { return this.connectionProvider; } @Override public boolean isSslEnabled() { return this.ssl; } @Override public SslContext getSslContext() throws GeneralSecurityException { SslContextBuilder sslContextBuilder = createSslContextBuilder(); TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); KeyStore ks = loadCustomTrustStore(); tmf.init(ks); TrustManager[] trustManagers = tmf.getTrustManagers(); TrustManager result; if (isSslEnabled() && !this.trustServerCertificate) { result = new ExpectedHostnameX509TrustManager((X509TrustManager) trustManagers[0], this.hostNameInCertificate); } else { result = TrustAllTrustManager.INSTANCE; } sslContextBuilder.trustManager(result); try { return this.sslContextBuilderCustomizer.apply(sslContextBuilder).build(); } catch (SSLException e) { throw new GeneralSecurityException(e); } } @Nullable KeyStore loadCustomTrustStore() throws GeneralSecurityException { if (this.trustStore == null) { return null; } KeyStore trustStoreInstance = KeyStore.getInstance(this.trustStoreType == null ? KeyStore.getDefaultType() : this.trustStoreType); try (FileInputStream fis = new FileInputStream(this.trustStore)) { trustStoreInstance.load(fis, this.trustStorePassword); return trustStoreInstance; } catch (IOException e) { throw new GeneralSecurityException(String.format("Could not load custom trust store from %s", this.trustStore), e); } } @Override public SslConfiguration getSslTunnelConfiguration() { if (this.sslTunnelSslContextBuilderCustomizer == null) { return ClientConfiguration.super.getSslTunnelConfiguration(); } return new SslConfiguration() { @Override public boolean isSslEnabled() { return true; } @Override public SslContext getSslContext() throws GeneralSecurityException { SslContextBuilder sslContextBuilder = createSslContextBuilder(); try { return DefaultClientConfiguration.this.sslTunnelSslContextBuilderCustomizer.apply(sslContextBuilder).build(); } catch (SSLException e) { throw new GeneralSecurityException(e); } } }; } } private static SslContextBuilder createSslContextBuilder() { SslContextBuilder sslContextBuilder = SslContextBuilder.forClient(); sslContextBuilder.sslProvider( OpenSsl.isAvailable() ? io.netty.handler.ssl.SslProvider.OPENSSL : io.netty.handler.ssl.SslProvider.JDK) .ciphers(null, IdentityCipherSuiteFilter.INSTANCE) .applicationProtocolConfig(null); return sslContextBuilder; } static class DefaultCursorPreference implements Predicate { static final DefaultCursorPreference INSTANCE = new DefaultCursorPreference(); @Override public boolean test(String sql) { if (sql.isEmpty()) { return false; } String lc = sql.trim().toLowerCase(Locale.ENGLISH); if (lc.contains("for xml") || lc.contains("for json")) { return false; } char c = sql.charAt(0); return (c == 's' || c == 'S') && lc.startsWith("select"); } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/MssqlConnectionFactory.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.client.Client; import io.r2dbc.mssql.client.ClientConfiguration; import io.r2dbc.mssql.client.ReactorNettyClient; import io.r2dbc.mssql.codec.DefaultCodecs; import io.r2dbc.mssql.message.tds.Redirect; import io.r2dbc.mssql.util.Assert; import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.R2dbcNonTransientResourceException; import io.r2dbc.spi.Row; import reactor.core.publisher.Mono; import java.util.function.Function; /** * An implementation of {@link ConnectionFactory} for creating connections to a Microsoft SQL Server database. * * @author Mark Paluch * @author Lars Haatveit */ public final class MssqlConnectionFactory implements ConnectionFactory { private final String METADATA_QUERY = " SELECT " + "CAST(SERVERPROPERTY('Edition') AS VARCHAR(255)) AS Edition, " + "CAST(@@VERSION AS VARCHAR(255)) as VersionString"; private final Function> clientFactory; private final MssqlConnectionConfiguration configuration; private final DefaultCodecs codecs = new DefaultCodecs(); /** * Creates a new connection factory. * * @param configuration the configuration to use connections * @throws IllegalArgumentException when {@link MssqlConnectionConfiguration} is {@code null}. */ public MssqlConnectionFactory(MssqlConnectionConfiguration configuration) { this(MssqlConnectionFactory::connect, configuration); } MssqlConnectionFactory(Function> clientFactory, MssqlConnectionConfiguration configuration) { this.clientFactory = Assert.requireNonNull(clientFactory, "clientFactory must not be null"); this.configuration = Assert.requireNonNull(configuration, "configuration must not be null"); } private static Mono connect(MssqlConnectionConfiguration configuration) { return Mono.defer(() -> { Assert.requireNonNull(configuration, "configuration must not be null"); return ReactorNettyClient.connect(configuration.toClientConfiguration(), configuration.getApplicationName(), configuration.getConnectionId()); }); } private Mono initializeClient(MssqlConnectionConfiguration configuration, boolean allowReroute) { LoginConfiguration loginConfiguration = configuration.getLoginConfiguration(); return this.clientFactory.apply(configuration) .delayUntil(client -> LoginFlow.exchange(client, loginConfiguration) .onErrorResume(e -> propagateError(client.close(), e))) .flatMap(client -> { return client.getRedirect().map(redirect -> { if (allowReroute) { return redirectClient(client, redirect); } else { return this.propagateError(client.close(), new MssqlRoutingException("Client was redirected more than once")); } }).orElse(Mono.just(client)); }); } private Mono redirectClient(Client client, Redirect redirect) { MssqlConnectionConfiguration routeConfiguration = this.configuration.withRedirect(redirect); return client.close().then(this.initializeClient(routeConfiguration, false)); } private Mono propagateError(Mono action, Throwable e) { return action.onErrorResume(suppressed -> { e.addSuppressed(suppressed); return Mono.error(e); }).then(Mono.error(e)); } @Override public Mono create() { return initializeClient(this.configuration, true) .flatMap(it -> { ConnectionOptions connectionOptions = getConnectionOptions(); Mono connectionMono = new SimpleMssqlStatement(it, connectionOptions, this.METADATA_QUERY).execute() .flatMap(result -> result.map((row, rowMetadata) -> toConnectionMetadata(it.getDatabaseVersion().orElse("unknown"), row))).map(metadata -> { return new MssqlConnection(it, metadata, connectionOptions); }).last(); if (this.configuration.getLockWaitTimeout() != null) { connectionMono = connectionMono.flatMap(connection -> connection.setLockWaitTimeout(this.configuration.getLockWaitTimeout()).thenReturn(connection)); } return connectionMono.onErrorResume(throwable -> { return it.close().then(Mono.error(new R2dbcNonTransientResourceException("Cannot connect to " + this.configuration.getHost() + ":" + this.configuration.getPort(), throwable))); }); }); } private static MssqlConnectionMetadata toConnectionMetadata(String version, Row row) { return MssqlConnectionMetadata.from(row.get("Edition", String.class), version, row.get("VersionString", String.class)); } MssqlConnectionConfiguration getConfiguration() { return this.configuration; } ClientConfiguration getClientConfiguration() { return this.configuration.toClientConfiguration(); } ConnectionOptions getConnectionOptions() { return this.configuration.toConnectionOptions(this.codecs); } @Override public MssqlConnectionFactoryMetadata getMetadata() { return MssqlConnectionFactoryMetadata.INSTANCE; } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [configuration=").append(this.configuration); sb.append(']'); return sb.toString(); } static class MssqlRoutingException extends R2dbcNonTransientResourceException { public MssqlRoutingException(String reason) { super(reason); } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/MssqlConnectionFactoryMetadata.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.spi.ConnectionFactoryMetadata; /** * An implementation of {@link ConnectionFactoryMetadata} for a Microsoft SQL Server database. * * @author Mark Paluch */ enum MssqlConnectionFactoryMetadata implements ConnectionFactoryMetadata { INSTANCE; /** * The name of the Microsoft SQL Server database product. */ public static final String NAME = "Microsoft SQL Server"; MssqlConnectionFactoryMetadata() { } @Override public String getName() { return NAME; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/MssqlConnectionFactoryProvider.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.netty.handler.ssl.SslContextBuilder; import io.r2dbc.mssql.util.Assert; import io.r2dbc.spi.ConnectionFactoryOptions; import io.r2dbc.spi.ConnectionFactoryProvider; import io.r2dbc.spi.Option; import reactor.netty.resources.ConnectionProvider; import reactor.util.Logger; import reactor.util.Loggers; import java.io.File; import java.util.UUID; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import static io.r2dbc.spi.ConnectionFactoryOptions.*; /** * An implementation of {@link ConnectionFactoryProvider} for creating {@link MssqlConnectionFactory}s. * * @author Mark Paluch * @author Paul Johe */ public final class MssqlConnectionFactoryProvider implements ConnectionFactoryProvider { private final Logger logger = Loggers.getLogger(this.getClass()); /** * Application name. */ public static final Option APPLICATION_NAME = Option.valueOf("applicationName"); /** * Connection Id */ public static final Option CONNECTION_ID = Option.valueOf("connectionId"); /** * Optional {@link reactor.netty.resources.ConnectionProvider} to control Netty configuration directly. Defaults to {@link ConnectionProvider#newConnection()}. * * @since 1.0.3 */ public static final Option CONNECTION_PROVIDER = Option.valueOf("connectionProvider"); /** * Expected Hostname in SSL certificate. Supports wildcards. */ public static final Option HOSTNAME_IN_CERTIFICATE = Option.valueOf("hostNameInCertificate"); /** * Configure whether to prefer cursored execution on a statement-by-statement basis. Value can be {@link Boolean}, a {@link Predicate}, or a {@link Class class name}. The {@link Predicate} * accepts the SQL query string and returns a boolean flag indicating preference. * {@code true} prefers cursors, {@code false} prefers direct execution. */ public static final Option PREFER_CURSORED_EXECUTION = Option.valueOf("preferCursoredExecution"); /** * Configure whether to send character data as unicode (NVARCHAR, NCHAR, NTEXT) or whether to use the database encoding. Enabled by default. * If disabled, {@link CharSequence} data is sent using the database-specific collation such as ASCII/MBCS instead of Unicode. */ public static final Option SEND_STRING_PARAMETERS_AS_UNICODE = Option.valueOf("sendStringParametersAsUnicode"); /** * Customizer {@link Function} for {@link SslContextBuilder}. * * @since 0.8.3 */ public static final Option> SSL_CONTEXT_BUILDER_CUSTOMIZER = Option.valueOf("sslContextBuilderCustomizer"); /** * Enable SSL tunnel usage to encrypt all traffic right from the connect phase by providing a customizer {@link Function}. This option is required when using a SSL tunnel (e.g. stunnel or other * SSL terminator) in front of the SQL server and it is not related to SQL Server's built-in SSL support. * * @since 0.8.5 */ public static final Option> SSL_TUNNEL = Option.valueOf("sslTunnel"); /** * Enable/Disable TCP KeepAlive. * * @since 0.8.5 */ public static final Option TCP_KEEPALIVE = Option.valueOf("tcpKeepAlive"); /** * Enable/Disable TCP NoDelay. * * @since 0.8.5 */ public static final Option TCP_NODELAY = Option.valueOf("tcpNoDelay"); /** * Allow using SSL by fully trusting the server certificate. Enabling this option skips certificate verification. * * @since 0.8.6 */ public static final Option TRUST_SERVER_CERTIFICATE = Option.valueOf("trustServerCertificate"); /** * Type of the TrustStore. * * @since 0.8.3 */ public static final Option TRUST_STORE_TYPE = Option.valueOf("trustStoreType"); /** * Path to the certificate TrustStore file. * * @since 0.8.3 */ public static final Option TRUST_STORE = Option.valueOf("trustStore"); /** * Password used to check the integrity of the TrustStore data. * * @since 0.8.3 */ public static final Option TRUST_STORE_PASSWORD = Option.valueOf("trustStorePassword"); /** * Driver option value. */ public static final String MSSQL_DRIVER = "sqlserver"; /** * Driver option value. */ public static final String ALTERNATE_MSSQL_DRIVER = "mssql"; @SuppressWarnings("unchecked") @Override public MssqlConnectionFactory create(ConnectionFactoryOptions connectionFactoryOptions) { Assert.requireNonNull(connectionFactoryOptions, "connectionFactoryOptions must not be null"); MssqlConnectionConfiguration.Builder builder = MssqlConnectionConfiguration.builder(); OptionMapper mapper = OptionMapper.create(connectionFactoryOptions); mapper.fromTyped(APPLICATION_NAME).to(builder::applicationName); mapper.from(CONNECTION_ID).map(OptionMapper::toUuid).to(builder::connectionId); mapper.fromTyped(CONNECTION_PROVIDER).to(builder::connectionProvider); mapper.from(CONNECT_TIMEOUT).map(OptionMapper::toDuration).to(builder::connectTimeout); mapper.fromTyped(DATABASE).to(builder::database); mapper.fromTyped(HOSTNAME_IN_CERTIFICATE).to(builder::hostNameInCertificate); mapper.from(LOCK_WAIT_TIMEOUT).map(OptionMapper::toDuration).to(builder::lockWaitTimeout); mapper.from(PORT).map(OptionMapper::toInteger).to(builder::port); mapper.from(PREFER_CURSORED_EXECUTION).map(OptionMapper::toStringPredicate).to(builder::preferCursoredExecution); mapper.from(SEND_STRING_PARAMETERS_AS_UNICODE).map(OptionMapper::toBoolean).to(builder::sendStringParametersAsUnicode); mapper.from(SSL).map(OptionMapper::toBoolean).to(ssl -> { if (ssl) { builder.enableSsl(); } }); mapper.fromTyped(SSL_CONTEXT_BUILDER_CUSTOMIZER).to(builder::sslContextBuilderCustomizer); mapper.from(SSL_TUNNEL).map(it -> { if (it instanceof Boolean) { if ((Boolean) it) { return Function.identity(); } return null; } return it; }).to(it -> { if (it != null) { builder.enableSslTunnel((Function) it); } }); mapper.from(TCP_KEEPALIVE).map(OptionMapper::toBoolean).to(builder::tcpKeepAlive); mapper.from(TCP_NODELAY).map(OptionMapper::toBoolean).to(builder::tcpNoDelay); mapper.from(TRUST_SERVER_CERTIFICATE).map(OptionMapper::toBoolean).to((Consumer) builder::trustServerCertificate); mapper.from(TRUST_STORE).map(OptionMapper::toFile).to(builder::trustStore); mapper.fromTyped(TRUST_STORE_TYPE).to(builder::trustStoreType); mapper.from(TRUST_STORE_PASSWORD).map(it -> it instanceof String ? ((String) it).toCharArray() : (char[]) it).to(builder::trustStorePassword); builder.host(connectionFactoryOptions.getRequiredValue(HOST).toString()); builder.password((CharSequence) connectionFactoryOptions.getRequiredValue(PASSWORD)); builder.username(connectionFactoryOptions.getRequiredValue(USER).toString()); MssqlConnectionConfiguration configuration = builder.build(); if (this.logger.isDebugEnabled()) { this.logger.debug(String.format("Creating MssqlConnectionFactory with configuration [%s] from options [%s]", configuration, connectionFactoryOptions)); } return new MssqlConnectionFactory(configuration); } @Override public boolean supports(ConnectionFactoryOptions connectionFactoryOptions) { Assert.requireNonNull(connectionFactoryOptions, "connectionFactoryOptions must not be null"); Object driver = connectionFactoryOptions.getValue(DRIVER); if (driver == null || !(driver.equals(MSSQL_DRIVER) || driver.equals(ALTERNATE_MSSQL_DRIVER))) { return false; } if (!connectionFactoryOptions.hasOption(HOST)) { return false; } if (!connectionFactoryOptions.hasOption(PASSWORD)) { return false; } return connectionFactoryOptions.hasOption(USER); } @Override public String getDriver() { return MSSQL_DRIVER; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/MssqlConnectionMetadata.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.spi.ConnectionMetadata; /** * Connection metadata for a connection connected to Microsoft SQL Server database. * * @author Mark Paluch */ public final class MssqlConnectionMetadata implements ConnectionMetadata { private final String databaseVersion; private final String databaseProductName; MssqlConnectionMetadata(String databaseProductName, String databaseVersion) { this.databaseVersion = databaseVersion; this.databaseProductName = databaseProductName; } /** * Construct {@link MssqlConnectionMetadata} from a metadata query. * * @param edition SQL Server edition. * @param version SQL Server version number. * @param spVersionOutput output of {@code @@VERSION()} function. * @return the {@link MssqlConnectionMetadata}. */ public static MssqlConnectionMetadata from(String edition, String version, String spVersionOutput) { String databaseProductName = spVersionOutput; int separator = spVersionOutput.indexOf(" - "); if (separator > -1) { databaseProductName = spVersionOutput.substring(0, separator) + " - " + edition; } return new MssqlConnectionMetadata(databaseProductName, version); } /** * Retrieves the name of this database product. * * @return database product name */ public String getDatabaseProductName() { return this.databaseProductName; } /** * Retrieves the version of this database product. * * @return database product name */ public String getDatabaseVersion() { return this.databaseVersion; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/MssqlException.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.spi.R2dbcException; /** * Interface for SQL Server-specific extension to {@link R2dbcException} providing {@link ErrorDetails}. * * @author Mark Paluch * @see ErrorDetails */ public interface MssqlException { /** * Returns additional error information. * * @return the {@link ErrorDetails}. */ ErrorDetails getErrorDetails(); } ================================================ FILE: src/main/java/io/r2dbc/mssql/MssqlIsolationLevel.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.spi.IsolationLevel; /** * SQL Server-specific transaction isolation levels. *

* For more information check: * SQL Server Isolation Levels * * @author Hebert Coelho * @author Mark Paluch * @see IsolationLevel */ public final class MssqlIsolationLevel { private MssqlIsolationLevel() { } /** * The read committed isolation level. */ public static final IsolationLevel READ_COMMITTED = IsolationLevel.READ_COMMITTED; /** * The read uncommitted isolation level. */ public static final IsolationLevel READ_UNCOMMITTED = IsolationLevel.READ_UNCOMMITTED; /** * The repeatable read isolation level. */ public static final IsolationLevel REPEATABLE_READ = IsolationLevel.REPEATABLE_READ; /** * The serializable isolation level. */ public static final IsolationLevel SERIALIZABLE = IsolationLevel.SERIALIZABLE; /** * The snapshot isolation level. */ public static final IsolationLevel SNAPSHOT = IsolationLevel.valueOf("SNAPSHOT"); /** * Unspecified isolation level. * * @since 0.9 */ public static final IsolationLevel UNSPECIFIED = IsolationLevel.valueOf("UNSPECIFIED"); } ================================================ FILE: src/main/java/io/r2dbc/mssql/MssqlResult.java ================================================ /* * Copyright 2021-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.spi.Result; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; /** * A strongly typed implementation of {@link Result} for a Microsoft SQL Server database. * * @author Mark Paluch */ public interface MssqlResult extends Result { /** * {@inheritDoc} */ @Override Mono getRowsUpdated(); /** * {@inheritDoc} */ @Override Flux map(BiFunction mappingFunction); /** * {@inheritDoc} */ @Override MssqlResult filter(Predicate filter); /** * {@inheritDoc} */ @Override Flux flatMap(Function> mappingFunction); } ================================================ FILE: src/main/java/io/r2dbc/mssql/MssqlReturnValues.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.netty.buffer.ByteBuf; import io.netty.util.ReferenceCounted; import io.r2dbc.mssql.codec.Codecs; import io.r2dbc.mssql.message.Message; import io.r2dbc.mssql.message.token.ReturnValue; import io.r2dbc.mssql.message.token.RowToken; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.util.Assert; import io.r2dbc.spi.OutParameters; import io.r2dbc.spi.Row; import reactor.util.annotation.Nullable; import java.util.List; /** * Microsoft SQL Server-specific {@link Row} implementation synthesized from {@link ReturnValue return values}. * This object is no longer usable once it was {@link #release() released}. * * @author Mark Paluch * @see #release() * @see ReferenceCounted */ final class MssqlReturnValues implements OutParameters, Message { private static final int STATE_ACTIVE = 0; private static final int STATE_RELEASED = 1; private final Codecs codecs; private final MssqlReturnValuesMetadata metadata; private final List returnValues; private volatile int state = STATE_ACTIVE; private MssqlReturnValues(Codecs codecs, List returnValues, MssqlReturnValuesMetadata metadata) { this.codecs = codecs; this.metadata = metadata; this.returnValues = returnValues; } /** * Create a new {@link MssqlReturnValues}. * * @param codecs the codecs to decode tabular data. * @param returnValues the return values. * @return */ static MssqlReturnValues toReturnValues(Codecs codecs, List returnValues) { Assert.requireNonNull(codecs, "Codecs must not be null"); Assert.requireNonNull(returnValues, "ReturnValues must not be null"); return new MssqlReturnValues(codecs, returnValues, new MssqlReturnValuesMetadata(codecs, returnValues.toArray(new ReturnValue[0]))); } /** * Returns the {@link MssqlRowMetadata} associated with this {@link Row}. * * @return the {@link MssqlRowMetadata} associated with this {@link Row}. */ public MssqlReturnValuesMetadata getMetadata() { return this.metadata; } @Override public T get(int index, Class type) { Assert.requireNonNull(type, "Type must not be null"); requireNotReleased(); ReturnValue returnValue = this.metadata.get(index); return doGet(returnValue, type); } @Override public T get(String name, Class type) { Assert.requireNonNull(name, "Name must not be null"); Assert.requireNonNull(type, "Type must not be null"); requireNotReleased(); ReturnValue returnValue = this.metadata.get(name); return doGet(returnValue, type); } @Nullable private T doGet(@Nullable ReturnValue returnValue, Class type) { if (returnValue == null) { return null; } if (returnValue.getValueType().getServerType() == SqlServerType.SQL_VARIANT) { throw new UnsupportedOperationException("sql_variant columns not supported. See https://github.com/r2dbc/r2dbc-mssql/issues/67."); } ByteBuf value = returnValue.getValue(); value.markReaderIndex(); try { return this.codecs.decode(value, returnValue.asDecodable(), type); } finally { value.resetReaderIndex(); } } /** * Decrement the reference count and release the {@link RowToken} to allow deallocation of underlying memory. */ public void release() { requireNotReleased(); this.state = STATE_RELEASED; this.returnValues.forEach(ReturnValue::release); } private void requireNotReleased() { if (this.state == STATE_RELEASED) { throw new IllegalStateException("Value cannot be retrieved after row has been released"); } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/MssqlReturnValuesMetadata.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.codec.Codecs; import io.r2dbc.mssql.message.token.ReturnValue; import io.r2dbc.mssql.util.Assert; import io.r2dbc.spi.OutParametersMetadata; import io.r2dbc.spi.RowMetadata; import reactor.util.annotation.Nullable; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Microsoft SQL Server-specific {@link RowMetadata} backed by {@link ReturnValue}. * * @author Mark Paluch */ final class MssqlReturnValuesMetadata extends NamedCollectionSupport implements OutParametersMetadata, Collection { private final Codecs codecs; @Nullable private Map metadataCache; /** * Creates a new {@link MssqlReturnValuesMetadata}. * * @param codecs the codec registry. * @param returnValues collection of {@link ReturnValue}s. */ MssqlReturnValuesMetadata(Codecs codecs, ReturnValue[] returnValues) { super(returnValues, toMap(returnValues, ReturnValue::getParameterName), ReturnValue::getParameterName, "return value"); this.codecs = Assert.requireNonNull(codecs, "Codecs must not be null"); } /** * Creates a new {@link MssqlReturnValuesMetadata}. * * @param codecs the codec registry. * @param columnMetadata the column metadata. */ public static MssqlReturnValuesMetadata create(Codecs codecs, List returnValues) { Assert.notNull(returnValues, "ReturnValues must not be null"); return new MssqlReturnValuesMetadata(codecs, returnValues.toArray(new ReturnValue[0])); } @Override public MssqlColumnMetadata getParameterMetadata(int index) { if (this.metadataCache == null) { this.metadataCache = new HashMap<>(); } return this.metadataCache.computeIfAbsent(this.get(index), returnValue -> new MssqlColumnMetadata(returnValue.asDecodable(), this.codecs)); } @Override public MssqlColumnMetadata getParameterMetadata(String identifier) { if (this.metadataCache == null) { this.metadataCache = new HashMap<>(); } return this.metadataCache.computeIfAbsent(this.get(identifier), returnValue -> new MssqlColumnMetadata(returnValue.asDecodable(), this.codecs)); } @Override public List getParameterMetadatas() { if (this.metadataCache == null) { this.metadataCache = new HashMap<>(); } List metadatas = new ArrayList<>(this.getCount()); for (int i = 0; i < this.getCount(); i++) { MssqlColumnMetadata columnMetadata = this.metadataCache.computeIfAbsent(this.get(i), returnValue -> new MssqlColumnMetadata(returnValue.asDecodable(), this.codecs)); metadatas.add(columnMetadata); } return metadatas; } @Override ReturnValue find(String name) { ReturnValue returnValue = super.find(name); if (returnValue == null && !name.startsWith("@")) { return super.find("@" + name); } return returnValue; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/MssqlRow.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.netty.buffer.ByteBuf; import io.netty.util.ReferenceCounted; import io.r2dbc.mssql.codec.Codecs; import io.r2dbc.mssql.message.token.Column; import io.r2dbc.mssql.message.token.RowToken; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.util.Assert; import io.r2dbc.spi.Row; import reactor.util.annotation.Nullable; /** * Microsoft SQL Server-specific {@link Row} implementation. * A {@link Row} is stateful regarding its data state. It holds a {@link RowToken} along with row data that needs to be deallocated after processing the row. This row is no longer usable once it * was {@link #release() released}. * * @author Mark Paluch * @see #release() * @see ReferenceCounted */ final class MssqlRow implements Row { private static final int STATE_ACTIVE = 0; private static final int STATE_RELEASED = 1; private final Codecs codecs; private final MssqlRowMetadata metadata; private final RowToken rowToken; private volatile int state = STATE_ACTIVE; MssqlRow(Codecs codecs, RowToken rowToken, MssqlRowMetadata metadata) { this.codecs = codecs; this.metadata = metadata; this.rowToken = rowToken; } /** * Create a new {@link MssqlRow}. * * @param codecs the codecs to decode tabular data. * @param rowToken the row data. * @param columns column specifications. * @return */ static MssqlRow toRow(Codecs codecs, RowToken rowToken, MssqlRowMetadata metadata) { Assert.requireNonNull(codecs, "Codecs must not be null"); Assert.requireNonNull(rowToken, "RowToken must not be null"); Assert.requireNonNull(metadata, "MssqlRowMetadata must not be null"); return new MssqlRow(codecs, rowToken, metadata); } /** * Returns the {@link MssqlRowMetadata} associated with this {@link Row}. * * @return the {@link MssqlRowMetadata} associated with this {@link Row}. */ public MssqlRowMetadata getMetadata() { return this.metadata; } @Override public T get(int index, Class type) { Assert.requireNonNull(type, "Type must not be null"); requireNotReleased(); Column column = this.metadata.get(index); return doGet(column, type); } @Override public T get(String name, Class type) { Assert.requireNonNull(name, "Name must not be null"); Assert.requireNonNull(type, "Type must not be null"); requireNotReleased(); Column column = this.metadata.get(name); return doGet(column, type); } @Nullable private T doGet(Column column, Class type) { ByteBuf columnData = this.rowToken.getColumnData(column.getIndex()); if (columnData == null) { return null; } if (column.getType().getServerType() == SqlServerType.SQL_VARIANT) { throw new UnsupportedOperationException("sql_variant columns not supported. See https://github.com/r2dbc/r2dbc-mssql/issues/67."); } columnData.markReaderIndex(); try { return this.codecs.decode(columnData, column, type); } finally { columnData.resetReaderIndex(); } } /** * Decrement the reference count and release the {@link RowToken} to allow deallocation of underlying memory. */ public void release() { requireNotReleased(); this.state = STATE_RELEASED; this.rowToken.release(); } private void requireNotReleased() { if (this.state == STATE_RELEASED) { throw new IllegalStateException("Value cannot be retrieved after row has been released"); } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/MssqlRowMetadata.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.codec.Codecs; import io.r2dbc.mssql.message.token.Column; import io.r2dbc.mssql.message.token.ColumnMetadataToken; import io.r2dbc.mssql.util.Assert; import io.r2dbc.spi.RowMetadata; import reactor.util.annotation.Nullable; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Microsoft SQL Server-specific {@link RowMetadata}. * * @author Mark Paluch */ final class MssqlRowMetadata extends NamedCollectionSupport implements RowMetadata, Collection { private final Codecs codecs; @Nullable private Map metadataCache; /** * Creates a new {@link MssqlColumnMetadata}. * * @param codecs the codec registry. * @param columns collection of {@link Column}s. * @param nameKeyedColumns name-keyed {@link Map} of {@link Column}s. */ MssqlRowMetadata(Codecs codecs, Column[] columns, Map nameKeyedColumns) { super(columns, nameKeyedColumns, Column::getName, "column"); this.codecs = Assert.requireNonNull(codecs, "Codecs must not be null"); } /** * Creates a new {@link MssqlColumnMetadata}. * * @param codecs the codec registry. * @param columnMetadata the column metadata. */ public static MssqlRowMetadata create(Codecs codecs, ColumnMetadataToken columnMetadata) { Assert.notNull(columnMetadata, "ColumnMetadata must not be null"); return new MssqlRowMetadata(codecs, columnMetadata.getColumns(), columnMetadata.toMap()); } @Override public MssqlColumnMetadata getColumnMetadata(int index) { if (this.metadataCache == null) { this.metadataCache = new HashMap<>(); } return this.metadataCache.computeIfAbsent(this.get(index), column -> new MssqlColumnMetadata(column, this.codecs)); } @Override public MssqlColumnMetadata getColumnMetadata(String identifier) { if (this.metadataCache == null) { this.metadataCache = new HashMap<>(); } return this.metadataCache.computeIfAbsent(this.get(identifier), column -> new MssqlColumnMetadata(column, this.codecs)); } @Override public List getColumnMetadatas() { if (this.metadataCache == null) { this.metadataCache = new HashMap<>(); } List metadatas = new ArrayList<>(this.getCount()); for (int i = 0; i < this.getCount(); i++) { MssqlColumnMetadata columnMetadata = this.metadataCache.computeIfAbsent(this.get(i), column -> new MssqlColumnMetadata(column, this.codecs)); metadatas.add(columnMetadata); } return metadatas; } @Override public boolean contains(String columnName) { return find(columnName) != null; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/MssqlSegmentResult.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.netty.util.AbstractReferenceCounted; import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCounted; import io.r2dbc.mssql.client.ConnectionContext; import io.r2dbc.mssql.codec.Codecs; import io.r2dbc.mssql.message.token.AbstractDoneToken; import io.r2dbc.mssql.message.token.AbstractInfoToken; import io.r2dbc.mssql.message.token.ColumnMetadataToken; import io.r2dbc.mssql.message.token.ErrorToken; import io.r2dbc.mssql.message.token.NbcRowToken; import io.r2dbc.mssql.message.token.ReturnValue; import io.r2dbc.mssql.message.token.RowToken; import io.r2dbc.mssql.util.Assert; import io.r2dbc.spi.OutParameters; import io.r2dbc.spi.R2dbcException; import io.r2dbc.spi.Readable; import io.r2dbc.spi.Result; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.Logger; import reactor.util.Loggers; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; /** * {@link Result} of query results. * * @author Mark Paluch * @since 0.9 */ final class MssqlSegmentResult implements MssqlResult { private static final Logger LOGGER = Loggers.getLogger(MssqlSegmentResult.class); public static final boolean DEBUG_ENABLED = LOGGER.isDebugEnabled(); private final String sql; private final ConnectionContext context; private final Codecs codecs; private final Flux segments; private MssqlSegmentResult(String sql, ConnectionContext context, Codecs codecs, Flux segments) { this.sql = sql; this.context = context; this.codecs = codecs; this.segments = segments; } /** * Create a {@link MssqlSegmentResult}. * * @param sql the underlying SQL statement. * @param codecs the codecs to use. * @param messages message stream. * @param expectReturnValues {@code true} if the result is expected to have result values. * @return {@link Result} object. */ static MssqlSegmentResult toResult(String sql, ConnectionContext context, Codecs codecs, Flux messages, boolean expectReturnValues) { Assert.requireNonNull(sql, "SQL must not be null"); Assert.requireNonNull(codecs, "Codecs must not be null"); Assert.requireNonNull(context, "ConnectionContext must not be null"); Assert.requireNonNull(messages, "Messages must not be null"); LOGGER.debug(context.getMessage("Creating new result")); return new MssqlSegmentResult(sql, context, codecs, toSegments(sql, codecs, messages, expectReturnValues)); } private static Flux toSegments(String sql, Codecs codecs, Flux messages, boolean expectReturnValues) { Flux returnValueStream = Flux.empty(); Flux messageStream = messages; if (expectReturnValues) { List returnValues = new ArrayList<>(); messageStream = messageStream.doOnNext(message -> { if (message instanceof ReturnValue) { returnValues.add((ReturnValue) message); } }).filter(it -> !(it instanceof ReturnValue)); returnValueStream = Flux.defer(() -> { if (returnValues.size() != 0) { return Flux.just(new MsqlOutSegment(codecs, returnValues)); } return Flux.empty(); }); } AtomicReference metadataRef = new AtomicReference<>(); Flux segments = messageStream.handle((message, sink) -> { if (message instanceof AbstractDoneToken) { AbstractDoneToken doneToken = (AbstractDoneToken) message; if (doneToken.isAttentionAck()) { sink.error(new ExceptionFactory.MssqlStatementCancelled(sql)); return; } } if (message.getClass() == ColumnMetadataToken.class) { ColumnMetadataToken token = (ColumnMetadataToken) message; if (token.hasColumns()) { metadataRef.set(MssqlRowMetadata.create(codecs, token)); } return; } if (message.getClass() == RowToken.class || message.getClass() == NbcRowToken.class) { MssqlRowMetadata rowMetadata = metadataRef.get(); if (rowMetadata == null) { return; } sink.next(new MssqlRowSegment(codecs, (RowToken) message, rowMetadata)); return; } if (message instanceof AbstractInfoToken) { sink.next(createMessage(sql, (AbstractInfoToken) message)); return; } if (message instanceof AbstractDoneToken) { AbstractDoneToken doneToken = (AbstractDoneToken) message; if (doneToken.hasCount()) { sink.next(doneToken); } } ReferenceCountUtil.release(message); }); if (expectReturnValues) { segments = segments.concatWith(returnValueStream); } return segments; } @Override public Mono getRowsUpdated() { return this.segments .handle((segment, sink) -> { if (segment instanceof UpdateCount) { UpdateCount updateCount = (UpdateCount) segment; if (DEBUG_ENABLED) { LOGGER.debug(this.context.getMessage("Incoming row count: {}"), updateCount); } sink.next(updateCount.value()); } if (isError(segment)) { sink.error(((Message) segment).exception()); return; } ReferenceCountUtil.release(segment); }).reduce(Long::sum); } @Override public Flux map(BiFunction mappingFunction) { Assert.requireNonNull(mappingFunction, "Mapping function must not be null"); return doMap(true, false, readable -> { Row row = (Row) readable; return mappingFunction.apply(row, row.getMetadata()); }); } @Override public Flux map(Function mappingFunction) { Assert.requireNonNull(mappingFunction, "Mapping function must not be null"); return doMap(true, true, mappingFunction); } private Flux doMap(boolean rows, boolean outparameters, Function mappingFunction) { return this.segments .handle((segment, sink) -> { if (rows && segment instanceof RowSegment) { RowSegment data = (RowSegment) segment; Row row = data.row(); try { sink.next(mappingFunction.apply(row)); } finally { ReferenceCountUtil.release(data); } return; } if (outparameters && segment instanceof OutSegment) { OutSegment data = (OutSegment) segment; OutParameters outParameters = data.outParameters(); try { sink.next(mappingFunction.apply(outParameters)); } finally { ReferenceCountUtil.release(data); } return; } if (isError(segment)) { sink.error(((Message) segment).exception()); return; } ReferenceCountUtil.release(segment); }); } @Override public MssqlResult filter(Predicate filter) { Assert.requireNonNull(filter, "filter must not be null"); Flux filteredSegments = this.segments.filter(message -> { if (filter.test(message)) { return true; } ReferenceCountUtil.release(message); return false; }); return new MssqlSegmentResult(this.sql, this.context, this.codecs, filteredSegments); } @Override @SuppressWarnings("unchecked") public Flux flatMap(Function> mappingFunction) { Assert.requireNonNull(mappingFunction, "mappingFunction must not be null"); return this.segments .concatMap(segment -> { Publisher result = mappingFunction.apply(segment); if (result == null) { return Mono.error(new IllegalStateException("The mapper returned a null Publisher")); } // doAfterTerminate to not release resources before they had a chance to get emitted if (result instanceof Mono) { return ((Mono) result).doFinally(s -> ReferenceCountUtil.release(segment)); } return Flux.from(result).doFinally(s -> ReferenceCountUtil.release(segment)); }); } private boolean isError(Segment segment) { return segment instanceof MssqlMessage && ((MssqlMessage) segment).isError(); } private static Message createMessage(String sql, AbstractInfoToken message) { ErrorDetails errorDetails = ExceptionFactory.createErrorDetails(message); return new MssqlMessage(message, sql, errorDetails); } static class MssqlMessage implements Message { private final ErrorDetails errorDetails; private final AbstractInfoToken message; private final String sql; public MssqlMessage(AbstractInfoToken message, String sql, ErrorDetails errorDetails) { this.message = message; this.sql = sql; this.errorDetails = errorDetails; } @Override public R2dbcException exception() { return ExceptionFactory.createException(this.message, this.sql); } @Override public int errorCode() { return (int) this.errorDetails.getNumber(); } @Override public String sqlState() { return this.errorDetails.getStateCode(); } @Override public String message() { return this.errorDetails.getMessage(); } public boolean isError() { return this.message instanceof ErrorToken; } } private static class MssqlRowSegment extends AbstractReferenceCounted implements RowSegment { private final RowToken rowToken; private final MssqlRow row; public MssqlRowSegment(Codecs codecs, RowToken rowToken, MssqlRowMetadata rowMetadata) { this.rowToken = rowToken; this.row = MssqlRow.toRow(codecs, this.rowToken, rowMetadata); } @Override public Row row() { return this.row; } @Override public ReferenceCounted touch(Object hint) { return this; } @Override protected void deallocate() { this.rowToken.release(); } } private static class MsqlOutSegment extends AbstractReferenceCounted implements OutSegment { private final MssqlReturnValues returnValues; public MsqlOutSegment(Codecs codecs, List returnValues) { this.returnValues = MssqlReturnValues.toReturnValues(codecs, returnValues); } @Override public OutParameters outParameters() { return this.returnValues; } @Override public ReferenceCounted touch(Object hint) { return this; } @Override protected void deallocate() { this.returnValues.release(); } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/MssqlStatement.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.spi.Statement; import reactor.core.publisher.Flux; /** * A strongly typed implementation of {@link Statement} for a Microsoft SQL Server database. *

* Microsoft SQL Server uses named parameters for parametrized statements: *

 * INSERT INTO person (id, first_name, last_name) VALUES(@id, @firstname, @lastname)
 * 
* Use {@link #bind(String, Object)} and {@link #bindNull(String, Class)} over positional ({@link #bind(int, Object)}) binding. * * @author Mark Paluch */ public interface MssqlStatement extends Statement { /** * {@inheritDoc} */ @Override MssqlStatement add(); /** * {@inheritDoc} */ @Override MssqlStatement bind(int index, Object value); /** * {@inheritDoc} */ @Override MssqlStatement bindNull(int index, Class type); /** * {@inheritDoc} */ @Override MssqlStatement bind(String identifier, Object value); /** * {@inheritDoc} */ @Override MssqlStatement bindNull(String identifier, Class type); /** * {@inheritDoc} */ @Override Flux execute(); /** * {@inheritDoc} */ @Override MssqlStatement returnGeneratedValues(String... columns); /** * {@inheritDoc} */ @Override MssqlStatement fetchSize(int fetchSize); } ================================================ FILE: src/main/java/io/r2dbc/mssql/MssqlStatementSupport.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.client.Client; import io.r2dbc.mssql.message.Message; import io.r2dbc.mssql.util.Assert; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; import reactor.util.annotation.Nullable; import java.sql.Statement; import java.time.Duration; import java.util.concurrent.TimeoutException; /** * Base class for {@link Statement} implementations. *

This class considers {@link #returnGeneratedValues(String...)} and {@link #fetchSize(int)} and cursor/direct execution preferences. *

Cursor/direct execution preference is considered as initial hint. A statement can be forced to be executed directly by setting {@link #fetchSize(int)} to zero. Alternatively, cursored * execution can be forced by setting {@link #fetchSize(int)} to a non-zero value. * * @author Mark Paluch */ abstract class MssqlStatementSupport implements MssqlStatement { static final int FETCH_SIZE = 128; static final int FETCH_UNCONFIGURED = -1; private final boolean preferCursoredExecution; @Nullable private String[] generatedColumns; private int fetchSize = FETCH_UNCONFIGURED; MssqlStatementSupport(boolean preferCursoredExecution) { this.preferCursoredExecution = preferCursoredExecution; } /** * Returns the effective fetch size. *

* A fetch size of zero indicates direct execution. * * @return the effective fetch size. Can be zero. Defaults to {@link #FETCH_SIZE} if cursored execution is preferred. */ int getEffectiveFetchSize() { if (this.preferCursoredExecution) { return this.fetchSize == FETCH_UNCONFIGURED ? FETCH_SIZE : this.fetchSize; } return this.fetchSize == FETCH_UNCONFIGURED ? 0 : this.fetchSize; } @Nullable String[] getGeneratedColumns() { return this.generatedColumns; } @Override public MssqlStatementSupport returnGeneratedValues(String... columns) { Assert.requireNonNull(columns, "columns must not be null"); this.generatedColumns = columns; return this; } @Override public MssqlStatementSupport fetchSize(int fetchSize) { Assert.isTrue(fetchSize >= 0, "Fetch size must be greater or equal to zero"); this.fetchSize = fetchSize; return this; } Flux potentiallyAttachTimeout(Flux exchange, ConnectionOptions connectionOptions, Client client, String sql) { Duration statementTimeout = connectionOptions.getStatementTimeout(); if (statementTimeout.isZero()) { return exchange; } Mono timeout = Mono.delay(statementTimeout, Schedulers.parallel()).onErrorReturn(0L); return exchange.timeout(timeout).onErrorResume(TimeoutException.class, e -> client.attention().then(Mono.error(new ExceptionFactory.MssqlStatementTimeoutException(String.format("Statement " + "did not yield a result within %dms", statementTimeout.toMillis()), sql)))); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/NamedCollectionSupport.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.message.token.Column; import io.r2dbc.mssql.util.Assert; import reactor.util.annotation.Nullable; import java.lang.reflect.Array; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; import java.util.function.Function; import java.util.stream.Collectors; /** * Object that provides access to items (columns, return values) by index and by name. The index starts at {@literal 0} (zero-based index). * * @author Mark Paluch */ abstract class NamedCollectionSupport implements Collection { private final N[] items; private final Map nameKeyed; private final Function nameMapper; private final String itemName; @SuppressWarnings("unchecked") NamedCollectionSupport(N[] items, Map nameKeyed, Function nameMapper, String itemName) { this.nameMapper = nameMapper; this.itemName = itemName; if (shouldStripROWSTAT(items)) { this.items = (N[]) Array.newInstance(items.getClass().getComponentType(), items.length - 1); System.arraycopy(items, 0, this.items, 0, this.items.length); this.nameKeyed = toMap(this.items, nameMapper); } else { this.items = items; this.nameKeyed = nameKeyed; } } static Map toMap(N[] named, Function nameMapper) { Map nameKeyed = new HashMap<>(named.length, 1); for (N n : named) { N old = nameKeyed.put(nameMapper.apply(n), n); if (old != null) { nameKeyed.put(nameMapper.apply(n), old); } } return nameKeyed; } // Hide ROWSTAT column from metatada if it's the last column. Typically synthesized in cursored fetch. private boolean shouldStripROWSTAT(N[] columns) { return columns.length > 0 && "ROWSTAT".equals(this.nameMapper.apply(columns[columns.length - 1])); } /** * Lookup {@link Column} by index or by its name. * * @param identifier the index or name. * @return the identifier. * @throws IllegalArgumentException if the item cannot be retrieved. * @throws IllegalArgumentException when {@code identifier} is {@code null}. */ N get(Object identifier) { Assert.requireNonNull(identifier, "Identifier must not be null"); if (identifier instanceof Integer) { return get((int) identifier); } if (identifier instanceof String) { return get((String) identifier); } throw new IllegalArgumentException(String.format("Identifier [%s] is not a valid identifier. Should either be an Integer index or a String %s name.", identifier, this.itemName)); } /** * Lookup item by its {@code index}. * * @param index the item index. Must be greater zero and less than {@link #getCount()}. * @return the item. */ N get(int index) { if (this.items.length > index && index >= 0) { return this.items[index]; } throw new IndexOutOfBoundsException(String.format("Index [%d] is larger than the number of %ss [%d]", index, this.itemName, this.items.length)); } /** * Lookup item by its {@code name}. * * @param name the item name. * @return the item. */ N get(String name) { N item = find(name); if (item == null) { throw new NoSuchElementException(String.format("[%s] does not exist in %s names %s", name, this.itemName, this.nameKeyed.keySet())); } return item; } /** * Lookup item by its {@code name}. * * @param name the item name. * @return the item. */ @Nullable N find(String name) { N item = this.nameKeyed.get(name); if (item == null) { name = EscapeAwareNameMatcher.find(name, this.nameKeyed.keySet()); if (name != null) { item = this.nameKeyed.get(name); } } return item; } /** * Returns the number of items. * * @return the number of itemss. */ int getCount() { return this.items.length; } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [").append(Arrays.stream(this.items).map(this.nameMapper).collect(Collectors.joining(", "))).append("]"); return sb.toString(); } @Override public int size() { return this.getCount(); } @Override public boolean isEmpty() { return size() == 0; } @Override public boolean contains(Object o) { if (o instanceof String) { return this.find((String) o) != null; } return false; } @Override public boolean containsAll(Collection c) { for (Object o : c) { if (!contains(o)) { return false; } } return true; } @Override public Iterator iterator() { N[] items = this.items; return new Iterator() { int index = 0; @Override public boolean hasNext() { return items.length > this.index; } @Override public String next() { N item = items[this.index++]; return NamedCollectionSupport.this.nameMapper.apply(item); } }; } @Override @SuppressWarnings("unchecked") public T[] toArray(T[] a) { if (a.length < size()) { a = (T[]) Array.newInstance(a.getClass().getComponentType(), size()); } for (int i = 0; i < size(); i++) { a[i] = (T) this.nameMapper.apply(this.get(i)); } return a; } @Override public Object[] toArray() { Object[] result = new Object[size()]; for (int i = 0; i < size(); i++) { result[i] = this.nameMapper.apply(this.get(i)); } return result; } @Override public boolean add(String s) { throw new UnsupportedOperationException(); } @Override public boolean remove(Object o) { throw new UnsupportedOperationException(); } @Override public boolean addAll(Collection c) { throw new UnsupportedOperationException(); } @Override public boolean removeAll(Collection c) { throw new UnsupportedOperationException(); } @Override public boolean retainAll(Collection c) { throw new UnsupportedOperationException(); } @Override public void clear() { throw new UnsupportedOperationException(); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/OptionMapper.java ================================================ /* * Copyright 2020-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.util.Assert; import io.r2dbc.spi.ConnectionFactoryOptions; import io.r2dbc.spi.Option; import java.io.File; import java.time.Duration; import java.util.UUID; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; /** * An utility data parser for {@link Option}. * * @author Mark Paluch * @since 0.8.3 */ final class OptionMapper { private final ConnectionFactoryOptions options; private OptionMapper(ConnectionFactoryOptions options) { this.options = options; } /** * Construct a new {@link OptionMapper} given {@link ConnectionFactoryOptions}. * * @param options must not be {@code null}. * @return the option mapper. */ public static OptionMapper create(ConnectionFactoryOptions options) { return new OptionMapper(options); } /** * Construct a new {@link Source} for a {@link Option}. Options without a value are not bound or mapped in the later stages of {@link Source}. * * @param option the option to apply. * @param inferred option type. * @return the source object. */ public Source from(Option option) { if (this.options.hasOption(option)) { return new AvailableSource<>(() -> { return this.options.getRequiredValue(option); }, option.name()); } return NullSource.instance(); } /** * Construct a new {@link Source} for a {@link Option} using type inference. Options without a value are not bound or mapped in the later stages of {@link Source}. * * @param option the option to apply. * @param inferred option type. * @return the source object. */ @SuppressWarnings("unchecked") public Source fromTyped(Option option) { if (this.options.hasOption(option)) { return new AvailableSource<>(() -> { return (T) this.options.getRequiredValue(option); }, option.name()); } return NullSource.instance(); } /** * Parse an {@link Option} to boolean. */ static boolean toBoolean(Object value) { if (value instanceof Boolean) { return ((Boolean) value).booleanValue(); } if (value instanceof String) { return Boolean.parseBoolean(value.toString()); } throw new IllegalArgumentException(String.format("Cannot convert value %s to boolean", value)); } /** * Parse an ISO-8601 formatted {@link Option} to {@link Duration}. */ static Duration toDuration(Object value) { if (value instanceof Duration) { return ((Duration) value); } if (value instanceof String) { return Duration.parse(value.toString()); } throw new IllegalArgumentException(String.format("Cannot convert value %s to Duration", value)); } /** * Parse an {@link Option} to {@link File}. */ static File toFile(Object value) { if (value instanceof File) { return (File) value; } if (value instanceof String) { return new File(value.toString()); } throw new IllegalArgumentException(String.format("Cannot convert value %s to File", value)); } /** * Parse an {@link Option} to int. */ static int toInteger(Object value) { if (value instanceof Number) { return ((Number) value).intValue(); } if (value instanceof String) { return Integer.parseInt(value.toString()); } throw new IllegalArgumentException(String.format("Cannot convert value %s to integer", value)); } /** * Parse an {@link Option} to {@link Predicate}. */ @SuppressWarnings("unchecked") static Predicate toStringPredicate(Object value) { if (value instanceof Predicate) { return (Predicate) value; } if (value instanceof Boolean) { boolean choice = (boolean) value; return s -> choice; } if (value instanceof String) { String stringValue = value.toString(); if ("true".equalsIgnoreCase(stringValue) || "false".equalsIgnoreCase(stringValue)) { return toStringPredicate(Boolean.parseBoolean(stringValue)); } else { try { Object predicate = Class.forName(stringValue).getDeclaredConstructor().newInstance(); if (predicate instanceof Predicate) { return toStringPredicate(predicate); } else { throw new IllegalArgumentException("Value '" + value + "' must be an instance of Predicate"); } } catch (ReflectiveOperationException e) { throw new IllegalArgumentException("Cannot instantiate '" + value + "'", e); } } } throw new IllegalArgumentException(String.format("Cannot convert value %s to Predicate", value)); } /** * Parse an {@link Option} to {@link UUID}. */ static UUID toUuid(Object value) { if (value instanceof UUID) { return (UUID) value; } if (value instanceof String) { return UUID.fromString(value.toString()); } throw new IllegalArgumentException(String.format("Cannot convert value %s to UUID", value)); } public interface Source { /** * Return an mapped version of the source changed via the given mapping function. * * @param the resulting type * @param mappingFunction the mapping function to apply * @return a new adapted source instance */ Source map(Function mappingFunction); /** * Complete the mapping by passing any non-filtered value to the specified * consumer. * * @param consumer the consumer that should accept the value if it's not been * filtered */ Otherwise to(Consumer consumer); /** * Complete the mapping by passing any non-filtered value to the specified * consumer. * * @param consumer the runnable that should be invoked. */ Otherwise to(Runnable consumer); } public interface Otherwise { /** * Invoked if the previous {@link Source} outcome did not match. * * @param consumer the runnable that should be invoked. */ void otherwise(Runnable consumer); } private enum Otherwises implements Otherwise { NONE { @Override public void otherwise(Runnable consumer) { // no-op } }, FALLBACK { @Override public void otherwise(Runnable consumer) { consumer.run(); } } } @SuppressWarnings({"unchecked", "rawtypes"}) private enum NullSource implements Source { INSTANCE; public static Source instance() { return (Source) INSTANCE; } @Override public Source map(Function mappingFunction) { return (Source) this; } @Override public Otherwise to(Consumer consumer) { return Otherwises.FALLBACK; } @Override public Otherwise to(Runnable consumer) { return Otherwises.FALLBACK; } } private static class AvailableSource implements Source { private final Supplier supplier; private final String optionName; private AvailableSource(Supplier supplier, String optionName) { this.supplier = supplier; this.optionName = optionName; } /** * Return an mapped version of the source changed via the given mapping function. * * @param the resulting type. * @param mappingFunction the mapping function to apply. * @return a new mapped source instance. */ @Override public Source map(Function mappingFunction) { Assert.requireNonNull(mappingFunction, "Mapping function must not be null"); Supplier supplier = () -> mappingFunction.apply(this.supplier.get()); return new AvailableSource<>(supplier, this.optionName); } /** * Complete the mapping by passing any non-filtered value to the specified * consumer. * * @param consumer the consumer that should accept the value. */ @Override public Otherwise to(Consumer consumer) { Assert.requireNonNull(consumer, "Consumer must not be null"); try { T value = this.supplier.get(); if (value != null) { consumer.accept(value); return Otherwises.NONE; } } catch (Exception e) { throw new IllegalArgumentException(String.format("Cannot assign option %s", this.optionName), e); } return Otherwises.FALLBACK; } @Override public Otherwise to(Runnable consumer) { return to(ignore -> consumer.run()); } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/ParametrizedMssqlStatement.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.client.Client; import io.r2dbc.mssql.client.ConnectionContext; import io.r2dbc.mssql.codec.Codecs; import io.r2dbc.mssql.codec.Encoded; import io.r2dbc.mssql.codec.RpcDirection; import io.r2dbc.mssql.codec.RpcParameterContext; import io.r2dbc.mssql.message.Message; import io.r2dbc.mssql.message.token.AbstractDoneToken; import io.r2dbc.mssql.message.token.DoneInProcToken; import io.r2dbc.mssql.util.Assert; import io.r2dbc.spi.Clob; import io.r2dbc.spi.Parameter; import io.r2dbc.spi.Statement; import reactor.core.publisher.Flux; import reactor.core.publisher.Sinks; import reactor.util.Logger; import reactor.util.Loggers; import reactor.util.annotation.Nullable; import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Parametrized {@link Statement} with parameter markers executed against a Microsoft SQL Server database. *

* T-SQL uses named parameters that are at-prefixed ({@literal @}). Examples for parameter names are: *

 * @p0
 *
 * @myparam
 *
 * @first_name
 * 
* * @author Mark Paluch */ final class ParametrizedMssqlStatement extends MssqlStatementSupport implements MssqlStatement { private static final Logger LOGGER = Loggers.getLogger(ParametrizedMssqlStatement.class); private static final boolean DEBUG_ENABLED = LOGGER.isDebugEnabled(); private static final Pattern PARAMETER_MATCHER = Pattern.compile("@([\\p{Alpha}@][@$\\d\\w_]{0,127})"); private final PreparedStatementCache statementCache; private final Client client; private final ConnectionOptions connectionOptions; private final ConnectionContext context; private final Codecs codecs; private final ParsedQuery parsedQuery; private final Bindings bindings = new Bindings(); private final boolean sendStringParametersAsUnicode; private volatile boolean executed = false; ParametrizedMssqlStatement(Client client, ConnectionOptions connectionOptions, String sql) { super(connectionOptions.prefersCursors(sql)); this.connectionOptions = connectionOptions; Assert.requireNonNull(client, "Client must not be null"); Assert.requireNonNull(connectionOptions, "ConnectionOptions must not be null"); Assert.requireNonNull(sql, "SQL must not be null"); this.statementCache = connectionOptions.getPreparedStatementCache(); this.client = client; this.context = client.getContext(); this.codecs = connectionOptions.getCodecs(); this.parsedQuery = this.statementCache.getParsedSql(sql, ParsedQuery::parse); this.sendStringParametersAsUnicode = connectionOptions.isSendStringParametersAsUnicode(); } @Override public ParametrizedMssqlStatement add() { assertNotExecuted(); this.bindings.finish(); this.bindings.getCurrent(); return this; } @Override public Flux execute() { int effectiveFetchSize = getEffectiveFetchSize(); return Flux.defer(() -> { assertNotExecuted(); this.executed = true; boolean useGeneratedKeysClause = GeneratedValues.shouldExpectGeneratedKeys(this.getGeneratedColumns()); String sql = useGeneratedKeysClause ? GeneratedValues.augmentQuery(this.parsedQuery.sql, getGeneratedColumns()) : this.parsedQuery.sql; if (this.bindings.bindings.isEmpty()) { Flux exchange = potentiallyAttachTimeout(QueryMessageFlow.exchange(this.client, sql), this.connectionOptions, this.client, sql); return exchange.windowUntil(AbstractDoneToken.class::isInstance).map(it -> DefaultMssqlResult.toResult(this.parsedQuery.getSql(), this.context, this.codecs, it, false)); } if (this.bindings.bindings.size() == 1) { Binding binding = this.bindings.bindings.get(0); Flux exchange = exchange(effectiveFetchSize, useGeneratedKeysClause, sql, binding); return exchange.windowUntil(DoneInProcToken.class::isInstance).map(it -> DefaultMssqlResult.toResult(this.parsedQuery.getSql(), this.context, this.codecs, it, binding.hasOutParameters())); } Sinks.Many sink = Sinks.many().unicast().onBackpressureBuffer(); Iterator iterator = this.bindings.bindings.iterator(); AtomicBoolean cancelled = new AtomicBoolean(); return sink.asFlux().flatMap(binding -> { Flux exchange = exchange(effectiveFetchSize, useGeneratedKeysClause, sql, binding); return exchange.doOnComplete(() -> { tryNextBinding(iterator, sink, cancelled); }).windowUntil(DoneInProcToken.class::isInstance).map(it -> DefaultMssqlResult.toResult(this.parsedQuery.getSql(), this.context, this.codecs, it, binding.hasOutParameters())); }).doOnSubscribe(it -> { Binding initial = iterator.next(); sink.emitNext(initial, Sinks.EmitFailureHandler.FAIL_FAST); }) .doOnCancel(() -> { cancelled.set(true); clearBindings(iterator); }) .doOnError(e -> clearBindings(iterator)); }); } private Flux exchange(int effectiveFetchSize, boolean useGeneratedKeysClause, String sql, Binding it) { Flux exchange; if (effectiveFetchSize > 0) { if (DEBUG_ENABLED) { LOGGER.debug(this.context.getMessage("Start cursored exchange for {} with fetch size {}"), sql, effectiveFetchSize); } exchange = RpcQueryMessageFlow.exchange(this.statementCache, this.client, this.codecs, sql, it, effectiveFetchSize); } else { if (DEBUG_ENABLED) { LOGGER.debug(this.context.getMessage("Start direct exchange for {}"), sql); } exchange = RpcQueryMessageFlow.exchange(this.client, sql, it); } if (useGeneratedKeysClause) { exchange = exchange.transform(GeneratedValues::reduceToSingleCountDoneToken); } return potentiallyAttachTimeout(exchange, this.connectionOptions, this.client, sql); } private void clearBindings(Iterator iterator) { while (iterator.hasNext()) { // exhaust iterator, ignore returned elements iterator.next(); } this.bindings.clear(); } @Override public ParametrizedMssqlStatement returnGeneratedValues(String... columns) { super.returnGeneratedValues(columns); return this; } @Override public ParametrizedMssqlStatement fetchSize(int fetchSize) { super.fetchSize(fetchSize); return this; } private static void tryNextBinding(Iterator iterator, Sinks.Many boundRequests, AtomicBoolean cancelled) { if (cancelled.get()) { return; } try { if (iterator.hasNext()) { boundRequests.emitNext(iterator.next(), Sinks.EmitFailureHandler.FAIL_FAST); } else { boundRequests.emitComplete(Sinks.EmitFailureHandler.FAIL_FAST); } } catch (Exception e) { boundRequests.emitError(e, Sinks.EmitFailureHandler.FAIL_FAST); } } @Override public ParametrizedMssqlStatement bind(String identifier, Object value) { Assert.requireNonNull(identifier, "identifier must not be null"); Assert.isInstanceOf(String.class, identifier, "identifier must be a String"); boolean isIn = !(value instanceof Parameter.Out); RpcParameterContext parameterContext = createContext(isIn, null); if (isTextual(value) || (value instanceof Parameter && isTextual(((Parameter) value).getValue()))) { parameterContext = createContext(isIn, new RpcParameterContext.CharacterValueContext(this.client.getRequiredCollation(), this.sendStringParametersAsUnicode)); } Encoded encoded = this.codecs.encode(this.client.getByteBufAllocator(), parameterContext, value); addBinding(getParameterName(identifier), isIn ? RpcDirection.IN : RpcDirection.OUT, encoded); return this; } @Override public ParametrizedMssqlStatement bind(int index, Object value) { Assert.requireNonNull(value, "value must not be null"); return bind(getParameterName(index), value); } @Override public ParametrizedMssqlStatement bindNull(String identifier, Class type) { Assert.requireNonNull(identifier, "Identifier must not be null"); Assert.isInstanceOf(String.class, identifier, "Identifier must be a String"); Assert.requireNonNull(type, "type must not be null"); if (this.executed) { throw new IllegalStateException("Statement was already executed"); } Encoded encoded = this.codecs.encodeNull(this.client.getByteBufAllocator(), type); addBinding(getParameterName(identifier), RpcDirection.IN, encoded); return this; } @Override public ParametrizedMssqlStatement bindNull(int index, Class type) { Assert.requireNonNull(type, "Type must not be null"); return bindNull(getParameterName(index), type); } private static RpcParameterContext createContext(boolean in, @Nullable RpcParameterContext.ValueContext value) { if (in) { return value != null ? RpcParameterContext.in(value) : RpcParameterContext.in(); } return value != null ? RpcParameterContext.out(value) : RpcParameterContext.out(); } private void addBinding(String name, RpcDirection rpcDirection, Encoded parameter) { assertNotExecuted(); this.bindings.getCurrent().add(name, rpcDirection, parameter); } private void assertNotExecuted() { if (this.executed) { throw new IllegalStateException("Statement was already executed"); } } /** * Returns the {@link Bindings}. * * @return the {@link Bindings}. */ Bindings getBindings() { return this.bindings; } private String getParameterName(int index) { return this.parsedQuery.getParameterName(index); } private String getParameterName(String name) { return this.parsedQuery.getParameterName(name); } /** * Returns whether the {@code sql} query is supported by this statement. * * @param sql the SQL to check. * @return {@code true} if supported. * @throws IllegalArgumentException when {@code sql} is {@code null}. */ public static boolean supports(String sql) { Assert.requireNonNull(sql, "SQL must not be null"); return sql.lastIndexOf('@') != -1; } private static boolean isTextual(@Nullable Object value) { return value instanceof CharSequence || value instanceof Clob; } /** * Locates the first occurrence of {@code needle} in {@code sql} starting at {@code offset}. The SQL string may contain: * *
    *
  • Literals, enclosed in single quotes ({@literal '})
  • *
  • Literals, enclosed in double quotes ({@literal "})
  • *
  • Escape sequences, enclosed in square brackets ({@literal []})
  • *
  • Escaped escapes or literal delimiters (i.e. {@literal ''}, {@literal ""} or {@literal ]])
  • *
  • C-style single-line comments beginning with {@literal --}
  • *
  • C-style multi-line comments beginning enclosed
  • *
* * @param needle the character to search for. * @param sql the SQL string to search in. * @param offset the offset to start searching. * @return the offset or {@literal -1} if not found. */ @SuppressWarnings({"fallthrough"}) private static int findCharacter(char needle, CharSequence sql, int offset) { char chQuote; char character; int length = sql.length(); while (offset < length && offset != -1) { character = sql.charAt(offset++); switch (character) { case '/': if (offset == length) { break; } if (sql.charAt(offset) == '*') { // If '/* ... */' comment while (++offset < length) { // consume comment if (sql.charAt(offset) == '*' && offset + 1 < length && sql.charAt(offset + 1) == '/') { // If // end // of // comment offset += 2; break; } } break; } if (sql.charAt(offset) == '-') { break; } // Fall through - will fail next if and end up in default case case '-': if (sql.charAt(offset) == '-') { // If '-- ... \n' comment while (++offset < length) { // consume comment if (sql.charAt(offset) == '\n' || sql.charAt(offset) == '\r') { // If end of comment offset++; break; } } break; } // Fall through to test character default: if (needle == character) { return offset - 1; } break; case '[': character = ']'; case '\'': case '"': chQuote = character; while (offset < length) { if (sql.charAt(offset++) == chQuote) { if (length == offset || sql.charAt(offset) != chQuote) { break; } ++offset; } } break; } } return -1; } /** * A parsed SQL query with its variable names. */ static class ParsedQuery { private final String sql; private final List parameters; private final Map parametersByName = new LinkedHashMap<>(); ParsedQuery(String sql, List parameters) { this.sql = sql; this.parameters = parameters; for (ParsedParameter parameter : parameters) { this.parametersByName.put(parameter.getName(), parameter); } } /** * Parse the {@code sql} query and resolve variable parameters. * * @param sql the SQL query to parse. * @return the parsed query. * @throws IllegalArgumentException when {@code sql} is {@code null}. */ static ParsedQuery parse(String sql) { Assert.requireNonNull(sql, "SQL must not be null"); List variables = new ArrayList<>(); int offset = 0; while (offset != -1) { offset = findCharacter('@', sql, offset); if (offset != -1) { Matcher matcher = PARAMETER_MATCHER.matcher(sql.substring(offset)); offset++; if (matcher.find()) { String name = matcher.group(1); variables.add(new ParsedParameter(name, offset)); } } } return new ParsedQuery(sql, variables); } /** * Returns the {@link ParsedParameter} name by {@code name}. * * @param name the parameter name. * @return the {@link ParsedParameter} name. */ String getParameterName(String name) { ParsedParameter parsedParameter = this.parametersByName.get(name); if (name.startsWith("@")) { parsedParameter = this.parametersByName.get(name.substring(1)); } if (parsedParameter == null) { throw new NoSuchElementException(String.format("Parameter [%s] does not exist in query [%s]", name, this.sql)); } return parsedParameter.getName(); } /** * Returns the parameter name at the positional {@code index}. * * @param index * @return */ public String getParameterName(int index) { if (index < 0) { throw new IndexOutOfBoundsException("Index must be greater or equal to zero"); } if (index >= getParameterCount()) { throw new IndexOutOfBoundsException(String.format("No such parameter with index [%d] in query [%s]", index, this.sql)); } return this.parameters.get(index).getName(); } public String getSql() { return this.sql; } /** * @return the number of parameters. */ public int getParameterCount() { return this.parameters.size(); } public List getParameters() { return this.parameters; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof ParsedQuery)) { return false; } ParsedQuery that = (ParsedQuery) o; return Objects.equals(this.sql, that.sql) && Objects.equals(this.parameters, that.parameters); } @Override public int hashCode() { return Objects.hash(this.sql, this.parameters); } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [sql='").append(this.sql).append('\''); sb.append(", variables=").append(this.parameters); sb.append(']'); return sb.toString(); } } /** * A SQL parameter within a SQL query. */ static class ParsedParameter { private final String name; private final int position; ParsedParameter(String name, int position) { this.name = name; this.position = position; } public String getName() { return this.name; } public int getPosition() { return this.position; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof ParsedParameter)) { return false; } ParsedParameter that = (ParsedParameter) o; return this.position == that.position && Objects.equals(this.name, that.name); } @Override public int hashCode() { return Objects.hash(this.name, this.position); } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [name='").append(this.name).append('\''); sb.append(", position=").append(this.position); sb.append(']'); return sb.toString(); } } static final class Bindings { private final List bindings = new ArrayList<>(); private Binding current; private void finish() { this.current = null; } Binding first() { return this.bindings.stream().findFirst().orElseThrow(() -> new IllegalStateException("No parameters have been bound")); } Binding getCurrent() { if (this.current == null) { this.current = new Binding(); this.bindings.add(this.current); } return this.current; } /** * Clear/release binding values. */ void clear() { this.bindings.forEach(Binding::clear); } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/PreparedStatementCache.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import java.util.function.Function; /** * Cache for prepared statements. * * @author Mark Paluch */ interface PreparedStatementCache { /** * Marker for no prepared statement found/no prepared statement. */ int UNPREPARED = 0; /** * Returns a prepared statement handle for the given {@code sql} query and the {@link Binding}. * * @param sql the SQL query. * @param binding bound parameters. Parameter types impact the prepared query. * @return the prepared statement handle. {@value 0} has a specific meaning as it indicates that no cached SQL statement was found. * @see #UNPREPARED */ int getHandle(String sql, Binding binding); /** * Returns a prepared statement {@code handle} for the given {@code sql} query and the {@link Binding}. * * @param handle the prepared statement handle. * @param sql the SQL query. * @param binding bound parameters. Parameter types impact the prepared query. */ void putHandle(int handle, String sql, Binding binding); /** * Returns the parsed and potentially cached representation of the {@code sql} statement. * * @param sql query to parse. * @param parseFunction parse function. * @param * @return the parsed SQL representation. */ T getParsedSql(String sql, Function parseFunction); /** * Returns the number of cached prepared statement handles in this cache. * * @return the number of prepared statement handles in this cache. */ int size(); } ================================================ FILE: src/main/java/io/r2dbc/mssql/QueryLogger.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.client.ConnectionContext; import reactor.util.Logger; import reactor.util.Loggers; /** * Query logger to log queries. * * @author Mark Paluch */ final class QueryLogger { private static final Logger QUERY_LOGGER = Loggers.getLogger("io.r2dbc.mssql.QUERY"); static void logQuery(ConnectionContext context, String query) { QUERY_LOGGER.debug(context.getMessage("Executing query: {}"), query); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/QueryMessageFlow.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCounted; import io.r2dbc.mssql.client.Client; import io.r2dbc.mssql.message.Message; import io.r2dbc.mssql.message.TransactionDescriptor; import io.r2dbc.mssql.message.token.DoneToken; import io.r2dbc.mssql.message.token.SqlBatch; import io.r2dbc.mssql.util.Assert; import io.r2dbc.mssql.util.Operators; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.SynchronousSink; import java.util.function.BiConsumer; /** * Simple (direct) query message flow using {@link SqlBatch}. *

* Commands require deferred creation because {@link Client} can be used concurrently and we must fetch the latest state (e.g. {@link TransactionDescriptor}) to issue a command with the appropriate * state. * * @author Mark Paluch */ final class QueryMessageFlow { /** * Execute a simple query using {@link SqlBatch}. Query execution terminates with a {@link DoneToken}. * * @param client the {@link Client} to exchange messages with. * @param query the query to execute. * @return the messages received in response to this exchange. */ static Flux exchange(Client client, String query) { Assert.requireNonNull(client, "Client must not be null"); Assert.requireNonNull(query, "Query must not be null"); return client.exchange(Mono.fromSupplier(() -> SqlBatch.create(1, client.getTransactionDescriptor(), query)), DoneToken::isDone) .doOnSubscribe(ignore -> QueryLogger.logQuery(client.getContext(), query)) .handle(DoneHandler.INSTANCE) .transform(Operators::discardOnCancel).doOnDiscard(ReferenceCounted.class, ReferenceCountUtil::release); } enum DoneHandler implements BiConsumer> { INSTANCE; @Override public void accept(Message message, SynchronousSink sink) { sink.next(message); if (DoneToken.isDone(message)) { sink.complete(); } } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/RpcQueryMessageFlow.java ================================================ /* * Copyright 2018-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCounted; import io.r2dbc.mssql.RpcQueryMessageFlow.CursorState.Phase; import io.r2dbc.mssql.client.Client; import io.r2dbc.mssql.codec.Codecs; import io.r2dbc.mssql.codec.RpcDirection; import io.r2dbc.mssql.message.ClientMessage; import io.r2dbc.mssql.message.Message; import io.r2dbc.mssql.message.TransactionDescriptor; import io.r2dbc.mssql.message.token.*; import io.r2dbc.mssql.message.type.Collation; import io.r2dbc.mssql.util.Assert; import io.r2dbc.mssql.util.Operators; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; import reactor.core.publisher.SynchronousSink; import reactor.util.Logger; import reactor.util.Loggers; import javax.annotation.processing.Completion; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.function.Consumer; import java.util.function.Predicate; import static io.r2dbc.mssql.util.PredicateUtils.or; /** * Query message flow using cursors. The cursored query message flow uses {@link RpcRequest RPC} calls to open, fetch and close cursors. *

* Commands require deferred creation because {@link Client} can be used concurrently and we must fetch the latest state (e.g. {@link TransactionDescriptor}) to issue a command with the appropriate * state. * * @author Mark Paluch * @see RpcRequest */ final class RpcQueryMessageFlow { private static final Predicate FILTER_PREDICATE = or(RowToken.class::isInstance, ColumnMetadataToken.class::isInstance, ReturnValue.class::isInstance, DoneInProcToken.class::isInstance, IntermediateCount.class::isInstance, AbstractInfoToken.class::isInstance, Completion.class::isInstance, AbstractDoneToken::isAttentionAck); private static final Logger logger = Loggers.getLogger(RpcQueryMessageFlow.class); static final RpcRequest.OptionFlags NO_METADATA = RpcRequest.OptionFlags.empty().disableMetadata(); // Constants for server-cursored result sets. // See the Engine Cursors Functional Specification for details. static final int FETCH_FIRST = 1; static final int FETCH_NEXT = 2; static final int FETCH_PREV = 4; static final int FETCH_LAST = 8; static final int FETCH_ABSOLUTE = 16; static final int FETCH_RELATIVE = 32; static final int FETCH_REFRESH = 128; static final int FETCH_INFO = 256; static final int FETCH_PREV_NOADJUST = 512; // Scroll options and concurrency options lifted out // of the the Yukon cursors spec for sp_cursoropen. final static int SCROLLOPT_KEYSET = 1; final static int SCROLLOPT_DYNAMIC = 2; final static int SCROLLOPT_FORWARD_ONLY = 4; final static int SCROLLOPT_STATIC = 8; final static int SCROLLOPT_FAST_FORWARD = 16; static final int SCROLLOPT_PARAMETERIZED_STMT = 4096; static final int CCOPT_READ_ONLY = 1; static final int CCOPT_ALLOW_DIRECT = 8192; /** * Execute a direct query with parameters. * * @param client the {@link Client} to exchange messages with. * @param query the query to execute. * @return the messages received in response to this exchange. */ static Flux exchange(Client client, String query, Binding binding) { Assert.requireNonNull(client, "Client must not be null"); Assert.requireNonNull(query, "Query must not be null"); CursorState state = new CursorState(); state.directMode = true; Flux exchange = client.exchange(Mono.fromSupplier(() -> spExecuteSql(query, binding, client.getRequiredCollation(), client.getTransactionDescriptor())), DoneProcToken::isDone); OnCursorComplete cursorComplete = new OnCursorComplete(); Flux messages = exchange // .handle((message, sink) -> { state.update(message); handleMessage(client, 0, state, message, sink, cursorComplete, true); }) .filter(FILTER_PREDICATE) .doOnCancel(cursorComplete); return messages.doOnSubscribe(subscription -> { QueryLogger.logQuery(client.getContext(), query); }) .transform(it -> Operators.discardOnCancel(it, state::cancel).doOnDiscard(ReferenceCounted.class, ReferenceCountUtil::release)).takeUntilOther(cursorComplete.takeUntil()); } /** * Execute a cursored query. * * @param client the {@link Client} to exchange messages with. * @param codecs the codecs to decode {@link ReturnValue}s from RPC calls. * @param query the query to execute. * @param fetchSize the number of rows to fetch. TODO: Try to determine fetch size from current demand and apply demand function. * @return the messages received in response to this exchange. */ static Flux exchange(Client client, Codecs codecs, String query, int fetchSize) { Assert.requireNonNull(client, "Client must not be null"); Assert.requireNonNull(query, "Query must not be null"); Sinks.Many outbound = Sinks.many().unicast().onBackpressureBuffer(); CursorState state = new CursorState(); Flux exchange = client.exchange(Flux.defer(() -> { outbound.emitNext(spCursorOpen(query, client.getRequiredCollation(), client.getTransactionDescriptor()), Sinks.EmitFailureHandler.FAIL_FAST); return outbound.asFlux(); }), isFinalToken(state)); OnCursorComplete cursorComplete = new OnCursorComplete(); Flux messages = exchange // .handle((message, sink) -> { boolean emit = true; if (message.getClass() == ReturnValue.class) { ReturnValue returnValue = (ReturnValue) message; // cursor Id if (returnValue.getOrdinal() == 0) { state.cursorId = parseCursorId(codecs, state, returnValue); } // skip spCursorOpen OUT if (returnValue.getOrdinal() < 5) { returnValue.release(); emit = false; } } state.update(message); handleMessage(client, fetchSize, outbound, state, message, sink, cursorComplete, emit); }) .filter(FILTER_PREDICATE); return messages.doOnSubscribe(subscription -> { QueryLogger.logQuery(client.getContext(), query); }) .transform(it -> Operators.discardOnCancel(it, state::cancel).doOnDiscard(ReferenceCounted.class, ReferenceCountUtil::release)).takeUntilOther(cursorComplete.takeUntil()); } /** * Execute a cursored query with RPC parameters. * * @param statementCache the {@link PreparedStatementCache} to keep track of prepared statement handles. * @param client the {@link Client} to exchange messages with. * @param codecs the codecs to decode {@link ReturnValue}s from RPC calls. * @param query the query to execute. * @param binding parameter bindings. * @param fetchSize the number of rows to fetch. TODO: Try to determine fetch size from current demand and apply demand function. * @return the messages received in response to this exchange. * @throws IllegalArgumentException when {@link Client} or {@code query} is {@code null}. */ static Flux exchange(PreparedStatementCache statementCache, Client client, Codecs codecs, String query, Binding binding, int fetchSize) { Assert.requireNonNull(client, "Client must not be null"); Assert.requireNonNull(query, "Query must not be null"); Sinks.Many outbound = Sinks.many().unicast().onBackpressureBuffer(); int handle = statementCache.getHandle(query, binding); AtomicBoolean retryReprepare = new AtomicBoolean(true); AtomicBoolean needsPrepare = new AtomicBoolean(false); Flux messageProducer; if (handle == PreparedStatementCache.UNPREPARED) { messageProducer = Flux.defer(() -> { outbound.emitNext(spCursorPrepExec(PreparedStatementCache.UNPREPARED, query, binding, client.getRequiredCollation(), client.getTransactionDescriptor()), Sinks.EmitFailureHandler.FAIL_FAST); return outbound.asFlux(); }); needsPrepare.set(true); } else { messageProducer = Flux.defer(() -> { outbound.emitNext(spCursorExec(handle, binding, client.getTransactionDescriptor()), Sinks.EmitFailureHandler.FAIL_FAST); return outbound.asFlux(); }); needsPrepare.set(false); } CursorState state = new CursorState(); Flux exchange = client.exchange(messageProducer, isFinalToken(state)); OnCursorComplete cursorComplete = new OnCursorComplete(); Flux messages = exchange // .handle((message, sink) -> { boolean emit = true; if (message.getClass() == ReturnValue.class) { ReturnValue returnValue = (ReturnValue) message; emit = handleSpCursorReturnValue(statementCache, codecs, query, binding, state, needsPrepare.get(), returnValue); if (!emit) { returnValue.release(); } } state.update(message); if (message instanceof ErrorToken) { if (isPreparedStatementNotFound(((ErrorToken) message).getNumber()) && retryReprepare.compareAndSet(true, false)) { logger.debug("Prepared statement no longer valid: {}", handle); state.update(Phase.PREPARE_RETRY); } } if (state.phase == Phase.PREPARE_RETRY) { emit = false; } if (DoneProcToken.isDone(message) && state.phase == Phase.PREPARE_RETRY) { logger.debug("Attempting to re-prepare statement: {}", query); needsPrepare.set(true); state.update(Phase.NONE); outbound.emitNext(spCursorPrepExec(PreparedStatementCache.UNPREPARED, query, binding, client.getRequiredCollation(), client.getTransactionDescriptor()), Sinks.EmitFailureHandler.FAIL_FAST); return; } handleMessage(client, fetchSize, outbound, state, message, sink, cursorComplete, emit); }) .filter(FILTER_PREDICATE); return messages.doOnSubscribe(subscription -> { QueryLogger.logQuery(client.getContext(), query); }) .transform(it -> Operators.discardOnCancel(it, state::cancel).doOnDiscard(ReferenceCounted.class, ReferenceCountUtil::release)).takeUntilOther(cursorComplete.takeUntil()); } /** * Check whether the error indicates a prepared statement requiring reprepare. *

*

  • 586: The prepared statement handle %d is not valid in this context. Please verify that current database, user * default schema ANSI_NULLS and QUOTED_IDENTIFIER set options are not changed since the handle is prepared.
  • *
  • 8179: Could not find prepared statement with handle %d.
  • *
* * @param errorNumber * @return */ private static boolean isPreparedStatementNotFound(long errorNumber) { return errorNumber == 8179 || errorNumber == 586; } private static boolean handleSpCursorReturnValue(PreparedStatementCache statementCache, Codecs codecs, String query, Binding binding, CursorState state, boolean needsPrepare, ReturnValue returnValue) { // cursor Id if (returnValue.getOrdinal() == 1) { state.cursorId = parseCursorId(codecs, state, returnValue); } if (needsPrepare) { // prepared statement handle if (returnValue.getOrdinal() == 0) { int preparedStatementHandle = codecs.decode(returnValue.getValue(), returnValue.asDecodable(), Integer.class); logger.debug("Prepared statement with handle: {}", preparedStatementHandle); statementCache.putHandle(preparedStatementHandle, query, binding); } // skip spCursorPrepExec OUT return returnValue.getOrdinal() >= 7; } else { // skip spCursorExec OUT return returnValue.getOrdinal() >= 5; } } private static int parseCursorId(Codecs codecs, CursorState state, ReturnValue returnValue) { Integer cursorId = codecs.decode(returnValue.getValue(), returnValue.asDecodable(), Integer.class); logger.debug("CursorId: {}", cursorId); return cursorId; } private static void handleMessage(Client client, int fetchSize, CursorState state, Message message, SynchronousSink sink, Runnable onCursorComplete, boolean emit) { handleMessage(client, fetchSize, it -> { throw new UnsupportedOperationException("Cannot accept subsequent messages"); }, state, message, sink, onCursorComplete, emit); } private static void handleMessage(Client client, int fetchSize, Sinks.Many requests, CursorState state, Message message, SynchronousSink sink, Runnable onCursorComplete, boolean emit) { handleMessage(client, fetchSize, t -> requests.emitNext(t, Sinks.EmitFailureHandler.FAIL_FAST), state, message, sink, onCursorComplete, emit); } private static void handleMessage(Client client, int fetchSize, Consumer requests, CursorState state, Message message, SynchronousSink sink, Runnable onCursorComplete, boolean emit) { if (message instanceof ColumnMetadataToken && !((ColumnMetadataToken) message).hasColumns()) { return; } if (message instanceof AbstractInfoToken) { // direct mode if (((AbstractInfoToken) message).getNumber() == 16954) { state.directMode = true; } } if (message instanceof DoneInProcToken) { DoneInProcToken doneToken = (DoneInProcToken) message; state.hasMore = doneToken.hasMore(); if (!state.directMode) { if (state.phase == Phase.FETCHING && doneToken.hasCount()) { sink.next(new IntermediateCount(doneToken)); } return; } sink.next(doneToken); return; } if (AbstractDoneToken.isAttentionAck(message)) { state.update(Phase.CLOSED); sink.next(message); return; } if (!(message instanceof DoneProcToken)) { if (emit) { sink.next(message); } return; } if (state.hasSeenError) { state.update(Phase.ERROR); } if (DoneProcToken.isDone(message)) { onDone(client, fetchSize, requests, state, onCursorComplete); } } static void onDone(Client client, int fetchSize, Consumer requests, CursorState state, Runnable completion) { Phase phase = state.phase; if (isFinalState(state)) { completion.run(); state.update(Phase.CLOSED); return; } if (phase == Phase.NONE || phase == Phase.FETCHING) { if (((state.hasMore && phase == Phase.NONE) || state.hasSeenRows) && state.wantsMore()) { if (phase == Phase.NONE) { state.update(Phase.FETCHING); } requests.accept(spCursorFetch(state.cursorId, FETCH_NEXT, fetchSize, client.getTransactionDescriptor())); } else { state.update(Phase.CLOSING); // TODO: spCursorClose should happen also if a subscriber cancels its subscription. requests.accept(spCursorClose(state.cursorId, client.getTransactionDescriptor())); } state.hasSeenRows = false; } } private static Predicate isFinalToken(CursorState state) { return message -> { if (!DoneProcToken.isDone(message)) { return false; } return isFinalState(state); }; } private static boolean isFinalState(CursorState state) { Phase phase = state.phase; if (phase == Phase.NONE || phase == Phase.FETCHING) { if (state.cursorId == 0) { return true; } } return phase == Phase.ERROR || phase == Phase.CLOSING || phase == Phase.CLOSED; } /** * Creates a {@link RpcRequest} for {@link RpcRequest#Sp_ExecuteSql} to execute a SQL statement that returns directly results. * * @param query the query to execute. * @param binding bound parameters * @param collation the database collation. * @param transactionDescriptor transaction descriptor. * @return {@link RpcRequest} for {@link RpcRequest#Sp_CursorOpen}. * @throws IllegalArgumentException when {@code query}, {@link Collation}, or {@link TransactionDescriptor} is {@code null}. */ static RpcRequest spExecuteSql(String query, Binding binding, Collation collation, TransactionDescriptor transactionDescriptor) { Assert.requireNonNull(query, "Query must not be null"); Assert.requireNonNull(collation, "Collation must not be null"); Assert.requireNonNull(transactionDescriptor, "TransactionDescriptor must not be null"); RpcRequest.Builder builder = RpcRequest.builder() // .withProcId(RpcRequest.Sp_ExecuteSql) // .withTransactionDescriptor(transactionDescriptor) // .withParameter(RpcDirection.IN, collation, query) // .withParameter(RpcDirection.IN, collation, binding.getFormalParameters()); // formal parameter defn binding.forEach((name, parameter) -> { builder.withNamedParameter(parameter.rpcDirection, name, parameter.encoded); }); return builder.build(); } /** * Creates a {@link RpcRequest} for {@link RpcRequest#Sp_CursorOpen} to execute a SQL statement that returns a cursor. * * @param query the query to execute. * @param collation the database collation. * @param transactionDescriptor transaction descriptor. * @return {@link RpcRequest} for {@link RpcRequest#Sp_CursorOpen}. * @throws IllegalArgumentException when {@code query}, {@link Collation}, or {@link TransactionDescriptor} is {@code null}. */ static RpcRequest spCursorOpen(String query, Collation collation, TransactionDescriptor transactionDescriptor) { Assert.requireNonNull(query, "Query must not be null"); Assert.requireNonNull(collation, "Collation must not be null"); Assert.requireNonNull(transactionDescriptor, "TransactionDescriptor must not be null"); int resultSetScrollOpt = SCROLLOPT_FORWARD_ONLY; int resultSetCCOpt = CCOPT_READ_ONLY | CCOPT_ALLOW_DIRECT; return RpcRequest.builder() // .withProcId(RpcRequest.Sp_CursorOpen) // .withTransactionDescriptor(transactionDescriptor) // .withParameter(RpcDirection.OUT, 0) // cursor .withParameter(RpcDirection.IN, collation, query) .withParameter(RpcDirection.IN, resultSetScrollOpt) // scrollopt .withParameter(RpcDirection.IN, resultSetCCOpt) // ccopt .withParameter(RpcDirection.OUT, 0) // rowcount .build(); } /** * Creates a {@link RpcRequest} for {@link RpcRequest#Sp_CursorFetch} to fetch {@code rowCount} from the given {@literal cursor}. * * @param cursor the cursor Id. * @param fetchType the type of fetch operation (first, next, …). * @param rowCount number of rows to fetch * @param transactionDescriptor transaction descriptor. * @return {@link RpcRequest} for {@link RpcRequest#Sp_CursorFetch}. * @throws IllegalArgumentException when {@link TransactionDescriptor} is {@code null}. * @throws IllegalArgumentException when {@code rowCount} is less than zero. */ static RpcRequest spCursorFetch(int cursor, int fetchType, int rowCount, TransactionDescriptor transactionDescriptor) { Assert.isTrue(rowCount >= 0, "Row count must be greater or equal to zero"); Assert.requireNonNull(transactionDescriptor, "TransactionDescriptor must not be null"); return RpcRequest.builder() // .withProcId(RpcRequest.Sp_CursorFetch) // .withTransactionDescriptor(transactionDescriptor) // .withOptionFlags(NO_METADATA) // .withParameter(RpcDirection.IN, cursor) // cursor .withParameter(RpcDirection.IN, fetchType) // fetch type .withParameter(RpcDirection.IN, 0) // startRow .withParameter(RpcDirection.IN, rowCount) // numRows .build(); } /** * Creates a {@link RpcRequest} for {@link RpcRequest#Sp_CursorClose} release server resources. * * @param cursor the cursor Id. * @param transactionDescriptor transaction descriptor. * @return {@link RpcRequest} for {@link RpcRequest#Sp_CursorFetch}. * @throws IllegalArgumentException when {@link TransactionDescriptor} is {@code null}. */ static RpcRequest spCursorClose(int cursor, TransactionDescriptor transactionDescriptor) { Assert.requireNonNull(transactionDescriptor, "TransactionDescriptor must not be null"); return RpcRequest.builder() // .withProcId(RpcRequest.Sp_CursorClose) // .withTransactionDescriptor(transactionDescriptor) // .withParameter(RpcDirection.IN, cursor) // cursor .build(); } /** * Creates a {@link RpcRequest} for {@link RpcRequest#Sp_CursorPrepare} to prepare and execute a {@code query}. * * @param preparedStatementHandle handle to a previously prepared statement. This call un-prepares a previously prepared statement. * @param query the query to execute. * @param binding bound parameters * @param collation the database collation. * @param transactionDescriptor transaction descriptor. * @return {@link RpcRequest} for {@link RpcRequest#Sp_CursorFetch}. */ static RpcRequest spCursorPrepExec(int preparedStatementHandle, String query, Binding binding, Collation collation, TransactionDescriptor transactionDescriptor) { int resultSetScrollOpt = SCROLLOPT_FORWARD_ONLY | (binding.isEmpty() ? 0 : SCROLLOPT_PARAMETERIZED_STMT); int resultSetCCOpt = CCOPT_READ_ONLY | CCOPT_ALLOW_DIRECT; RpcRequest.Builder builder = RpcRequest.builder() // .withProcId(RpcRequest.Sp_CursorPrepExec) // .withTransactionDescriptor(transactionDescriptor) // // // IN (reprepare): Old handle to unprepare before repreparing // OUT: The newly prepared handle .withParameter(RpcDirection.OUT, preparedStatementHandle) .withParameter(RpcDirection.OUT, 0) // cursor .withParameter(RpcDirection.IN, collation, binding.getFormalParameters()) // formal parameter defn .withParameter(RpcDirection.IN, collation, query) // statement .withParameter(RpcDirection.IN, resultSetScrollOpt) // scrollopt .withParameter(RpcDirection.IN, resultSetCCOpt) // ccopt .withParameter(RpcDirection.OUT, 0);// rowcount binding.forEach((name, parameter) -> { builder.withNamedParameter(parameter.rpcDirection, name, parameter.encoded); }); return builder.build(); } /** * Creates a {@link RpcRequest} for {@link RpcRequest#Sp_CursorExecute} to and execute prepared statement. * * @param preparedStatementHandle handle to a previously prepared statement. * @param binding bound parameters * @param transactionDescriptor transaction descriptor. * @return {@link RpcRequest} for {@link RpcRequest#Sp_CursorFetch}. */ static RpcRequest spCursorExec(int preparedStatementHandle, Binding binding, TransactionDescriptor transactionDescriptor) { Assert.isTrue(preparedStatementHandle != PreparedStatementCache.UNPREPARED, "Invalid PreparedStatement handle"); int resultSetScrollOpt = SCROLLOPT_FORWARD_ONLY; int resultSetCCOpt = CCOPT_READ_ONLY | CCOPT_ALLOW_DIRECT; RpcRequest.Builder builder = RpcRequest.builder() // .withProcId(RpcRequest.Sp_CursorExecute) // .withTransactionDescriptor(transactionDescriptor) // // // IN (reprepare): Old handle to unprepare before repreparing // OUT: The newly prepared handle .withParameter(RpcDirection.IN, preparedStatementHandle) .withParameter(RpcDirection.OUT, 0) // cursor .withParameter(RpcDirection.IN, resultSetScrollOpt) // scrollopt .withParameter(RpcDirection.IN, resultSetCCOpt) // ccopt .withParameter(RpcDirection.OUT, 0);// rowcount binding.forEach((name, parameter) -> { builder.withNamedParameter(parameter.rpcDirection, name, parameter.encoded); }); return builder.build(); } /** * Cursoring state. */ static class CursorState { volatile int cursorId; // hasMore flag from the DoneInProc token volatile boolean hasMore; // hasMore typically reports true, but we need to check whether we've seen rows to determine whether to end cursoring. volatile boolean hasSeenRows; volatile boolean hasSeenError; volatile boolean directMode; volatile boolean cancelRequested; volatile ErrorToken errorToken; Phase phase = Phase.NONE; boolean wantsMore() { return !this.cancelRequested; } void cancel() { this.cancelRequested = true; } void update(Message it) { if (it instanceof RowToken) { this.hasSeenRows = true; } if (it instanceof ErrorToken) { this.errorToken = (ErrorToken) it; this.hasSeenError = true; } } public void update(Phase newPhase) { this.phase = newPhase; if (newPhase == Phase.PREPARE_RETRY) { errorToken = null; hasSeenError = false; } } enum Phase { NONE, FETCHING, PREPARE_RETRY, CLOSING, CLOSED, ERROR } } static class IntermediateCount extends AbstractDoneToken { public IntermediateCount(DoneInProcToken token) { super(token.getType(), token.getStatus(), token.getCurrentCommand(), token.getRowCount()); } @Override public String getName() { return "INTERMEDIATE_COUNT"; } } static class OnCursorComplete implements Runnable { private static final int STATE_ACTIVE = 0; private static final int STATE_CANCELLED = 1; private static final AtomicIntegerFieldUpdater STATE_UPDATER = AtomicIntegerFieldUpdater.newUpdater(OnCursorComplete.class, "state"); private final Sinks.Empty trigger = Sinks.empty(); private volatile int state = STATE_ACTIVE; @Override public void run() { if (STATE_UPDATER.compareAndSet(this, STATE_ACTIVE, STATE_CANCELLED)) { this.trigger.tryEmitEmpty(); } } public Publisher takeUntil() { return this.trigger.asMono(); } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/SimpleMssqlStatement.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.client.Client; import io.r2dbc.mssql.client.ConnectionContext; import io.r2dbc.mssql.codec.Codecs; import io.r2dbc.mssql.message.Message; import io.r2dbc.mssql.message.token.AbstractDoneToken; import io.r2dbc.mssql.message.token.DoneInProcToken; import io.r2dbc.mssql.message.token.SqlBatch; import io.r2dbc.mssql.util.Assert; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.util.Logger; import reactor.util.Loggers; import java.util.function.Predicate; /** * Simple SQL statement without SQL parameter (variables) using direct ({@link SqlBatch}) execution. * * @author Mark Paluch */ final class SimpleMssqlStatement extends MssqlStatementSupport implements MssqlStatement { private static final Logger logger = Loggers.getLogger(SimpleMssqlStatement.class); private final Client client; private final Codecs codecs; private final ConnectionOptions connectionOptions; private final ConnectionContext context; private final String sql; /** * Creates a new {@link SimpleMssqlStatement}. * * @param client the client to exchange messages with. * @param connectionOptions the connection options. * @param sql the query to execute. * @throws IllegalArgumentException when {@link Client}, {@link ConnectionOptions}, or {@code sql} is {@code null}. */ SimpleMssqlStatement(Client client, ConnectionOptions connectionOptions, String sql) { super(connectionOptions.prefersCursors(sql)); this.connectionOptions = connectionOptions; Assert.requireNonNull(client, "Client must not be null"); Assert.requireNonNull(connectionOptions, "ConnectionOptions must not be null"); Assert.requireNonNull(sql, "SQL must not be null"); Assert.isTrue(sql.trim().length() > 0, "SQL must contain text"); this.client = client; this.context = client.getContext(); this.codecs = connectionOptions.getCodecs(); this.sql = sql; } @Override public SimpleMssqlStatement add() { return this; } @Override public SimpleMssqlStatement bind(String identifier, Object value) { throw new UnsupportedOperationException( String.format("Binding parameters is not supported for the statement [%s]", this.sql)); } @Override public SimpleMssqlStatement bind(int index, Object value) { throw new UnsupportedOperationException( String.format("Binding parameters is not supported for the statement [%s]", this.sql)); } @Override public SimpleMssqlStatement bindNull(String identifier, Class type) { throw new UnsupportedOperationException( String.format("Binding parameters is not supported for the statement [%s]", this.sql)); } @Override public SimpleMssqlStatement bindNull(int index, Class type) { throw new UnsupportedOperationException( String.format("Binding parameters is not supported for the statement [%s]", this.sql)); } @Override public Flux execute() { int effectiveFetchSize = getEffectiveFetchSize(); return Flux.defer(() -> { boolean useGeneratedKeysClause = GeneratedValues.shouldExpectGeneratedKeys(this.getGeneratedColumns()); String sql = useGeneratedKeysClause ? GeneratedValues.augmentQuery(this.sql, getGeneratedColumns()) : this.sql; Flux exchange; if (effectiveFetchSize > 0) { if (logger.isDebugEnabled()) { logger.debug(this.context.getMessage("Start cursored exchange for {} with fetch size {}"), sql, effectiveFetchSize); } exchange = potentiallyAttachTimeout(RpcQueryMessageFlow.exchange(this.client, this.codecs, this.sql, effectiveFetchSize), this.connectionOptions, this.client, this.sql); return createResultStream(useGeneratedKeysClause, exchange, DoneInProcToken.class::isInstance); } else { if (logger.isDebugEnabled()) { logger.debug(this.context.getMessage("Start direct exchange for {}"), sql); } exchange = potentiallyAttachTimeout(QueryMessageFlow.exchange(this.client, sql), this.connectionOptions, this.client, this.sql); return createResultStream(useGeneratedKeysClause, exchange, AbstractDoneToken.class::isInstance); } }); } private Publisher createResultStream(boolean useGeneratedKeysClause, Flux exchange, Predicate windowUntil) { if (useGeneratedKeysClause) { exchange = exchange.transform(GeneratedValues::reduceToSingleCountDoneToken); } return exchange.windowUntil(windowUntil) // .map(it -> DefaultMssqlResult.toResult(this.sql, this.context, this.codecs, it, false)); } @Override public SimpleMssqlStatement returnGeneratedValues(String... columns) { super.returnGeneratedValues(columns); return this; } @Override public SimpleMssqlStatement fetchSize(int fetchSize) { super.fetchSize(fetchSize); return this; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/api/MssqlTransactionDefinition.java ================================================ /* * Copyright 2021-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.api; import io.r2dbc.spi.IsolationLevel; import io.r2dbc.spi.Option; import io.r2dbc.spi.TransactionDefinition; import java.time.Duration; /** * {@link TransactionDefinition} for a SQL Server database. * * @author Mark Paluch * @since 0.9 */ public interface MssqlTransactionDefinition extends TransactionDefinition { /** * The {@code WITH MARK} description is a string that marks the transaction in the transaction log and being stored in the {@code msdb.dbo.logmarkhistory} table. */ Option MARK = Option.valueOf("mark"); /** * Creates a {@link MssqlTransactionDefinition} given {@link IsolationLevel}. * * @param isolationLevel the isolation level to use during the transaction. * @return a new {@link MssqlTransactionDefinition} using {@link IsolationLevel}. * @throws IllegalArgumentException if {@code isolationLevel} is {@code null}. */ static MssqlTransactionDefinition from(IsolationLevel isolationLevel) { return SimpleTransactionDefinition.EMPTY.isolationLevel(isolationLevel); } /** * Creates a {@link MssqlTransactionDefinition} specifying the transaction name. * * @param name the transaction name. Must not exceed 32 characters. The name is always case sensitive, even when the instance of SQL Server is not case sensitive * @return a new {@link MssqlTransactionDefinition} using transaction {@code name}. */ static MssqlTransactionDefinition named(String name) { return SimpleTransactionDefinition.EMPTY.name(name); } /** * Creates a {@link MssqlTransactionDefinition} retaining all configured options and applying {@link IsolationLevel}. * * @param isolationLevel the isolation level to use during the transaction. * @return a new {@link MssqlTransactionDefinition} retaining all configured options and applying {@link IsolationLevel}. * @throws IllegalArgumentException if {@code isolationLevel} is {@code null}. */ MssqlTransactionDefinition isolationLevel(IsolationLevel isolationLevel); /** * Creates a {@link MssqlTransactionDefinition} retaining all configured options and applying {@link Duration lock timeout}. * * @param timeout the lock timeout. * @return a new {@link MssqlTransactionDefinition} retaining all configured options and applying {@link Duration lock timeout}. * @throws IllegalArgumentException if {@code timeout} is {@code null}. */ MssqlTransactionDefinition lockTimeout(Duration timeout); /** * Creates a {@link MssqlTransactionDefinition} retaining all configured options and using the given transaction {@code name}. * * @param name the transaction name. Must not exceed 32 characters. The name is always case sensitive, even when the instance of SQL Server is not case sensitive * @return a new {@link MssqlTransactionDefinition} retaining all configured options and using the given transaction {@code name}. * @throws IllegalArgumentException if {@code name} is {@code null}. */ MssqlTransactionDefinition name(String name); /** * Creates a {@link MssqlTransactionDefinition} retaining all configured options and using the given transaction {@code mark}. * Specifies that the transaction is marked in the log. This method updates the transaction name to {@code mark} if no name was set. *

* If {@code WITH MARK} is used, a transaction name must be specified. {@code WITH MARK} allows for restoring a transaction log to a named mark. * * @param mark describes the mark. A description longer than 128 characters is truncated * to 128 characters before being stored in the {@code msdb.dbo.logmarkhistory} table. * @return a new {@link MssqlTransactionDefinition} retaining all configured options and using the given transaction {@code mark}. * @throws IllegalArgumentException if {@code mark} is {@code null}. */ MssqlTransactionDefinition mark(String mark); } ================================================ FILE: src/main/java/io/r2dbc/mssql/api/SimpleTransactionDefinition.java ================================================ /* * Copyright 2021-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.api; import io.r2dbc.mssql.util.Assert; import io.r2dbc.spi.IsolationLevel; import io.r2dbc.spi.Option; import io.r2dbc.spi.TransactionDefinition; import java.time.Duration; import java.util.Collections; import java.util.HashMap; import java.util.Map; @SuppressWarnings("unchecked") final class SimpleTransactionDefinition implements MssqlTransactionDefinition { public static final SimpleTransactionDefinition EMPTY = new SimpleTransactionDefinition(Collections.emptyMap()); private final Map, Object> options; SimpleTransactionDefinition(Map, Object> options) { this.options = options; } @Override public T getAttribute(Option option) { return (T) this.options.get(option); } public MssqlTransactionDefinition with(Option option, Object value) { Map, Object> options = new HashMap<>(this.options); options.put(Assert.requireNonNull(option, "option must not be null"), Assert.requireNonNull(value, "value must not be null")); return new SimpleTransactionDefinition(options); } @Override public MssqlTransactionDefinition isolationLevel(IsolationLevel isolationLevel) { return with(MssqlTransactionDefinition.ISOLATION_LEVEL, isolationLevel); } @Override public MssqlTransactionDefinition lockTimeout(Duration timeout) { return with(MssqlTransactionDefinition.LOCK_WAIT_TIMEOUT, timeout); } @Override public MssqlTransactionDefinition name(String name) { return with(MssqlTransactionDefinition.NAME, name); } @Override public MssqlTransactionDefinition mark(String mark) { if (getAttribute(TransactionDefinition.NAME) == null) { name(mark); } return with(MARK, mark); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/api/package-info.java ================================================ /* * Copyright 2021-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * R2DBC driver API with SQL Server-specific extensions. */ @NonNullApi package io.r2dbc.mssql.api; import reactor.util.annotation.NonNullApi; ================================================ FILE: src/main/java/io/r2dbc/mssql/client/Client.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.message.ClientMessage; import io.r2dbc.mssql.message.Message; import io.r2dbc.mssql.message.TransactionDescriptor; import io.r2dbc.mssql.message.tds.Redirect; import io.r2dbc.mssql.message.type.Collation; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.Optional; import java.util.function.Predicate; /** * An abstraction that wraps the networking part of exchanging {@link Message}s. * * @author Mark Paluch * @author Lars Haatveit */ public interface Client { /** * Send an attention token to interrupt an active statement. * * @return * @since 0.9 */ Mono attention(); /** * Release any resources held by the {@link Client}. * * @return a {@link Mono} that indicates that a client has been closed */ Mono close(); /** * Perform an exchange of messages. Calling this method while a previous exchange is active will return a deferred handle and queue the request until the previous exchange terminates. * * @param requests the publisher of outbound messages * @param takeUntil {@link Predicate} determining the last response frame to {@link Subscriber#onComplete() complete} the stream and prevent multiple subscribers from consuming * previous, active response streams. Note that the last frame that matches {@code takeUntil} is emitted through the resulting {@link Flux}. * @return a {@link Flux} of incoming messages that ends with the end of the frame. */ Flux exchange(Publisher requests, Predicate takeUntil); /** * Returns the {@link ByteBufAllocator}. * * @return the {@link ByteBufAllocator} */ ByteBufAllocator getByteBufAllocator(); /** * Returns the {@link ConnectionContext}. * * @return the {@link ConnectionContext}. */ ConnectionContext getContext(); /** * Returns the database {@link Collation}. * * @return the database {@link Collation}. */ Optional getDatabaseCollation(); /** * Returns the database version. * * @return the database version. */ Optional getDatabaseVersion(); /** * Returns the server {@link Redirect}. * * @return the server redirect. * @since 0.8.2 */ Optional getRedirect(); /** * Returns the {@link TransactionDescriptor}. * * @return the {@link TransactionDescriptor} describing the server-side transaction. */ TransactionDescriptor getTransactionDescriptor(); /** * Returns the {@link TransactionStatus}. * * @return the current {@link TransactionStatus}. */ TransactionStatus getTransactionStatus(); /** * @return the required {@link Collation} for the current database. * @throws IllegalStateException if no {@link Collation} is available. */ default Collation getRequiredCollation() { return getDatabaseCollation().orElseThrow(() -> new IllegalStateException("Collation not available")); } /** * Returns whether the server supports column encryption. * * @return {@code true} if the server supports column encryption. */ boolean isColumnEncryptionSupported(); /** * Returns whether the client is connected to a server. * * @return {@literal true} if the client is connected to a server. */ boolean isConnected(); } ================================================ FILE: src/main/java/io/r2dbc/mssql/client/ClientConfiguration.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client; import io.r2dbc.mssql.client.ssl.SslConfiguration; import reactor.netty.resources.ConnectionProvider; import java.time.Duration; /** * Connection configuration details. * * @author Mark Paluch */ public interface ClientConfiguration extends SslConfiguration { /** * @return server hostname. */ String getHost(); /** * @return server port. */ int getPort(); /** * @return connection timeout. */ Duration getConnectTimeout(); /** * @return whether TCP KeepAlive is enabled. * @since 0.8.5 */ boolean isTcpKeepAlive(); /** * @return whether TCP NoDelay is enabled. * @since 0.8.5 */ boolean isTcpNoDelay(); /** * @return connection provider. */ ConnectionProvider getConnectionProvider(); /** * @return the SSL tunnel configuration. * @since 0.8.5 */ default SslConfiguration getSslTunnelConfiguration() { return DisabledSslTunnel.INSTANCE; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/client/ConnectionContext.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client; import io.r2dbc.mssql.MssqlConnectionConfiguration; import reactor.util.Logger; import reactor.util.Loggers; import javax.annotation.Nullable; import java.util.UUID; import java.util.concurrent.atomic.AtomicLong; /** * Value object capturing diagnostic connection context. Allows for log-message post-processing with {@link #getMessage(String) if the logger category for * {@code io.r2dbc.mssql.client.ConnectionContext} is enabled for DEBUG/TRACE logs. *

* Captures also the configured {@link MssqlConnectionConfiguration#getApplicationName()} application name} and {@link MssqlConnectionConfiguration#getConnectionId() connection Id}. * * @author Mark Paluch */ public class ConnectionContext { private final static Logger LOGGER = Loggers.getLogger(ConnectionContext.class.getName() + ".context"); private final static boolean CONTEXT_ENABLED = LOGGER.isDebugEnabled(); private final static boolean CHANNEL_ID_ENABLED = LOGGER.isTraceEnabled(); private static final AtomicLong CONN_ID = new AtomicLong(); @Nullable private final String applicationName; @Nullable private final UUID connectionId; @Nullable private final String channelId; private final String connectionCounter; private final String connectionIdPrefix; /** * Create a new {@link ConnectionContext} with a unique connection Id. */ public ConnectionContext() { this.applicationName = null; this.connectionId = null; this.connectionCounter = incrementConnectionCounter(); this.connectionIdPrefix = getConnectionIdPrefix(); this.channelId = null; } /** * Create a new {@link ConnectionContext} with a unique connection Id. */ public ConnectionContext(@Nullable String applicationName, @Nullable UUID connectionId) { this.applicationName = applicationName; this.connectionId = connectionId; this.connectionCounter = incrementConnectionCounter(); this.connectionIdPrefix = getConnectionIdPrefix(); this.channelId = null; } private ConnectionContext(@Nullable String applicationName, @Nullable UUID connectionId, @Nullable String channelId, String connectionCounter, String connectionIdPrefix) { this.applicationName = applicationName; this.connectionId = connectionId; this.channelId = channelId; this.connectionCounter = connectionCounter; this.connectionIdPrefix = connectionIdPrefix; } private String incrementConnectionCounter() { return Long.toHexString(CONN_ID.incrementAndGet()); } private String getConnectionIdPrefix() { return "[cid: 0x" + this.connectionCounter + "] "; } /** * Process the {@code original} message to inject potentially debug information such as the channel Id or the connection Id. * * @param original the original message. * @return the post-processed log message. */ public String getMessage(String original) { if (CHANNEL_ID_ENABLED) { return this.connectionIdPrefix + this.channelId + " " + original; } if (CONTEXT_ENABLED) { return this.connectionIdPrefix + original; } return original; } /** * Create a new {@link ConnectionContext} by associating the {@code channelId}. * * @param channelId the channel identifier. * @return a new {@link ConnectionContext} with all previously set values and the associated {@code channelId}. */ public ConnectionContext withChannelId(String channelId) { return new ConnectionContext(this.applicationName, this.connectionId, channelId, this.connectionCounter, this.connectionIdPrefix); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/client/ConnectionState.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client; import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; import io.netty.handler.ssl.SslHandler; import io.r2dbc.mssql.client.ssl.SslState; import io.r2dbc.mssql.message.Message; import io.r2dbc.mssql.message.header.Header; import io.r2dbc.mssql.message.header.Type; import io.r2dbc.mssql.message.tds.ProtocolException; import io.r2dbc.mssql.message.token.AbstractDoneToken; import io.r2dbc.mssql.message.token.DoneToken; import io.r2dbc.mssql.message.token.Login7; import io.r2dbc.mssql.message.token.Prelogin; import io.r2dbc.mssql.message.token.Tabular; import io.r2dbc.mssql.util.Assert; import reactor.core.publisher.SynchronousSink; import reactor.netty.Connection; import java.util.Collections; import java.util.List; import static io.r2dbc.mssql.message.header.Status.StatusBit; /** * Connection state according to the TDS state machine. The flow is defined as: *

    *
  • Create the transport connection from the client to the server
  • *
  • Enter {@link #PRELOGIN} state and send a {@link Prelogin} message
  • *
  • If encryption is required/off/on, then enter {@link #PRELOGIN_SSL_NEGOTIATION}. Note that * {@link Prelogin.Encryption#ENCRYPT_OFF} requires SSL negotiation for the {@link Login7} message.
  • *
  • Enter {@link #LOGIN} state once SSL is negotiated and send a {@link Login7} message
  • *
  • Enter {@link #POST_LOGIN} after receiving login ack
  • *
* Connection states can {@link #canAdvance(Message) advance} triggered by a received {@link Message}. A state can provide a {@link MessageDecoder} function to decode messages exchanged in that * state. Note that message decoding is not supported in all states as per TDS state specification. * * @author Mark Paluch */ public enum ConnectionState { /** * State directly after the establishing the transport connection. *

* The only allowed message to send and receive is {@link Prelogin}. */ PRELOGIN { @Override MessageDecoder decoder(Client client) { return (header, byteBuf) -> { Assert.isTrue(header.getType() == Type.TABULAR_RESULT, () -> "Expected tabular message, header type is: " + header.getType()); Assert.isTrue(header.is(StatusBit.EOM), "Prelogin response packet must not be chunked"); return Collections.singletonList(Prelogin.decode(byteBuf)); }; } @Override public boolean canAdvance(Message message) { Prelogin prelogin = (Prelogin) message; Prelogin.Version version = prelogin.getRequiredToken(Prelogin.Version.class); if (version.getVersion() >= 9) { return true; } throw ProtocolException.unsupported("Unsupported SQL server version: " + version.getVersion()); } @Override public ConnectionState next(Message message, Connection connection) { Prelogin prelogin = (Prelogin) message; Prelogin.Encryption encryption = prelogin.getRequiredToken(Prelogin.Encryption.class); if (encryption.requiresLoginSslHandshake()) { Channel channel = connection.channel(); channel.pipeline().fireUserEventTriggered(SslState.LOGIN_ONLY); return PRELOGIN_SSL_NEGOTIATION; } if (encryption.requiresConnectionSslHandshake()) { Channel channel = connection.channel(); channel.pipeline().fireUserEventTriggered(SslState.CONNECTION); return PRELOGIN_SSL_NEGOTIATION; } return PRELOGIN; } }, /** * SSL negotiation state. This state is handled entirely on the transport level. * * @see SslHandler */ PRELOGIN_SSL_NEGOTIATION { @Override public boolean canAdvance(Message message) { return message == SslState.NEGOTIATED; } @Override public ConnectionState next(Message message, Connection connection) { return LOGIN; } @Override MessageDecoder decoder(Client client) { return (header, byteBuf) -> { throw ProtocolException.invalidTds("Nothing to decode during SSL negotiation"); }; } }, /** * State during login. * * @see Login7 */ LOGIN { @Override public boolean canAdvance(Message message) { return message instanceof DoneToken; } @Override public ConnectionState next(Message message, Connection connection) { if (AbstractDoneToken.isDone(message)) { return POST_LOGIN; } return LOGIN_FAILED; } @Override MessageDecoder decoder(Client client) { return (header, byteBuf) -> { Assert.isTrue(header.getType() == Type.TABULAR_RESULT, () -> "Expected tabular message, header type is: " + header.getType()); Assert.isTrue(header.is(StatusBit.EOM), "Login response packet must not be chunked"); Tabular tabular = Tabular.decode(byteBuf, client.isColumnEncryptionSupported()); return tabular.getTokens(); }; } }, /** * State after successful login. */ POST_LOGIN { @Override public boolean canAdvance(Message message) { return false; } @Override public ConnectionState next(Message message, Connection connection) { return null; } @Override MessageDecoder decoder(Client client) { Tabular.TabularDecoder decoder = Tabular.createDecoder(client.isColumnEncryptionSupported()); return new MessageDecoder() { @Override public List apply(Header header, ByteBuf byteBuf) { Assert.isTrue(header.getType() == Type.TABULAR_RESULT, () -> "Expected tabular message, header type is: " + header.getType()); return decoder.decode(byteBuf); } @Override public boolean decode(Header header, ByteBuf buffer, SynchronousSink sink) { return decoder.decode(buffer, sink); } }; } }, /** * State after failed login. */ LOGIN_FAILED { @Override public boolean canAdvance(Message message) { return false; } @Override public ConnectionState next(Message message, Connection connection) { return null; } @Override MessageDecoder decoder(Client client) { return null; } }; /** * Check whether the state can advance from the given {@link Message} into a differen {@link ConnectionState}. * * @param message the message to inspect. * @return {@code true} if the state can advance. */ public abstract boolean canAdvance(Message message); /** * Return the next {@link ConnectionState} using the given {@link Message} and {@link Connection transport connection}. * * @param message the message that triggered connection state change. * @param connection the transport connection. * @return the next {@link ConnectionState}. */ public abstract ConnectionState next(Message message, Connection connection); /** * Returns the {@link MessageDecoder} that is applicable for the current {@link ConnectionState}. * Message decoding is not supported in all states. * * @param client the client instance. * @return the {@link MessageDecoder}. */ abstract MessageDecoder decoder(Client client); } ================================================ FILE: src/main/java/io/r2dbc/mssql/client/DisabledSslTunnel.java ================================================ /* * Copyright 2020-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client; import io.netty.handler.ssl.SslContext; import io.r2dbc.mssql.client.ssl.SslConfiguration; enum DisabledSslTunnel implements SslConfiguration { INSTANCE; @Override public boolean isSslEnabled() { return false; } @Override public SslContext getSslContext() { throw new IllegalStateException("SSL tunnel is disabled"); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/client/EnvironmentChangeEvent.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client; import io.r2dbc.mssql.message.token.EnvChangeToken; import io.r2dbc.mssql.util.Assert; /** * Environment change event based on a {@link EnvChangeToken}. * * @author Mark Paluch */ public class EnvironmentChangeEvent { private final EnvChangeToken token; /** * Create a new {@link EnvironmentChangeEvent}. * * @param token the environment change token. */ public EnvironmentChangeEvent(EnvChangeToken token) { this.token = Assert.requireNonNull(token, "EnvChangeToken must not be null"); } /** * @return the environment change token. */ public EnvChangeToken getToken() { return this.token; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/client/EnvironmentChangeListener.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client; /** * Listener interface for {@link EnvironmentChangeEvent}s. This interface is intended for objects that want to be * notified about environment changes such as a changed database or packet size. * * @author Mark Paluch */ @FunctionalInterface public interface EnvironmentChangeListener { /** * Event listener callback for environment change events. * * @param event environment change event */ void onEnvironmentChange(EnvironmentChangeEvent event); } ================================================ FILE: src/main/java/io/r2dbc/mssql/client/MessageDecoder.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.Message; import io.r2dbc.mssql.message.header.Header; import reactor.core.publisher.SynchronousSink; import java.util.List; import java.util.function.BiFunction; /** * Decoder interface that accepts a {@link Header} and {@link ByteBuf data buffer} to attempt to decode {@link Message}s. * * @author Mark Paluch * @see StreamDecoder */ interface MessageDecoder extends BiFunction> { /** * Apply the decoder function {@link #decode(Header, ByteBuf, SynchronousSink)} and notify {@link SynchronousSink} about every decoded {@link Message}. * * @param header * @param buffer * @param sink * @return */ default boolean decode(Header header, ByteBuf buffer, SynchronousSink sink) { List messages = apply(header, buffer); if (messages.isEmpty()) { return false; } messages.forEach(sink::next); return true; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/client/ReactorNettyClient.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslHandler; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; import io.r2dbc.mssql.client.ssl.SslConfiguration; import io.r2dbc.mssql.client.ssl.TdsSslHandler; import io.r2dbc.mssql.message.ClientMessage; import io.r2dbc.mssql.message.Message; import io.r2dbc.mssql.message.TransactionDescriptor; import io.r2dbc.mssql.message.header.PacketIdProvider; import io.r2dbc.mssql.message.tds.ProtocolException; import io.r2dbc.mssql.message.tds.Redirect; import io.r2dbc.mssql.message.token.*; import io.r2dbc.mssql.message.type.Collation; import io.r2dbc.mssql.util.Assert; import io.r2dbc.spi.R2dbcException; import io.r2dbc.spi.R2dbcNonTransientResourceException; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.publisher.*; import reactor.netty.Connection; import reactor.netty.NettyOutbound; import reactor.netty.resources.ConnectionProvider; import reactor.netty.tcp.SslProvider; import reactor.netty.tcp.TcpClient; import reactor.netty.tcp.TcpSslContextSpec; import reactor.util.Logger; import reactor.util.Loggers; import reactor.util.concurrent.Queues; import reactor.util.context.Context; import reactor.util.context.ContextView; import javax.annotation.Nullable; import java.security.GeneralSecurityException; import java.time.Duration; import java.util.Collections; import java.util.Optional; import java.util.Queue; import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; /** * An implementation of a TDS client based on the Reactor Netty project. * * @see TcpClient */ public final class ReactorNettyClient implements Client { private static final Logger logger = Loggers.getLogger(ReactorNettyClient.class); private static final boolean DEBUG_ENABLED = logger.isDebugEnabled(); private static final Supplier UNEXPECTED = () -> new MssqlConnectionClosedException("Connection unexpectedly closed"); private static final Supplier EXPECTED = () -> new MssqlConnectionClosedException("Connection closed"); private static final Supplier CLOSED = () -> new MssqlConnectionClosedException("Cannot exchange messages because the connection is closed"); private final ConnectionContext context; private final ByteBufAllocator byteBufAllocator; private final Connection connection; private final TdsEncoder tdsEncoder; private final Consumer handleEnvChange; private final Consumer featureAckChange = (token) -> { for (FeatureExtAckToken.FeatureToken featureToken : token.getFeatureTokens()) { if (featureToken instanceof FeatureExtAckToken.ColumnEncryption) { this.encryptionSupported = true; } } }; private final AtomicBoolean isClosed = new AtomicBoolean(false); private final AtomicLong attentionPropagation = new AtomicLong(); private final AtomicLong outstandingRequests = new AtomicLong(); private final Sinks.Many requestSink = Sinks.many().unicast().onBackpressureBuffer(); private final Sinks.Many responseProcessor = Sinks.many().multicast().onBackpressureBuffer(512, false); private final TransactionListener transactionListener = new TransactionListener(); private final CollationListener collationListener = new CollationListener(); private final RedirectListener redirectListener = new RedirectListener(); private final RequestQueue requestQueue; // May change during initialization. Values remain the same after connection initialization. private ConnectionState state = ConnectionState.PRELOGIN; private MessageDecoder decodeFunction = ConnectionState.PRELOGIN.decoder(this); private boolean encryptionSupported = false; private volatile Optional databaseCollation = Optional.empty(); private Optional databaseVersion = Optional.empty(); private volatile Optional redirect = Optional.empty(); // May change during driver interaction, may be read on other threads. private volatile TransactionDescriptor transactionDescriptor = TransactionDescriptor.empty(); private volatile TransactionStatus transactionStatus = TransactionStatus.AUTO_COMMIT; /** * Creates a new frame processor connected to a given TCP connection. * * @param connection the TCP connection * @param connectionContext the connection context */ private ReactorNettyClient(Connection connection, TdsEncoder tdsEncoder, ConnectionContext connectionContext) { Assert.requireNonNull(connection, "Connection must not be null"); Assert.state(this.responseProcessor.asFlux() instanceof Subscriber, () -> "Response processor " + this.responseProcessor + " is not a Subscriber. Cannot proceed."); this.context = connectionContext; StreamDecoder decoder = new StreamDecoder(); this.handleEnvChange = (token) -> { EnvironmentChangeEvent event = new EnvironmentChangeEvent(token); try { tdsEncoder.onEnvironmentChange(event); this.transactionListener.onEnvironmentChange(event); this.collationListener.onEnvironmentChange(event); this.redirectListener.onEnvironmentChange(event); } catch (Exception e) { logger.warn(this.context.getMessage("Failed onEnvironmentChange() in {}"), "", e); } }; this.byteBufAllocator = connection.outbound().alloc(); this.connection = connection; this.tdsEncoder = tdsEncoder; this.requestQueue = new RequestQueue(this.context); Consumer handleStateChange = (message) -> { if (message.getClass() == LoginAckToken.class) { LoginAckToken loginAckToken = (LoginAckToken) message; this.databaseVersion = Optional.of(loginAckToken.getVersion().toString()); } ConnectionState connectionState = this.state; if (connectionState.canAdvance(message)) { ConnectionState nextState = connectionState.next(message, connection); this.state = nextState; this.decodeFunction = nextState.decoder(this); } }; AtomicReference subscriptionRef = new AtomicReference<>(); SynchronousSink sink = new SynchronousSink() { @Override public void complete() { throw new UnsupportedOperationException(); } @Deprecated @Override public Context currentContext() { return Context.empty(); } @Override public ContextView contextView() { return Context.empty(); } @Override public void error(Throwable e) { Throwable errorToUse = e; if (!(errorToUse instanceof R2dbcException)) { errorToUse = new MssqlConnectionException(errorToUse); } ReactorNettyClient.this.responseProcessor.emitError(errorToUse, Sinks.EmitFailureHandler.FAIL_FAST); } @Override public void next(Message message) { if (DEBUG_ENABLED) { onInfoToken(message); } handleStateChange.accept(message); if (message.getClass() == EnvChangeToken.class) { ReactorNettyClient.this.handleEnvChange.accept((EnvChangeToken) message); } if (message.getClass() == FeatureExtAckToken.class) { ReactorNettyClient.this.featureAckChange.accept((FeatureExtAckToken) message); } Subscription subscription = subscriptionRef.get(); if (AbstractDoneToken.isAttentionAck(message)) { long current; do { current = ReactorNettyClient.this.attentionPropagation.get(); if (current == 0) { if (DEBUG_ENABLED) { logger.debug(ReactorNettyClient.this.context.getMessage("Swallowing attention acknowledged, no pending requests: {}. "), message); } // update demand for dropped next signal if (subscription != null) { subscription.request(1); } return; } } while (!ReactorNettyClient.this.attentionPropagation.compareAndSet(current, current - 1)); } long attentionPropagation = ReactorNettyClient.this.attentionPropagation.get(); if (attentionPropagation > 0 && !AbstractDoneToken.isAttentionAck(message)) { if (DEBUG_ENABLED) { logger.debug(ReactorNettyClient.this.context.getMessage("Discard message {}. Draining frames until attention acknowledgement."), message); } // update demand for dropped next signal if (subscription != null) { subscription.request(1); } return; } ReactorNettyClient.this.responseProcessor.emitNext(message, Sinks.EmitFailureHandler.FAIL_FAST); } }; connection.inbound().receiveObject() // .concatMapIterable(it -> { if (it instanceof ByteBuf) { ByteBuf buffer = (ByteBuf) it; return decoder.decode(buffer, this.decodeFunction); } if (it instanceof Message) { return Collections.singleton((Message) it); } throw ProtocolException.unsupported(String.format("Unexpected protocol message: [%s]", it)); }) .onErrorResume(this::resumeError) .subscribe(new CoreSubscriber() { @Override public void onSubscribe(Subscription s) { subscriptionRef.set(s); ((Subscriber) ReactorNettyClient.this.responseProcessor.asFlux()).onSubscribe(s); } @Override public void onNext(Message message) { sink.next(message); } @Override public void onError(Throwable t) { sink.error(t); } @Override public void onComplete() { handleClose(); } }); this.requestSink .asFlux() .concatMap( message -> { Object encoded = encodeForSend(message); if (encoded instanceof Publisher) { return connection.outbound().sendObject((Publisher) encoded); } return connection.outbound().sendObject(encoded); }) .onErrorResume(this::resumeError) .doAfterTerminate(this::handleClose) .subscribe(); } private Object encodeForSend(ClientMessage message) { if (DEBUG_ENABLED) { logger.debug(this.context.getMessage("Request: {}"), message); } return message.encode(this.connection.outbound().alloc(), this.tdsEncoder.getPacketSize()); } @SuppressWarnings("unchecked") private Mono resumeError(Throwable throwable) { logger.error(this.context.getMessage("Error: {}"), throwable.getMessage(), throwable); this.requestSink.emitComplete((signalType, emitResult) -> { if (emitResult.isFailure()) { logger.error(this.context.getMessage("Error: {}"), emitResult); } return false; }); handleConnectionError(throwable); return (Mono) close(); } private void onInfoToken(Message message) { logger.debug(this.context.getMessage("Response: {}"), message); if (message instanceof AbstractInfoToken) { AbstractInfoToken token = (AbstractInfoToken) message; if (token.getClassification() == AbstractInfoToken.Classification.INFORMATIONAL) { logger.debug(this.context.getMessage("Info: Code [{}] Severity [{}]: {}"), token.getNumber(), token.getClassification(), token.getMessage()); } else { logger.debug(this.context.getMessage("Warning: Code [{}] Severity [{}]: {}"), token.getNumber(), token.getClassification(), token.getMessage()); } } } /** * Creates a new frame processor connected to a given host. * * @param host the host to connect to * @param port the port to connect to */ public static Mono connect(String host, int port) { Assert.requireNonNull(host, "host must not be null"); return connect(host, port, Duration.ofSeconds(30)); } /** * Creates a new frame processor connected to a given host. * * @param host the host to connect to * @param port the port to connect to * @param connectTimeout the connect timeout */ public static Mono connect(String host, int port, Duration connectTimeout) { Assert.requireNonNull(connectTimeout, "connect timeout must not be null"); Assert.requireNonNull(host, "host must not be null"); return connect(new ClientConfiguration() { @Override public String getHost() { return host; } @Override public int getPort() { return port; } @Override public Duration getConnectTimeout() { return connectTimeout; } @Override public boolean isTcpKeepAlive() { return false; } @Override public boolean isTcpNoDelay() { return true; } @Override public ConnectionProvider getConnectionProvider() { return ConnectionProvider.newConnection(); } @Override public boolean isSslEnabled() { return false; } @Override public SslContext getSslContext() { return SslProvider.builder() .sslContext(TcpSslContextSpec.forClient()) .build().getSslContext(); } }, null, null); } /** * Creates a new frame processor connected to {@link ClientConfiguration}. * * @param configuration the client configuration * @param applicationName * @param connectionId */ public static Mono connect(ClientConfiguration configuration, @Nullable String applicationName, @Nullable UUID connectionId) { Assert.requireNonNull(configuration, "configuration must not be null"); ConnectionContext connectionContext = new ConnectionContext(applicationName, connectionId); logger.debug(connectionContext.getMessage("connect()")); PacketIdProvider packetIdProvider = PacketIdProvider.atomic(); TdsEncoder tdsEncoder = new TdsEncoder(packetIdProvider); Mono connection = TcpClient.create(configuration.getConnectionProvider()) .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, Math.toIntExact(configuration.getConnectTimeout().toMillis())) .option(ChannelOption.SO_KEEPALIVE, configuration.isTcpKeepAlive()) .option(ChannelOption.TCP_NODELAY, configuration.isTcpNoDelay()) .host(configuration.getHost()) .port(configuration.getPort()) .connect() .doOnNext(it -> { SslConfiguration tunnel = configuration.getSslTunnelConfiguration(); ChannelPipeline pipeline = it.channel().pipeline(); if (tunnel.isSslEnabled()) { logger.debug(connectionContext.getMessage("Enabling SSL tunnel")); try { pipeline.addFirst("sslTunnel", createSslTunnelHandler(it.channel().alloc(), tunnel)); } catch (GeneralSecurityException e) { it.channel().close(); throw new IllegalStateException("Cannot configure SSL tunnel", e); } pipeline.addAfter("sslTunnel", tdsEncoder.getClass().getName(), tdsEncoder); } else { pipeline.addFirst(tdsEncoder.getClass().getName(), tdsEncoder); } TdsSslHandler handler = new TdsSslHandler(packetIdProvider, configuration, connectionContext.withChannelId(it.channel().toString())); pipeline.addAfter(tdsEncoder.getClass().getName(), handler.getClass().getName(), handler); InternalLogger logger = InternalLoggerFactory.getInstance(ReactorNettyClient.class); if (logger.isTraceEnabled()) { pipeline.addBefore(tdsEncoder.getClass().getName(), LoggingHandler.class.getSimpleName(), new LoggingHandler(ReactorNettyClient.class, LogLevel.TRACE)); } }); return connection.map(it -> new ReactorNettyClient(it, tdsEncoder, connectionContext.withChannelId(it.channel().toString()))); } private static SslHandler createSslTunnelHandler(ByteBufAllocator allocator, SslConfiguration tunnel) throws GeneralSecurityException { return new SslHandler(tunnel.getSslContext().newEngine(allocator)); } @Override public Mono attention() { return Mono.defer(() -> Mono.fromFuture(send(Mono.just(Attention.create(1, getTransactionDescriptor()))).toFuture())); } @Override public Mono close() { logger.debug(this.context.getMessage("close()")); return Mono.defer(() -> { logger.debug(this.context.getMessage("close(subscribed)")); if (this.isClosed.compareAndSet(false, true)) { this.connection.dispose(); return this.connection.onDispose(); } return Mono.empty(); }); } @Override public ByteBufAllocator getByteBufAllocator() { return this.byteBufAllocator; } @Override public ConnectionContext getContext() { return this.context; } @Override public Optional getDatabaseCollation() { return this.databaseCollation; } @Override public Optional getDatabaseVersion() { return this.databaseVersion; } @Override public Optional getRedirect() { return this.redirect; } @Override public TransactionDescriptor getTransactionDescriptor() { return this.transactionDescriptor; } @Override public TransactionStatus getTransactionStatus() { return this.transactionStatus; } @Override public boolean isColumnEncryptionSupported() { return this.encryptionSupported; } @Override public boolean isConnected() { if (this.isClosed.get()) { return false; } Channel channel = this.connection.channel(); return channel.isOpen(); } @Override public Flux exchange(Publisher requests, Predicate takeUntil) { Assert.requireNonNull(takeUntil, "takeUntil must not be null"); Assert.requireNonNull(requests, "Requests must not be null"); if (DEBUG_ENABLED) { logger.debug(this.context.getMessage("exchange()")); } ExchangeRequest exchangeRequest = new ExchangeRequest(); Flux handle = Mono.>create(sink -> { if (DEBUG_ENABLED) { logger.debug(this.context.getMessage("exchange(subscribed)")); } if (!isConnected()) { sink.error(CLOSED.get()); } Flux requestMessages = this.responseProcessor.asFlux() .doOnSubscribe(ignore -> { this.outstandingRequests.incrementAndGet(); Flux.from(requests).subscribe(t -> { if (!isConnected()) { sink.error(CLOSED.get()); return; } this.requestSink.emitNext(t, Sinks.EmitFailureHandler.FAIL_FAST); }, e -> this.requestSink.emitError(e, Sinks.EmitFailureHandler.FAIL_FAST), () -> { if (!isConnected()) { sink.error(CLOSED.get()); } }); }); try { exchangeRequest.submit(this.requestQueue, sink, requestMessages); } catch (Exception e) { sink.error(e); } }).flatMapMany(Function.identity()).handle((message, sink) -> { sink.next(message); if (takeUntil.test(message)) { exchangeRequest.complete(); sink.complete(); } }); return handle.doAfterTerminate(this.requestQueue).doFinally(it -> this.outstandingRequests.decrementAndGet()).doOnCancel(() -> { if (!exchangeRequest.isComplete()) { logger.error("Exchange cancelled while exchange is active. This is likely a bug leading to unpredictable outcome."); } }); } private Mono send(Publisher requests) { return Flux.from(requests).concatMap(message -> { NettyOutbound nettyOutbound = this.connection.outbound().sendObject(encodeForSend(message)); if (message instanceof Attention && this.outstandingRequests.longValue() != 0) { return Mono.from(nettyOutbound).doOnSuccess(v -> this.attentionPropagation.incrementAndGet()); } return nettyOutbound; }).then(); } private void handleClose() { if (this.isClosed.compareAndSet(false, true)) { logger.warn(ReactorNettyClient.this.context.getMessage("Connection has been closed by peer")); drainError(UNEXPECTED); } else { drainError(EXPECTED); } } private void handleConnectionError(Throwable error) { drainError(() -> new MssqlConnectionException(error)); } private void drainError(Supplier supplier) { Sinkable receiver; while ((receiver = this.requestQueue.poll()) != null) { receiver.onError(supplier.get()); } this.responseProcessor.emitError(supplier.get(), Sinks.EmitFailureHandler.FAIL_FAST); } /** * Request queue to collect incoming exchange requests. *

Submission conditionally queues requests if an ongoing exchange was active by the time of subscription. * Drains queued commands on exchange completion if there are queued commands or disable active flag. */ static class RequestQueue implements Runnable { private final Queue requestQueue = Queues.small().get(); private final AtomicBoolean active = new AtomicBoolean(); private final ConnectionContext context; RequestQueue(ConnectionContext context) { this.context = context; } @Nullable public Sinkable poll() { return this.requestQueue.poll(); } @Override public void run() { Sinkable nextCommand = this.requestQueue.poll(); if (nextCommand != null) { if (DEBUG_ENABLED) { logger.debug(this.context.getMessage("Initiating queued exchange")); } nextCommand.onSuccess(); return; } if (DEBUG_ENABLED) { logger.debug(this.context.getMessage("Conversation complete")); } this.active.compareAndSet(true, false); } /** * Submit a {@code exchangeRequest}. Requests are either executed directly (without an active exchange) or queued (if another exchange is currently active). * * @param exchangeRequest */ void submit(Sinkable exchangeRequest) { if (this.active.compareAndSet(false, true)) { if (DEBUG_ENABLED) { logger.debug(this.context.getMessage("Initiating exchange")); } exchangeRequest.onSuccess(); } else { if (DEBUG_ENABLED) { logger.debug(this.context.getMessage("Queueing exchange")); } if (!this.requestQueue.offer(exchangeRequest)) { throw new IllegalStateException("Request queue is full"); } drainRequestQueue(); } } void drainRequestQueue() { if (this.active.compareAndSet(false, true)) { Sinkable runnable = this.requestQueue.poll(); if (runnable != null) { runnable.onSuccess(); } else { this.active.compareAndSet(true, false); } } } } /** * Ensure a command request is submitted and subscribed to only once. */ static class ExchangeRequest { private static final AtomicIntegerFieldUpdater COMPLETED = AtomicIntegerFieldUpdater.newUpdater(ExchangeRequest.class, "completed"); private static final AtomicIntegerFieldUpdater SUBMITTED = AtomicIntegerFieldUpdater.newUpdater(ExchangeRequest.class, "submitted"); // access via COMPLETED private volatile int completed = 0; // access via SUBMITTED private volatile int submitted = 0; public void complete() { COMPLETED.set(this, 1); } public boolean isComplete() { return COMPLETED.get(this) == 1; } void submit(RequestQueue queue, MonoSink> sink, Flux requestMessages) { if (!SUBMITTED.compareAndSet(this, 0, 1)) { throw new IllegalStateException("Client exchange can be subscribed only once"); } queue.submit(new Sinkable() { @Override public void onSuccess() { sink.success(requestMessages); } @Override public void onError(Throwable throwable) { sink.error(throwable); } }); } } class TransactionListener implements EnvironmentChangeListener { @Override public void onEnvironmentChange(EnvironmentChangeEvent event) { EnvChangeToken token = event.getToken(); if (token.getChangeType() == EnvChangeToken.EnvChangeType.BeginTx || token.getChangeType() == EnvChangeToken.EnvChangeType.EnlistDTC) { byte[] descriptor = token.getNewValue(); if (descriptor.length != TransactionDescriptor.LENGTH) { throw ProtocolException.invalidTds("Transaction descriptor length mismatch"); } if (DEBUG_ENABLED) { String op; if (token.getChangeType() == EnvChangeToken.EnvChangeType.BeginTx) { op = "started"; } else { op = "enlisted"; } logger.debug(String.format(ReactorNettyClient.this.context.getMessage("Transaction %s"), op)); } updateStatus(TransactionStatus.STARTED, TransactionDescriptor.from(descriptor)); } if (token.getChangeType() == EnvChangeToken.EnvChangeType.CommitTx) { if (DEBUG_ENABLED) { logger.debug(ReactorNettyClient.this.context.getMessage("Transaction committed")); } updateStatus(TransactionStatus.EXPLICIT, TransactionDescriptor.empty()); } if (token.getChangeType() == EnvChangeToken.EnvChangeType.RollbackTx) { if (DEBUG_ENABLED) { logger.debug(ReactorNettyClient.this.context.getMessage("Transaction rolled back")); } updateStatus(TransactionStatus.EXPLICIT, TransactionDescriptor.empty()); } } private void updateStatus(TransactionStatus status, TransactionDescriptor descriptor) { ReactorNettyClient.this.transactionStatus = status; ReactorNettyClient.this.transactionDescriptor = descriptor; } } class CollationListener implements EnvironmentChangeListener { @Override public void onEnvironmentChange(EnvironmentChangeEvent event) { if (event.getToken().getChangeType() == EnvChangeToken.EnvChangeType.SQLCollation) { Collation collation = Collation.decode(Unpooled.wrappedBuffer(event.getToken().getNewValue())); ReactorNettyClient.this.databaseCollation = Optional.of(collation); } } } class RedirectListener implements EnvironmentChangeListener { @Override public void onEnvironmentChange(EnvironmentChangeEvent event) { if (event.getToken().getChangeType() == EnvChangeToken.EnvChangeType.Routing) { Redirect redirect = Redirect.decode(Unpooled.wrappedBuffer(event.getToken().getNewValue())); ReactorNettyClient.this.redirect = Optional.of(redirect); } } } interface Sinkable { void onSuccess(); void onError(Throwable throwable); } static class MssqlConnectionClosedException extends R2dbcNonTransientResourceException { public MssqlConnectionClosedException(String reason) { super(reason); } } static class MssqlConnectionException extends R2dbcNonTransientResourceException { public MssqlConnectionException(Throwable cause) { super(cause); } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/client/StreamDecoder.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.Message; import io.r2dbc.mssql.message.header.Header; import io.r2dbc.mssql.message.header.Status; import io.r2dbc.mssql.util.Assert; import reactor.core.publisher.Flux; import reactor.core.publisher.SynchronousSink; import reactor.util.annotation.Nullable; import reactor.util.context.Context; import reactor.util.context.ContextView; import java.util.ArrayList; import java.util.List; /** * A TDS decoder that reads {@link ByteBuf}s and returns a {@link Flux} of decoded {@link Message}s. *

* TDS messages consist of a header ({@link Header#LENGTH 8 byte length}) and a body. Messages can be either self-contained ({@link Status.StatusBit#EOM}) or chunked. This decoder attempts to * decode messages from a {@link ByteBuf stream} by emitting zero, one or many {@link Message}s. Data buffers are aggregated and de-chunked until reaching a message boundary, then adaptive decoding * attempts to decode the aggregated and de-chunked body as far as possible. Remaining (non-decodable) data buffers are aggregated until the next attempt. *

* This decoder is stateful and should be used in a try-to-decode fashion. * * @author Mark Paluch * @see Message * @see Header */ final class StreamDecoder { private DecoderState state; /** * Decode a {@link ByteBuf} into a {@link Flux} of {@link Message}s. If the {@link ByteBuf} does not end on a * {@link Message} boundary, the {@link ByteBuf} will be retained until the concatenated contents of all retained * {@link ByteBuf}s is a {@link Message} boundary. * * @param in the {@link ByteBuf} to decode * @return a {@link Flux} of {@link Message}s */ public List decode(ByteBuf in, MessageDecoder messageDecoder) { Assert.requireNonNull(in, "in must not be null"); Assert.requireNonNull(messageDecoder, "MessageDecoder must not be null"); ListSink result = new ListSink<>(); decode(in, messageDecoder, result); return result; } /** * Decode a {@link ByteBuf} into a stream of {@link Message}s notifying {@link SynchronousSink}. If the {@link ByteBuf} does not end on a * {@link Message} boundary, the {@link ByteBuf} will be retained until the concatenated contents of all retained * {@link ByteBuf}s is a {@link Message} boundary. * * @param in the {@link ByteBuf} to decode * @return a {@link Flux} of {@link Message}s */ public void decode(ByteBuf in, MessageDecoder messageDecoder, SynchronousSink sink) { Assert.requireNonNull(in, "in must not be null"); Assert.requireNonNull(messageDecoder, "MessageDecoder must not be null"); DecoderState decoderState = this.state; this.state = null; DecoderState state = decoderState == null ? DecoderState.initial(in) : decoderState.andChunk(in); do { state = withState(messageDecoder, sink, state); } while (state != null); } @Nullable private DecoderState withState(MessageDecoder messageDecoder, SynchronousSink sink, DecoderState state) { if (state.header == null) { if (!Header.canDecode(state.remainder)) { return retain(state); } state = state.readHeader(); } try { Header header = state.getRequiredHeader(); if (!state.canReadChunk()) { return retain(state); } state = state.readChunk(); int readerIndex = state.aggregatedBodyReaderIndex(); boolean hasMessages = messageDecoder.decode(header, state.aggregatedBody, sink); if (hasMessages) { if (state.hasRawRemainder()) { return state; } if (state.hasAggregatedBodyRemainder()) { return retain(state); } } else { state.aggregatedBodyReaderIndex(readerIndex); return retain(state); } state.release(); return null; } catch (Exception e) { sink.error(e); } return state; } @Nullable private DecoderState retain(DecoderState state) { this.state = state; return null; } @Nullable DecoderState getDecoderState() { return this.state; } /** * The current decoding state. Encapsulates the raw transport stream buffers ("remainder") and the aggregated (de-chunked) body along an optional {@link Header}. */ static class DecoderState { ByteBuf remainder; ByteBuf aggregatedBody; @Nullable Header header; private DecoderState(ByteBuf remainder, ByteBuf aggregatedBody, @Nullable Header header) { this.remainder = remainder; this.header = header; this.aggregatedBody = aggregatedBody; } /** * Create a new, initial {@link DecoderState}. * * @param initialBuffer the data buffer. * @return the initial {@link DecoderState}. */ static DecoderState initial(ByteBuf initialBuffer) { ByteBuf composite = initialBuffer.alloc().buffer(); composite.writeBytes(initialBuffer); ByteBuf aggregatedBody = initialBuffer.alloc().buffer(); return new DecoderState(composite, aggregatedBody, null); } /** * Create a new {@link DecoderState} by appending a new raw remaining {@link ByteBuf data buffer}. * * @param in * @return */ DecoderState andChunk(ByteBuf in) { this.remainder.writeBytes(in); //this.remainder.addComponent(true, in.retain()); return newState(this.remainder, this.aggregatedBody, this.header); } DecoderState newState(ByteBuf remainder, ByteBuf aggregatedBody, @Nullable Header header) { this.remainder = remainder; this.aggregatedBody = aggregatedBody; this.header = header; return this; } boolean canReadChunk() { int requiredChunkLength = getChunkLength(); return this.remainder.readableBytes() >= requiredChunkLength; } /** * @return {@code true} if the remaining raw bytes (raw transport buffer) are not yet fully consumed. */ boolean hasRawRemainder() { return this.remainder.readableBytes() != 0; } /** * @return {@code true} if the remaining aggregated body bytes (aggregation of body buffers without header) are not yet fully consumed. */ boolean hasAggregatedBodyRemainder() { return this.aggregatedBody.readableBytes() != 0; } /** * @return the aggregated body reader index. */ int aggregatedBodyReaderIndex() { return this.aggregatedBody.readerIndex(); } /** * Reset the aggregated body reader index. * * @param index the reader index. */ void aggregatedBodyReaderIndex(int index) { this.aggregatedBody.readerIndex(index); } /** * @return the required {@link Header}. */ Header getRequiredHeader() { if (this.header == null) { throw new IllegalStateException("DecoderState has no header"); } return this.header; } // ---------------------------------------- // State-changing methods. // ---------------------------------------- /** * Create a new {@link DecoderState} with a decoded {@link Header}. * * @return the new {@link DecoderState}. */ DecoderState readHeader() { return newState(this.remainder, this.aggregatedBody, Header.decode(this.remainder)); } /** * Read the body chunk and create a new {@link DecoderState}. * Body is read from the remainder by copying the contents to decouple the remainder from dechunked data. Otherwise we would probably overwrite remainder data with dechunking. * Retains a new header if we were able to decode the header but not the rest of the chunk. Not retaining the header causes the header to be decoded on the next pass and that causes * protocol out of sync. * Drop the header if we were able to dechunk the remainder. * * @return the new {@link DecoderState}. */ DecoderState readChunk() { boolean hasNewHeader; do { hasNewHeader = false; this.aggregatedBody.writeBytes(this.remainder, getChunkLength()); if (Header.canDecode(this.remainder)) { hasNewHeader = true; this.header = Header.decode(this.remainder); } } while (canReadChunk()); if (hasNewHeader) { return newState(this.remainder, this.aggregatedBody, this.header); } return newState(this.remainder, this.aggregatedBody, null); } /** * Retain this {@link DecoderState} (i.e. increment ref count). * * @return {@code this} {@link DecoderState}. */ DecoderState retain() { //this.remainder.consolidate(); this.remainder.retain(); //this.aggregatedBody.consolidate(); this.aggregatedBody.retain(); return this; } /** * Release this {@link DecoderState} (i.e. decrement ref count). */ void release() { this.remainder.release(); this.aggregatedBody.release(); } int getChunkLength() { return getRequiredHeader().getLength() - Header.LENGTH; } } static class ListSink extends ArrayList implements SynchronousSink { public ListSink() { super(2); } @Override public void complete() { throw new UnsupportedOperationException(); } @Deprecated @Override public Context currentContext() { throw new UnsupportedOperationException(); } @Override public ContextView contextView() { throw new UnsupportedOperationException(); } @Override public void error(Throwable e) { throw new RuntimeException(e); } @Override public void next(T message) { add(message); } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/client/TdsEncoder.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client; import io.netty.buffer.ByteBuf; import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.ChannelPromise; import io.netty.util.concurrent.PromiseCombiner; import io.r2dbc.mssql.message.header.Header; import io.r2dbc.mssql.message.header.HeaderOptions; import io.r2dbc.mssql.message.header.PacketIdProvider; import io.r2dbc.mssql.message.header.Status; import io.r2dbc.mssql.message.tds.ContextualTdsFragment; import io.r2dbc.mssql.message.tds.FirstTdsFragment; import io.r2dbc.mssql.message.tds.LastTdsFragment; import io.r2dbc.mssql.message.tds.TdsFragment; import io.r2dbc.mssql.message.tds.TdsPacket; import io.r2dbc.mssql.message.token.EnvChangeToken; import io.r2dbc.mssql.util.Assert; /** * Encoder for TDS packets. This encoder can apply various strategies regarding TDS header handling: *

    *
  • Pass-thru {@link ByteBuf} messages (typically used for SSL traffic)
  • *
  • Prefix {@link ByteBuf} messages with TDS {@link Header} (typically used for SSL Handshake during PRELOGIN)
  • *
  • Apply {@link HeaderOptions} state for subsequent messages (typically used to set a header context until receiving * {@link LastTdsFragment} or {@link ResetHeader}) when initiated by a written {@link HeaderOptions} or * {@link FirstTdsFragment}.
  • *
  • Reset {@link HeaderOptions} when a {@link ResetHeader#INSTANCE ResetHeader} is written.
  • *
* * @author Mark Paluch * @see FirstTdsFragment * @see LastTdsFragment * @see TdsPacket * @see HeaderOptions * @see TdsFragment */ public final class TdsEncoder extends ChannelOutboundHandlerAdapter implements EnvironmentChangeListener { /** * Initial (default) packet size for TDS packets. */ public static final int INITIAL_PACKET_SIZE = 8000; private CompositeByteBuf lastChunkRemainder; private final PacketIdProvider packetIdProvider; private int packetSize; private HeaderOptions headerOptions; /** * Creates a new {@link TdsEncoder} using the default {@link #INITIAL_PACKET_SIZE packet size.}. * * @param packetIdProvider provider for the {@literal packetId}. */ public TdsEncoder(PacketIdProvider packetIdProvider) { this(packetIdProvider, INITIAL_PACKET_SIZE); } /** * Creates a new {@link TdsEncoder} using the given {@code packetSize} * * @param packetIdProvider provider for the {@literal packetId}. * @throws IllegalArgumentException when {@link PacketIdProvider} is {@code null}. */ public TdsEncoder(PacketIdProvider packetIdProvider, int packetSize) { this.packetIdProvider = Assert.requireNonNull(packetIdProvider, "PacketId Provider must not be null"); this.packetSize = packetSize; } @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { if (msg == ResetHeader.INSTANCE) { this.headerOptions = null; ctx.write(Unpooled.EMPTY_BUFFER, promise); return; } if (msg instanceof HeaderOptions) { this.headerOptions = (HeaderOptions) msg; ctx.write(Unpooled.EMPTY_BUFFER, promise); return; } // Expect ByteBuf to be self-contained messages that do not require further chunking (for now). if (msg instanceof ByteBuf) { if (this.headerOptions == null) { ctx.write(msg, promise); return; } ByteBuf message = (ByteBuf) msg; if (message.readableBytes() <= 0) { ctx.write(msg, promise); return; } doWriteFragment(ctx, promise, message, this.headerOptions, true); return; } // Write entire TDSPacket if (msg instanceof TdsPacket) { TdsPacket packet = (TdsPacket) msg; ByteBuf message = packet.encode(ctx.alloc(), this.packetIdProvider); Assert.state(message.readableBytes() <= this.packetSize, "Packet size exceeded"); ctx.write(message, promise); return; } // Write message use HeaderOptions for subsequent packets and apply HeaderOptions if (msg instanceof FirstTdsFragment) { FirstTdsFragment fragment = (FirstTdsFragment) msg; this.headerOptions = fragment.getHeaderOptions(); doWriteFragment(ctx, promise, fragment.getByteBuf(), this.headerOptions, false); return; } // Write message use HeaderOptions for subsequent packets and apply HeaderOptions if (msg instanceof ContextualTdsFragment) { ContextualTdsFragment fragment = (ContextualTdsFragment) msg; doWriteFragment(ctx, promise, fragment.getByteBuf(), fragment.getHeaderOptions(), true); return; } // Write message, apply HeaderOptions and clear HeaderOptions if (msg instanceof LastTdsFragment) { Assert.state(this.headerOptions != null, "HeaderOptions must not be null!"); TdsFragment fragment = (TdsFragment) msg; doWriteFragment(ctx, promise, fragment.getByteBuf(), this.headerOptions, true); this.headerOptions = null; return; } // Write message and apply HeaderOptions if (msg instanceof TdsFragment) { Assert.state(this.headerOptions != null, "HeaderOptions must not be null!"); TdsFragment fragment = (TdsFragment) msg; doWriteFragment(ctx, promise, fragment.getByteBuf(), this.headerOptions, false); return; } throw new IllegalArgumentException(String.format("Unsupported message type: %s", msg)); } @Override public void onEnvironmentChange(EnvironmentChangeEvent event) { EnvChangeToken token = event.getToken(); if (token.getChangeType() == EnvChangeToken.EnvChangeType.Packetsize) { setPacketSize(Integer.parseInt(token.getNewValueString())); } } public void setPacketSize(int packetSize) { this.packetSize = packetSize; } public int getPacketSize() { return this.packetSize; } private static HeaderOptions getLastHeader(HeaderOptions headerOptions) { return headerOptions.and(Status.StatusBit.EOM); } private static HeaderOptions getChunkedHeaderOptions(HeaderOptions headerOptions) { return headerOptions.not(Status.StatusBit.EOM); } private void doWriteFragment(ChannelHandlerContext ctx, ChannelPromise promise, ByteBuf body, HeaderOptions headerOptions, boolean lastLogicalPacket) { if (requiresChunking(body.readableBytes())) { writeChunkedMessage(ctx, promise, body, headerOptions, lastLogicalPacket); } else { writeSingleMessage(ctx, promise, body, headerOptions, lastLogicalPacket); } body.release(); } private void writeSingleMessage(ChannelHandlerContext ctx, ChannelPromise promise, ByteBuf body, HeaderOptions headerOptions, boolean lastLogicalPacket) { if (lastLogicalPacket || getBytesToWrite(body.readableBytes()) == getPacketSize()) { HeaderOptions optionsToUse = lastLogicalPacket ? getLastHeader(headerOptions) : headerOptions; int messageLength = getBytesToWrite(body.readableBytes()); ByteBuf buffer = ctx.alloc().buffer(messageLength); Header.encode(buffer, optionsToUse, messageLength, this.packetIdProvider); if (this.lastChunkRemainder != null) { buffer.writeBytes(this.lastChunkRemainder); this.lastChunkRemainder.release(); this.lastChunkRemainder = null; } buffer.writeBytes(body); ctx.write(buffer, promise); } else { // Prevent partial packets/buffer underrun if not the last packet. if (this.lastChunkRemainder == null) { this.lastChunkRemainder = body.alloc().compositeBuffer(); } this.lastChunkRemainder.addComponent(true, body.retain()); ctx.write(Unpooled.EMPTY_BUFFER, promise); } } private void writeChunkedMessage(ChannelHandlerContext ctx, ChannelPromise promise, ByteBuf body, HeaderOptions headerOptions, boolean lastLogicalPacket) { PromiseCombiner combiner = new PromiseCombiner(ctx.executor()); try { while (body.readableBytes() > 0) { if (this.lastChunkRemainder != null) { ByteBuf chunk = body.alloc().buffer(estimateChunkSize(getBytesToWrite(body.readableBytes()))); int combinedSize = this.lastChunkRemainder.readableBytes() + body.readableBytes(); HeaderOptions optionsToUse = isLastTransportPacket(combinedSize, lastLogicalPacket) ? getLastHeader(headerOptions) : getChunkedHeaderOptions(headerOptions); Header.encode(chunk, optionsToUse, this.packetSize, this.packetIdProvider); int actualBodyReadableBytes = this.packetSize - Header.LENGTH - this.lastChunkRemainder.readableBytes(); chunk.writeBytes(this.lastChunkRemainder); chunk.writeBytes(body, actualBodyReadableBytes); this.lastChunkRemainder.release(); this.lastChunkRemainder = null; combiner.add(ctx.write(chunk, ctx.newPromise())); } else { if (!lastLogicalPacket && !requiresChunking(body.readableBytes())) { // Prevent partial packets/buffer underrun if not the last packet. this.lastChunkRemainder = body.alloc().compositeBuffer(); this.lastChunkRemainder.addComponent(true, body.retain()); break; } ByteBuf chunk = body.alloc().buffer(estimateChunkSize(getBytesToWrite(body.readableBytes()))); HeaderOptions optionsToUse = isLastTransportPacket(body.readableBytes(), lastLogicalPacket) ? getLastHeader(headerOptions) : getChunkedHeaderOptions(headerOptions); int byteCount = getEffectiveChunkSizeWithoutHeader(body.readableBytes()); Header.encode(chunk, optionsToUse, Header.LENGTH + byteCount, this.packetIdProvider); chunk.writeBytes(body, byteCount); combiner.add(ctx.write(chunk, ctx.newPromise())); } } combiner.finish(promise); } catch (RuntimeException e) { promise.tryFailure(e); throw e; } } int estimateChunkSize(int readableBytes) { return Math.min(readableBytes + Header.LENGTH, this.packetSize); } private boolean requiresChunking(int readableBytes) { return getBytesToWrite(readableBytes) > this.packetSize; } private int getBytesToWrite(int readableBytes) { int bytesToWrite = Header.LENGTH; bytesToWrite += this.lastChunkRemainder != null ? this.lastChunkRemainder.readableBytes() : 0; bytesToWrite += readableBytes; return bytesToWrite; } private int getEffectiveChunkSizeWithoutHeader(int readableBytes) { return Math.min(readableBytes, this.packetSize - Header.LENGTH); } private boolean isLastTransportPacket(int readableBytes, boolean lastLogicalPacket) { if (requiresChunking(readableBytes)) { return false; } return lastLogicalPacket; } /** * Marker message to reset {@link HeaderOptions}. */ public enum ResetHeader { INSTANCE } } ================================================ FILE: src/main/java/io/r2dbc/mssql/client/TransactionStatus.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client; /** * Transactional state. * * @author Mark Paluch */ public enum TransactionStatus { /** * Default (initial) state on startup using implicit transactions. */ AUTO_COMMIT, /** * State after committing or rolling back a transaction. */ EXPLICIT, /** * Started transaction. */ STARTED } ================================================ FILE: src/main/java/io/r2dbc/mssql/client/package-info.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * The infrastructure for exchanging messages with the server. */ @NonNullApi package io.r2dbc.mssql.client; import reactor.util.annotation.NonNullApi; ================================================ FILE: src/main/java/io/r2dbc/mssql/client/ssl/ContextProxy.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client.ssl; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.ssl.SslHandler; /** * Empty proxy handler as {@link ChannelHandlerContext} target for {@link SslHandler}. TDS requires wrapping of the SSL * handshake during prelogin and we need to wrap/reuse the SSL handler to prepend TDS prelogin headers during the * handshake. * * @author Mark Paluch */ class ContextProxy extends ChannelDuplexHandler { } ================================================ FILE: src/main/java/io/r2dbc/mssql/client/ssl/ExpectedHostnameX509TrustManager.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client.ssl; import reactor.util.Logger; import reactor.util.Loggers; import reactor.util.annotation.Nullable; import javax.net.ssl.X509TrustManager; import java.net.IDN; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.List; import java.util.function.Predicate; /** * {@link X509TrustManager} implementation using {@link HostNamePredicate} to verify {@link X509Certificate}s. * * @author Mark Paluch */ public final class ExpectedHostnameX509TrustManager implements X509TrustManager { private static final Logger logger = Loggers.getLogger(TdsSslHandler.class); private final X509TrustManager defaultTrustManager; private final String expectedHostName; private final Predicate matcher; public ExpectedHostnameX509TrustManager(X509TrustManager tm, String expectedHostName) { this.defaultTrustManager = tm; this.expectedHostName = expectedHostName; this.matcher = HostNamePredicate.of(expectedHostName); } public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { if (logger.isDebugEnabled()) { logger.debug("Forwarding ClientTrusted"); } this.defaultTrustManager.checkClientTrusted(chain, authType); } public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { if (logger.isDebugEnabled()) { logger.debug("Forwarding ServerTrusted"); } this.defaultTrustManager.checkServerTrusted(chain, authType); if (logger.isDebugEnabled()) { logger.debug("ServerTrusted succeeded proceeding with server name validation"); } validateServerNameInCertificate(chain[0]); } public X509Certificate[] getAcceptedIssuers() { return this.defaultTrustManager.getAcceptedIssuers(); } private void validateServerNameInCertificate(X509Certificate cert) throws CertificateException { String subjectCN = X509CertificateUtil.getHostName(cert); if (logger.isDebugEnabled()) { logger.debug(String.format("Expecting server name: [%s]", this.expectedHostName)); logger.debug(String.format("Name in certificate: [%s]", subjectCN)); } boolean isServerNameValidated; // the name in cert is in RFC2253 format parse it to get the actual subject name isServerNameValidated = validateServerName(subjectCN); if (!isServerNameValidated) { List subjectAlternativeNames = X509CertificateUtil.getSubjectAlternativeNames(cert); if (logger.isDebugEnabled()) { logger.debug(String.format("Expecting server name validation failed. Checking alternative names (SAN) [%s]", subjectAlternativeNames)); } for (String subjectAlternativeName : subjectAlternativeNames) { isServerNameValidated = validateServerName(subjectAlternativeName); if (isServerNameValidated) { break; } } } if (!isServerNameValidated) { throw new CertificateException(String.format("Cannot validate certificate: %s", subjectCN)); } } private boolean validateServerName(@Nullable String nameInCert) { // Failed to get the common name from DN or empty CN if (null == nameInCert) { return false; } if (nameInCert.startsWith("xn--")) { nameInCert = IDN.toUnicode(nameInCert); } boolean matches = this.matcher.test(nameInCert); if (matches) { logSuccessMessage(nameInCert); } else { logFailMessage(nameInCert); } return matches; } private void logFailMessage(String nameInCert) { if (logger.isDebugEnabled()) { logger.debug(String.format("The name in certificate [%s] does not match with the server name [%s].", nameInCert, this.expectedHostName)); } } private void logSuccessMessage(String nameInCert) { if (logger.isDebugEnabled()) { logger.debug(String.format("The name in certificate [%s] validated against server name [%s].", nameInCert, this.expectedHostName)); } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/client/ssl/HostNamePredicate.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client.ssl; import io.r2dbc.mssql.util.Assert; import java.util.Locale; import java.util.function.Predicate; import java.util.regex.Pattern; /** * Hostname matcher. Accepts either a simple, fully-qualified or hostname with wildcards to match against SSL certificate hostnames. * *
    *
  • {@code foo} matches {@code foo} but not {@code bar}
  • *
  • {@code foo.bar} matches {@code foo.bar} but not {@code foo.baz}
  • *
  • {@code *.bar} matches {@code foo.bar} but not {@code foo.baz}
  • *
  • {@code f**.bar} matches {@code foo.bar} but not {@code boo.bar}
  • *
* * @author Mark Paluch */ class HostNamePredicate implements Predicate { private final Pattern hostnamePattern; private HostNamePredicate(Pattern hostnamePattern) { this.hostnamePattern = hostnamePattern; } /** * Create a new {@link HostNamePredicate} instance. * * @param expectedHostname expected host name. * @return the {@link HostNamePredicate}. * @throws IllegalArgumentException if {@code expectedHostname} is {@code null}. */ public static HostNamePredicate of(String expectedHostname) { Assert.notNull(expectedHostname, "Expected hostname must not be null"); StringBuilder builder = new StringBuilder(); // canonicalize to lower-case String[] segments = expectedHostname.toLowerCase(Locale.ENGLISH).split("\\."); for (String segment : segments) { if (builder.length() != 0) { builder.append("\\."); } StringBuilder rewrittenSegment = new StringBuilder(segment.length()); StringBuilder part = new StringBuilder(segment.length()); for (char c : segment.toCharArray()) { if (c == '*') { if (part.length() != 0) { rewrittenSegment.append(Pattern.quote(part.toString())); part = new StringBuilder(segment.length()); } rewrittenSegment.append("([^\\.]*)"); } else { part.append(c); } } if (part.length() != 0) { rewrittenSegment.append(Pattern.quote(part.toString())); } builder.append(rewrittenSegment); } return new HostNamePredicate(Pattern.compile(builder.toString())); } @Override public boolean test(String s) { return this.hostnamePattern.matcher(s).matches(); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/client/ssl/SslConfiguration.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client.ssl; import io.netty.handler.ssl.SslContext; import java.security.GeneralSecurityException; /** * SSL Configuration for SQL Server connections. *

Microsoft SQL server supports various SSL setups: * *

    *
  • Not enabled
  • *
  • Supported
  • *
  • Enabled/required
  • *
*

* Supported mode uses SSL during login to encrypt login credentials. SSL is disabled after login. * The client supports login-time SSL even when {@link #isSslEnabled()} is {@code false}. This mode does not validate certificates. *

Enabling {@link #isSslEnabled() SSL} enables also SSL certificate validation. * * @author Mark Paluch */ public interface SslConfiguration { /** * @return {@code true} if SSL is enabled. Enabling SSL enables certificate validation. {@code false} to disable SSL. */ boolean isSslEnabled(); /** * Return the {@link SslContext} if {@link #isSslEnabled() SSL is enabled}. * * @return the {@link SslContext}. * @throws GeneralSecurityException if setting up the SSL provider fails. * @throws IllegalStateException if the SSL configuration is not enabled * @since 0.8.3 */ SslContext getSslContext() throws GeneralSecurityException; } ================================================ FILE: src/main/java/io/r2dbc/mssql/client/ssl/SslEventHandler.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client.ssl; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.ssl.SslHandshakeCompletionEvent; /** * Event handler for SSL negotiation events. * * @author Mark Paluch */ class SslEventHandler extends ChannelDuplexHandler { @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt == SslHandshakeCompletionEvent.SUCCESS) { ctx.channel().pipeline().fireUserEventTriggered(SslState.NEGOTIATED); } if (evt == SslState.NEGOTIATED) { ctx.fireChannelRead(SslState.NEGOTIATED); } super.userEventTriggered(ctx, evt); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/client/ssl/SslState.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client.ssl; import io.r2dbc.mssql.message.Message; import io.r2dbc.mssql.message.token.Login7; /** * Represents the SSL state aspect of a connection. * * @author Mark Paluch */ public enum SslState implements Message { /** * SSL not enabled (default). */ OFF, /** * SSL handshake negotiated. */ NEGOTIATED, /** * SSL requested during the {@link Login7} message only. */ LOGIN_ONLY, /** * SSL requested for the entire connection. */ CONNECTION, /** * SSL disabled once it was used for the login message. */ AFTER_LOGIN_ONLY } ================================================ FILE: src/main/java/io/r2dbc/mssql/client/ssl/TdsSslHandler.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client.ssl; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.handler.ssl.SslHandler; import io.r2dbc.mssql.client.ConnectionContext; import io.r2dbc.mssql.client.ConnectionState; import io.r2dbc.mssql.client.TdsEncoder; import io.r2dbc.mssql.message.header.Header; import io.r2dbc.mssql.message.header.HeaderOptions; import io.r2dbc.mssql.message.header.PacketIdProvider; import io.r2dbc.mssql.message.header.Status; import io.r2dbc.mssql.message.header.Type; import io.r2dbc.mssql.message.tds.ContextualTdsFragment; import io.r2dbc.mssql.message.tds.TdsFragment; import io.r2dbc.mssql.util.Assert; import reactor.util.Logger; import reactor.util.Loggers; import reactor.util.annotation.Nullable; import javax.net.ssl.SSLEngine; import java.security.GeneralSecurityException; /** * SSL handling for TDS connections. *

* This handler wraps or passes thru read and write data depending on the {@link SslState}. Because TDS requires header * wrapping, we're not mounting the {@link SslHandler} directly into the pipeline but delegating read and write events * to it.

* This {@link ChannelHandler} supports also full SSL mode and requires to be reordered once the handshake is done therefor it's marked as {@code @Sharable}. * * @author Mark Paluch * @see SslHandler * @see ConnectionState */ @ChannelHandler.Sharable public final class TdsSslHandler extends ChannelDuplexHandler { private static final Logger LOGGER = Loggers.getLogger(TdsSslHandler.class); public static final boolean DEBUG_ENABLED = LOGGER.isDebugEnabled(); private final ConnectionContext connectionContext; private final PacketIdProvider packetIdProvider; private final SslConfiguration sslConfiguration; private volatile SslHandler sslHandler; private ChannelHandlerContext context; private ByteBuf outputBuffer; private SslState state = SslState.OFF; private boolean handshakeDone; @Nullable private Chunk chunk; /** * Creates a new {@link TdsSslHandler}. * * @param packetIdProvider the {@link PacketIdProvider} to create {@link Header}s to wrap the SSL handshake. * @param sslConfiguration SSL config. * @param context Value object capturing diagnostic connection context. */ public TdsSslHandler(PacketIdProvider packetIdProvider, SslConfiguration sslConfiguration, ConnectionContext context) { Assert.requireNonNull(packetIdProvider, "PacketIdProvider must not be null"); Assert.requireNonNull(sslConfiguration, "SslConfiguration must not be null"); Assert.requireNonNull(context, "ConnectionContext must not be null"); this.packetIdProvider = packetIdProvider; this.sslConfiguration = sslConfiguration; this.connectionContext = context; } void setSslHandler(SslHandler sslHandler) { this.sslHandler = sslHandler; } void setState(SslState state) { this.state = state; } /** * Create the {@link SslHandler}. * * @param sslConfiguration the SSL configuration. * @return the configured {@link SslHandler}. * @throws GeneralSecurityException thrown on security API errors. */ private static SslHandler createSslHandler(SslConfiguration sslConfiguration, ByteBufAllocator allocator) throws GeneralSecurityException { SSLEngine sslEngine = sslConfiguration.getSslContext() .newEngine(allocator); return new SslHandler(sslEngine); } /** * Lazily register {@link SslHandler} if needed. * * @param ctx the {@link ChannelHandlerContext} for which the event is made. * @param evt the user event. * @throws Exception thrown if an error occurs */ @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt == SslState.LOGIN_ONLY || evt == SslState.CONNECTION) { this.state = (SslState) evt; this.sslHandler = createSslHandler(this.sslConfiguration, ctx.alloc()); LOGGER.debug(this.connectionContext.getMessage("Registering Context Proxy and SSL Event Handlers to propagate SSL events to channelRead()")); ctx.pipeline().addAfter(getClass().getName(), ContextProxy.class.getName(), new ContextProxy()); ctx.pipeline().addAfter(ContextProxy.class.getName(), SslEventHandler.class.getName(), new SslEventHandler()); this.context = ctx.channel().pipeline().context(ContextProxy.class.getName()); ctx.write(HeaderOptions.create(Type.PRE_LOGIN, Status.empty())); this.sslHandler.handlerAdded(this.context); } if (evt == SslState.NEGOTIATED) { LOGGER.debug(this.connectionContext.getMessage("SSL Handshake done")); ctx.write(TdsEncoder.ResetHeader.INSTANCE, ctx.voidPromise()); this.handshakeDone = true; // Reorder handlers: // 1. Apply TLS first // 2. Logging next // 3. TDS if (this.state == SslState.CONNECTION) { LOGGER.debug(this.connectionContext.getMessage("Reordering handlers for full SSL usage")); ctx.pipeline().remove(this); ctx.pipeline().addFirst(this); } } super.userEventTriggered(ctx, evt); } @Override public void handlerAdded(ChannelHandlerContext ctx) { this.outputBuffer = ctx.alloc().buffer(); } @Override public void handlerRemoved(ChannelHandlerContext ctx) { if (this.outputBuffer != null) { this.outputBuffer.release(); this.outputBuffer = null; } } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { if (this.sslHandler != null) { this.sslHandler.channelInactive(ctx); } Chunk chunk = this.chunk; if (chunk != null) { chunk.fullMessage.release(); chunk.aggregator.release(); this.chunk = null; } } /** * Write data either directly (SSL disabled), to an intermediate buffer for {@link Header} wrapping, or via * {@link SslHandler} for entire packet encryption without prepending a header. Note that {@link SslState#LOGIN_ONLY} * is swapped to {@link SslState#AFTER_LOGIN_ONLY} once login payload is written. We don't check actually whether the * payload is a login packet but rely on higher level layers to send the appropriate data. * * @param ctx the {@link ChannelHandlerContext} for which the write operation is made * @param msg the message to write * @param promise the {@link ChannelPromise} to notify once the operation completes * @throws Exception thrown if an error occurs */ @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { if (this.handshakeDone && (this.state == SslState.NEGOTIATED || this.state == SslState.LOGIN_ONLY || this.state == SslState.CONNECTION)) { msg = unwrap(ctx.alloc(), msg); this.sslHandler.write(ctx, msg, promise); this.sslHandler.flush(ctx); if (this.state == SslState.LOGIN_ONLY) { this.state = SslState.AFTER_LOGIN_ONLY; } return; } if (requiresWrapping()) { if (DEBUG_ENABLED) { LOGGER.debug(this.connectionContext.getMessage("Write wrapping: Append to output buffer")); } ByteBuf sslPayload = (ByteBuf) msg; this.outputBuffer.writeBytes(sslPayload); sslPayload.release(); } else { super.write(ctx, msg, promise); } } private Object unwrap(ByteBufAllocator allocator, Object msg) { if (msg instanceof ContextualTdsFragment) { ContextualTdsFragment tdsFragment = (ContextualTdsFragment) msg; HeaderOptions headerOptions = tdsFragment.getHeaderOptions(); Status eom = headerOptions.getStatus().and(Status.StatusBit.EOM); Header header = new Header(headerOptions.getType(), eom, Header.LENGTH + tdsFragment.getByteBuf().readableBytes(), 0, this.packetIdProvider.nextPacketId(), 0); ByteBuf buffer = allocator.buffer(header.getLength()); header.encode(buffer); buffer.writeBytes(tdsFragment.getByteBuf()); tdsFragment.getByteBuf().release(); // unwrap ByteBuffer so we can write it using SSL. return buffer; } if (msg instanceof TdsFragment) { // unwrap ByteBuffer so we can write it using SSL. return ((TdsFragment) msg).getByteBuf(); } return msg; } /** * Wrap SSL handshake in prelogin {@link Header}s. Delaying write to flush instead of writing each packet in a single * header. * * @param ctx the {@link ChannelHandlerContext} for which the flush operation is made. * @throws Exception thrown if an error occurs. */ @Override public void flush(ChannelHandlerContext ctx) throws Exception { if (requiresWrapping()) { if (DEBUG_ENABLED) { LOGGER.debug(this.connectionContext.getMessage("Write wrapping: Flushing output buffer and enable auto-read")); } ByteBuf message = this.outputBuffer; this.outputBuffer = ctx.alloc().buffer(); ctx.writeAndFlush(message); ctx.channel().config().setAutoRead(true); } else { super.flush(ctx); } } /** * SSL quirk: Make sure to flush the output buffer during SSL handshake. The SSL handshake can end in the read * underrun that wants to read from the transport. We need to make sure that write requests (that don't get flushed * explicitly) are sent so we can expect eventually a read. * * @param ctx the {@link ChannelHandlerContext} for which the read complete operation is made. * @throws Exception thrown if an error occurs. */ @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { if (isInHandshake() && this.outputBuffer.readableBytes() > 0) { flush(ctx); } super.channelReadComplete(ctx); } /** * Route read events to the {@link SslHandler}. Strip off {@link Header} during handshake. * * @param ctx the {@link ChannelHandlerContext} for which the read operation is made. * @param msg the message to read. * @throws Exception thrown if an error occurs. */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (isInHandshake()) { ByteBuf buffer = (ByteBuf) msg; Chunk chunk = this.chunk; if (chunk != null || Header.canDecode(buffer)) { Header header; if (chunk == null) { header = Header.decode(buffer); // sub-chunk read if (!Chunk.isCompletePacketAvailable(header, buffer)) { ByteBuf defragmented = buffer.alloc().buffer(header.getLength()); defragmented.writeBytes(buffer); buffer.release(); this.chunk = new Chunk(header, defragmented, buffer.alloc().compositeBuffer()); ctx.read(); return; } } else { chunk.defragment(buffer); if (!chunk.isCompleteHandshakeAvailable()) { return; } buffer = chunk.fullMessage; header = chunk.header; this.chunk.aggregator.release(); this.chunk = null; } if (header.getType() == Type.PRE_LOGIN) { this.sslHandler.channelRead(this.context, buffer); } if (header.is(Status.StatusBit.IGNORE)) { return; } } return; } if (this.handshakeDone && this.state == SslState.CONNECTION) { this.sslHandler.channelRead(ctx, msg); return; } super.channelRead(ctx, msg); } private boolean isInHandshake() { return requiresWrapping() && !this.handshakeDone; } private boolean requiresWrapping() { return (this.state == SslState.LOGIN_ONLY || this.state == SslState.CONNECTION); } /** * Chunk remainder for incomplete packet reads. */ static class Chunk { Header header; final ByteBuf fullMessage; final CompositeByteBuf aggregator; int decoded = 0; Chunk(Header header, ByteBuf fullMessage, CompositeByteBuf aggregator) { this.header = header; this.fullMessage = fullMessage; this.aggregator = aggregator; } /** * Gradually defragment chunks into a complete message. Retain {@code chunk} across defragmenation attempts. * * @param chunk */ void defragment(ByteBuf chunk) { this.aggregator.addComponent(true, chunk); while (this.aggregator.isReadable()) { int remainder = getRemainingLength(); if (this.aggregator.readableBytes() >= remainder) { this.fullMessage.writeBytes(this.aggregator, remainder); } else { break; } if (Header.canDecode(this.aggregator)) { updateHeader(Header.decode(this.aggregator)); } else { break; } if (isCompleteHandshakeAvailable()) { break; } } } void updateHeader(Header header) { this.decoded = this.decoded + (this.header.getLength() - Header.LENGTH); this.header = header; } /** * Check if the full handshake arrived. * * @return */ boolean isCompleteHandshakeAvailable() { return this.header.is(Status.StatusBit.EOM) && getRemainingLength() <= 0; } int getRemainingLength() { return this.header.getLength() - ((this.fullMessage.readableBytes() - this.decoded) + Header.LENGTH); } /** * Check if the full packet arrived. Since we've already read the header, we need to add {@link Header#LENGTH} to the calculation. * * @param header TDS header. * @param buffer body buffer. * @return */ static boolean isCompletePacketAvailable(Header header, ByteBuf buffer) { return (buffer.readableBytes() + Header.LENGTH) >= header.getLength(); } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/client/ssl/TrustAllTrustManager.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client.ssl; import io.r2dbc.mssql.message.token.Login7; import javax.net.ssl.X509TrustManager; import java.security.cert.X509Certificate; /** * Accepts all {@link X509Certificate}s. Used when SSL is not enabled on the client side to allow SSL during {@link Login7} exchange. * * @author Mark Paluch */ public enum TrustAllTrustManager implements X509TrustManager { INSTANCE; public void checkClientTrusted(X509Certificate[] chain, String authType) { } public void checkServerTrusted(X509Certificate[] chain, String authType) { } public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/client/ssl/X509CertificateUtil.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client.ssl; import reactor.util.annotation.Nullable; import java.net.IDN; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Locale; /** * Miscellaneous {@link X509Certificate} utility methods. * * @author Mark Paluch */ final class X509CertificateUtil { /** * Extract the host name from the given {@link X509Certificate}. * * @param cert the certificate. * @return the extracted host name. */ @Nullable static String getHostName(X509Certificate cert) { return extractCommonName(cert.getSubjectX500Principal().getName("canonical")); } /** * Extract common name from RFC 2253 format. * Returns the common name if successful, {@code null} if failed to find the common name. * * @param distinguishedName the DN * @return the extracted host name. */ @Nullable private static String extractCommonName(String distinguishedName) { int index; // canonical name converts entire name to lowercase index = distinguishedName.indexOf("cn="); if (index == -1) { return null; } distinguishedName = distinguishedName.substring(index + 3); // Parse until a comma or end is reached // Note the parser will handle gracefully (essentially will return empty string) , inside the quotes (e.g // cn="Foo, bar") however // RFC 952 says that the hostName cant have commas however the parser should not (and will not) crash if it // sees a , within quotes. for (index = 0; index < distinguishedName.length(); index++) { if (distinguishedName.charAt(index) == ',') { break; } } String commonName = distinguishedName.substring(0, index); // strip any quotes if (commonName.length() > 1 && ('\"' == commonName.charAt(0))) { if ('\"' == commonName.charAt(commonName.length() - 1)) { commonName = commonName.substring(1, commonName.length() - 1); } else { // Be safe the name is not ended in " return null so the common Name wont match commonName = null; } } return commonName; } /** * Extract SubjectAlternativeNames from the given {@link X509Certificate} and return these as {@link IDN}-unicode {@link List} of {@link String hostnames}. * * @param cert the certificate. * @return {@link IDN}-unicode {@link List} of {@link String hostnames}. * @throws CertificateParsingException if the extension cannot be decoded. */ static List getSubjectAlternativeNames(X509Certificate cert) throws CertificateParsingException { List san = new ArrayList<>(); Collection> sanCollection = cert.getSubjectAlternativeNames(); if (sanCollection == null) { return san; } // find a subjectAlternateName entry corresponding to DNS Name for (List sanEntry : sanCollection) { if (sanEntry == null || sanEntry.size() < 2) { continue; } Object key = sanEntry.get(0); Object value = sanEntry.get(1); // Documentation(http://download.oracle.com/javase/6/docs/api/java/security/cert/X509Certificate.html): // "Note that the Collection returned may contain // more than one name of the same type." // So, more than one entry of dnsNameType can be present. // Java docs guarantee that the first entry in the list will be an integer. // 2 is the sequence no of a dnsName if (key instanceof Integer && ((Integer) key == 2) && value instanceof String) { // As per RFC2459, the DNSName will be in the // "preferred name syntax" as specified by RFC // 1034 and the name can be in upper or lower case. // And no significance is attached to case. // Java docs guarantee that the second entry in the list // will be a string for dnsName String dnsNameInSANCert = (String) value; // Use English locale to avoid Turkish i issues. dnsNameInSANCert = dnsNameInSANCert.toLowerCase(Locale.ENGLISH); san.add(IDN.toUnicode(dnsNameInSANCert)); } } return san; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/client/ssl/package-info.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * SSL support classes. */ @NonNullApi package io.r2dbc.mssql.client.ssl; import reactor.util.annotation.NonNullApi; ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/AbstractCodec.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.message.type.Length; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.Assert; import reactor.util.annotation.Nullable; /** * Abstract codec class that provides a basis for all concrete * implementations of a {@link Codec}. * * @param the type that is handled by this {@link Codec}. */ abstract class AbstractCodec implements Codec { private final Class type; /** * Creates a new {@link AbstractCodec}. * * @param type the type handled by this codec. */ AbstractCodec(Class type) { this.type = Assert.requireNonNull(type, "Type must not be null"); } @Override public boolean canEncode(Object value) { Assert.requireNonNull(value, "Value must not be null"); return this.type.isInstance(value); } @Override public final Encoded encode(ByteBufAllocator allocator, RpcParameterContext context, T value) { Assert.requireNonNull(allocator, "ByteBufAllocator must not be null"); Assert.requireNonNull(context, "RpcParameterContext must not be null"); Assert.requireNonNull(value, "Value must not be null"); return doEncode(allocator, context, value); } @Override public final boolean canEncodeNull(Class type) { Assert.requireNonNull(type, "Type must not be null"); return this.type.isAssignableFrom(type); } @Override public final Encoded encodeNull(ByteBufAllocator allocator) { Assert.requireNonNull(allocator, "ByteBufAllocator must not be null"); return doEncodeNull(allocator); } @Override public final boolean canDecode(Decodable decodable, Class type) { Assert.requireNonNull(decodable, "Decodable must not be null"); Assert.requireNonNull(type, "Type must not be null"); return type.isAssignableFrom(this.type) && doCanDecode(decodable.getType()); } @Nullable public T decode(@Nullable ByteBuf buffer, Decodable decodable, Class type) { Assert.requireNonNull(decodable, "Decodable must not be null"); Assert.requireNonNull(type, "Type must not be null"); if (buffer == null) { return null; } Length length = Length.decode(buffer, decodable.getType()); return doDecode(buffer, length, decodable.getType(), type); } @Override public Class getType() { return this.type; } /** * @param allocator the allocator to allocate encoding buffers. * @param context parameter context. * @param value the {@code value}. * @return the encoded value. */ abstract Encoded doEncode(ByteBufAllocator allocator, RpcParameterContext context, T value); /** * Encode a {@code null} value. * * @param allocator the allocator to allocate encoding buffers. * @return the encoded {@code null} value. */ abstract Encoded doEncodeNull(ByteBufAllocator allocator); /** * Determine whether this {@link Codec} is capable of decoding column values based on the given {@link TypeInformation}. * * @param typeInformation the column type. * @return {@code true} if this codec is able to decode values of {@link TypeInformation}. */ abstract boolean doCanDecode(TypeInformation typeInformation); /** * Decode the {@link ByteBuf data} into the {@link Class value type}. * * @param buffer the data buffer. * @param length length of the column data. * @param type the type descriptor. * @param valueType the desired value type. * @return the decoded value. Can be {@code null} if the column value is {@code null}. */ @Nullable abstract T doDecode(ByteBuf buffer, Length length, TypeInformation type, Class valueType); } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/AbstractNumericCodec.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.message.tds.Decode; import io.r2dbc.mssql.message.tds.ProtocolException; import io.r2dbc.mssql.message.type.Length; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import java.math.BigDecimal; import java.math.BigInteger; import java.util.EnumSet; import java.util.Set; import java.util.function.Function; /** * Abstract codec class that provides a basis for concrete * implementations of a {@link Codec} for integer numeric data types. * * @author Mark Paluch */ abstract class AbstractNumericCodec extends AbstractCodec { /** * Length in bytes required to represent {@literal BIGINT}. */ static final int SIZE_BIGINT = 8; /** * Length in bytes required to represent {@literal INT}. */ static final int SIZE_INT = 4; /** * Length in bytes required to represent {@literal SMALLINT}. */ static final int SIZE_SMALL_INT = 2; /** * Length in bytes required to represent {@literal TINYINT}. */ static final int SIZE_TINY_INT = 1; private static final Set SUPPORTED_TYPES = EnumSet.of(SqlServerType.BIT, SqlServerType.TINYINT, SqlServerType.SMALLINT, SqlServerType.INTEGER, SqlServerType.BIGINT, SqlServerType.DECIMAL, SqlServerType.NUMERIC); private final LongToObjectFunction converter; AbstractNumericCodec(Class type, LongToObjectFunction converter) { super(type); this.converter = converter; } @Override public boolean canEncodeNull(SqlServerType serverType) { return SUPPORTED_TYPES.contains(serverType); } @Override boolean doCanDecode(TypeInformation typeInformation) { return SUPPORTED_TYPES.contains(typeInformation.getServerType()); } @Override T doDecode(ByteBuf buffer, Length length, TypeInformation typeInformation, Class valueType) { if (length.isNull()) { return null; } // TODO how to deal with precission loss? if (typeInformation.getServerType() == SqlServerType.DECIMAL || typeInformation.getServerType() == SqlServerType.NUMERIC) { return this.converter.apply(decodeDecimal(buffer, length.getLength(), typeInformation.getScale()).longValue()); } switch (length.getLength()) { case SIZE_BIGINT: return this.converter.apply(Decode.bigint(buffer)); case SIZE_INT: return this.converter.apply(Decode.asInt(buffer)); case SIZE_SMALL_INT: return this.converter.apply(Decode.smallInt(buffer)); case SIZE_TINY_INT: return this.converter.apply(Decode.tinyInt(buffer)); default: throw ProtocolException.invalidTds(String.format("Unexpected value length: %d", length.getLength())); } } static BigDecimal decodeDecimal(ByteBuf buffer, int length, int scale) { byte signByte = buffer.readByte(); int sign = (0 == signByte) ? -1 : 1; byte[] magnitude = new byte[length - 1]; // read magnitude LE for (int i = 0; i < magnitude.length; i++) { magnitude[magnitude.length - 1 - i] = buffer.readByte(); } return new BigDecimal(new BigInteger(sign, magnitude), scale); } @Override public Encoded encodeNull(ByteBufAllocator allocator, SqlServerType serverType) { return RpcEncoding.encodeNull(allocator, serverType); } /** * Represents a function that produces a object-valued result. * * @param the type of the input to the function * @see Function */ @FunctionalInterface interface LongToObjectFunction { /** * Applies this function to the given argument. * * @param value the function argument * @return the function result */ T apply(long value); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/BigIntegerCodec.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.message.type.Length; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import java.math.BigDecimal; import java.math.BigInteger; /** * Codec for numeric values that are represented as {@link BigInteger}. * *

    *
  • Server types: Integer numbers
  • *
  • Java type: {@link BigInteger}
  • *
* * @author Mark Paluch */ final class BigIntegerCodec extends AbstractNumericCodec { /** * Singleton instance. */ static final BigIntegerCodec INSTANCE = new BigIntegerCodec(); private static final byte[] NULL = ByteArray.fromEncoded((alloc) -> RpcEncoding.encodeNull(alloc, SqlServerType.BIGINT)); private BigIntegerCodec() { super(BigInteger.class, BigInteger::valueOf); } @Override Encoded doEncode(ByteBufAllocator allocator, RpcParameterContext context, BigInteger value) { return DecimalCodec.INSTANCE.encode(allocator, context, new BigDecimal(value)); } @Override public Encoded doEncodeNull(ByteBufAllocator allocator) { return RpcEncoding.wrap(NULL, SqlServerType.BIGINT); } @Override BigInteger doDecode(ByteBuf buffer, Length length, TypeInformation typeInformation, Class valueType) { if (typeInformation.getServerType() == SqlServerType.NUMERIC || typeInformation.getServerType() == SqlServerType.DECIMAL) { BigDecimal decimal = DecimalCodec.INSTANCE.doDecode(buffer, length, typeInformation, BigDecimal.class); if (decimal == null) { return null; } return decimal.toBigInteger(); } return super.doDecode(buffer, length, typeInformation, valueType); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/BinaryCodec.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.message.type.*; import io.r2dbc.mssql.util.Assert; import io.r2dbc.spi.Blob; import reactor.core.publisher.Mono; import reactor.util.annotation.Nullable; import java.nio.ByteBuffer; import java.util.EnumSet; import java.util.Set; import java.util.function.IntFunction; import java.util.function.Supplier; /** * Codec for binary values that are represented as {@code byte[]} or {@link ByteBuffer}. * *
    *
  • Server types: {@link SqlServerType#BINARY}, {@link SqlServerType#VARBINARY}, {@link SqlServerType#VARBINARYMAX}, and {@link SqlServerType#IMAGE}.
  • *
  • Java type: {@code byte[]}, {@link ByteBuffer}
  • *
  • Downcast: none
  • *
* * @author Mark Paluch */ class BinaryCodec implements Codec { /** * Singleton instance. */ public static final BinaryCodec INSTANCE = new BinaryCodec(); private static final byte[] NULL = ByteArray.fromBuffer((alloc) -> { ByteBuf buffer = alloc.buffer(4); Encode.uShort(buffer, SqlServerType.VARBINARY.getMaxLength()); Encode.uShort(buffer, Length.USHORT_NULL); return buffer; }); private static final Set SUPPORTED_TYPES = EnumSet.of(SqlServerType.BINARY, SqlServerType.VARBINARY, SqlServerType.VARBINARYMAX, SqlServerType.IMAGE); private BinaryCodec() { } @Override public boolean canEncode(Object value) { Assert.requireNonNull(value, "Value must not be null"); return value instanceof byte[] || value instanceof ByteBuffer; } @Override public Encoded encode(ByteBufAllocator allocator, RpcParameterContext context, Object value) { Assert.requireNonNull(allocator, "ByteBufAllocator must not be null"); Assert.requireNonNull(context, "RpcParameterContext must not be null"); Assert.requireNonNull(value, "Value must not be null"); int length; if (value instanceof byte[]) { byte[] bytes = (byte[]) value; if (exceedsBigVarbinary(bytes.length)) { return BlobCodec.INSTANCE.encode(allocator, context, Blob.from(Mono.just(ByteBuffer.wrap(bytes)))); } length = bytes.length; } else { ByteBuffer bytes = (ByteBuffer) value; if (exceedsBigVarbinary(bytes.remaining())) { return BlobCodec.INSTANCE.encode(allocator, context, Blob.from(Mono.just(bytes))); } length = bytes.remaining(); } IntFunction encoder = actualLength -> { ByteBuf buffer; if (value instanceof byte[]) { byte[] bytes = (byte[]) value; buffer = RpcEncoding.prepareBuffer(allocator, TdsDataType.BIGVARBINARY.getLengthStrategy(), SqlServerType.VARBINARY.getMaxLength(), actualLength); buffer.writeBytes(bytes); } else { ByteBuffer bytes = (ByteBuffer) value; buffer = RpcEncoding.prepareBuffer(allocator, TdsDataType.BIGVARBINARY.getLengthStrategy(), SqlServerType.VARBINARY.getMaxLength(), actualLength); buffer.writeBytes(bytes.asReadOnlyBuffer()); } return buffer; }; return new VarbinaryEncoded(TdsDataType.BIGVARBINARY, Encoded.ofLengthAware(length, encoder)); } @Override public boolean canEncodeNull(SqlServerType serverType) { return SUPPORTED_TYPES.contains(serverType); } @Override public boolean canEncodeNull(Class type) { Assert.requireNonNull(type, "Type must not be null"); // Accept subtypes of ByteBuffer return type.isAssignableFrom(byte[].class) || ByteBuffer.class.isAssignableFrom(type); } @SuppressWarnings("unchecked") @Override public Class getType() { return (Class) ByteBuffer.class; } @Override public Encoded encodeNull(ByteBufAllocator allocator) { return new VarbinaryEncoded(TdsDataType.BIGVARBINARY, () -> Unpooled.wrappedBuffer(NULL)); } @Override public Encoded encodeNull(ByteBufAllocator allocator, SqlServerType serverType) { return new VarbinaryEncoded(TdsDataType.BIGVARBINARY, () -> Unpooled.wrappedBuffer(NULL)); } @Override public boolean canDecode(Decodable decodable, Class type) { Assert.requireNonNull(decodable, "Decodable must not be null"); Assert.requireNonNull(type, "Type must not be null"); return SUPPORTED_TYPES.contains(decodable.getType().getServerType()) && canEncodeNull(type); } @Nullable public Object decode(@Nullable ByteBuf buffer, Decodable decodable, Class type) { Assert.requireNonNull(decodable, "Decodable must not be null"); Assert.requireNonNull(type, "Type must not be null"); if (buffer == null) { return null; } Length length; if (decodable.getType().getLengthStrategy() == LengthStrategy.PARTLENTYPE) { PlpLength plpLength = PlpLength.decode(buffer, decodable.getType()); length = Length.of(Math.toIntExact(plpLength.getLength()), plpLength.isNull()); } else { length = Length.decode(buffer, decodable.getType()); } if (length.isNull()) { return null; } return doDecode(buffer, length, decodable.getType(), type); } Object doDecode(ByteBuf buffer, Length length, TypeInformation type, Class valueType) { byte[] bytes = new byte[length.getLength()]; if (type.getLengthStrategy() == LengthStrategy.PARTLENTYPE) { int index = 0; while (buffer.isReadable()) { Length chunkLength = Length.decode(buffer, type); buffer.readBytes(bytes, index, chunkLength.getLength()); index += chunkLength.getLength(); } } else { buffer.readBytes(bytes); } // accept Object.class and ByteBuffer subclasses if (valueType.isAssignableFrom(ByteBuffer.class) || ByteBuffer.class.isAssignableFrom(valueType)) { return ByteBuffer.wrap(bytes); } return bytes; } static class VarbinaryEncoded extends RpcEncoding.HintedEncoded { private static final String FORMAL_TYPE = SqlServerType.VARBINARY + "(" + TypeUtils.SHORT_VARTYPE_MAX_BYTES + ")"; VarbinaryEncoded(TdsDataType dataType, Supplier value) { super(dataType, SqlServerType.VARBINARY, value); } @Override public String getFormalType() { return FORMAL_TYPE; } } private static boolean exceedsBigVarbinary(int length) { return length > TypeUtils.SHORT_VARTYPE_MAX_BYTES; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/BlobCodec.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; import io.r2dbc.mssql.message.type.*; import io.r2dbc.mssql.util.Assert; import io.r2dbc.mssql.util.ReferenceCountUtil; import io.r2dbc.spi.Blob; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.annotation.Nullable; import java.nio.ByteBuffer; import java.util.*; /** * Codec for binary values that are represented as {@link Blob}. * *
    *
  • Server types: {@link SqlServerType#BINARY}, {@link SqlServerType#VARBINARY}, {@link SqlServerType#VARBINARYMAX}, and {@link SqlServerType#IMAGE}.
  • *
  • Java type: {@link Blob}
  • *
  • Downcast: none
  • *
* * @author Mark Paluch * @author Tomasz Marciniak */ public class BlobCodec extends AbstractCodec { /** * Singleton instance. */ public static final BlobCodec INSTANCE = new BlobCodec(); private static final Set SUPPORTED_TYPES = EnumSet.of(SqlServerType.BINARY, SqlServerType.VARBINARY, SqlServerType.VARBINARYMAX, SqlServerType.IMAGE); private BlobCodec() { super(Blob.class); } @Override Encoded doEncode(ByteBufAllocator allocator, RpcParameterContext context, Blob value) { return new PlpEncoded(SqlServerType.VARBINARYMAX, allocator, Flux.from(value.stream()).map(Unpooled::wrappedBuffer), () -> Mono.from(value.discard()).toFuture()); } @Override public boolean canEncodeNull(SqlServerType serverType) { return SUPPORTED_TYPES.contains(serverType); } @Override Encoded doEncodeNull(ByteBufAllocator allocator) { return BinaryCodec.INSTANCE.encodeNull(allocator); } @Override public Encoded encodeNull(ByteBufAllocator allocator, SqlServerType serverType) { return BinaryCodec.INSTANCE.encodeNull(allocator, serverType); } @Override boolean doCanDecode(TypeInformation typeInformation) { return SUPPORTED_TYPES.contains(typeInformation.getServerType()); } @Nullable public Blob decode(@Nullable ByteBuf buffer, Decodable decodable, Class type) { Assert.requireNonNull(decodable, "Decodable must not be null"); Assert.requireNonNull(type, "Type must not be null"); if (buffer == null) { return null; } Length length; if (decodable.getType().getLengthStrategy() == LengthStrategy.PARTLENTYPE) { PlpLength plpLength = PlpLength.decode(buffer, decodable.getType()); length = Length.of(Math.toIntExact(plpLength.getLength()), plpLength.isNull()); } else { length = Length.decode(buffer, decodable.getType()); } if (length.isNull()) { return null; } return doDecode(buffer, length, decodable.getType(), type); } @Override Blob doDecode(ByteBuf buffer, Length length, TypeInformation type, Class valueType) { if (length.isNull()) { return null; } if (type.getLengthStrategy() == LengthStrategy.PARTLENTYPE) { List chunks = new ArrayList<>(); while (buffer.isReadable()) { Length chunkLength = Length.decode(buffer, type); chunks.add(buffer.readRetainedSlice(chunkLength.getLength())); } return new ScalarBlob(chunks); } return new ScalarBlob(Collections.singletonList(buffer.readRetainedSlice(length.getLength()))); } /** * Scalar {@link Blob} backed by an already received and de-chunked {@link List} of {@link ByteBuf}. */ static class ScalarBlob implements Blob { final List buffers; ScalarBlob(List buffers) { this.buffers = buffers; this.buffers.forEach(byteBuf -> byteBuf.touch("ScalarBlob")); } @Override public Publisher stream() { return Flux.fromIterable(this.buffers).map(it -> { if (!it.isReadable()) { it.release(); return ByteBuffer.wrap(new byte[0]); } ByteBuffer result = ByteBuffer.allocate(it.readableBytes()); it.readBytes(result); it.release(); result.flip(); return result; }).doOnDiscard(ByteBuf.class, ReferenceCountUtil::maybeRelease).doOnCancel(() -> { for (ByteBuf buffer : this.buffers) { ReferenceCountUtil.maybeRelease(buffer); } }); } @Override public Publisher discard() { return Mono.fromRunnable(() -> { for (ByteBuf buffer : this.buffers) { ReferenceCountUtil.maybeRelease(buffer); } }); } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/BooleanCodec.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.message.type.SqlServerType; /** * Codec for numeric values that are represented as {@link Boolean}. * *
    *
  • Server types: Integer numbers
  • *
  • Java type: {@link Boolean}
  • *
  • Downcast: {@code true} if the value is not zero
  • *
* * @author Mark Paluch */ final class BooleanCodec extends AbstractNumericCodec { /** * Singleton instance. */ static final BooleanCodec INSTANCE = new BooleanCodec(); private static final byte[] NULL = ByteArray.fromEncoded((alloc) -> RpcEncoding.encodeNull(alloc, SqlServerType.TINYINT)); private BooleanCodec() { super(Boolean.class, value -> value != 0); } @Override Encoded doEncode(ByteBufAllocator allocator, RpcParameterContext context, Boolean value) { return RpcEncoding.encodeFixed(allocator, SqlServerType.TINYINT, value, (buffer, b) -> Encode.asByte(buffer, b ? 1 : 0)); } @Override Encoded doEncodeNull(ByteBufAllocator allocator) { return RpcEncoding.wrap(NULL, SqlServerType.TINYINT); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/ByteArray.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufUtil; import io.r2dbc.mssql.util.Assert; import io.r2dbc.mssql.util.ReferenceCountUtil; import java.util.function.Function; /** * Utility to create byte arrays. * * @author Mark Paluch * @author Tomasz Marciniak */ abstract class ByteArray { /** * Create a {@code byte[]} from a {@link Function encode function} accepting {@link ByteBufAllocator} returning {@link Encoded}. * * @param encodeFunction encode function. * @return the {@code byte[]} containing the encoded buffer. */ static byte[] fromEncoded(Function encodeFunction) { Assert.notNull(encodeFunction, "Encode Function must not be null"); Encoded encoded = encodeFunction.apply(ByteBufAllocator.DEFAULT); ByteBuf buffer = encoded.getValue(); try { return ByteBufUtil.getBytes(buffer); } finally { encoded.dispose(); ReferenceCountUtil.maybeRelease(buffer); } } /** * Create a {@code byte[]} from a {@link Function encode function} accepting {@link ByteBufAllocator} returning {@link ByteBuf}. * * @param encodeFunction encode function. * @return the {@code byte[]} containing the encoded buffer. */ static byte[] fromBuffer(Function encodeFunction) { ByteBuf buffer = encodeFunction.apply(ByteBufAllocator.DEFAULT); try { return ByteBufUtil.getBytes(buffer); } finally { buffer.release(); } } // Utility constructor private ByteArray() { } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/ByteCodec.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.message.type.SqlServerType; /** * Codec for numeric values that are represented as {@link Byte}. * *
    *
  • Server types: Integer numbers
  • *
  • Java type: {@link Byte}
  • *
  • Downcast: to {@link Byte}
  • *
* * @author Mark Paluch */ final class ByteCodec extends AbstractNumericCodec { /** * Singleton instance. */ static final ByteCodec INSTANCE = new ByteCodec(); private static final byte[] NULL = ByteArray.fromEncoded((alloc) -> RpcEncoding.encodeNull(alloc, SqlServerType.TINYINT)); private ByteCodec() { super(Byte.class, value -> (byte) value); } @Override Encoded doEncode(ByteBufAllocator allocator, RpcParameterContext context, Byte value) { return RpcEncoding.encodeFixed(allocator, SqlServerType.TINYINT, value, Encode::asByte); } @Override Encoded doEncodeNull(ByteBufAllocator allocator) { return RpcEncoding.wrap(NULL, SqlServerType.TINYINT); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/CharacterEncoder.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import io.r2dbc.mssql.codec.RpcParameterContext.CharacterValueContext; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.message.tds.ServerCharset; import io.r2dbc.mssql.message.type.*; import io.r2dbc.spi.Clob; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.annotation.Nullable; import java.nio.CharBuffer; import java.util.function.Function; import java.util.function.Supplier; import static io.r2dbc.mssql.message.type.SqlServerType.Category.NCHARACTER; /** * Basic {@link CharSequence} encoding utilities. * * @author Mark Paluch */ class CharacterEncoder { private static final byte[] NULL = ByteArray.fromBuffer(alloc -> { ByteBuf buffer = alloc.buffer(8); Encode.uShort(buffer, TypeUtils.SHORT_VARTYPE_MAX_BYTES); Collation.RAW.encode(buffer); Encode.uShort(buffer, -1); return buffer; }); /** * Encode a {@code VARCHAR NULL}. * * @return the {@link Encoded} {@code VARCHAR NULL}. */ static Encoded encodeNull(SqlServerType serverType) { if (isNational(serverType)) { return new VarcharEncoded(TdsDataType.NVARCHAR, () -> Unpooled.wrappedBuffer(NULL)); } return new NvarcharEncoded(TdsDataType.NVARCHAR, () -> Unpooled.wrappedBuffer(NULL)); } /** * Encode a {@link CharSequence} to {@code VARCHAR} or {@code NVARCHAR} depending on {@code sendStringParametersAsUnicode}. * * @return the {@link Encoded} {@link CharSequence}. */ static Encoded encodeBigVarchar(ByteBufAllocator allocator, RpcDirection direction, @Nullable SqlServerType serverType, Collation collation, boolean sendStringParametersAsUnicode, @Nullable CharSequence value) { int initialCapacity = (value != null ? value.length() * 2 : 0) + 7; Function encoder = unicode -> { ByteBuf buffer = allocator.buffer(initialCapacity); encodeBigVarchar(buffer, direction, collation, unicode, value); return buffer; }; if (isNational(serverType) || sendStringParametersAsUnicode) { return new NvarcharEncoded(TdsDataType.NVARCHAR, () -> encoder.apply(true)); } return new VarcharEncoded(TdsDataType.BIGVARCHAR, Encoded.ofLengthAware(initialCapacity, i -> encoder.apply(false))); } /** * Encode a {@link CharSequence} to {@code VARCHAR} or {@code NVARCHAR} depending on {@code sendStringParametersAsUnicode}. Uses either {@code (N)VARCHAR} or {@code (N)VARCHAR(MAX)}, depending * on the string size. */ static void encodeBigVarchar(ByteBuf buffer, RpcDirection direction, Collation collation, boolean sendStringParametersAsUnicode, @Nullable CharSequence value) { ByteBuf characterData = encodeCharSequence(buffer.alloc(), collation, sendStringParametersAsUnicode, value); int valueLength = characterData.readableBytes(); boolean isShortValue = valueLength <= TypeUtils.SHORT_VARTYPE_MAX_BYTES; boolean isNull = value == null; // Textual RPC requires a collation. If none is provided, as is the case when // the SSType is non-textual, then use the database collation by default. // Use PLP encoding on Yukon and later with long values and OUT parameters boolean usePLP = (!isShortValue || direction == RpcDirection.OUT); if (usePLP) { // Send v*max length indicator 0xFFFF. Encode.uShort(buffer, (short) 0xFFFF); // Send collation if requested. collation.encode(buffer); // Handle null here and return, we're done here if it's null. if (isNull) { // Null header for v*max types is 0xFFFFFFFFFFFFFFFF. Encode.uLongLong(buffer, 0xFFFFFFFFFFFFFFFFL); } else if (Length.UNKNOWN_STREAM_LENGTH == valueLength) { // Append v*max length. // UNKNOWN_PLP_LEN is 0xFFFFFFFFFFFFFFFE Encode.uLongLong(buffer, 0xFFFFFFFFFFFFFFFEL); // NOTE: Don't send the first chunk length, this will be calculated by caller. } else { // For v*max types with known length, length is // We're sending same total length as chunk length (as we're sending 1 chunk). Encode.uLongLong(buffer, valueLength); } // Send the data. if (!isNull) { if (valueLength > 0) { Encode.asInt(buffer, valueLength); buffer.writeBytes(characterData); characterData.release(); } } // Send the terminator PLP chunk. Encode.asInt(buffer, 0); } else { // Write maximum length of data Encode.uShort(buffer, TypeUtils.SHORT_VARTYPE_MAX_BYTES); collation.encode(buffer); // Write actual length of data Encode.uShort(buffer, valueLength); // If length is zero, we're done. if (0 != valueLength) { buffer.writeBytes(characterData); characterData.release(); } } } private static ByteBuf encodeCharSequence(ByteBufAllocator alloc, Collation collation, boolean sendStringParametersAsUnicode, @Nullable CharSequence value) { if (value == null || value.length() == 0) { return Unpooled.EMPTY_BUFFER; } if (sendStringParametersAsUnicode) { ByteBuf buffer = alloc.buffer(value.length() * 2); Encode.rpcString(buffer, value); return buffer; } ByteBuf buffer = alloc.buffer((int) (value.length() * 1.5)); Encode.rpcString(buffer, value, collation.getCharset()); return buffer; } static Encoded encodePlp(ByteBufAllocator allocator, @Nullable SqlServerType serverType, CharacterValueContext valueContext, CharSequence value) { Flux binaryStream = Flux.just(value).map(it -> { return encodeCharSequence(allocator, isNational(serverType), valueContext, it); }); return new PlpEncodedCharacters(getPlpType(serverType, valueContext), valueContext.getCollation(), allocator, binaryStream, () -> { }); } private static boolean isNational(@Nullable SqlServerType serverType) { return serverType != null && serverType.getCategory() == NCHARACTER; } static Encoded encodePlp(ByteBufAllocator allocator, @Nullable SqlServerType serverType, CharacterValueContext valueContext, Clob value) { Flux binaryStream = Flux.from(value.stream()).map(it -> { return encodeCharSequence(allocator, isNational(serverType), valueContext, it); }); return new PlpEncodedCharacters(getPlpType(serverType, valueContext), valueContext.getCollation(), allocator, binaryStream, () -> Mono.from(value.discard()).toFuture()); } private static SqlServerType getPlpType(@Nullable SqlServerType serverType, CharacterValueContext valueContext) { return isNational(serverType) || valueContext.isSendStringParametersAsUnicode() ? SqlServerType.NVARCHARMAX : SqlServerType.VARCHARMAX; } private static ByteBuf encodeCharSequence(ByteBufAllocator allocator, boolean isNational, CharacterValueContext valueContext, CharSequence it) { return ByteBufUtil.encodeString(allocator, CharBuffer.wrap(it), isNational || valueContext.isSendStringParametersAsUnicode() ? ServerCharset.UNICODE.charset() : valueContext.getCollation().getCharset()); } private static class NvarcharEncoded extends RpcEncoding.HintedEncoded { private static final String FORMAL_TYPE = SqlServerType.NVARCHAR + "(" + (TypeUtils.SHORT_VARTYPE_MAX_BYTES / 2) + ")"; NvarcharEncoded(TdsDataType dataType, Supplier value) { super(dataType, SqlServerType.NVARCHAR, value); } @Override public String getFormalType() { return FORMAL_TYPE; } } private static class VarcharEncoded extends RpcEncoding.HintedEncoded { private static final String FORMAL_TYPE = SqlServerType.VARCHAR + "(" + TypeUtils.SHORT_VARTYPE_MAX_BYTES + ")"; VarcharEncoded(TdsDataType dataType, Supplier value) { super(dataType, SqlServerType.NVARCHAR, value); } @Override public String getFormalType() { return FORMAL_TYPE; } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/ClobCodec.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.CompositeByteBuf; import io.r2dbc.mssql.codec.RpcParameterContext.CharacterValueContext; import io.r2dbc.mssql.message.type.*; import io.r2dbc.mssql.util.Assert; import io.r2dbc.mssql.util.ReferenceCountUtil; import io.r2dbc.spi.Clob; import io.r2dbc.spi.R2dbcNonTransientException; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.annotation.Nullable; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharsetDecoder; import java.nio.charset.CoderResult; import java.nio.charset.CodingErrorAction; import java.nio.charset.MalformedInputException; import java.util.EnumSet; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; /** * Codec for character values that are represented as {@link Clob}. *

* BlobCodec *

  • Server types: {@link SqlServerType#CHAR}, {@link SqlServerType#NCHAR}, {@link SqlServerType#VARCHAR}, {@link SqlServerType#NVARCHAR}, {@link SqlServerType#VARCHARMAX}, * {@link SqlServerType#NVARCHARMAX}, {@link SqlServerType#TEXT} and {@link SqlServerType#NTEXT}.
  • *
  • Java type: {@link Clob}
  • *
  • Downcast: none
  • * * * @author Mark Paluch */ public class ClobCodec extends AbstractCodec { /** * Singleton instance. */ public static final ClobCodec INSTANCE = new ClobCodec(); private static final Set SUPPORTED_TYPES = EnumSet.of(SqlServerType.CHAR, SqlServerType.NCHAR, SqlServerType.VARCHAR, SqlServerType.NVARCHAR, SqlServerType.VARCHARMAX, SqlServerType.NVARCHARMAX, SqlServerType.TEXT, SqlServerType.NTEXT); private ClobCodec() { super(Clob.class); } @Override Encoded doEncode(ByteBufAllocator allocator, RpcParameterContext context, Clob value) { return CharacterEncoder.encodePlp(allocator, context.getServerType(), context.getRequiredValueContext(CharacterValueContext.class), value); } @Override public boolean canEncodeNull(SqlServerType serverType) { return SUPPORTED_TYPES.contains(serverType); } @Override Encoded doEncodeNull(ByteBufAllocator allocator) { return StringCodec.INSTANCE.doEncodeNull(allocator); } @Override public Encoded encodeNull(ByteBufAllocator allocator, SqlServerType serverType) { return StringCodec.INSTANCE.encodeNull(allocator, serverType); } @Override boolean doCanDecode(TypeInformation typeInformation) { return SUPPORTED_TYPES.contains(typeInformation.getServerType()); } @Nullable public Clob decode(@Nullable ByteBuf buffer, Decodable decodable, Class type) { Assert.requireNonNull(decodable, "Decodable must not be null"); Assert.requireNonNull(type, "Type must not be null"); if (buffer == null) { return null; } Length length; if (decodable.getType().getLengthStrategy() == LengthStrategy.PARTLENTYPE) { PlpLength plpLength = buffer.isReadable() ? PlpLength.decode(buffer, decodable.getType()) : PlpLength.nullLength(); length = Length.of(Math.toIntExact(plpLength.getLength()), plpLength.isNull()); } else { length = Length.decode(buffer, decodable.getType()); } if (length.isNull()) { return null; } return doDecode(buffer, length, decodable.getType(), type); } @Override Clob doDecode(ByteBuf buffer, Length length, TypeInformation type, Class valueType) { if (length.isNull()) { return null; } if (type.getLengthStrategy() == LengthStrategy.PARTLENTYPE) { int startIndex = buffer.readerIndex(); while (buffer.isReadable()) { Length chunkLength = Length.decode(buffer, type); buffer.skipBytes(chunkLength.getLength()); } int endIndex = buffer.readerIndex(); buffer.readerIndex(startIndex); return new ScalarClob(type, length, buffer.readRetainedSlice(endIndex - startIndex)); } return new ScalarClob(type, length, buffer.readRetainedSlice(length.getLength())); } /** * Scalar {@link Clob} backed by an already received and de-chunked {@link List} of {@link ByteBuf}. */ static class ScalarClob implements Clob { private final TypeInformation type; private final Length valueLength; private final ByteBuf buffer; private final CompositeByteBuf remainder; ScalarClob(TypeInformation type, Length valueLength, ByteBuf buffer) { this.type = type; this.valueLength = valueLength; this.buffer = buffer.touch("ScalarClob"); this.remainder = buffer.alloc().compositeBuffer(); } @Override public Publisher stream() { CharsetDecoder decoder = this.type.getCharset().newDecoder().onMalformedInput(CodingErrorAction.REPORT).onUnmappableCharacter(CodingErrorAction.REPORT); AtomicReference result = new AtomicReference<>(); AtomicInteger counter = new AtomicInteger(); return createBufferStream(this.buffer, this.valueLength, this.type).handle((buffer, sink) -> { if (!buffer.isReadable()) { // ensure release if not consumed buffer.release(); return; } this.remainder.addComponent(true, buffer); ByteBuffer byteBuffer = this.remainder.nioBuffer(); int size = byteBuffer.remaining(); CharBuffer outBuffer = CharBuffer.allocate(byteBuffer.remaining()); CoderResult decode; synchronized (decoder) { decode = decoder.decode(byteBuffer, outBuffer, false); } result.set(decode); int consumed = size - byteBuffer.remaining(); if (consumed > 0) { this.remainder.skipBytes(consumed); } else { sink.error(new MalformedInputException(consumed)); return; } if (counter.incrementAndGet() % 16 == 0) { this.remainder.discardSomeReadBytes(); } outBuffer.flip(); sink.next(outBuffer.toString()); }).doOnComplete(() -> { CoderResult coderResult = result.get(); if (coderResult != null && coderResult.isError()) { if (coderResult.isMalformed()) { throw new ClobDecodeException("Cannot decode CLOB data. Malformed character input"); } if (coderResult.isUnmappable()) { throw new ClobDecodeException("Cannot decode CLOB data. Unmappable characters"); } } if (this.remainder.isReadable()) { throw new ClobDecodeException("Cannot decode CLOB data. Buffer has remainder: " + ByteBufUtil.hexDump(this.remainder)); } }) .doFinally(s -> ReferenceCountUtil.maybeRelease(this.remainder)); } @Override public Publisher discard() { return Mono.fromRunnable(this::releaseBuffers); } private void releaseBuffers() { ReferenceCountUtil.maybeSafeRelease(this.remainder); ReferenceCountUtil.maybeSafeRelease(this.buffer); } private static Flux createBufferStream(ByteBuf plpStream, Length valueLength, TypeInformation type) { return Flux.generate(sink -> { try { if (!plpStream.isReadable()) { sink.complete(); return; } Length length; if (type.getLengthStrategy() == LengthStrategy.PARTLENTYPE) { length = Length.decode(plpStream, type); } else { length = valueLength; } sink.next(plpStream.readRetainedSlice(length.getLength())); } catch (Exception e) { sink.error(e); } }) .doFinally(s -> { ReferenceCountUtil.maybeSafeRelease(plpStream); }); } } static class ClobDecodeException extends R2dbcNonTransientException { public ClobDecodeException(String reason) { super(reason); } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/Codec.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import reactor.util.annotation.Nullable; /** * Codec to encode and decode values based on Server types and Java value types.

    * Codecs can decode one or more {@link SqlServerType server-specific data types} and represent them as a specific Java {@link Class type}. The type parameter of {@link Codec} * indicates the interchange type that is handled by this codec. *

    * Codecs that can decode various types (e.g. {@literal uniqueidentifier} and {@literal char}) use the most appropriate method to represent the value by casting or using the * {@link Object#toString() toString} method. * * @param the type that is handled by this codec. * @see TypeInformation * @see SqlServerType */ interface Codec { /** * Determine whether this {@link Codec} is capable of encoding the {@code value}. * * @param value the parameter value. * @return {@code true} if this {@link Codec} is able to encode the {@code value}. * @see #encodeNull */ boolean canEncode(Object value); /** * Encode the {@code value} to be used as RPC parameter. * * @param allocator the allocator to allocate encoding buffers. * @param context parameter context. * @param value the {@code null} {@code value}. * @return the encoded value. */ Encoded encode(ByteBufAllocator allocator, RpcParameterContext context, T value); /** * Determine whether this {@link Codec} is capable of encoding a {@code null} value for the given {@link Class} type. * * @param type the desired value type. * @return {@code true} if this {@link Codec} is able to encode {@code null} values for the given {@link Class} type. * @see #encodeNull */ boolean canEncodeNull(Class type); /** * Determine whether this {@link Codec} is capable of encoding a {@code null} value for the given {@link SqlServerType} type. * * @param type the desired value type. * @return {@code true} if this {@link Codec} is able to encode {@code null} values for the given {@link SqlServerType} type. * @see #encodeNull * @since 0.9 */ boolean canEncodeNull(SqlServerType serverType); /** * Encode a {@code null} value. * * @param allocator the allocator to allocate encoding buffers. * @return the encoded {@code null} value. */ Encoded encodeNull(ByteBufAllocator allocator); /** * Encode a {@code null} value. * * @param allocator the allocator to allocate encoding buffers. * @return the encoded {@code null} value. * @since 0.9 */ Encoded encodeNull(ByteBufAllocator allocator, SqlServerType serverType); /** * Determine whether this {@link Codec} is capable of decoding a value for the given {@link Decodable} and whether it can represent the decoded value as the desired {@link Class type}. * {@link Decodable} represents typically a column or RPC return value. * * @param decodable the decodable metadata. * @param type the desired value type. * @return {@code true} if this codec is able to decode values of {@link TypeInformation}. */ boolean canDecode(Decodable decodable, Class type); /** * Decode the {@link ByteBuf data} and return it as the requested {@link Class type}. * * @param buffer the data buffer. * @param decodable the decodable descriptor. * @param type the desired value type. * @return the decoded value. Can be {@code null} if the value is {@code null}. */ @Nullable T decode(@Nullable ByteBuf buffer, Decodable decodable, Class type); /** * Returns the Java {@link Class type} of this codec. * * @return the Java type. */ Class getType(); } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/Codecs.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.util.ReferenceCounted; import io.r2dbc.mssql.message.token.Column; import io.r2dbc.mssql.message.token.ReturnValue; import io.r2dbc.mssql.message.token.RowToken; import io.r2dbc.mssql.message.type.TypeInformation; import reactor.util.annotation.Nullable; /** * Registry for {@link Codec}s to encode RPC parameters and decode tabular values. * * @see Codec * @see ReturnValue * @see Column * @see RowToken */ public interface Codecs { /** * Encode a non-{@code null} {@code value} as RPC parameter. * * @param allocator the allocator to allocate encoding buffers. * @param context parameter context. * @param value the {@code null} {@code value}. * @return the encoded value. Must be {@link ReferenceCounted#release() released} after usage. */ Encoded encode(ByteBufAllocator allocator, RpcParameterContext context, Object value); /** * Encode a {@code null} value for a specific {@link Class type}. * * @param allocator the allocator to allocate encoding buffers. * @param type the type to represent {@code null}. * @return the encoded {@code null} value. */ Encoded encodeNull(ByteBufAllocator allocator, Class type); /** * Decode a data to a value. * * @param buffer the {@link ByteBuf} to decode. * @param decodable the decodable metadata. * @param type the type to decode to. * @param the type of item being returned. * @return the decoded value. Can be {@code null} if the column value is {@code null}. */ @Nullable T decode(@Nullable ByteBuf buffer, Decodable decodable, Class type); /** * Returns the Java {@link Class type} to which this {@link TypeInformation type descriptor} decodes to. The resulting type is considered the native type for the {@link TypeInformation type * descriptor}. * * @param type the type descriptor. * @return the most appropriate Java {@link Class type}. * @throws IllegalArgumentException if {@code type} is {@code null} */ Class getJavaType(TypeInformation type); } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/DecimalCodec.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.message.type.Length; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TdsDataType; import io.r2dbc.mssql.message.type.TypeInformation; import java.math.BigDecimal; import java.math.BigInteger; import java.util.function.Supplier; /** * Codec for fixed floating-point values that are represented as {@link BigDecimal}. * *

      *
    • Server types: {@link SqlServerType#NUMERIC} and {@link SqlServerType#DECIMAL}
    • *
    • Java type: {@link BigDecimal}
    • *
    • Downcast: none
    • *
    * * @author Mark Paluch */ final class DecimalCodec extends AbstractNumericCodec { private final static BigInteger MAX_VALUE = new BigInteger("99999999999999999999999999999999999999"); static final DecimalCodec INSTANCE = new DecimalCodec(); private static final int MAX_PRECISION = 38; private static final byte[] NULL = ByteArray.fromBuffer(alloc -> { ByteBuf buffer = alloc.buffer(4); Encode.asByte(buffer, 0x11); Encode.asByte(buffer, SqlServerType.DECIMAL.getMaxLength()); Encode.asByte(buffer, 0); // scale Encode.asByte(buffer, 0); // length return buffer; }); private DecimalCodec() { super(BigDecimal.class, BigDecimal::valueOf); } @Override Encoded doEncode(ByteBufAllocator allocator, RpcParameterContext context, BigDecimal value) { BigDecimal valueToUse; // Handle negative scale as a special case for Java 1.5 and later if (value.scale() < 0) { valueToUse = value.setScale(0); } else { valueToUse = value; } if (exceedsMaxPrecisionOrScale(valueToUse)) { throw new IllegalArgumentException("One or more values is out of range of values for the DECIMAL SQL type"); } return new DecimalEncoded(TdsDataType.DECIMALN, () -> { ByteBuf buffer = RpcEncoding.prepareBuffer(allocator, TdsDataType.DECIMALN.getLengthStrategy(), 0x11, SqlServerType.DECIMAL.getMaxLength()); encodeBigDecimal(buffer, valueToUse); return buffer; }, MAX_PRECISION, valueToUse.scale()); } @Override Encoded doEncodeNull(ByteBufAllocator allocator) { return new DecimalEncoded(TdsDataType.DECIMALN, () -> Unpooled.wrappedBuffer(NULL), MAX_PRECISION, 0); } @Override BigDecimal doDecode(ByteBuf buffer, Length length, TypeInformation type, Class valueType) { if (length.isNull()) { return null; } if (type.getServerType() == SqlServerType.DECIMAL || type.getServerType() == SqlServerType.NUMERIC) { return decodeDecimal(buffer, length.getLength(), type.getScale()); } return super.doDecode(buffer, length, type, valueType); } private static void encodeBigDecimal(ByteBuf buffer, BigDecimal value) { boolean isNegative = (value.signum() < 0); BigInteger valueToUse = value.unscaledValue(); if (isNegative) { valueToUse = valueToUse.negate(); } byte[] unscaledBytes = valueToUse.toByteArray(); Encode.asByte(buffer, value.scale()); Encode.asByte(buffer, unscaledBytes.length + 1); // data length + sign Encode.asByte(buffer, isNegative ? 0 : 1); // 1 = +ve, 0 = -ve for (int i = unscaledBytes.length - 1; i >= 0; i--) { Encode.asByte(buffer, unscaledBytes[i]); } } private static boolean exceedsMaxPrecisionOrScale(BigDecimal value) { // Maximum scale allowed is same as maximum precision allowed. if (value.scale() > MAX_PRECISION) { return true; } // Convert to unscaled integer value, then compare with maxRPCDecimalValue. // NOTE: Handle negative scale as a special case for Java 1.5 and later BigInteger bi = value.unscaledValue(); if (value.signum() < 0) { bi = bi.negate(); } return bi.compareTo(MAX_VALUE) > 0; } static class DecimalEncoded extends RpcEncoding.HintedEncoded { private final int length; private final int scale; DecimalEncoded(TdsDataType dataType, Supplier value, int length, int scale) { super(dataType, SqlServerType.DECIMAL, value); this.length = length; this.scale = scale; } @Override public String getFormalType() { return super.getFormalType() + "(" + this.length + "," + this.scale + ")"; } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/Decodable.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.r2dbc.mssql.message.type.TypeInformation; /** * Interface declaring metadata to allow decoding of a related value. * * @author Mark Paluch */ public interface Decodable { /** * Returns the type that is associated with the decodable value. * * @return the type that is associated with the decodable value. */ TypeInformation getType(); /** * Returns the name of the decodable item. This is typically a parameter name or a column name. * * @return the name of the decodable. */ String getName(); } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/DefaultCodecs.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.Assert; import io.r2dbc.spi.Parameter; import io.r2dbc.spi.R2dbcType; import io.r2dbc.spi.Type; import reactor.util.annotation.Nullable; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * The default {@link Codec} implementation. Delegates to type-specific codec implementations. */ public final class DefaultCodecs implements Codecs { private final Codec[] codecs; private final Map> codecPreferences = new HashMap<>(); private final Map, Codec> codecNullCache = new ConcurrentHashMap<>(); /** * Creates a new instance of {@link DefaultCodecs}. */ @SuppressWarnings("rawtypes") public DefaultCodecs() { this.codecs = Arrays.asList( // Prioritized Codecs StringCodec.INSTANCE, BinaryCodec.INSTANCE, BooleanCodec.INSTANCE, ByteCodec.INSTANCE, ShortCodec.INSTANCE, FloatCodec.INSTANCE, DoubleCodec.INSTANCE, IntegerCodec.INSTANCE, LongCodec.INSTANCE, BigIntegerCodec.INSTANCE, LocalTimeCodec.INSTANCE, LocalDateCodec.INSTANCE, LocalDateTimeCodec.INSTANCE, UuidCodec.INSTANCE, DecimalCodec.INSTANCE, MoneyCodec.INSTANCE, TimestampCodec.INSTANCE, OffsetDateTimeCodec.INSTANCE, ZonedDateTimeCodec.INSTANCE, BlobCodec.INSTANCE, ClobCodec.INSTANCE ).toArray(new Codec[0]); this.codecPreferences.put(SqlServerType.BIT, BooleanCodec.INSTANCE); this.codecPreferences.put(SqlServerType.TINYINT, ByteCodec.INSTANCE); this.codecPreferences.put(SqlServerType.SMALLINT, ShortCodec.INSTANCE); this.codecPreferences.put(SqlServerType.INTEGER, IntegerCodec.INSTANCE); this.codecPreferences.put(SqlServerType.BIGINT, LongCodec.INSTANCE); this.codecPreferences.put(SqlServerType.REAL, FloatCodec.INSTANCE); this.codecPreferences.put(SqlServerType.FLOAT, DoubleCodec.INSTANCE); this.codecPreferences.put(SqlServerType.GUID, UuidCodec.INSTANCE); this.codecPreferences.put(SqlServerType.NUMERIC, DecimalCodec.INSTANCE); this.codecPreferences.put(SqlServerType.DECIMAL, DecimalCodec.INSTANCE); } @SuppressWarnings({"unchecked", "rawtpes"}) @Override public Encoded encode(ByteBufAllocator allocator, RpcParameterContext context, Object value) { Assert.requireNonNull(allocator, "ByteBufAllocator must not be null"); Assert.requireNonNull(context, "RpcParameterContext must not be null"); Assert.requireNonNull(value, "Value must not be null"); Object parameterValue = value; SqlServerType serverType; if (value instanceof Parameter) { Parameter parameter = (Parameter) value; parameterValue = parameter.getValue(); if (parameter.getType() instanceof Type.InferredType && parameterValue == null) { return encodeNull(allocator, parameter.getType().getJavaType()); } serverType = getServerType(parameter); } else { serverType = null; } if (serverType == null) { for (Codec codec : this.codecs) { if (codec.canEncode(parameterValue)) { return ((Codec) codec).encode(allocator, context, parameterValue); } } } else { if (parameterValue == null) { for (Codec codec : this.codecs) { if (codec.canEncodeNull(serverType)) { return codec.encodeNull(allocator, serverType); } } } else { for (Codec codec : this.codecs) { if (codec.canEncode(parameterValue)) { return ((Codec) codec).encode(allocator, context.withServerType(serverType), parameterValue); } } } } throw new IllegalArgumentException(String.format("Cannot encode [%s] parameter of type [%s]", parameterValue, value.getClass().getName())); } @Nullable private SqlServerType getServerType(Parameter parameter) { if (parameter.getType() instanceof Type.InferredType) { return null; } if (parameter.getType() instanceof R2dbcType) { return SqlServerType.of((R2dbcType) parameter.getType()); } if (parameter.getType() instanceof SqlServerType) { return (SqlServerType) parameter.getType(); } return SqlServerType.of(parameter.getType().getName()); } @Override public Encoded encodeNull(ByteBufAllocator allocator, Class type) { Assert.requireNonNull(allocator, "ByteBufAllocator must not be null"); Assert.requireNonNull(type, "Type must not be null"); Codec codecToUse = this.codecNullCache.computeIfAbsent(type, key -> { for (Codec codec : this.codecs) { if (codec.canEncodeNull(key)) { return codec; } } throw new IllegalArgumentException(String.format("Cannot encode [null] parameter of type [%s]", type.getName())); }); return codecToUse.encodeNull(allocator); } @Override public T decode(@Nullable ByteBuf buffer, Decodable decodable, Class type) { Assert.requireNonNull(decodable, "Decodable must not be null"); Assert.requireNonNull(type, "Type must not be null"); if (buffer == null) { return null; } Codec codec = getDecodingCodec(decodable, type); return doDecode(codec, buffer, decodable, type); } @Nullable private T doDecode(Codec codec, @Nullable ByteBuf buffer, Decodable decodable, Class type) { return codec.decode(buffer, decodable, type); } @Override public Class getJavaType(TypeInformation type) { Assert.requireNonNull(type, "Type must not be null"); Codec decodingCodec = getDecodingCodec(new TypeInformationWrapper(type), Object.class); return decodingCodec.getType(); } @SuppressWarnings("unchecked") private Codec getDecodingCodec(Decodable decodable, Class requestedType) { Codec preferredCodec = this.codecPreferences.get(decodable.getType().getServerType()); if (preferredCodec != null && preferredCodec.canDecode(decodable, requestedType)) { return (Codec) preferredCodec; } for (Codec codec : this.codecs) { if (codec.canDecode(decodable, requestedType)) { return (Codec) codec; } } throw new IllegalArgumentException(String.format("Cannot decode value of type [%s], name [%s] server type [%s]", requestedType.getName(), decodable.getName(), decodable.getType().getServerType())); } static class TypeInformationWrapper implements Decodable { private final TypeInformation typeInformation; TypeInformationWrapper(TypeInformation typeInformation) { this.typeInformation = typeInformation; } @Override public TypeInformation getType() { return this.typeInformation; } @Override public String getName() { return this.typeInformation.getServerTypeName(); } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/DoubleCodec.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.message.tds.Decode; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.message.type.Length; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; /** * Codec for floating-point values that are represented as {@link Double}. * *
      *
    • Server types: {@link SqlServerType#FLOAT} (8-byte) and {@link SqlServerType#REAL} (4-byte)
    • *
    • Java type: {@link Double}
    • *
    • Downcast: none
    • *
    * * @author Mark Paluch */ final class DoubleCodec extends AbstractCodec { public static final DoubleCodec INSTANCE = new DoubleCodec(); private static final byte[] NULL = ByteArray.fromEncoded((alloc) -> RpcEncoding.encodeNull(alloc, SqlServerType.FLOAT)); private DoubleCodec() { super(Double.class); } @Override Encoded doEncode(ByteBufAllocator allocator, RpcParameterContext context, Double value) { return RpcEncoding.encodeFixed(allocator, SqlServerType.FLOAT, value, Encode::asDouble); } @Override public boolean canEncodeNull(SqlServerType serverType) { return serverType == SqlServerType.FLOAT; } @Override Encoded doEncodeNull(ByteBufAllocator allocator) { return RpcEncoding.wrap(NULL, SqlServerType.FLOAT); } @Override public Encoded encodeNull(ByteBufAllocator allocator, SqlServerType serverType) { return RpcEncoding.encodeNull(allocator, serverType); } @Override boolean doCanDecode(TypeInformation typeInformation) { return typeInformation.getServerType() == SqlServerType.FLOAT || typeInformation.getServerType() == SqlServerType.REAL; } @Override Double doDecode(ByteBuf buffer, Length length, TypeInformation type, Class valueType) { if (length.isNull()) { return null; } if (length.getLength() == 4) { return (double) Decode.asFloat(buffer); } return Decode.asDouble(buffer); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/Encoded.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TdsDataType; import io.r2dbc.mssql.util.ReferenceCountUtil; import reactor.core.Disposable; import java.util.function.IntFunction; import java.util.function.Supplier; /** * Encoded value, either providing a singleton {@link ByteBuf} or a {@link Supplier} of buffers. * * @author Mark Paluch */ public class Encoded implements Disposable { private final TdsDataType dataType; private final Supplier encoder; Encoded(TdsDataType dataType, Supplier encoder) { this.dataType = dataType; this.encoder = encoder; } public static Encoded of(TdsDataType dataType, ByteBuf value) { return new Encoded(dataType, new DisposableSupplier(value)); } public static Encoded of(TdsDataType dataType, Supplier value) { return new Encoded(dataType, value); } public TdsDataType getDataType() { return this.dataType; } public ByteBuf getValue() { return this.encoder.get(); } /** * Returns the formal type such as {@literal INTEGER} or {@literal VARCHAR(255)} * * @return */ public String getFormalType() { for (SqlServerType serverType : SqlServerType.values()) { for (TdsDataType tdsType : serverType.getFixedTypes()) { if (tdsType == this.dataType) { return serverType.toString(); } } } throw new IllegalStateException(String.format("Cannot determine a formal type for %s", this.dataType)); } /** * Attempt to estimate the length of the buffer to apply allocation optimizations. * * @return the estimated length. Can be an approximation or zero, if the buffer size cannot be estimated. */ public int estimateLength() { if (this.encoder instanceof DisposableSupplier) { return ((DisposableSupplier) this.encoder).get().readableBytes(); } if (this.encoder instanceof LengthAwareSupplier) { return ((LengthAwareSupplier) this.encoder).getLength(); } return 0; } public static Supplier ofLengthAware(int length, IntFunction supplier) { return new LengthAwareSupplier(length, supplier); } @Override public void dispose() { if (this.encoder instanceof DisposableSupplier) { ((DisposableSupplier) this.encoder).dispose(); } } static class DisposableSupplier implements Supplier, Disposable { private final ByteBuf buf; DisposableSupplier(ByteBuf buf) { this.buf = buf; } @Override public ByteBuf get() { return this.buf.asReadOnly(); } @Override public void dispose() { ReferenceCountUtil.maybeSafeRelease(this.buf); } @Override public boolean isDisposed() { return this.buf.refCnt() == 0; } } static class LengthAwareSupplier implements Supplier { private final int length; private final IntFunction delegate; public LengthAwareSupplier(int length, IntFunction delegate) { this.length = length; this.delegate = delegate; } @Override public ByteBuf get() { return this.delegate.apply(this.length); } public int getLength() { return this.length; } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/FloatCodec.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.message.type.Length; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; /** * Codec for floating-point values that are represented as {@link Float}. * *
      *
    • Server types: {@link SqlServerType#FLOAT} (8-byte) and {@link SqlServerType#REAL} (4-byte)
    • *
    • Java type: {@link Float}
    • *
    • Downcast: to {@link Float}
    • *
    * * @author Mark Paluch */ final class FloatCodec extends AbstractCodec { static final FloatCodec INSTANCE = new FloatCodec(); private static final byte[] NULL = ByteArray.fromEncoded(alloc -> RpcEncoding.encodeNull(alloc, SqlServerType.REAL)); private FloatCodec() { super(Float.class); } @Override Encoded doEncode(ByteBufAllocator allocator, RpcParameterContext context, Float value) { return RpcEncoding.encodeFixed(allocator, SqlServerType.REAL, value, Encode::asFloat); } @Override public boolean canEncodeNull(SqlServerType serverType) { return serverType == SqlServerType.REAL; } @Override Encoded doEncodeNull(ByteBufAllocator allocator) { return RpcEncoding.wrap(NULL, SqlServerType.REAL); } @Override public Encoded encodeNull(ByteBufAllocator allocator, SqlServerType serverType) { return RpcEncoding.encodeNull(allocator, serverType); } @Override boolean doCanDecode(TypeInformation typeInformation) { return DoubleCodec.INSTANCE.doCanDecode(typeInformation); } @Override Float doDecode(ByteBuf buffer, Length length, TypeInformation type, Class valueType) { Double value = DoubleCodec.INSTANCE.doDecode(buffer, length, type, Double.class); return value == null ? null : value.floatValue(); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/IntegerCodec.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.message.type.SqlServerType; /** * Codec for numeric values that are represented as {@link Integer}. * *
      *
    • Server types: Integer numbers
    • *
    • Java type: {@link Integer}
    • *
    • Downcast: to {@link Integer}
    • *
    * * @author Mark Paluch */ final class IntegerCodec extends AbstractNumericCodec { /** * Singleton instance. */ static final IntegerCodec INSTANCE = new IntegerCodec(); private static final byte[] NULL = ByteArray.fromEncoded((alloc) -> RpcEncoding.encodeNull(alloc, SqlServerType.INTEGER)); private IntegerCodec() { super(Integer.class, value -> (int) value); } @Override Encoded doEncode(ByteBufAllocator allocator, RpcParameterContext context, Integer value) { return RpcEncoding.encodeFixed(allocator, SqlServerType.INTEGER, value, Encode::asInt); } @Override Encoded doEncodeNull(ByteBufAllocator allocator) { return RpcEncoding.wrap(NULL, SqlServerType.INTEGER); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/LocalDateCodec.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.message.type.*; import java.time.LocalDate; import java.time.temporal.ChronoUnit; /** * Codec for date types that are represented as {@link LocalDate}. * *
      *
    • Server types: {@link SqlServerType#DATE}
    • *
    • Java type: {@link LocalDate}
    • *
    • Downcast: none
    • *
    * * @author Mark Paluch */ final class LocalDateCodec extends AbstractCodec { /** * Singleton instance. */ public static final LocalDateCodec INSTANCE = new LocalDateCodec(); /** * Date base date: 0001-01-01. */ private static final LocalDate DATE_ZERO = LocalDate.of(1, 1, 1); private static final byte[] NULL = ByteArray.fromEncoded((alloc) -> RpcEncoding.encodeTemporalNull(alloc, SqlServerType.DATE)); private LocalDateCodec() { super(LocalDate.class); } @Override Encoded doEncode(ByteBufAllocator allocator, RpcParameterContext context, LocalDate value) { return new RpcEncoding.HintedEncoded(TdsDataType.DATEN, SqlServerType.DATE, () -> { ByteBuf buffer = allocator.buffer(4); buffer.writeByte(TypeUtils.DAYS_INTO_CE_LENGTH); encode(buffer, value); return buffer; }); } @Override public boolean canEncodeNull(SqlServerType serverType) { return serverType == SqlServerType.DATE; } @Override public Encoded doEncodeNull(ByteBufAllocator allocator) { return RpcEncoding.wrap(NULL, SqlServerType.DATE); } @Override public Encoded encodeNull(ByteBufAllocator allocator, SqlServerType serverType) { return RpcEncoding.encodeTemporalNull(allocator, serverType); } @Override boolean doCanDecode(TypeInformation typeInformation) { return typeInformation.getServerType() == SqlServerType.DATE; } @Override LocalDate doDecode(ByteBuf buffer, Length length, TypeInformation type, Class valueType) { if (length.isNull()) { return null; } int days = (buffer.readByte() & 0xFF) | (buffer.readByte() & 0xFF) << 8 | (buffer.readByte() & 0xFF) << 16; return DATE_ZERO.plusDays(days); } /** * Write the {@link LocalDate} value to the {@link ByteBuf data buffer}. */ static void encode(ByteBuf buffer, LocalDate value) { long days = ChronoUnit.DAYS.between(DATE_ZERO, value); buffer.writeByte((byte) days & 0xFF); buffer.writeByte((byte) (days >> 8) & 0xFF); buffer.writeByte((byte) (days >> 16) & 0xFF); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/LocalDateTimeCodec.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.message.tds.Decode; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.message.type.Length; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.message.type.TypeUtils; import java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.temporal.ChronoUnit; import java.util.concurrent.TimeUnit; /** * Codec for temporal types that are represented as {@link LocalDateTime}. * *
      *
    • Server types: {@link SqlServerType#SMALLDATETIME}, {@link SqlServerType#DATETIME}, and {@link SqlServerType#DATETIME2}
    • *
    • Java type: {@link LocalDateTime}
    • *
    • Downcast: none
    • *
    * * @author Mark Paluch */ final class LocalDateTimeCodec extends AbstractCodec { /** * Singleton instance. */ public static final LocalDateTimeCodec INSTANCE = new LocalDateTimeCodec(); /** * Date-Time base date: 1900-01-01T00:00:00.0. */ private static final LocalDateTime DATETIME_ZERO = LocalDateTime.of(1900, 1, 1, 0, 0, 0, 0); private static final long NANOS_PER_SECOND = TimeUnit.SECONDS.toNanos(1); private static final long NANOS_PER_MILLISECOND = TimeUnit.MILLISECONDS.toNanos(1); private static final byte[] NULL = ByteArray.fromEncoded((alloc) -> RpcEncoding.encodeTemporalNull(alloc, SqlServerType.DATETIME2, 7)); private LocalDateTimeCodec() { super(LocalDateTime.class); } @Override Encoded doEncode(ByteBufAllocator allocator, RpcParameterContext context, LocalDateTime value) { return RpcEncoding.encode(allocator, SqlServerType.DATETIME2, 8, value, (buffer, localDateTime) -> { encode(buffer, SqlServerType.DATETIME2, TypeUtils.MAX_FRACTIONAL_SECONDS_SCALE, localDateTime); }); } @Override public boolean canEncodeNull(SqlServerType serverType) { return serverType == SqlServerType.DATETIME2; } @Override Encoded doEncodeNull(ByteBufAllocator allocator) { return RpcEncoding.wrap(NULL, SqlServerType.DATETIME2); } @Override public Encoded encodeNull(ByteBufAllocator allocator, SqlServerType serverType) { return RpcEncoding.encodeTemporalNull(allocator, serverType, 7); } @Override boolean doCanDecode(TypeInformation typeInformation) { return typeInformation.getServerType() == SqlServerType.SMALLDATETIME || typeInformation.getServerType() == SqlServerType.DATETIME || typeInformation.getServerType() == SqlServerType.DATETIME2; } @Override LocalDateTime doDecode(ByteBuf buffer, Length length, TypeInformation type, Class valueType) { if (length.isNull()) { return null; } if (type.getServerType() == SqlServerType.SMALLDATETIME) { int daysSinceBaseDate = Decode.uShort(buffer); int minutesSinceMidnight = Decode.uShort(buffer); return DATETIME_ZERO.plusDays(daysSinceBaseDate).plusMinutes(minutesSinceMidnight); } if (type.getServerType() == SqlServerType.DATETIME) { int daysSinceBaseDate = Decode.asInt(buffer); long ticksSinceMidnight = (Decode.asInt(buffer) * 10 + 1) / 3; int subSecondNanos = (int) ((ticksSinceMidnight * NANOS_PER_MILLISECOND) % NANOS_PER_SECOND); return DATETIME_ZERO.plusDays(daysSinceBaseDate).plus(ticksSinceMidnight, ChronoUnit.MILLIS).withNano(subSecondNanos); } if (type.getServerType() == SqlServerType.DATETIME2) { LocalTime localTime = LocalTimeCodec.INSTANCE.doDecode(buffer, length, type, LocalTime.class); LocalDate localDate = LocalDateCodec.INSTANCE.doDecode(buffer, length, type, LocalDate.class); return localTime.atDate(localDate); } throw new UnsupportedOperationException(String.format("Cannot decode value from server type [%s]", type.getServerType())); } static void encode(ByteBuf buffer, SqlServerType type, int scale, LocalDateTime value) { if (type == SqlServerType.SMALLDATETIME) { LocalDateTime midnight = value.truncatedTo(ChronoUnit.DAYS); int daysSinceBaseDate = Math.toIntExact(Duration.between(DATETIME_ZERO, midnight).toDays()); int minutesSinceMidnight = (int) Duration.between(midnight, value).toMinutes(); Encode.uShort(buffer, daysSinceBaseDate); Encode.uShort(buffer, minutesSinceMidnight); return; } if (type == SqlServerType.DATETIME) { LocalDateTime midnight = value.truncatedTo(ChronoUnit.DAYS); int daysSinceBaseDate = Math.toIntExact(Duration.between(DATETIME_ZERO, midnight).toDays()); Duration time = Duration.between(midnight, value); long ticksSinceMidnight = (3 * time.toMillis() + 5) / 10; Encode.asInt(buffer, daysSinceBaseDate); Encode.asInt(buffer, (int) ticksSinceMidnight); return; } if (type == SqlServerType.DATETIME2) { LocalTimeCodec.doEncode(buffer, scale, value.toLocalTime()); LocalDateCodec.encode(buffer, value.toLocalDate()); return; } throw new UnsupportedOperationException(String.format("Cannot encode [%s] to server type [%s]", value, type)); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/LocalTimeCodec.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.message.type.Length; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.message.type.TypeUtils; import io.r2dbc.mssql.util.Assert; import java.time.LocalTime; /** * Codec for scaled time types that are represented as {@link LocalTime}. * *
      *
    • Server types: {@link SqlServerType#TIME}
    • *
    • Java type: {@link LocalTime}
    • *
    • Downcast: none
    • *
    * * @author Mark Paluch */ final class LocalTimeCodec extends AbstractCodec { /** * Singleton instance. */ public static final LocalTimeCodec INSTANCE = new LocalTimeCodec(); /** * Using known multipliers is faster than calculating these (10^n). */ private static final int[] SCALED_MULTIPLIERS = new int[]{10000000, 1000000, 100000, 10000, 1000, 100, 10, 1}; private static final byte[] NULL = ByteArray.fromEncoded((alloc) -> RpcEncoding.encodeTemporalNull(alloc, SqlServerType.TIME, 7)); private LocalTimeCodec() { super(LocalTime.class); } @Override Encoded doEncode(ByteBufAllocator allocator, RpcParameterContext context, LocalTime value) { return RpcEncoding.encode(allocator, SqlServerType.TIME, TypeUtils.getTimeValueLength(TypeUtils.MAX_FRACTIONAL_SECONDS_SCALE), value, (buffer, localTime) -> doEncode(buffer, TypeUtils.MAX_FRACTIONAL_SECONDS_SCALE, localTime)); } @Override public boolean canEncodeNull(SqlServerType serverType) { return serverType == SqlServerType.TIME; } @Override Encoded doEncodeNull(ByteBufAllocator allocator) { return RpcEncoding.wrap(NULL, SqlServerType.TIME); } @Override public Encoded encodeNull(ByteBufAllocator allocator, SqlServerType serverType) { return RpcEncoding.encodeTemporalNull(allocator, serverType, 7); } @Override boolean doCanDecode(TypeInformation typeInformation) { return typeInformation.getServerType() == SqlServerType.TIME; } @Override LocalTime doDecode(ByteBuf buffer, Length length, TypeInformation type, Class valueType) { if (length.isNull()) { return null; } long hundredNanosSinceMidnight = 0; int scale = type.getScale(); Assert.isTrue(scale >= 0 && scale <= TypeUtils.MAX_FRACTIONAL_SECONDS_SCALE, "Invalid fractional scale"); int valueLength = TypeUtils.getTimeValueLength(scale); for (int i = 0; i < valueLength; i++) { hundredNanosSinceMidnight |= (buffer.readByte() & 0xFFL) << (8 * i); } hundredNanosSinceMidnight *= SCALED_MULTIPLIERS[scale]; return LocalTime.ofNanoOfDay(hundredNanosSinceMidnight * 100); } static void doEncode(ByteBuf buffer, int scale, LocalTime value) { int valueLength = TypeUtils.getTimeValueLength(scale); doEncodeValue(buffer, valueLength, value); } private static void doEncodeValue(ByteBuf buffer, int valueLength, LocalTime value) { long nanosSinceMidnight = value.toNanoOfDay(); nanosSinceMidnight /= SCALED_MULTIPLIERS[valueLength]; for (int i = 0; i < valueLength; i++) { buffer.writeByte((byte) ((nanosSinceMidnight >> (8 * i)) & 0xFF)); } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/LongCodec.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.message.type.SqlServerType; /** * Codec for numeric values that are represented as {@link Long}. * *
      *
    • Server types: Integer numbers
    • *
    • Java type: {@link Long}
    • *
    • Downcast: none
    • *
    * * @author Mark Paluch */ final class LongCodec extends AbstractNumericCodec { /** * Singleton instance. */ static final LongCodec INSTANCE = new LongCodec(); private static final byte[] NULL = ByteArray.fromEncoded((alloc) -> RpcEncoding.encodeNull(alloc, SqlServerType.BIGINT)); private LongCodec() { super(Long.class, value -> value); } @Override Encoded doEncode(ByteBufAllocator allocator, RpcParameterContext context, Long value) { return RpcEncoding.encodeFixed(allocator, SqlServerType.BIGINT, value, Encode::bigint); } @Override public Encoded doEncodeNull(ByteBufAllocator allocator) { return RpcEncoding.wrap(NULL, SqlServerType.BIGINT); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/MoneyCodec.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.message.tds.Decode; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.message.tds.ProtocolException; import io.r2dbc.mssql.message.type.Length; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import java.math.BigDecimal; import java.math.BigInteger; /** * Codec for fixed floating-point values that are represented as {@link BigDecimal}. * *
      *
    • Server types: {@link SqlServerType#MONEY} (8-byte) and {@link SqlServerType#SMALLMONEY} (4-byte)
    • *
    • Java type: {@link BigDecimal}
    • *
    • Downcast: none
    • *
    * * @author Mark Paluch */ final class MoneyCodec extends AbstractCodec { /** * Singleton instance. */ static final MoneyCodec INSTANCE = new MoneyCodec(); /** * Value length of {@link SqlServerType#MONEY}. */ private static final int BIG_MONEY_LENGTH = 8; /** * Value length of {@link SqlServerType#SMALLMONEY}. */ private static final int SMALL_MONEY_LENGTH = 4; private static final byte[] NULL = ByteArray.fromEncoded(alloc -> RpcEncoding.encodeNull(alloc, SqlServerType.MONEY)); private MoneyCodec() { super(BigDecimal.class); } @Override Encoded doEncode(ByteBufAllocator allocator, RpcParameterContext context, BigDecimal value) { return RpcEncoding.encodeFixed(allocator, SqlServerType.MONEY, value, (buffer, bigDecimal) -> Encode.money(buffer, bigDecimal.unscaledValue())); } @Override public boolean canEncodeNull(SqlServerType serverType) { return serverType == SqlServerType.MONEY || serverType == SqlServerType.SMALLMONEY; } @Override public Encoded doEncodeNull(ByteBufAllocator allocator) { return RpcEncoding.wrap(NULL, SqlServerType.MONEY); } @Override public Encoded encodeNull(ByteBufAllocator allocator, SqlServerType serverType) { return RpcEncoding.encodeNull(allocator, serverType); } @Override boolean doCanDecode(TypeInformation typeInformation) { return typeInformation.getServerType() == SqlServerType.MONEY; } @Override BigDecimal doDecode(ByteBuf buffer, Length length, TypeInformation type, Class valueType) { BigInteger decoded = decode(buffer, length.getLength()); return new BigDecimal(decoded, 4); } private static BigInteger decode(ByteBuf buffer, int length) { switch (length) { case BIG_MONEY_LENGTH: int intBitsHi = Decode.asInt(buffer); int intBitsLo = Decode.asInt(buffer); return BigInteger.valueOf(((long) intBitsHi << 32) | (intBitsLo & 0xFFFFFFFFL)); case SMALL_MONEY_LENGTH: return BigInteger.valueOf(Decode.asInt(buffer)); default: throw ProtocolException.invalidTds(String.format("Unexpected value length: %d", length)); } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/OffsetDateTimeCodec.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.message.tds.Decode; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.message.type.*; import java.time.LocalDate; import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.ZoneOffset; /** * Codec for temporal types that are represented as {@link OffsetDateTime}. * *
      *
    • Server types: {@link SqlServerType#DATETIMEOFFSET}
    • *
    • Java type: {@link OffsetDateTime}
    • *
    • Downcast: none
    • *
    * * @author Mark Paluch */ final class OffsetDateTimeCodec extends AbstractCodec { /** * Singleton instance. */ public static final OffsetDateTimeCodec INSTANCE = new OffsetDateTimeCodec(); private static final byte[] NULL = ByteArray.fromEncoded((alloc) -> RpcEncoding.encodeTemporalNull(alloc, SqlServerType.DATETIMEOFFSET, 7)); private OffsetDateTimeCodec() { super(OffsetDateTime.class); } @Override Encoded doEncode(ByteBufAllocator allocator, RpcParameterContext context, OffsetDateTime value) { return new RpcEncoding.HintedEncoded(TdsDataType.DATETIMEOFFSETN, SqlServerType.DATETIMEOFFSET, () -> { ByteBuf buffer = allocator.buffer(12); Encode.asByte(buffer, 7); // scale Encode.asByte(buffer, 0x0a); // length doEncode(buffer, value.minusSeconds(value.getOffset().getTotalSeconds())); return buffer; }); } @Override public boolean canEncodeNull(SqlServerType serverType) { return serverType == SqlServerType.DATETIMEOFFSET; } @Override public Encoded doEncodeNull(ByteBufAllocator allocator) { return RpcEncoding.wrap(NULL, SqlServerType.DATETIMEOFFSET); } @Override public Encoded encodeNull(ByteBufAllocator allocator, SqlServerType serverType) { return RpcEncoding.encodeTemporalNull(allocator, serverType, 7); } @Override boolean doCanDecode(TypeInformation typeInformation) { return typeInformation.getServerType() == SqlServerType.DATETIMEOFFSET; } @Override OffsetDateTime doDecode(ByteBuf buffer, Length length, TypeInformation type, Class valueType) { if (length.isNull()) { return null; } LocalTime localTime = LocalTimeCodec.INSTANCE.doDecode(buffer, length, type, LocalTime.class); LocalDate localDate = LocalDateCodec.INSTANCE.doDecode(buffer, length, type, LocalDate.class); int localMinutesOffset = Decode.smallInt(buffer); ZoneOffset offset = ZoneOffset.ofTotalSeconds(localMinutesOffset * 60); return OffsetDateTime.of(localTime.atDate(localDate), offset).plusMinutes(localMinutesOffset); } static void doEncode(ByteBuf buffer, OffsetDateTime value) { LocalTimeCodec.doEncode(buffer, TypeUtils.MAX_FRACTIONAL_SECONDS_SCALE, value.toLocalTime()); LocalDateCodec.encode(buffer, value.toLocalDate()); int localMinutesOffset = value.getOffset().getTotalSeconds() / 60; Encode.smallInt(buffer, localMinutesOffset); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/PlpEncoded.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.Unpooled; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.message.type.Length; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.PlpLength; import io.r2dbc.mssql.message.type.SqlServerType; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Disposable; import reactor.core.publisher.Flux; import reactor.core.publisher.FluxOperator; import reactor.core.publisher.Operators; import reactor.util.annotation.Nullable; import reactor.util.context.Context; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLongFieldUpdater; import java.util.function.IntSupplier; /** * Partial-length-prefixed extension to {@link Encoded}. Consumes a upstream {@link Publisher}. * * @author Mark Paluch */ public class PlpEncoded extends Encoded { private final SqlServerType serverType; private final ByteBufAllocator allocator; private final Publisher dataStream; private final Disposable disposable; public PlpEncoded(SqlServerType dataType, ByteBufAllocator allocator, Publisher dataStream, Disposable disposable) { super(dataType.getNullableType(), () -> Unpooled.EMPTY_BUFFER); this.serverType = dataType; this.allocator = allocator; this.dataStream = dataStream; this.disposable = disposable; } public void encodeHeader(ByteBuf byteBuf) { // Send v*max length indicator 0xFFFF. Encode.uShort(byteBuf, Length.USHORT_NULL); } @Override public boolean isDisposed() { return this.disposable.isDisposed(); } @Override public void dispose() { this.disposable.dispose(); } /** * Transform the backing binary stream to a stream of binary chunks at the size provided by {@link IntSupplier chunk size supplier}. * * @param chunkSize expected chunk size. * @return */ public Flux chunked(IntSupplier chunkSize) { return chunked(chunkSize, false); } /** * Transform the backing binary stream to a stream of binary chunks at the size provided by {@link IntSupplier chunk size supplier}. * * @param chunkSize expected chunk size. * @param withSizeHeaders {@code true} to include PLP length headers (one unknown length and chunk length per chunk). * @return */ public Flux chunked(IntSupplier chunkSize, boolean withSizeHeaders) { return new ChunkOperator(Flux.from(this.dataStream), this.allocator, chunkSize, withSizeHeaders); } @Override public String getFormalType() { switch (this.serverType) { case VARBINARYMAX: return "VARBINARY(MAX)"; case VARCHARMAX: return "VARCHAR(MAX)"; case NVARCHARMAX: return "NVARCHAR(MAX)"; } throw new UnsupportedOperationException("Type " + this.serverType + " not supported"); } /** * Operator for chunked encoding of {@link ByteBuf}. Chunk size is obtained from {@link IntSupplier} on */ static class ChunkOperator extends FluxOperator { private final ByteBufAllocator allocator; private final IntSupplier chunkSizeSupplier; private final boolean withSizeHeaders; ChunkOperator(Flux source, ByteBufAllocator allocator, IntSupplier chunkSizeSupplier, boolean withSizeHeaders) { super(source); this.allocator = allocator; this.chunkSizeSupplier = chunkSizeSupplier; this.withSizeHeaders = withSizeHeaders; } @Override public void subscribe(CoreSubscriber actual) { this.source.subscribe(new ChunkSubscriber(actual, this.allocator, this.chunkSizeSupplier, this.withSizeHeaders)); } } static class ChunkSubscriber extends AtomicLong implements CoreSubscriber, Subscription { private static final int STATUS_WIP = 0; private static final int STATUS_DONE = 1; private final CoreSubscriber actual; private final ByteBufAllocator allocator; private final IntSupplier chunkSizeSupplier; private final boolean withSizeHeaders; private boolean first = true; private volatile int nextChunkSize; @Nullable private volatile CompositeByteBuf aggregator; volatile long requested; @SuppressWarnings("rawtypes") static final AtomicLongFieldUpdater REQUESTED = AtomicLongFieldUpdater.newUpdater(ChunkSubscriber.class, "requested"); volatile int status; @SuppressWarnings("rawtypes") static final AtomicIntegerFieldUpdater STATUS = AtomicIntegerFieldUpdater.newUpdater(ChunkSubscriber.class, "status"); private boolean doneUpstream; private Subscription s; ChunkSubscriber(CoreSubscriber actual, ByteBufAllocator allocator, IntSupplier chunkSizeSupplier, boolean withSizeHeaders) { this.actual = actual; this.allocator = allocator; this.chunkSizeSupplier = chunkSizeSupplier; this.withSizeHeaders = withSizeHeaders; } @Override public Context currentContext() { return this.actual.currentContext(); } @Override public void onSubscribe(Subscription s) { if (Operators.validate(this.s, s)) { this.s = s; this.actual.onSubscribe(this); } } @Override public void onNext(ByteBuf byteBuf) { byteBuf.touch("PlpEncoded.onNext(…)"); if (STATUS.get(this) == STATUS_DONE) { byteBuf.release(); Operators.onNextDropped(byteBuf, this.actual.currentContext()); return; } CompositeByteBuf aggregator = this.aggregator; if (aggregator == null) { this.aggregator = aggregator = this.allocator.compositeBuffer(); } aggregator.addComponent(true, byteBuf); drain(); if (!this.doneUpstream && REQUESTED.get(this) > 0) { this.s.request(1); } } private void drain() { CompositeByteBuf aggregator = this.aggregator; if (aggregator == null) { if (this.doneUpstream && STATUS.compareAndSet(this, STATUS_WIP, STATUS_DONE)) { this.actual.onComplete(); } return; } while (STATUS.get(this) == STATUS_WIP && aggregator.readableBytes() >= this.nextChunkSize && REQUESTED.get(this) > 0) { long demand = REQUESTED.get(this); if (demand > 0) { if (REQUESTED.compareAndSet(this, demand, demand - 1)) { emitNext(aggregator, this.nextChunkSize); this.nextChunkSize = this.chunkSizeSupplier.getAsInt(); } } } if (STATUS.get(this) == STATUS_WIP && this.doneUpstream && aggregator.isReadable() && REQUESTED.get(this) > 0) { long demand = REQUESTED.get(this); if (demand > 0) { if (REQUESTED.compareAndSet(this, demand, demand - 1)) { emitNext(aggregator, aggregator.readableBytes()); } } } if (this.doneUpstream && !aggregator.isReadable() && STATUS.compareAndSet(this, STATUS_WIP, STATUS_DONE)) { aggregator.release(); this.aggregator = null; this.actual.onComplete(); } else { aggregator.discardReadComponents(); } } /** * Emit a chunk from the buffer. At this point we need to make sure that buffers which get emitted do not hold a reference to the aggregator as they might cross thread boundaries. * * @param aggregator * @param bytesToRead */ private void emitNext(CompositeByteBuf aggregator, int bytesToRead) { ByteBuf buffer = aggregator.alloc().buffer(bytesToRead); buffer.writeBytes(aggregator, bytesToRead); if (this.withSizeHeaders) { CompositeByteBuf composite = this.allocator.compositeBuffer(); ByteBuf header = this.allocator.buffer(); if (this.first) { this.first = false; PlpLength.unknown().encode(header); } Length chunkLength = Length.of(bytesToRead); chunkLength.encode(header, LengthStrategy.PARTLENTYPE); composite.addComponent(true, header); composite.addComponent(true, buffer); buffer = composite; } this.actual.onNext(buffer); } @Override public void onError(Throwable t) { CompositeByteBuf aggregator = this.aggregator; if (STATUS.compareAndSet(this, STATUS_WIP, STATUS_DONE)) { this.doneUpstream = true; this.actual.onError(t); if (aggregator != null) { aggregator.release(); } return; } Operators.onErrorDropped(t, this.actual.currentContext()); } @Override public void onComplete() { this.doneUpstream = true; drain(); } @Override public void request(long n) { if (Operators.validate(n)) { Operators.addCap(REQUESTED, this, n); drain(); this.nextChunkSize = this.chunkSizeSupplier.getAsInt(); if (!this.doneUpstream && REQUESTED.get(this) > 0) { this.s.request(1); } } } @Override public void cancel() { if (!this.doneUpstream) { this.doneUpstream = true; this.s.cancel(); } if (STATUS.compareAndSet(this, STATUS_WIP, STATUS_DONE)) { CompositeByteBuf aggregator = this.aggregator; if (aggregator != null) { aggregator.release(); } } } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/PlpEncodedCharacters.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.message.type.Collation; import io.r2dbc.mssql.message.type.SqlServerType; import org.reactivestreams.Publisher; import reactor.core.Disposable; /** * Extension to {@link PlpEncoded} associated with a {@link Collation}. * * @author Mark Paluch */ class PlpEncodedCharacters extends PlpEncoded { private final Collation collation; PlpEncodedCharacters(SqlServerType dataType, Collation collation, ByteBufAllocator allocator, Publisher dataStream, Disposable disposable) { super(dataType, allocator, dataStream, disposable); this.collation = collation; } public void encodeHeader(ByteBuf byteBuf) { super.encodeHeader(byteBuf); this.collation.encode(byteBuf); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/RpcDirection.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; /** * Direction for RPC parameters * * @author Mark Paluch */ public enum RpcDirection { /** * Input parameter that is expected by the server. */ IN, /** * Output parameter that is returned by the server. */ OUT } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/RpcEncoding.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.message.type.Collation; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TdsDataType; import io.r2dbc.mssql.util.Assert; import io.r2dbc.mssql.util.StringUtils; import reactor.util.annotation.Nullable; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Supplier; /** * Utility methods to encode RPC parameters. * * @author Mark Paluch */ public final class RpcEncoding { private RpcEncoding() { } /** * Encode a string parameter as {@literal NVARCHAR} or {@literal NTEXT} (depending on the size). * * @param buffer the data buffer. * @param name optional parameter name. * @param direction RPC parameter direction (in/out) * @param collation parameter value encoding. * @param value the parameter value, can be {@code null}. */ public static void encodeString(ByteBuf buffer, @Nullable String name, RpcDirection direction, Collation collation, @Nullable String value) { encodeHeader(buffer, name, direction, TdsDataType.NVARCHAR); CharacterEncoder.encodeBigVarchar(buffer, direction, collation, true, value); } /** * Encode an integer parameter as {@literal INTn}. * * @param buffer the data buffer. * @param name optional parameter name. * @param direction RPC parameter direction (in/out) * @param value the parameter value, can be {@code null}. */ public static void encodeInteger(ByteBuf buffer, @Nullable String name, RpcDirection direction, @Nullable Integer value) { encodeHeader(buffer, name, direction, TdsDataType.INTN); Encode.asByte(buffer, 4); // max-len if (value == null) { Encode.asByte(buffer, 0); // len of data bytes } else { Encode.asByte(buffer, 4); // len of data bytes Encode.asInt(buffer, value); } } /** * Encode an RPC header that writes {@code name}, {@link RpcDirection}, and the {@link TdsDataType}. * * @param buffer the data buffer. * @param name name of the parameter, can be {@code null}. * @param direction the parameter direction. * @param dataType TDS data type. */ public static void encodeHeader(ByteBuf buffer, @Nullable String name, RpcDirection direction, TdsDataType dataType) { if (StringUtils.hasText(name)) { Encode.asByte(buffer, name.length() + 1); char at = '@'; writeChar(buffer, at); for (int i = 0; i < name.length(); i++) { char ch = name.charAt(i); writeChar(buffer, ch); } } else { Encode.asByte(buffer, 0); } Encode.asByte(buffer, direction == RpcDirection.OUT ? 1 : 0); Encode.asByte(buffer, dataType.getValue()); } private static void writeChar(ByteBuf buffer, char ch) { buffer.writeByte((byte) (ch & 0xFF)); buffer.writeByte((byte) ((ch >> 8) & 0xFF)); } /** * Encode a RPC parameter that uses a fixed-length, nullable data type. * * @param allocator the allocator to allocate encoding buffers. * @param serverType the server type. Used to derive the nullable {@link TdsDataType}. * @param value the value to encode. * @param valueEncoder encoder function. Using a {@link BiFunction} to allow non-capturing lambdas. * @param * @return */ public static Encoded encodeFixed(ByteBufAllocator allocator, SqlServerType serverType, T value, BiConsumer valueEncoder) { Assert.notNull(serverType.getNullableType(), "Server type provides no nullable type"); LengthStrategy lengthStrategy = serverType.getNullableType().getLengthStrategy(); return new HintedEncoded(serverType.getNullableType(), serverType, () -> { ByteBuf buffer = prepareBuffer(allocator, lengthStrategy, serverType.getMaxLength(), serverType.getMaxLength()); valueEncoder.accept(buffer, value); return buffer; }); } /** * Encode a RPC parameter that declares length and max-length attributes and apply a {@link SqlServerType} hint. * * @param allocator the allocator to allocate encoding buffers. * @param serverType the server data type. Used to derive the nullable {@link TdsDataType}. * @param length actual data length. * @param value the value to encode. * @param valueEncoder encoder function. Using a {@link BiFunction} to allow non-capturing lambdas. * @param * @return */ public static Encoded encode(ByteBufAllocator allocator, SqlServerType serverType, int length, T value, BiConsumer valueEncoder) { Assert.notNull(serverType.getNullableType(), "Server type provides no nullable type"); TdsDataType dataType = serverType.getNullableType(); return new HintedEncoded(dataType, serverType, () -> { ByteBuf buffer = prepareBuffer(allocator, dataType.getLengthStrategy(), serverType.getMaxLength(), length); valueEncoder.accept(buffer, value); return buffer; }); } /** * Encode a {@code null} RPC parameter that declares length and max-length attributes and apply a {@link SqlServerType} hint. * * @param allocator the allocator to allocate encoding buffers. * @param serverType the server data type. Used to derive the nullable {@link TdsDataType}. * @return the encoded {@code null} value. */ public static Encoded encodeNull(ByteBufAllocator allocator, SqlServerType serverType) { Assert.notNull(serverType.getNullableType(), "Server type does not declare a nullable type"); return new HintedEncoded(serverType.getNullableType(), serverType, () -> prepareBuffer(allocator, serverType.getNullableType().getLengthStrategy(), serverType.getMaxLength(), 0)); } /** * Wrap a binary encoded RPC parameter and apply a {@link SqlServerType} hint. * * @param buffer the encoded buffer. * @param serverType the server data type. Used to derive the nullable {@link TdsDataType}. * @return the encoded {@code null} value. */ public static Encoded wrap(byte[] buffer, SqlServerType serverType) { Assert.isTrue(serverType.getMaxLength() > 0, "Server type does not declare a max length"); Assert.notNull(serverType.getNullableType(), "Server type does not declare a nullable type"); return new HintedEncoded(serverType.getNullableType(), serverType, () -> Unpooled.wrappedBuffer(buffer)); } /** * Encode a temporal typed {@code null} RPC parameter. * * @param allocator the allocator to allocate encoding buffers. * @param serverType the server data type. Used to derive the nullable {@link TdsDataType}. * @return the encoded {@code null} value. */ public static Encoded encodeTemporalNull(ByteBufAllocator allocator, SqlServerType serverType) { Assert.notNull(serverType.getNullableType(), "Server type does not declare a nullable type"); return new HintedEncoded(serverType.getNullableType(), serverType, () -> { ByteBuf buffer = allocator.buffer(1); Encode.asByte(buffer, 0); return buffer; }); } /** * Encode a temporal scaled {@code null} RPC parameter. * * @param allocator the allocator to allocate encoding buffers. * @param serverType the server data type. Used to derive the nullable {@link TdsDataType}. * @param scale type scale. * @return the encoded {@code null} value. */ public static Encoded encodeTemporalNull(ByteBufAllocator allocator, SqlServerType serverType, int scale) { Assert.notNull(serverType.getNullableType(), "Server type does not declare a nullable type"); return new HintedEncoded(serverType.getNullableType(), serverType, () -> { ByteBuf buffer = allocator.buffer(1); Encode.asByte(buffer, scale); Encode.asByte(buffer, 0); // value length return buffer; }); } static ByteBuf prepareBuffer(ByteBufAllocator allocator, LengthStrategy lengthStrategy, int maxLength, int length) { ByteBuf buffer; switch (lengthStrategy) { case PARTLENTYPE: buffer = allocator.buffer(8 + 8 + length); buffer.writeLong(maxLength).writeLong(length); return buffer; case BYTELENTYPE: buffer = allocator.buffer(1 + 1 + length); Encode.asByte(buffer, maxLength); Encode.asByte(buffer, length); return buffer; case FIXEDLENTYPE: buffer = allocator.buffer(); return buffer; case USHORTLENTYPE: buffer = allocator.buffer(1 + 1 + length); Encode.uShort(buffer, maxLength); Encode.uShort(buffer, length); return buffer; default: throw new UnsupportedOperationException(lengthStrategy.toString()); } } /** * Extension to {@link Encoded} that applies a {@link SqlServerType} hint. */ static class HintedEncoded extends Encoded { private final SqlServerType sqlServerType; public HintedEncoded(TdsDataType dataType, SqlServerType sqlServerType, Supplier value) { super(dataType, value); this.sqlServerType = sqlServerType; } @Override public String getFormalType() { return this.sqlServerType.toString(); } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/RpcParameterContext.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.r2dbc.mssql.message.type.Collation; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.util.Assert; import reactor.util.annotation.Nullable; /** * Parameter context for RPC parameters. Encapsulated {@link RpcDirection} and an optional {@link ValueContext}. * * @author Mark Paluch */ public final class RpcParameterContext { private static final RpcParameterContext IN = new RpcParameterContext(RpcDirection.IN, null, null); private static final RpcParameterContext OUT = new RpcParameterContext(RpcDirection.OUT, null, null); private final RpcDirection direction; @Nullable private final ValueContext valueContext; @Nullable private final SqlServerType serverType; private RpcParameterContext(RpcDirection direction, @Nullable ValueContext valueContext, @Nullable SqlServerType serverType) { this.direction = direction; this.serverType = serverType; this.valueContext = valueContext; } /** * Returns a context for in (client to server) parameters. * * @return a context for in (client to server) parameters. * @see RpcDirection#IN */ public static RpcParameterContext in() { return IN; } /** * Returns a context for in (client to server) parameters with the associated {@link ValueContext}. * * @param valueContext the value context. * @return a context for in (client to server) parameters with the associated {@link ValueContext}. * @see RpcDirection#IN */ public static RpcParameterContext in(ValueContext valueContext) { return new RpcParameterContext(RpcDirection.IN, Assert.requireNonNull(valueContext, "ValueContext must not be null"), null); } /** * Returns a context for out (server to client) parameters. * * @return a context for out (server to client) parameters. * @see RpcDirection#OUT */ public static RpcParameterContext out() { return OUT; } /** * Returns a context for out (server to client) parameters with the associated {@link ValueContext}. * * @param valueContext the value context. * @return a context for out (server to client) parameters with the associated {@link ValueContext}. * @see RpcDirection#IN */ public static RpcParameterContext out(ValueContext valueContext) { return new RpcParameterContext(RpcDirection.OUT, Assert.requireNonNull(valueContext, "ValueContext must not be null"), null); } /** * @return the RPC direction. */ public RpcDirection getDirection() { return this.direction; } /** * @return {@code true} if this parameter is a in parameter. */ public boolean isIn() { return this.direction == RpcDirection.IN; } /** * @return {@code true} if this parameter is a out parameter. */ public boolean isOut() { return this.direction == RpcDirection.OUT; } /** * @return the value context, can be {@code null}. */ @Nullable public ValueContext getValueContext() { return this.valueContext; } @Nullable public SqlServerType getServerType() { return this.serverType; } /** * Return the required {@link ValueContext} or throw {@link IllegalStateException} if no {@link ValueContext} is set.. * * @return the value context. * @throws IllegalArgumentException if the {@link ValueContext} is not set. */ public ValueContext getRequiredValueContext() { ValueContext valueContext = getValueContext(); if (valueContext == null) { throw new IllegalStateException("No ValueContext set"); } return valueContext; } /** * Return the required {@link ValueContext} as typed {@code T} or throw {@link IllegalStateException} if no {@link ValueContext} is set. * * @return the value context. * @throws IllegalArgumentException if the {@link ValueContext} is not set. * @throws ClassCastException if the {@link ValueContext} is not of type {@code contextType}. */ public T getRequiredValueContext(Class contextType) { ValueContext valueContext = getRequiredValueContext(); return contextType.cast(valueContext); } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [direction=").append(this.direction); sb.append(", valueContext=").append(this.valueContext); sb.append(']'); return sb.toString(); } public RpcParameterContext withServerType(SqlServerType serverType) { Assert.requireNonNull(serverType, "SqlServerType must not be null"); return new RpcParameterContext(this.direction, this.valueContext, serverType); } /** * Marker interface for additional contextual information that are used for value encoding. */ public interface ValueContext { static ValueContext character(Collation collation, boolean sendStringParametersAsUnicode) { return new CharacterValueContext(collation, sendStringParametersAsUnicode); } } /** * Marker interface for additional contextual information. */ public static class CharacterValueContext implements ValueContext { private final Collation collation; private final boolean sendStringParametersAsUnicode; public CharacterValueContext(Collation collation, boolean sendStringParametersAsUnicode) { this.collation = collation; this.sendStringParametersAsUnicode = sendStringParametersAsUnicode; } public Collation getCollation() { return this.collation; } public boolean isSendStringParametersAsUnicode() { return this.sendStringParametersAsUnicode; } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/ShortCodec.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.message.type.SqlServerType; /** * Codec for numeric values that are represented as {@link Short}. * *
      *
    • Server types: Integer numbers
    • *
    • Java type: {@link Short}
    • *
    • Downcast: to {@link Short}
    • *
    * * @author Mark Paluch */ final class ShortCodec extends AbstractNumericCodec { /** * Singleton instance. */ static final ShortCodec INSTANCE = new ShortCodec(); private static final byte[] NULL = ByteArray.fromEncoded((alloc) -> RpcEncoding.encodeNull(alloc, SqlServerType.SMALLINT)); private ShortCodec() { super(Short.class, value -> (short) value); } @Override Encoded doEncode(ByteBufAllocator allocator, RpcParameterContext context, Short value) { return RpcEncoding.encodeFixed(allocator, SqlServerType.SMALLINT, value, Encode::smallInt); } @Override public Encoded doEncodeNull(ByteBufAllocator allocator) { return RpcEncoding.wrap(NULL, SqlServerType.SMALLINT); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/StringCodec.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; import io.r2dbc.mssql.message.type.Length; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.PlpLength; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.message.type.TypeUtils; import io.r2dbc.mssql.util.Assert; import reactor.util.annotation.Nullable; import java.nio.charset.Charset; import java.util.EnumSet; import java.util.Locale; import java.util.Set; import java.util.UUID; /** * Codec for character values that are represented as {@link String}. * *
      *
    • Server types: (N)(VAR)CHAR, (N)TEXT {@link SqlServerType#GUID}
    • *
    • Java type: {@link String}
    • *
    • Downcast: to {@link UUID#toString()}
    • *
    * * @author Mark Paluch * @author Anton Duyun */ final class StringCodec extends AbstractCodec { /** * Singleton instance. */ static final StringCodec INSTANCE = new StringCodec(); private static final Set SUPPORTED_TYPES = EnumSet.of(SqlServerType.CHAR, SqlServerType.NCHAR, SqlServerType.VARCHAR, SqlServerType.NVARCHAR, SqlServerType.VARCHARMAX, SqlServerType.NVARCHARMAX, SqlServerType.TEXT, SqlServerType.NTEXT, SqlServerType.GUID); private StringCodec() { super(String.class); } @Override Encoded doEncode(ByteBufAllocator allocator, RpcParameterContext context, String value) { RpcParameterContext.CharacterValueContext valueContext = context.getRequiredValueContext(RpcParameterContext.CharacterValueContext.class); SqlServerType serverType = context.getServerType(); if (exceedsBigVarchar(context.getDirection(), value) || serverType == SqlServerType.VARCHARMAX || serverType == SqlServerType.NVARCHARMAX) { return CharacterEncoder.encodePlp(allocator, serverType, valueContext, value); } return CharacterEncoder.encodeBigVarchar(allocator, context.getDirection(), serverType, valueContext.getCollation(), valueContext.isSendStringParametersAsUnicode(), value); } @Override public boolean canEncodeNull(SqlServerType serverType) { return serverType == SqlServerType.VARCHAR || serverType == SqlServerType.NVARCHAR; } @Override public Encoded doEncodeNull(ByteBufAllocator allocator) { return encodeNull(allocator, SqlServerType.NVARCHAR); } @Override public Encoded encodeNull(ByteBufAllocator allocator, SqlServerType serverType) { return CharacterEncoder.encodeNull(serverType); } @Override boolean doCanDecode(TypeInformation typeInformation) { return SUPPORTED_TYPES.contains(typeInformation.getServerType()); } @Nullable public String decode(@Nullable ByteBuf buffer, Decodable decodable, Class type) { Assert.requireNonNull(decodable, "Decodable must not be null"); Assert.requireNonNull(type, "Type must not be null"); if (buffer == null) { return null; } Length length; if (decodable.getType().getLengthStrategy() == LengthStrategy.PARTLENTYPE) { PlpLength plpLength = PlpLength.decode(buffer, decodable.getType()); length = Length.of(Math.toIntExact(plpLength.getLength()), plpLength.isNull()); } else { length = Length.decode(buffer, decodable.getType()); } return doDecode(buffer, length, decodable.getType(), type); } @Override String doDecode(ByteBuf buffer, Length length, TypeInformation typeInformation, Class valueType) { if (length.isNull()) { return null; } if (typeInformation.getServerType() == SqlServerType.GUID) { UUID uuid = UuidCodec.INSTANCE.doDecode(buffer, length, typeInformation, UUID.class); return uuid != null ? uuid.toString().toUpperCase(Locale.ENGLISH) : null; } Charset charset = typeInformation.getCharset(); if (typeInformation.getLengthStrategy() == LengthStrategy.PARTLENTYPE) { CompositeByteBuf result = buffer.alloc().compositeBuffer(); try { while (buffer.isReadable()) { Length chunkLength = Length.decode(buffer, typeInformation); result.addComponent(true, buffer.readRetainedSlice(chunkLength.getLength())); } return result.toString(charset); } finally { result.release(); } } String value = buffer.toString(buffer.readerIndex(), length.getLength(), charset); buffer.skipBytes(length.getLength()); return valueType.cast(value); } static boolean exceedsBigVarchar(RpcDirection direction, String value) { int valueLength = (value.length() * 2); boolean isShortValue = valueLength <= TypeUtils.SHORT_VARTYPE_MAX_BYTES; // Use PLP encoding on Yukon and later with long values and OUT parameters return (!isShortValue || direction == RpcDirection.OUT); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/TimestampCodec.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.message.type.Length; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; /** * Codec for binary timestamp values that are represented as {@code byte[]}. * *
      *
    • Server types: {@link SqlServerType#TIMESTAMP} (8-byte)
    • *
    • Java type: {@code byte[]}
    • *
    • Downcast: none
    • *
    * * @author Mark Paluch */ final class TimestampCodec extends AbstractCodec { static final TimestampCodec INSTANCE = new TimestampCodec(); private TimestampCodec() { super(byte[].class); } @Override public boolean canEncode(Object value) { return false; } @Override Encoded doEncode(ByteBufAllocator allocator, RpcParameterContext context, byte[] value) { throw new UnsupportedOperationException(); } @Override public boolean canEncodeNull(SqlServerType serverType) { return false; } @Override protected Encoded doEncodeNull(ByteBufAllocator allocator) { throw new UnsupportedOperationException(); } @Override public Encoded encodeNull(ByteBufAllocator allocator, SqlServerType serverType) { throw new UnsupportedOperationException(); } @Override boolean doCanDecode(TypeInformation typeInformation) { return typeInformation.getServerType() == SqlServerType.TIMESTAMP; } @Override byte[] doDecode(ByteBuf buffer, Length length, TypeInformation type, Class valueType) { byte[] value = new byte[length.getLength()]; buffer.readBytes(value); return value; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/UuidCodec.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.message.type.Length; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import java.util.UUID; /** * @author Mark Paluch */ final class UuidCodec extends AbstractCodec { /** * Singleton instance. */ static final UuidCodec INSTANCE = new UuidCodec(); private static final byte[] NULL = ByteArray.fromEncoded(alloc -> RpcEncoding.encodeNull(alloc, SqlServerType.GUID)); private UuidCodec() { super(UUID.class); } @Override Encoded doEncode(ByteBufAllocator allocator, RpcParameterContext context, UUID value) { return RpcEncoding.encode(allocator, SqlServerType.GUID, 16, value, (buffer, uuid) -> { long msb = value.getMostSignificantBits(); long lsb = value.getLeastSignificantBits(); buffer.writeBytes(swapForWrite(msb)); buffer.writeLong(lsb); }); } @Override public boolean canEncodeNull(SqlServerType serverType) { return serverType == SqlServerType.GUID; } @Override public Encoded doEncodeNull(ByteBufAllocator allocator) { return RpcEncoding.wrap(NULL, SqlServerType.GUID); } @Override public Encoded encodeNull(ByteBufAllocator allocator, SqlServerType serverType) { return RpcEncoding.encodeNull(allocator, serverType); } @Override boolean doCanDecode(TypeInformation typeInformation) { return typeInformation.getServerType() == SqlServerType.GUID; } @Override UUID doDecode(ByteBuf buffer, Length length, TypeInformation typeInformation, Class valueType) { if (length.isNull()) { return null; } byte[] bytes = new byte[8]; buffer.readBytes(bytes); long msb = swapForRead(bytes); long lsb = buffer.readLong(); return new UUID(msb, lsb); } /** * Swap bytes. MSB is represented in order 3,2,1,0 and 5,4,7,6 * * @param memory * @return */ private static long swapForRead(byte[] memory) { return ((long) memory[3] & 0xff) << 56 | ((long) memory[2] & 0xff) << 48 | ((long) memory[1] & 0xff) << 40 | ((long) memory[0] & 0xff) << 32 | ((long) memory[5] & 0xff) << 24 | ((long) memory[4] & 0xff) << 16 | ((long) memory[7] & 0xff) << 8 | (long) memory[6] & 0xff; } /** * Swap bytes. MSB is represented in order 3,2,1,0 and 5,4,7,6 * * @param msb * @return */ private byte[] swapForWrite(long msb) { byte[] bytes = new byte[8]; bytes[3] = (byte) ((msb >> 56) & 0xff); bytes[2] = (byte) ((msb >> 48) & 0xff); bytes[1] = (byte) ((msb >> 40) & 0xff); bytes[0] = (byte) ((msb >> 32) & 0xff); bytes[5] = (byte) ((msb >> 24) & 0xff); bytes[4] = (byte) ((msb >> 16) & 0xff); bytes[7] = (byte) ((msb >> 8) & 0xff); bytes[6] = (byte) ((msb) & 0xff); return bytes; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/ZonedDateTimeCodec.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.message.type.Length; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import java.time.OffsetDateTime; import java.time.ZonedDateTime; /** * Codec for temporal types that are represented as {@link ZonedDateTime}. * *
      *
    • Server types: {@link SqlServerType#DATETIMEOFFSET}
    • *
    • Java type: {@link ZonedDateTime}
    • *
    • Downcast: none
    • *
    * * @author Mark Paluch */ final class ZonedDateTimeCodec extends AbstractCodec { /** * Singleton instance. */ static final ZonedDateTimeCodec INSTANCE = new ZonedDateTimeCodec(); private static final byte[] NULL = ByteArray.fromEncoded((alloc) -> RpcEncoding.encodeTemporalNull(alloc, SqlServerType.DATETIMEOFFSET, 7)); private ZonedDateTimeCodec() { super(ZonedDateTime.class); } @Override Encoded doEncode(ByteBufAllocator allocator, RpcParameterContext context, ZonedDateTime value) { return OffsetDateTimeCodec.INSTANCE.encode(allocator, context, value.toOffsetDateTime()); } @Override public boolean canEncodeNull(SqlServerType serverType) { return serverType == SqlServerType.DATETIMEOFFSET; } @Override public Encoded doEncodeNull(ByteBufAllocator allocator) { return RpcEncoding.wrap(NULL, SqlServerType.DATETIMEOFFSET); } @Override public Encoded encodeNull(ByteBufAllocator allocator, SqlServerType serverType) { return RpcEncoding.encodeTemporalNull(allocator, serverType, 7); } @Override boolean doCanDecode(TypeInformation typeInformation) { return typeInformation.getServerType() == SqlServerType.DATETIMEOFFSET; } @Override ZonedDateTime doDecode(ByteBuf buffer, Length length, TypeInformation type, Class valueType) { if (length.isNull()) { return null; } OffsetDateTime offsetDateTime = OffsetDateTimeCodec.INSTANCE.doDecode(buffer, length, type, OffsetDateTime.class); return offsetDateTime.toZonedDateTime(); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/codec/package-info.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Encoders and Decoders for the type that the service provider understands. */ @NonNullApi package io.r2dbc.mssql.codec; import reactor.util.annotation.NonNullApi; ================================================ FILE: src/main/java/io/r2dbc/mssql/message/ClientMessage.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.message.tds.TdsFragment; import org.reactivestreams.Publisher; /** * A message sent from a client to a server. * * @author Mark Paluch */ public interface ClientMessage extends Message { /** * Encode a message into a {@link TdsFragment data buffer}. Can encode either to a scalar {@link TdsFragment} or a {@link Publisher} of {@link TdsFragment}. * * @param allocator the {@link ByteBufAllocator} to use to get a {@link ByteBuf data buffer} to write into. * @param packetSize packet size hint. * @return a scalar {@link TdsFragment} or a {@link Publisher} that produces the {@link TdsFragment} containing the encoded message. */ Object encode(ByteBufAllocator allocator, int packetSize); } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/Message.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message; import io.r2dbc.mssql.message.token.TokenStream; /** * A TDS message. Messages declare typically encode or decode functions. * * @see ClientMessage * @see TokenStream */ public interface Message { } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/TDSVersion.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message; import io.r2dbc.mssql.util.Assert; /** * TDS protocol versions. * * @author Mark Paluch */ public enum TDSVersion { // TDS 7.4 VER_DENALI(0x74000004), // TDS 7.3B(includes null bit compression) VER_KATMAI(0x730B0003), // TDS 7.2 VER_YUKON(0x72090002), UNKNOWN(0x00000000); private final int version; TDSVersion(int version) { this.version = version; } public int getVersion() { return this.version; } /** * Check is the reference {@link TDSVersion} is greater or equal to {@code this} version. * * @param reference the reference version. * @return {@code true} if the reference {@link TDSVersion} is greater or equal to {@code this} version. */ public boolean isGreateOrEqualsTo(TDSVersion reference) { Assert.requireNonNull(reference, "Reference version must not be null"); return getVersion() >= reference.getVersion(); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/TransactionDescriptor.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message; import io.netty.buffer.ByteBufUtil; import io.r2dbc.mssql.util.Assert; import java.util.Arrays; /** * Descriptor for the transaction state. * * @author Mark Paluch */ public final class TransactionDescriptor { /** * Length in bytes of the binary transaction descriptor representation. */ public static final int LENGTH = 8; private final byte[] descriptor; private TransactionDescriptor(byte[] descriptor) { Assert.requireNonNull(descriptor, "Descriptor bytes must not be null"); Assert.isTrue(descriptor.length == LENGTH, "Descriptor must be 8 bytes long"); this.descriptor = descriptor; } /** * Creates an empty {@link TransactionDescriptor}. * * @return the empty {@link TransactionDescriptor}. */ public static TransactionDescriptor empty() { return new TransactionDescriptor(new byte[8]); } /** * Creates an {@link TransactionDescriptor} from {@code descriptor} bytes. * * @param descriptor descriptor bytes. Must be 8 bytes long. * @return the {@link TransactionDescriptor} for {@code descriptor} bytes. */ public static TransactionDescriptor from(byte[] descriptor) { return new TransactionDescriptor(descriptor); } /** * @return the binary representation of this {@link TransactionDescriptor}. */ public byte[] toBytes() { return this.descriptor; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof TransactionDescriptor)) { return false; } TransactionDescriptor that = (TransactionDescriptor) o; return Arrays.equals(this.descriptor, that.descriptor); } @Override public int hashCode() { return Arrays.hashCode(this.descriptor); } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [").append(ByteBufUtil.hexDump(this.descriptor)); sb.append(']'); return sb.toString(); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/header/DefaultHeaderOptions.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.header; /** * Default implementation of {@link HeaderOptions}. * * @author Mark Paluch */ class DefaultHeaderOptions implements HeaderOptions { private static final HeaderOptions[][] HEADER_CACHE = new HeaderOptions[Type.values().length][(-Byte.MIN_VALUE) + Byte.MAX_VALUE]; static { for (Type value : Type.values()) { for (byte b = Byte.MIN_VALUE; b < Byte.MAX_VALUE; b++) { int index = b - Byte.MIN_VALUE; HEADER_CACHE[value.ordinal()][index] = new DefaultHeaderOptions(value, Status.fromBitmask(b)); } } } static HeaderOptions get(Type value, Status status) { int index = status.getValue() - Byte.MIN_VALUE; return HEADER_CACHE[value.ordinal()][index]; } private final Type type; private final Status status; DefaultHeaderOptions(Type type, Status status) { this.type = type; this.status = status; } @Override public Type getType() { return this.type; } @Override public Status getStatus() { return this.status; } @Override public boolean is(Status.StatusBit bit) { return this.status.is(bit); } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [type=").append(this.type); sb.append(", status=").append(this.status); sb.append(']'); return sb.toString(); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/header/Header.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.header; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.util.Assert; import java.util.Objects; /** * A header exchanged between client and server. */ public class Header implements HeaderOptions { /** * Number of bytes required to represent the header. */ public static final int LENGTH = 8; /** * Type defines the type of message. 1-byte. */ private final Type type; /** * Status is a bit field used to indicate the message state. 1-byte. */ private final Status status; /** * Length is the size of the packet including the 8 bytes in the packet header. It is the number of bytes from the * start of this header to the start of the next packet header. Length is a 2-byte, unsigned short int and is * represented in network byte order (big-endian). *

    * Starting with TDS 7.3, the Length MUST be the negotiated packet size when sending a packet from client to server, * unless it is the last packet of a request (that is, the EOM bit in Status is ON), or the client has not logged in. */ private final short length; /** * Spid is the process ID on the server, corresponding to the current connection. *

    * This information is sent by the server to the client and is useful for identifying which thread on the server sent * the TDS packet. It is provided for debugging purposes. The client MAY send the SPID value to the server. If the * client does not, then a value of {@code 0x0000} SHOULD be sent to the server. This is a 2-byte value and is * represented in network byte order (big-endian). */ private final short spid; /** * PacketID is used for numbering message packets that contain data in addition to the packet header. *

    * PacketID is a 1-byte, unsigned char. Each time packet data is sent, the value of PacketID is incremented by 1, * modulo 256.<7> This allows the receiver to track the sequence of TDS packets for a given message. This value is * currently ignored. */ private final byte packetId; /** * This 1 byte is currently not used. This byte SHOULD be set to 0x00 and SHOULD be ignored by the receiver. */ private final byte window; public Header(Type type, Status status, int length, int spid) { this(type, status, (short) length, (short) spid, (byte) 0, (byte) 0); } public Header(Type type, Status status, int length, int spid, int packetId, int window) { this(type, status, (short) length, (short) spid, (byte) packetId, (byte) window); } public Header(Type type, Status status, short length, short spid, byte packetId, byte window) { Assert.requireNonNull(type, "Type must not be null"); Assert.requireNonNull(status, "sStatus must not be null"); Assert.isTrue(length >= 8, "Header length must be greater or equal to 8"); this.type = type; this.status = status; this.length = length; this.spid = spid; this.packetId = packetId; this.window = window; } /** * Create a {@link Header} given {@link HeaderOptions}, packet {@code length}, and {@link PacketIdProvider}. * * @param options the {@link HeaderOptions}. * @param length packet length. * @param packetIdProvider the {@link PacketIdProvider}. * @return the {@link Header}. * @throws IllegalArgumentException when {@link HeaderOptions} or {@link PacketIdProvider} is {@code null}. */ public static Header create(HeaderOptions options, int length, PacketIdProvider packetIdProvider) { Assert.requireNonNull(options, "HeaderOptions must not be null"); Assert.requireNonNull(packetIdProvider, "PacketIdProvider must not be null"); return new Header(options.getType(), options.getStatus(), length, 0, packetIdProvider.nextPacketId(), 0); } public Type getType() { return this.type; } public Status getStatus() { return this.status; } public boolean is(Status.StatusBit bit) { return this.status.is(bit); } public short getSpid() { return this.spid; } public byte getPacketId() { return this.packetId; } public byte getWindow() { return this.window; } public short getLength() { return this.length; } /** * Encode a header into a {@link ByteBuf}. * * @param buffer the target {@link ByteBuf}. */ public void encode(ByteBuf buffer) { encode(buffer, this.type, this.status, this.length, this.spid, this.packetId, this.window); } /** * Encode a header into a {@link ByteBuf}. * * @param buffer the target {@link ByteBuf}. * @param packetIdProvider must not be {@code null}. * @throws IllegalArgumentException when {@link HeaderOptions} or {@link PacketIdProvider} is {@code null}. */ public void encode(ByteBuf buffer, PacketIdProvider packetIdProvider) { encode(buffer, this.type, this.status, this.length, this.spid, packetIdProvider.nextPacketId(), this.window); } /** * Encode a header into a {@link ByteBuf}. * * @param buffer the target {@link ByteBuf}. * @param options header options. * @param length packet length. * @param packetIdProvider must not be {@code null}. * @throws IllegalArgumentException when {@link HeaderOptions} or {@link PacketIdProvider} is {@code null}. */ public static void encode(ByteBuf buffer, HeaderOptions options, int length, PacketIdProvider packetIdProvider) { encode(buffer, options.getType(), options.getStatus(), length, (short) 0, packetIdProvider.nextPacketId(), (byte) 0); } /** * Encode the {@link Header}. * * @param buffer the target {@link ByteBuf}. * @param type packet type. * @param status fragmentation/message status. * @param length packet length. * @param spid the spid (unused). * @param packetId the packet Id. * @param window the window (unused). */ public static void encode(ByteBuf buffer, Type type, Status status, int length, short spid, byte packetId, byte window) { buffer.ensureWritable(8); buffer.writeByte(type.getValue()); buffer.writeByte(status.getValue()); buffer.writeShort(length); buffer.writeShort(spid); buffer.writeByte(packetId); buffer.writeByte(window); } /** * @param buffer the data buffer to inspect. * @return {@code true} if the header can be decoded. */ public static boolean canDecode(ByteBuf buffer) { return buffer.readableBytes() >= LENGTH; } /** * @param buffer the data buffer. * @return the decoded {@link Header}. */ public static Header decode(ByteBuf buffer) { Type type = Type.valueOf(buffer.readByte()); Status status = Status.fromBitmask(buffer.readByte()); short length = buffer.readShort(); short spid = buffer.readShort(); byte packetId = buffer.readByte(); byte window = buffer.readByte(); return new Header(type, status, length, spid, packetId, window); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof Header)) { return false; } Header header = (Header) o; return this.length == header.length && this.spid == header.spid && this.packetId == header.packetId && this.window == header.window && this.type == header.type && Objects.equals(this.status, header.status); } @Override public int hashCode() { return Objects.hash(this.type, this.status, this.length, this.spid, this.packetId, this.window); } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [type=").append(this.type); sb.append(", status=").append(this.status); sb.append(", length=").append(this.length); sb.append(", spid=").append(this.spid); sb.append(", packetId=").append(this.packetId); sb.append(", window=").append(this.window); sb.append(']'); return sb.toString(); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/header/HeaderOptions.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.header; import io.r2dbc.mssql.util.Assert; /** * Base header options defining {@link Type} and {@link Status}. Typically used to provide a TDS packet context so * lower-level components can form a {@link Header} packet from this options and TDS payload. */ public interface HeaderOptions { /** * Create a {@link HeaderOptions} object with {@link Status.StatusBit} set. * * @param bit status bit to set. * @return the {@link HeaderOptions}. */ default HeaderOptions and(Status.StatusBit bit) { Status status = getStatus(); Status newStatus = status.and(bit); if (status == newStatus) { return this; } return DefaultHeaderOptions.get(getType(), newStatus); } /** * Create a {@link HeaderOptions} object with {@link Status.StatusBit} removed. * * @param bit status bit to remove. * @return the {@link HeaderOptions}. */ default HeaderOptions not(Status.StatusBit bit) { Status status = getStatus(); Status newStatus = status.not(bit); if (status == newStatus) { return this; } return DefaultHeaderOptions.get(getType(), newStatus); } /** * Defines the type of message. 1-byte. * * @return the message type. */ Type getType(); /** * Status is a bit field used to indicate the message state. 1-byte. * * @return the {@link Status.StatusBit}. */ Status getStatus(); /** * Check if the header status has set the {@link Status.StatusBit}. * * @param bit the status bit. * @return {@code true} of the bit is set; {@code false} otherwise. */ boolean is(Status.StatusBit bit); /** * Create {@link HeaderOptions} given {@link Type} and {@link Status}. * * @param type the header {@link Type}. * @param status the {@link Status}. * @return the {@link HeaderOptions}. */ static HeaderOptions create(Type type, Status status) { Assert.requireNonNull(type, "Type must not be null"); Assert.requireNonNull(status, "Status must not be null"); return new DefaultHeaderOptions(type, status); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/header/PacketIdProvider.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.header; import io.netty.buffer.ByteBuf; import java.util.concurrent.atomic.AtomicLong; /** * Provider for the PacketId {@link Header} field. * * @author Mark Paluch * @see Header#getPacketId() * @see Header#encode(ByteBuf, PacketIdProvider) */ @FunctionalInterface public interface PacketIdProvider { /** * @return the next packet id. */ byte nextPacketId(); /** * Static packet Id provider. * * @param value packet number. * @return {@link PacketIdProvider} returning the static {@code value}. */ static PacketIdProvider just(int value) { return just((byte) (value % 256)); } /** * Static packet Id provider. * * @param value packet number. * @return {@link PacketIdProvider} returning the static {@code value}. */ static PacketIdProvider just(byte value) { return () -> value; } /** * Atomic/concurrent packetId provider. * * @return a thread-safe packet counter. */ static PacketIdProvider atomic() { AtomicLong counter = new AtomicLong(); return () -> (byte) (counter.incrementAndGet() % 256); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/header/Status.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.header; import io.r2dbc.mssql.util.Assert; import java.util.Collection; import java.util.EnumSet; import java.util.Objects; import java.util.Set; /** * Packet header status as defined in ch {@literal 2.2.3.1.2 Status} of the TDS v20180912 spec. *

    * Status is a bit field used to indicate the message state. Status is a 1-byte unsigned char. The following Status bit * flags are defined. * * @author Mark Paluch * @see StatusBit */ public class Status { private static final Status[] STATUS_CACHE = new Status[(-Byte.MIN_VALUE) + Byte.MAX_VALUE]; static { for (byte b = Byte.MIN_VALUE; b < Byte.MAX_VALUE; b++) { STATUS_CACHE[b - Byte.MIN_VALUE] = fromBitmask0(b); } } private final byte value; private Status(Set statusBits) { this.value = getStatusValue(statusBits); } /** * Return an empty {@link Status}. * * @return the empty {@link Status}. */ public static Status empty() { return fromBitmask((byte) 0); } /** * Create {@link Status} {@link Set} from the given {@code bitmask}. * * @param bitmask status bitmask. * @return the {@link Status} for {@code bitmask}. */ public static Status fromBitmask(byte bitmask) { return STATUS_CACHE[bitmask - Byte.MIN_VALUE]; } private static Status fromBitmask0(byte bitmask) { EnumSet result = EnumSet.noneOf(StatusBit.class); for (StatusBit status : StatusBit.values()) { if ((bitmask & status.getBits()) != 0) { result.add(status); } } return new Status(result); } /** * Create a {@link Status} from the given {@link StatusBit}. * * @param bit the status bit. * @return the {@link Status} from the given {@link StatusBit}. */ public static Status of(StatusBit bit) { Assert.requireNonNull(bit, "StatusBit must not be null"); return fromBitmask(bit.bits); } /** * Create a {@link Status} from the given {@link StatusBit}s. * * @param bit the status bit. * @param other the status bits. * @return the {@link Status} from the given {@link StatusBit}. */ public static Status of(StatusBit bit, StatusBit... other) { Assert.requireNonNull(bit, "StatusBit must not be null"); Assert.requireNonNull(other, "StatusBits must not be null"); byte result = bit.bits; for (Status.StatusBit s : other) { result |= s.bits; } return fromBitmask(result); } /** * Create a {@link Status} from the current state and add the {@link StatusBit}. * * @param bit the status bit. * @return the {@link Status} from the given {@link StatusBit}. */ public Status and(StatusBit bit) { // If bit set, then we can optimize. if (is(bit)) { return this; } byte mask = (byte) (this.getValue() | bit.bits); return fromBitmask(mask); } /** * Create a {@link Status} from the current state and remove the {@link StatusBit}. * * @param bit the status bit. * @return the {@link Status} from the given {@link StatusBit}. */ public Status not(StatusBit bit) { // If bit not set, then we can optimize. if (!is(bit)) { return this; } byte mask = this.getValue(); mask &= ~bit.bits; return fromBitmask(mask); } /** * Check if the header status has set the {@link Status.StatusBit}. * * @param bit the status bit. * @return {@code true} of the bit is set; {@code false} otherwise. */ public boolean is(Status.StatusBit bit) { return (this.value & bit.bits) != 0; } /** * @return the status byte. */ public byte getValue() { return this.value; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof Status)) { return false; } Status status = (Status) o; return this.value == status.value; } @Override public int hashCode() { return Objects.hash(this.value); } private static byte getStatusValue(Collection statusBits) { byte result = 0; for (Status.StatusBit s : statusBits) { result |= s.getBits(); } return result; } @Override public String toString() { return Integer.toHexString(this.value); } /** * Packet header status bits as defined in ch {@literal 2.2.3.1.2 Status} of the TDS v20180912 spec. *

    * Status is a bit field used to indicate the message state. Status is a 1-byte unsigned char. The following Status * bit flags are defined. */ public enum StatusBit { NORMAL(0x00), EOM(0x01), IGNORE(0x02), /** * RESETCONNECTION * * @since TDS 7.1 */ RESET_CONNECTION(0x08), /** * RESETCONNECTIONSKIPTRAN * * @since TDS 7.3 */ RESET_CONNECTION_SKIP_TRAN(0x10); StatusBit(int bits) { this.bits = Integer.valueOf(bits).byteValue(); } private final byte bits; public int getBits() { return this.bits; } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/header/Type.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.header; /** * Packet header type as defined in ch {@literal 2.2.3.1.1 Type} of the TDS v20180912 spec. *

    * Type defines the type of message. Type is a 1-byte unsigned char. */ public enum Type { SQL_BATCH(1), PRE_TDS7_LOGIN(2), RPC(3), TABULAR_RESULT(4), ATTENTION(6), BULK_LOAD_DATA(7), FED_AUTH_TOKEN( 8), TX_MGR(14), TDS7_LOGIN(16), SSPI(17), PRE_LOGIN(18); Type(int value) { this.value = Integer.valueOf(value).byteValue(); } private final byte value; /** * Resolve header {@code value} into {@link Type}. * * @param value packet type identifier. * @return the resolved {@link Type}. */ public static Type valueOf(byte value) { switch (value) { case 1: return SQL_BATCH; case 2: return PRE_TDS7_LOGIN; case 3: return RPC; case 4: return TABULAR_RESULT; case 6: return ATTENTION; case 7: return BULK_LOAD_DATA; case 8: return FED_AUTH_TOKEN; case 14: return TX_MGR; case 16: return TDS7_LOGIN; case 17: return SSPI; case 18: return PRE_LOGIN; } throw new IllegalArgumentException(String.format("Invalid header type: 0x%01X", value)); } public byte getValue() { return this.value; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/package-info.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * The messages that are both sent from a client to a server and from a server to a client. */ @NonNullApi package io.r2dbc.mssql.message; import reactor.util.annotation.NonNullApi; ================================================ FILE: src/main/java/io/r2dbc/mssql/message/tds/ContextualTdsFragment.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.tds; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.header.HeaderOptions; import io.r2dbc.mssql.util.Assert; /** * Represents a TDS message with associated {@link HeaderOptions}. The encoder may split this packet into multiple * packets if the packet size exceeds the negotiated packet size. * * @author Mark Paluch */ public class ContextualTdsFragment extends TdsFragment { private final HeaderOptions headerOptions; /** * Creates a new {@link ContextualTdsFragment}. * * @param headerOptions header options. * @param byteBuf the buffer. */ public ContextualTdsFragment(HeaderOptions headerOptions, ByteBuf byteBuf) { super(byteBuf); this.headerOptions = Assert.requireNonNull(headerOptions, "HeaderOptions must not be null"); } /** * @return the header options. */ public HeaderOptions getHeaderOptions() { return this.headerOptions; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/tds/Decode.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.tds; import io.netty.buffer.ByteBuf; import reactor.util.annotation.Nullable; /** * TDS-specific decode methods. This utility provides decoding methods according to TDS types. * * @author Mark Paluch */ public final class Decode { private Decode() { } /** * Decode a byte. SQL server type {@code BYTE}. * * @param buffer the data buffer. * @return the decoded {@code BYTE}. */ public static byte asByte(ByteBuf buffer) { return buffer.readByte(); } /** * Decode an unsigned byte. SQL server type {@code BYTE} * * @param buffer the data buffer. * @return the decoded unsigned {@code BYTE}. */ public static int uByte(ByteBuf buffer) { return buffer.readUnsignedByte(); } /** * Decode a double word. SQL server type {@code DWORD}. * * @param buffer the data buffer. * @return the decoded {@code DWORD}. */ public static long dword(ByteBuf buffer) { return buffer.readUnsignedIntLE(); } /** * Decode byte number. SQL server type {@code BIT}. * * @param buffer the data buffer. * @return the decoded {@code BIT}. */ public static byte bit(ByteBuf buffer) { return asByte(buffer); } /** * Decode float number. SQL server type {@code REAL}. * * @param buffer the data buffer. * @return the decoded {@code REAL}. */ public static float asFloat(ByteBuf buffer) { return Float.intBitsToFloat(buffer.readIntLE()); } /** * Decode double number. SQL server type {@code FLOAT}. * * @param buffer the data buffer. * @return the decoded {@code FLOAT}. */ public static double asDouble(ByteBuf buffer) { return Double.longBitsToDouble(buffer.readLongLE()); } /** * Decode byte number. SQL server type {@code TINYINT}. * * @param buffer the data buffer. * @return the decoded {@code TINYINT}. */ public static byte tinyInt(ByteBuf buffer) { return asByte(buffer); } /** * Decode short number. SQL server type {@code SMALLINT}. * * @param buffer the data buffer. * @return the decoded {@code SMALLINT}. */ public static short smallInt(ByteBuf buffer) { return buffer.readShortLE(); } /** * Decode integer number. SQL server type {@code INT}. * * @param buffer the data buffer. * @return the decoded {@code INT}. */ public static int asInt(ByteBuf buffer) { return buffer.readIntLE(); } /** * Decode long number. SQL server type {@code BIGINT}. * * @param buffer the data buffer. * @return the decoded {@code BIGINT}. */ public static long bigint(ByteBuf buffer) { return buffer.readLongLE(); } /** * Decode long number. SQL server type {@code LONG}. * * @param buffer the data buffer. * @return the decoded {@code LONG}. */ public static int asLong(ByteBuf buffer) { return buffer.readIntLE(); } /** * Decode unsigned long number. SQL server type {@code LONGLONG}. * * @param buffer the data buffer. * @return the decoded {@code LONGLONG}. */ public static long uLongLong(ByteBuf buffer) { return buffer.readLongLE(); } /** * Decode a unsigned short. SQL server type {@code USHORT}. * * @param buffer the data buffer. * @return the decoded {@code USHORT}. */ public static int uShort(ByteBuf buffer) { return buffer.readUnsignedShortLE(); } /** * Peek onto the next {@link #uShort(ByteBuf)}. This method retains the {@link ByteBuf#readerIndex()} and returns the {@code USHORT} value if it is readable (i.e. if the buffer has at least two * readable bytes). Returns {@code null} if not readable. * * @param buffer the data buffer. * @return the peeked {@code USHORT} value or {@code null}. */ @Nullable public static Integer peekUShort(ByteBuf buffer) { if (buffer.readableBytes() >= 2) { buffer.markReaderIndex(); int peek = Decode.uShort(buffer); buffer.resetReaderIndex(); return peek; } return null; } /** * Read an integer with big endian encoding. Typically used to evaluate bit masks. * * @param buffer the data buffer. * @return the decoded integer as big endian. */ public static int intBigEndian(ByteBuf buffer) { return buffer.readInt(); } /** * Decode a unicode ({@code VARCHAR}) string from {@link ByteBuf} with {@code unsigned short} length. * * @param buffer the data buffer. * @return the decoded {@link String}. */ public static String unicodeUString(ByteBuf buffer) { int length = buffer.readUnsignedShortLE() * 2; return decodeUnicode(buffer, length); } /** * Decode a unicode ({@code VARCHAR}) string from {@link ByteBuf} with {@code byte} length. * * @param buffer the data buffer. * @return the decoded {@link String}. */ public static String unicodeBString(ByteBuf buffer) { int length = buffer.readByte() * 2; return decodeUnicode(buffer, length); } private static String decodeUnicode(ByteBuf buffer, int length) { String result = buffer.toString(buffer.readerIndex(), length, ServerCharset.UNICODE.charset()); buffer.skipBytes(length); return result; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/tds/Encode.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.tds; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import java.math.BigInteger; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; /** * Encode utilities for TDS. Encoding methods typically accept {@link ByteBuffer} and a value and write the encoded value to the given buffer. * * @author Mark Paluch */ public final class Encode { public static final int U_SHORT_MAX_VALUE = Math.abs(Short.MIN_VALUE) + Short.MAX_VALUE; private Encode() { } /** * Encode a byte. SQL server type {@code BYTE}. * * @param buffer the data buffer. * @param value the value to encode. */ public static void asByte(ByteBuf buffer, int value) { buffer.writeByte(value); } /** * Encode a byte. SQL server type {@code BYTE}. * * @param buffer the data buffer. * @param value the value to encode. */ public static void asByte(ByteBuf buffer, byte value) { buffer.writeByte(value); } /** * Encode a float. SQL server type {@code REAL}. * * @param buffer the data buffer. * @param value the value to encode. */ public static void asFloat(ByteBuf buffer, float value) { buffer.writeIntLE(Float.floatToIntBits(value)); } /** * Encode a double. SQL server type {@code FLOAT}. * * @param buffer the data buffer. * @param value the value to encode. */ public static void asDouble(ByteBuf buffer, double value) { buffer.writeLongLE(Double.doubleToLongBits(value)); } /** * Encode a double word. SQL server type {@code DWORD}. * * @param buffer the data buffer. * @param value the value to encode. */ public static void dword(ByteBuf buffer, int value) { buffer.writeIntLE(value); } /** * Encode long number. SQL server type {@code LONG}. * * @param buffer the data buffer. * @param value the value to encode. */ public static void asLong(ByteBuf buffer, int value) { buffer.writeIntLE(value); } /** * Encode an unscaled {@link BigInteger} value. SQL server type {@code SMALLMONEY}. * * @param buffer the data buffer. * @param value the value to encode. */ public static void smallMoney(ByteBuf buffer, BigInteger value) { buffer.writeIntLE(value.intValue()); } /** * Encode an unscaled {@link BigInteger} value. SQL server type {@code MONEY}. * * @param buffer the data buffer. * @param value the value to encode. */ public static void money(ByteBuf buffer, BigInteger value) { int intBitsHi = (int) (value.longValue() >> 32 & 0xFFFFFFFFL); int intBitsLo = (int) (value.longValue() & 0xFFFFFFFFL); buffer.writeIntLE(intBitsHi); buffer.writeIntLE(intBitsLo); } /** * Encode a bit. SQL server type {@code BIT}. * * @param buffer the data buffer. * @param value the value to encode. */ public static void bit(ByteBuf buffer, boolean value) { asByte(buffer, (byte) (value ? 1 : 0)); } /** * Encode byte number. SQL server type {@code TINYINT}. * * @param buffer the data buffer. * @param value the value to encode. */ public static void tinyInt(ByteBuf buffer, byte value) { asByte(buffer, value); } /** * Encode short number. SQL server type {@code SMALLINT}. * * @param buffer the data buffer. * @param value the value to encode. */ public static void smallInt(ByteBuf buffer, short value) { buffer.writeShortLE(value); } /** * Encode short number. SQL server type {@code SMALLINT}. * * @param buffer the data buffer. * @param value the value to encode. */ public static void smallInt(ByteBuf buffer, int value) { buffer.writeShortLE(value); } /** * Encode integer number. SQL server type {@code INT}. * * @param buffer the data buffer. * @param value the value to encode. */ public static void asInt(ByteBuf buffer, int value) { buffer.writeIntLE(value); } /** * Encode long number. SQL server type {@code BIGINT}. * * @param buffer the data buffer. * @param value the value to encode. */ public static void bigint(ByteBuf buffer, long value) { buffer.writeLongLE(value); } /** * Encode unsigned long number. SQL server type {@code LONGLONG}. * * @param buffer the data buffer. * @param value the value to encode. */ public static void uLongLong(ByteBuf buffer, long value) { buffer.writeLongLE(value); } /** * Encode a unsigned short. SQL server type {@code USHORT}. * * @param buffer the data buffer. * @param value the value to encode. */ public static void uShort(ByteBuf buffer, int value) { if (value > U_SHORT_MAX_VALUE) { throw new IllegalArgumentException("Value " + value + " exceeds uShort.MAX_VALUE"); } buffer.writeShortLE(value); } /** * Encode an integer with big endian encoding. Typically used to write bit masks. * * @param buffer the data buffer. * @param value the value to encode. */ public static void intBigEndian(ByteBuf buffer, int value) { buffer.writeInt(value); } /** * Encode a short big endian. * * @param buffer the data buffer. * @param value the value to encode. */ public static void shortBE(ByteBuf buffer, short value) { buffer.writeShort(value); } /** * Encode a short (ushort) big endian. * * @param buffer the data buffer. * @param value the value to encode. */ public static void uShortBE(ByteBuf buffer, int value) { if (value > U_SHORT_MAX_VALUE) { throw new IllegalArgumentException("Value " + value + " exceeds uShort.MAX_VALUE"); } buffer.writeShort(value); } /** * Encode a string. SQL server type {@code VARCHAR}/{@code NVARCHAR}. * * @param buffer the data buffer. * @param value the value to encode. * @param charset the charset to use. */ public static void uString(ByteBuf buffer, String value, Charset charset) { ByteBuf encoded = ByteBufUtil.encodeString(buffer.alloc(), CharBuffer.wrap(value), charset); uShort(buffer, encoded.readableBytes()); buffer.writeBytes(encoded); encoded.release(); } /** * Encode a {@link String} as unicode. SQL server type {@code UNICODESTREAM}. * * @param buffer the data buffer. * @param value the value to encode. */ public static void unicodeStream(ByteBuf buffer, String value) { ByteBuf encoded = ByteBufUtil.encodeString(buffer.alloc(), CharBuffer.wrap(value), ServerCharset.UNICODE.charset()); buffer.writeBytes(encoded); encoded.release(); } /** * Encode a {@link CharSequence} as RPC string using {@code UNICODE} encoding. * * @param buffer the data buffer. * @param value the value to encode. */ public static void rpcString(ByteBuf buffer, CharSequence value) { for (int i = 0; i < value.length(); i++) { char ch = value.charAt(i); buffer.writeByte((byte) (ch & 0xFF)); buffer.writeByte((byte) ((ch >> 8) & 0xFF)); } } /** * Encode a {@link CharSequence} as RPC string using {@link Charset} encoding. * * @param buffer the data buffer. * @param value the value to encode. * @param charset the encoding to use. */ public static void rpcString(ByteBuf buffer, CharSequence value, Charset charset) { buffer.writeCharSequence(value, charset); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/tds/FirstTdsFragment.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.tds; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.header.HeaderOptions; /** * First chunk of a TDS message. This message type signals the encoder to associate subsequent messages with the * attached {@link HeaderOptions}. * * @author Mark Paluch * @see ContextualTdsFragment * @see LastTdsFragment */ public final class FirstTdsFragment extends ContextualTdsFragment { /** * Creates a new {@link FirstTdsFragment}. * * @param headerOptions header options. * @param byteBuf the buffer. */ FirstTdsFragment(HeaderOptions headerOptions, ByteBuf byteBuf) { super(headerOptions, byteBuf); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/tds/LastTdsFragment.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.tds; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.header.HeaderOptions; /** * Last chunk of a TDS message. This message type signals the encoder send this fragment with the previously associated * {@link HeaderOptions} and clear these after sending this message. * * @author Mark Paluch * @see FirstTdsFragment */ public final class LastTdsFragment extends TdsFragment { /** * Creates a new {@link LastTdsFragment}. * * @param byteBuf the buffer. * @param headerOptions header options. */ LastTdsFragment(ByteBuf byteBuf) { super(byteBuf); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/tds/ProtocolException.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.tds; import io.r2dbc.spi.R2dbcNonTransientResourceException; import reactor.util.annotation.Nullable; /** * Exception indicating unsupported or invalid protocol states. This exception is thrown in cases where e.g. the clients * receives an invalid length, unexpected protocol frame or cannot decode a particular protocol frame. If a * {@link ProtocolException} is thrown, then the underlying transport connection is closed. * * @author Mark Paluch */ public final class ProtocolException extends R2dbcNonTransientResourceException { public static final int DRIVER_ERROR_NONE = 0; public static final int DRIVER_ERROR_FROM_DATABASE = 2; public static final int DRIVER_ERROR_IO_FAILED = 3; public static final int DRIVER_ERROR_INVALID_TDS = 4; public static final int DRIVER_ERROR_SSL_FAILED = 5; public static final int DRIVER_ERROR_UNSUPPORTED_CONFIG = 6; public static final int DRIVER_ERROR_INTERMITTENT_TLS_FAILED = 7; public static final int ERROR_SOCKET_TIMEOUT = 8; public static final int ERROR_QUERY_TIMEOUT = 9; /** * Creates a new exception. * * @param reason the reason for the error. Set as the exception's message and retrieved with {@link #getMessage()}. */ public ProtocolException(@Nullable String reason) { super(reason, null, DRIVER_ERROR_NONE); } /** * Creates a new exception. * * @param reason the reason for the error. Set as the exception's message and retrieved with {@link #getMessage()}. * @param driverErrorCode the driver error code. */ public ProtocolException(@Nullable String reason, int driverErrorCode) { super(reason, null, driverErrorCode); } /** * Creates a new exception. * * @param reason the reason for the error. Set as the exception's message and retrieved with {@link #getMessage()}. * @param cause the cause. */ public ProtocolException(@Nullable String reason, @Nullable Throwable cause) { super(reason, null, DRIVER_ERROR_NONE, cause); } /** * Creates a new exception. * * @param reason the reason for the error. Set as the exception's message and retrieved with {@link #getMessage()}. * @param cause the cause. * @param driverErrorCode the driver error code. */ public ProtocolException(@Nullable String reason, @Nullable Throwable cause, int driverErrorCode) { super(reason, null, driverErrorCode, cause); } /** * Create a new {@link ProtocolException} for invalid TDS. * * @param reason the reason for the error. Set as the exception's message and retrieved with {@link #getMessage()}. * @return the {@link ProtocolException}. */ public static ProtocolException invalidTds(String reason) { return new ProtocolException(reason, DRIVER_ERROR_INVALID_TDS); } /** * Create a new {@link ProtocolException} for an unsupported configuration. * * @param reason the reason for the error. Set as the exception's message and retrieved with {@link #getMessage()}. * @return the {@link ProtocolException}. */ public static ProtocolException unsupported(String reason) { return new ProtocolException(reason, DRIVER_ERROR_UNSUPPORTED_CONFIG); } /** * Create a new {@link ProtocolException} for an unsupported configuration. * * @param cause the cause. * @return the {@link ProtocolException}. */ public static ProtocolException unsupported(Throwable cause) { return new ProtocolException(null, cause, DRIVER_ERROR_UNSUPPORTED_CONFIG); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/tds/Redirect.java ================================================ package io.r2dbc.mssql.message.tds; import io.netty.buffer.ByteBuf; /** * Represents a client redirection to a different server. * * @author Lars Haatveit * @see io.r2dbc.mssql.message.token.EnvChangeToken.EnvChangeType#Routing * @since 0.8.2 */ public final class Redirect { private static final int PROTOCOL_TCP_IP = 0; private final String serverName; private final int port; /** * Get the alternate server name. * * @return the server name */ public String getServerName() { return this.serverName; } /** * Get the alternate port. * * @return the port */ public int getPort() { return this.port; } private Redirect(String serverName, int port) { this.serverName = serverName; this.port = port; } /** * Creates a new {@link Redirect} * * @param serverName the server name. * @param port the TCP port. * @return the {@link Redirect}. */ public static Redirect create(String serverName, int port) { return new Redirect(serverName, port); } /** * Decode a {@link Redirect} from {@link ByteBuf}. * * @param buffer the data buffer * @return the decoded {@link Redirect} */ public static Redirect decode(ByteBuf buffer) { int routingDataValueLength = buffer.readUnsignedShortLE(); if (routingDataValueLength <= 5) { throw new ProtocolException("Decoding error, buffer is too short"); } int protocol = buffer.readUnsignedByte(); if (protocol != PROTOCOL_TCP_IP) { throw new ProtocolException("Unknown route protocol"); } // The ProtocolProperty field represents the remote port when the protocol is TCP/IP. // https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/2b3eb7e5-d43d-4d1b-bf4d-76b9e3afc791 int port = buffer.readUnsignedShortLE(); String serverName = Decode.unicodeUString(buffer); return Redirect.create(serverName, port); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/tds/ServerCharset.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.tds; import java.nio.charset.Charset; /** * Enumeration of encodings that are supported by SQL Server (and hopefully the JVM). See, for example, * https://docs.oracle.com/javase/8/docs/technotes/guides/intl/encoding.doc.html * for a complete list of supported encodings with their canonical names. */ public enum ServerCharset { // @formatter:off UNICODE ("UTF-16LE", true, false), UTF8 ("UTF-8", true, false), CP437 ("Cp437", false, false), CP850 ("Cp850", false, false), CP874 ("MS874", true, true), CP932 ("MS932", true, false), CP936 ("MS936", true, false), CP949 ("MS949", true, false), CP950 ("MS950", true, false), CP1250 ("Cp1250", true, true), CP1251 ("Cp1251", true, true), CP1252 ("Cp1252", true, true), CP1253 ("Cp1253", true, true), CP1254 ("Cp1254", true, true), CP1255 ("Cp1255", true, true), CP1256 ("Cp1256", true, true), CP1257 ("Cp1257", true, true), CP1258 ("Cp1258", true, true); // @formatter:on private final String charsetName; private final boolean supportsAsciiConversion; private final boolean hasAsciiCompatibleSBCS; private boolean jvmSupportConfirmed; private Charset charset; ServerCharset(String charsetName, boolean supportsAsciiConversion, boolean hasAsciiCompatibleSBCS) { this.charsetName = charsetName; this.supportsAsciiConversion = supportsAsciiConversion; this.hasAsciiCompatibleSBCS = hasAsciiCompatibleSBCS; } /** * Lazily check codepage ({@link Charset}) support. * * @throws UnsupportedOperationException if the charset is not supported. */ private void checkSupported() { if (!this.jvmSupportConfirmed) { // Checks for support by converting a java.lang.String // This works for all of the code pages above in SE 5 and later. if (!Charset.isSupported(this.charsetName)) { throw new UnsupportedOperationException(String.format("Code page not supported: %s", this.charsetName)); } this.jvmSupportConfirmed = true; } } /** * Return the {@link Charset} for this encoding. * * @return the charset for this encoding. */ public Charset charset() { checkSupported(); if (this.charset == null) { this.charset = Charset.forName(this.charsetName); } return this.charset; } /** * @return {@code true} if the collation supports conversion to {@code true}. */ public boolean supportsAsciiConversion() { return this.supportsAsciiConversion; } /** * @return {@code true} if the collation supports conversion to {@literal ascii} AND it uses a single-byte character set. */ public boolean hasAsciiCompatibleSBCS() { return this.hasAsciiCompatibleSBCS; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/tds/TdsFragment.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.tds; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.util.Assert; /** * Fragment of a TDS message. Can be a partial or complete TDS message. Headers are supplied during encoding. * * @author Mark Paluch */ public class TdsFragment { private final ByteBuf byteBuf; /** * Create a TDS fragment. * * @param buffer the data buffer. */ TdsFragment(ByteBuf buffer) { this.byteBuf = Assert.requireNonNull(buffer, "Data buffer must not be null"); } /** * @return the data buffer. */ public ByteBuf getByteBuf() { return this.byteBuf; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/tds/TdsPacket.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.tds; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.message.header.Header; import io.r2dbc.mssql.message.header.PacketIdProvider; import io.r2dbc.mssql.util.Assert; /** * Self-contained TDS packet containing a {@link Header} and {@link ByteBuf data}. Self-contained TDS packets must * consist of a single message that does not exceed the negotiated packet size. * * @author Mark Paluch */ public final class TdsPacket extends TdsFragment { private final Header header; TdsPacket(Header header, ByteBuf buffer) { super(buffer); this.header = Assert.requireNonNull(header, "Header must not be null!"); int expectedBodySize = header.getLength() - Header.LENGTH; Assert.isTrue(buffer.readableBytes() == expectedBodySize, () -> String.format( "ByteBuffer body size does not match length field in header. Expected body size [%d], actual size [%d]", buffer.readableBytes(), expectedBodySize)); } /** * Encode this packet using by obtaining a buffer from {@link ByteBufAllocator}. * * @param allocator the allocator. * @return the encoded buffer. * @throws IllegalArgumentException when {@link ByteBufAllocator} is {@code null}. */ public ByteBuf encode(ByteBufAllocator allocator) { Assert.requireNonNull(allocator, "ByteBufAllocator must not be null"); ByteBuf buffer = allocator.buffer(this.header.getLength()); this.header.encode(buffer); buffer.writeBytes(getByteBuf()); getByteBuf().release(); return buffer; } /** * Encode this packet using by obtaining a buffer from {@link ByteBufAllocator} and calculate the packet Id from * {@link PacketIdProvider}. * * @param allocator the allocator. * @param packetIdProvider the {@link PacketIdProvider}. * @return the encoded buffer. * @throws IllegalArgumentException when {@link ByteBufAllocator} or {@link PacketIdProvider} is {@code null}. */ public ByteBuf encode(ByteBufAllocator allocator, PacketIdProvider packetIdProvider) { Assert.requireNonNull(allocator, "ByteBufAllocator must not be null"); Assert.requireNonNull(packetIdProvider, "PacketIdProvider must not be null"); ByteBuf buffer = allocator.buffer(this.header.getLength()); this.header.encode(buffer, packetIdProvider); buffer.writeBytes(getByteBuf()); getByteBuf().release(); return buffer; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/tds/TdsPackets.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.tds; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.header.Header; import io.r2dbc.mssql.message.header.HeaderOptions; /** * Factory class for {@link TdsFragment} instances. * * @author Mark Paluch */ public interface TdsPackets { /** * Create a contextual TDS fragment. * * @param options the {@link HeaderOptions} to apply for the final {@link Header} creation. * @param buffer the TDS message. * @return the {@link ContextualTdsFragment}. */ static ContextualTdsFragment create(HeaderOptions options, ByteBuf buffer) { return new ContextualTdsFragment(options, buffer); } /** * Create a TDS fragment. * * @param buffer the TDS message. * @return the {@link TdsFragment}. */ static TdsFragment create(ByteBuf buffer) { return new TdsFragment(buffer); } /** * Create a first contextual TDS fragment. * * @param options header options to form an actual {@link Header} during encoding. * @param buffer the TDS message. * @return the {@link ContextualTdsFragment}. */ static FirstTdsFragment first(HeaderOptions options, ByteBuf buffer) { return new FirstTdsFragment(options, buffer); } /** * Create a last contextual TDS fragment. * * @param buffer the TDS message. * @return the {@link ContextualTdsFragment}. */ static LastTdsFragment last(ByteBuf buffer) { return new LastTdsFragment(buffer); } /** * Create a self-contained TDS Packet. * * @param header the TDS header. * @param buffer the TDS message. * @return the {@link TdsPacket}. */ static TdsPacket create(Header header, ByteBuf buffer) { return new TdsPacket(header, buffer); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/tds/package-info.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * TDS protocol support, message envelopes and encoding/decoding helpers. */ @NonNullApi package io.r2dbc.mssql.message.tds; import reactor.util.annotation.NonNullApi; ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/AbstractDataToken.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; /** * Data token. A data token is typed by a single byte and provides a symbolic name. * * @author Mark Paluch */ public abstract class AbstractDataToken implements DataToken { private final byte type; /** * Creates a new {@link AbstractDataToken}. * * @param type token type. */ protected AbstractDataToken(byte type) { this.type = type; } /** * @return the token type. */ public byte getType() { return this.type; } /** * @return symbolic name of the {@link AbstractDataToken}. */ public abstract String getName(); } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/AbstractDoneToken.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.Message; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.util.Assert; import io.r2dbc.spi.Result; import java.util.Objects; /** * Abstract base class for Done token implementation classes. * * @author Mark Paluch */ public abstract class AbstractDoneToken extends AbstractDataToken implements Result.UpdateCount { /** * Packet length in bytes. */ public static final int LENGTH = 13; /** * This DONE is the final DONE in the request. */ static final int DONE_FINAL = 0x00; /** * This DONE message is not the final DONE message in the response. Subsequent data streams to follow. */ static final int DONE_MORE = 0x01; /** * An error occurred on the current SQL statement. A preceding ERROR token SHOULD be sent when this bit is set. */ static final int DONE_ERROR = 0x02; /** * A transaction is in progress. */ static final int DONE_INXACT = 0x04; /** * The DoneRowCount value is valid. This is used to distinguish between * a valid value of 0 for DoneRowCount or just an initialized variable. */ static final int DONE_COUNT = 0x10; /** * The DONE message is a server acknowledgement of a client ATTENTION message. */ static final int DONE_ATTN = 0x20; /** * This DONEPROC message is associated with an RPC within a set of batched RPCs. This flag is not set on the last RPC in the RPC batch. */ static final int DONE_RPCINBATCH = 0x80; /** * Used in place of DONE_ERROR when an error occurred on the current SQL statement, which is severe enough to require the result set, if any, to be discarded. */ static final int DONE_SRVERROR = 0x100; static final int CACHE_SIZE = 48; private final int status; /** * The token of the current SQL statement. The token value is provided and controlled by the application layer, which utilizes TDS. The TDS layer does not evaluate the value. */ private final int currentCommand; /** * The count of rows that were affected by the SQL statement. The value of DoneRowCount is valid if the value of Status includes {@link #DONE_COUNT}. */ private final long rowCount; /** * Creates a new {@link AbstractDoneToken}. * * @param type token type. * @param status status flags, see {@link AbstractDoneToken} constants. * @param currentCommand the current command counter. * @param rowCount number of columns if {@link #hasCount()} is set. */ protected AbstractDoneToken(byte type, int status, int currentCommand, long rowCount) { super(type); this.status = status; this.currentCommand = currentCommand; this.rowCount = rowCount; } /** * Check whether the {@link Message} represents a attention acknowledgement. * * @param message the message to inspect. * @return {@literal true} if the {@link Message} represents a attention acknowledgement. * @since 0.9 */ public static boolean isAttentionAck(Message message) { if (message instanceof AbstractDoneToken) { return ((AbstractDoneToken) message).isAttentionAck(); } return false; } /** * Check whether the {@link Message} represents a finished done token. * * @param message the message to inspect. * @return {@literal true} if the {@link Message} represents a finished done token. */ public static boolean isDone(Message message) { if (message instanceof AbstractDoneToken) { return ((AbstractDoneToken) message).isDone(); } return false; } /** * Check whether the the {@link Message} has a count. * * @param message the message to inspect. * @return {@literal true} if the {@link Message} has a count. */ public static boolean hasCount(Message message) { if (message instanceof AbstractDoneToken) { return ((AbstractDoneToken) message).hasCount(); } return false; } /** * Check whether the {@link ByteBuf} can be decoded into an entire {@link AbstractDoneToken}. * * @param buffer the data buffer. * @return {@code true} if the buffer contains sufficient data to entirely decode a {@link AbstractDoneToken}. * @throws IllegalArgumentException when {@link ByteBuf} is {@code null}. */ public static boolean canDecode(ByteBuf buffer) { Assert.requireNonNull(buffer, "Data buffer must not be null"); return buffer.readableBytes() >= LENGTH - 1 /* Decoding always decodes the token type first so no need to check the for the type byte */; } /** * Encode this token. * * @param buffer the data buffer. * @throws IllegalArgumentException when {@link ByteBuf} is {@code null}. */ public void encode(ByteBuf buffer) { Assert.requireNonNull(buffer, "Data buffer must not be null"); buffer.writeByte(getType()); Encode.uShort(buffer, getStatus()); Encode.uShort(buffer, getCurrentCommand()); Encode.uLongLong(buffer, getRowCount()); } public int getStatus() { return this.status; } /** * @return {@code true} if this token indicates the response is acknowledging the attention request. */ public boolean isAttentionAck() { return (getStatus() & DONE_ATTN) != 0; } /** * @return {@code true} if this token indicates the response is done and has no more rows. */ public boolean isDone() { return (getStatus() & DONE_MORE) == 0; } /** * @return {@code true} if this token indicates the response is done with a preceding error. */ public boolean isError() { return (getStatus() & DONE_ERROR) != 0; } /** * @return {@code true} if this token indicates the response is not done yet and the stream contains more data. */ public boolean hasMore() { return (getStatus() & DONE_MORE) != 0; } /** * @return {@code true} if this token contains a row count and {@link #getRowCount()} has a valid value. */ public boolean hasCount() { return (getStatus() & DONE_COUNT) != 0; } /** * @return the application-level command counter. */ public int getCurrentCommand() { return this.currentCommand; } /** * @return the row count. Only valid if {@link #hasCount()} is set. */ public long getRowCount() { return this.rowCount; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof AbstractDoneToken)) { return false; } AbstractDoneToken doneToken = (AbstractDoneToken) o; return this.status == doneToken.status && this.currentCommand == doneToken.currentCommand && this.rowCount == doneToken.rowCount; } @Override public int hashCode() { return Objects.hash(this.status, this.currentCommand, this.rowCount); } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [done=").append(isDone()); sb.append(", hasCount=").append(hasCount()); sb.append(", rowCount=").append(getRowCount()); sb.append(", hasMore=").append(hasMore()); sb.append(", attnAck=").append(isAttentionAck()); sb.append(", currentCommand=").append(getCurrentCommand()); sb.append(']'); return sb.toString(); } @Override public long value() { return hasCount() ? getRowCount() : 0; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/AbstractInfoToken.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.tds.Decode; import io.r2dbc.mssql.util.Assert; /** * Info token. * * @author Mark Paluch */ public abstract class AbstractInfoToken extends AbstractDataToken { /* * The total length of the INFO data stream, in bytes. */ private final long length; /** * Info number. */ private final long number; /** * The error state, used as a modifier to the info Number. */ private final byte state; /** * The class (severity) of the error. A class of less than 10 indicates an informational message. */ private final byte infoClass; /** * Classification constant. */ private final Classification classification; /** * The message text length and message text using US_VARCHAR format. */ private final String message; /** * The server name length and server name using B_VARCHAR format. */ private final String serverName; /** * The stored procedure name length and stored procedure name using B_VARCHAR format. */ private final String procName; /** * The line number in the SQL batch or stored procedure that caused the error. *

    * Line numbers begin at 1; therefore, if the line number is not applicable to the message as determined by the upper * layer, the value of LineNumber will be 0. */ private final long lineNumber; AbstractInfoToken(byte type, long length, long number, byte state, byte infoClass, String message, String serverName, String procName, long lineNumber) { super(type); this.length = length; this.number = number; this.state = state; this.infoClass = infoClass; this.classification = Classification.valueOf(this.infoClass); this.message = message; this.serverName = serverName; this.procName = procName; this.lineNumber = lineNumber; } /** * Check whether the {@link ByteBuf} can be decoded into an entire {@link AbstractInfoToken}. * * @param buffer the data buffer. * @return {@code true} if the buffer contains sufficient data to entirely decode a {@link AbstractInfoToken}. */ public static boolean canDecode(ByteBuf buffer) { Assert.requireNonNull(buffer, "Data buffer must not be null"); Integer requiredLength = Decode.peekUShort(buffer); return requiredLength != null && buffer.readableBytes() >= (requiredLength + /* length field */ 2); } public long getNumber() { return this.number; } public byte getState() { return this.state; } public byte getInfoClass() { return this.infoClass; } public Classification getClassification() { return this.classification; } public String getMessage() { return this.message; } public String getServerName() { return this.serverName; } public String getProcName() { return this.procName; } public long getLineNumber() { return this.lineNumber; } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [number=").append(this.number); sb.append(", state=").append(this.state); sb.append(", infoClass=").append(this.infoClass); sb.append(", message='").append(this.message).append('\"'); sb.append(", serverName='").append(this.serverName).append('\"'); sb.append(", procName='").append(this.procName).append('\"'); sb.append(", lineNumber=").append(this.lineNumber); sb.append(']'); return sb.toString(); } public enum Classification { /** * Informational messages that return status information or report errors that are not severe. */ INFORMATIONAL(0, 10), /** * The given object or entity does not exist. */ OBJECT_DOES_NOT_EXIST(11), /** * A special severity for SQL statements that do not use locking because of special options. In some cases, read operations performed by these SQL statements could result in inconsistent * data, because locks are not taken to guarantee consistency. */ INCONSISTENT_NO_LOCK(12), /** * Transaction deadlock errors. */ TX_DEADLOCK(13), /** * Security-related errors, such as permission denied. */ SECURITY(14), /** * Syntax errors in the SQL statement. */ SYNTAX_ERROR(15), /** * General errors that can be corrected by the user. */ GENERAL_ERROR(16), /** * The SQL statement caused the database server to run out of resources (such as memory, locks, or disk space for the database) or to exceed some limit set by the system administrator. */ OUT_OF_RESOURCES(17), /** * There is a problem in the Database Engine software, but the SQL statement completes execution, and the connection to the instance of the Database Engine is maintained. System * administrator action is required. */ DATABASE_ENGINE_FAILURE(18), /** * A non-configurable Database Engine limit has been exceeded and the current SQL batch has been terminated. Error messages with a severity level of 19 or higher stop the execution of the * current SQL batch. */ DATABASE_LIMIT(19), /** * Indicates that a SQL statement has encountered a problem. Because the problem has affected only the current task, it is unlikely that the database itself has been damaged. */ SYSTEM_SQL_PROBLEM(20), /** * Indicates that a problem has been encountered that affects all tasks in the current database, but it is unlikely that the database itself has been damaged. */ ALL_TASKS_PROBLEM(21), /** * Indicates that the table or index specified in the message has been damaged by a software or hardware problem. */ INDEX_PROBLEM(22), /** * Indicates that the integrity of the entire database is in question because of a hardware or software problem. */ DATABASE_INTEGRITY_PROBLEM(23), /** * Indicates a media failure. The system administrator might have to restore the database or resolve a hardware issue. */ MEDIA_ERROR(24), /** * Unknown classification. */ UNKNOWN(-1); final int from; final int to; Classification(int code) { this(code, code); } Classification(int from, int to) { this.from = from; this.to = to; } /** * Lookup classification by its class value. * * @param value the class value. * @return the matching {@link Classification} or {@link Classification#UNKNOWN} if it cannot be resolved. */ static Classification valueOf(int value) { for (Classification classification : Classification.values()) { if (value >= classification.from && value <= classification.to) { return classification; } } return Classification.UNKNOWN; } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/AllHeaders.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.TransactionDescriptor; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.util.Assert; import java.util.Arrays; import java.util.Objects; /** * All Headers data structure. * * @author Mark Paluch */ public final class AllHeaders { private final byte[] transactionDescriptor; private final int outstandingRequestCount; private final int length; private AllHeaders(byte[] transactionDescriptor, int outstandingRequestCount) { this.transactionDescriptor = transactionDescriptor; this.outstandingRequestCount = outstandingRequestCount; int totalLength = 4; // DWORD totalLength += transactionDescriptor.length + /* outstanding request count */ 4 + 4 /* Length field DWORD */ + 2 /* type */; this.length = totalLength; } /** * Creates {@link AllHeaders} containing only a {@link TransactionDescriptor transactional} descriptor. * * @param transactionDescriptor the binary transaction descriptor. * @param outstandingRequests number of outstanding requests * @return the {@link AllHeaders} for {@link TransactionDescriptor} and {@literal outstandingRequests}. * @throws IllegalArgumentException when {@link TransactionDescriptor} is {@code null}. */ public static AllHeaders transactional(TransactionDescriptor transactionDescriptor, int outstandingRequests) { Assert.requireNonNull(transactionDescriptor, "Transaction descriptor must not be null"); return transactional(transactionDescriptor.toBytes(), outstandingRequests); } /** * Creates {@link AllHeaders} containing only a {@link TransactionDescriptor transactional} descriptor. * * @param transactionDescriptor the binary transaction descriptor. * @param outstandingRequests number of outstanding requests * @return the {@link AllHeaders} for {@literal transactionDescriptor} and {@literal outstandingRequests}. * @throws IllegalArgumentException when {@link TransactionDescriptor} is {@code null}. */ public static AllHeaders transactional(byte[] transactionDescriptor, int outstandingRequests) { Assert.requireNonNull(transactionDescriptor, "Transaction descriptor must not be null"); return new AllHeaders(transactionDescriptor, outstandingRequests); } /** * Encode the header. * * @param buffer the data buffer. */ public void encode(ByteBuf buffer) { Encode.dword(buffer, this.length); Encode.dword(buffer, this.transactionDescriptor.length + /* outstanding request count */ 4 + 6); Encode.uShort(buffer, 0x02); buffer.writeBytes(this.transactionDescriptor); Encode.dword(buffer, this.outstandingRequestCount); } public int getLength() { return this.length; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof AllHeaders)) { return false; } AllHeaders that = (AllHeaders) o; return this.outstandingRequestCount == that.outstandingRequestCount && this.length == that.length && Arrays.equals(this.transactionDescriptor, that.transactionDescriptor); } @Override public int hashCode() { int result = Objects.hash(this.outstandingRequestCount, this.length); result = 31 * result + Arrays.hashCode(this.transactionDescriptor); return result; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/Attention.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; import io.r2dbc.mssql.message.ClientMessage; import io.r2dbc.mssql.message.TransactionDescriptor; import io.r2dbc.mssql.message.header.HeaderOptions; import io.r2dbc.mssql.message.header.Status; import io.r2dbc.mssql.message.header.Type; import io.r2dbc.mssql.message.tds.TdsFragment; import io.r2dbc.mssql.message.tds.TdsPackets; import io.r2dbc.mssql.util.Assert; import io.r2dbc.mssql.util.ReferenceCountUtil; import java.util.Objects; /** * Attention signal to cancel a running operation. * * @author Mark Paluch * @author Tomasz Marciniak * @since 0.9 */ public final class Attention implements ClientMessage, TokenStream { private static final HeaderOptions header = HeaderOptions.create(Type.ATTENTION, Status.empty()); private final AllHeaders allHeaders; /** * Creates a new {@link Attention} token. * * @param outstandingRequests the number of outstanding requests. * @param transactionDescriptor the transaction descriptor (8 byte). */ private Attention(int outstandingRequests, byte[] transactionDescriptor) { Assert.requireNonNull(transactionDescriptor, "Transaction descriptor must not be null"); this.allHeaders = AllHeaders.transactional(transactionDescriptor, outstandingRequests); } /** * Creates a new {@link Attention}. * * @param outstandingRequests the number of outstanding requests. * @param transactionDescriptor the transaction descriptor * @return the {@link Attention} token. */ public static Attention create(int outstandingRequests, TransactionDescriptor transactionDescriptor) { Assert.requireNonNull(transactionDescriptor, "Transaction descriptor must not be null"); return new Attention(outstandingRequests, transactionDescriptor.toBytes()); } @Override public TdsFragment encode(ByteBufAllocator allocator, int packetSize) { Assert.requireNonNull(allocator, "ByteBufAllocator must not be null"); int length = this.allHeaders.getLength(); ByteBuf buffer = allocator.buffer(length); encode(buffer); ReferenceCountUtil.maybeRelease(buffer); return TdsPackets.create(header, Unpooled.EMPTY_BUFFER); } void encode(ByteBuf buffer) { this.allHeaders.encode(buffer); } @Override public String getName() { return "ATTN"; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof Attention)) { return false; } Attention attn = (Attention) o; return Objects.equals(this.allHeaders, attn.allHeaders); } @Override public int hashCode() { return Objects.hash(this.allHeaders); } @Override public String toString() { return getName(); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/ColInfoToken.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.tds.Decode; import reactor.util.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * Column info token. Describes the column information in browse mode. * * @author Mark Paluch */ public class ColInfoToken extends AbstractDataToken { public static final byte TYPE = (byte) 0xA5; public static final int STATUS_EXPRESSION = 0x04; public static final int STATUS_KEY = 0x08; public static final int STATUS_HIDDEN = 0x10; public static final int STATUS_DIFFERENT_NAME = 0x20; private static final ColInfoToken EMPTY = new ColInfoToken(Collections.emptyList()); private final List columns; private ColInfoToken(List columns) { super(TYPE); this.columns = columns; } /** * Decode a {@link ColInfoToken}. * * @param buffer the data buffer. * @return the {@link ColInfoToken}. */ public static ColInfoToken skip(ByteBuf buffer) { int length = Decode.uShort(buffer); buffer.skipBytes(length); return EMPTY; } /** * Decode a {@link ColInfoToken}. * * @param buffer the data buffer. * @return the {@link ColInfoToken}. */ public static ColInfoToken decode(ByteBuf buffer) { int length = Decode.uShort(buffer); int readerIndex = buffer.readerIndex(); List columns = new ArrayList<>(); while (buffer.readerIndex() - readerIndex < length) { byte column = Decode.asByte(buffer); byte table = Decode.asByte(buffer); byte status = Decode.asByte(buffer); String columnName = (status & STATUS_DIFFERENT_NAME) != 0 ? Decode.unicodeBString(buffer) : null; columns.add(new ColInfo(column, table, status, columnName)); } return new ColInfoToken(columns); } /** * Check whether the {@link ByteBuf} can be decoded into an entire {@link ColInfoToken}. * * @param buffer the data buffer. * @return {@code true} if the buffer contains sufficient data to entirely decode a {@link ColInfoToken}. */ public static boolean canDecode(ByteBuf buffer) { if (buffer.readableBytes() >= 5) { Integer requiredLength = Decode.peekUShort(buffer); return requiredLength != null && buffer.readableBytes() >= (requiredLength + /* length field */ 2); } return false; } public List getColumns() { return this.columns; } @Override public String getName() { return "COLINFO"; } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [columns=").append(this.columns); sb.append(']'); return sb.toString(); } /** * Column info for a column returned by a sp_cursor operation. */ static final class ColInfo { /** * The column number in the result set. */ private final byte column; /** * The number of the base table that the column was derived from. The value is 0 if the value of Status is EXPRESSION. */ private final byte table; /** * 0x4: EXPRESSION (the column was the result of an expression). 0x8: KEY (the column is part of a key for the associated table). * 0x10: HIDDEN (the column was not requested, but was added because it was part of a key for the associated table). * 0x20: DIFFERENT_NAME (the column name is different than the requested column name in the case of a column alias). */ private final byte status; /** * The base column name. This only occurs if DIFFERENT_NAME is set in Status. */ @Nullable private final String name; private ColInfo(byte column, byte table, byte status, @Nullable String name) { this.column = column; this.table = table; this.status = status; this.name = name; } public byte getColumn() { return this.column; } public byte getTable() { return this.table; } public byte getStatus() { return this.status; } @Nullable public String getName() { return this.name; } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [column=").append(this.column); sb.append(", table=").append(this.table); sb.append(", status=").append(this.status); sb.append(", columnName=\"").append(this.name).append('\"'); sb.append(']'); return sb.toString(); } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/Column.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.r2dbc.mssql.codec.Decodable; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.Assert; import reactor.util.annotation.Nullable; import java.util.Objects; /** * A {@link Decodable} column within a result set. * * @author Mark Paluch */ public class Column implements Decodable { private final int index; private final String name; private final TypeInformation type; @Nullable private final Identifier table; /** * Creates a new {@link Column}. * * @param index the index (ordinal position) within the result set, zero-based. * @param name the column name. * @param type the associated type of this column. */ public Column(int index, String name, TypeInformation type) { this(index, name, type, null); } /** * Creates a new {@link Column}. * * @param index the index (ordinal position) within the result set, zero-based. * @param name the column name. * @param type the associated type of this column. * @param table the optional {@link Identifier table name}. */ public Column(int index, String name, TypeInformation type, @Nullable Identifier table) { this.index = index; this.name = Assert.requireNonNull(name, "Column name must not be null"); this.type = Assert.requireNonNull(type, "Type information must not be null"); this.table = table; } /** * Returns the column index. * * @return the column index. */ public int getIndex() { return this.index; } /** * Returns the column name. * * @return the column name. */ @Override public String getName() { return this.name; } /** * Returns the column {@link TypeInformation type}. * * @return the column {@link TypeInformation type}. */ @Override public TypeInformation getType() { return this.type; } /** * Returns the {@link Identifier table} name. * * @return the {@link Identifier table} name, can be {@code null}. */ @Nullable public Identifier getTable() { return this.table; } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [name='").append(this.name).append('\"'); sb.append(", type=").append(this.type); sb.append(", table=").append(this.table); sb.append(']'); return sb.toString(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof Column)) { return false; } Column column = (Column) o; return this.index == column.index && Objects.equals(this.name, column.name) && Objects.equals(this.table, column.table); } @Override public int hashCode() { return Objects.hash(this.index, this.name, this.table); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/ColumnMetadataToken.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.tds.Decode; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Column metadata token. * * @author Mark Paluch */ public final class ColumnMetadataToken extends AbstractDataToken { public static final byte TYPE = (byte) 0x81; public static final int NO_COLUMNS = 0xFFFF; private static final byte TYPE_SQLDATACLASSIFICATION = (byte) 0xa3; private static final ColumnMetadataToken EMPTY = new ColumnMetadataToken(new Column[0]); private final Column[] columns; private final Map namedColumns; /** * Creates a new {@link ColumnMetadataToken}. * * @param columns the columns. */ private ColumnMetadataToken(Column[] columns) { super(TYPE); this.columns = columns; if (columns.length == 1) { this.namedColumns = Collections.singletonMap(columns[0].getName(), columns[0]); } else { Map byName = new HashMap<>(this.columns.length, 1); for (Column column : columns) { Column old = byName.put(column.getName(), column); if (old != null) { byName.put(column.getName(), old); } } this.namedColumns = byName; } } /** * Creates a new {@link ColumnMetadataToken} given {@link List} of {@link Column}s. * * @param columns the columns. * @return the {@link ColumnMetadataToken}. */ public static ColumnMetadataToken create(Column[] columns) { return new ColumnMetadataToken(columns); } /** * Decode the {@link ColumnMetadataToken} response from a {@link ByteBuf}. * * @param buffer must not be null. * @param encryptionSupported whether encryption is supported. * @return the decoded {@link ColumnMetadataToken}. */ public static ColumnMetadataToken decode(ByteBuf buffer, boolean encryptionSupported) { int columnCount = Decode.uShort(buffer); // Handle the magic NoMetaData value if (columnCount == NO_COLUMNS) { return EMPTY; } if (encryptionSupported) { int tableSize = Decode.uShort(buffer); if (tableSize != 0) { throw new UnsupportedOperationException("Driver does not support encryption"); } } Column[] columns = new Column[columnCount]; for (int i = 0; i < columnCount; i++) { columns[i] = decodeColumn(buffer, encryptionSupported, i); decodeDataClassification(buffer); } return new ColumnMetadataToken(columns); } /** * Check whether the {@link ByteBuf} can be decoded into an entire {@link ColumnMetadataToken}. * * @param buffer the data buffer. * @param encryptionSupported whether encryption is supported. * @return {@code true} if the buffer contains sufficient data to entirely decode a {@link ColumnMetadataToken}. */ public static boolean canDecode(ByteBuf buffer, boolean encryptionSupported) { if (buffer.readableBytes() < 2) { return false; } int readerIndex = buffer.readerIndex(); try { int columnCount = Decode.uShort(buffer); // Handle the magic NoMetaData value if (columnCount == NO_COLUMNS) { return true; } if (encryptionSupported) { if (buffer.readableBytes() < 2) { return false; } buffer.skipBytes(2); } for (int i = 0; i < columnCount; i++) { if (!TypeInformation.canDecode(buffer, true)) { return false; } if (!canDecodeColumn(buffer, encryptionSupported)) { return false; } } } finally { buffer.readerIndex(readerIndex); } return true; } private static boolean canDecodeColumn(ByteBuf buffer, boolean encryptionSupported) { TypeInformation typeInfo = TypeInformation.decode(buffer, true); if (typeInfo.getServerType() == SqlServerType.TEXT || typeInfo.getServerType() == SqlServerType.NTEXT || typeInfo.getServerType() == SqlServerType.IMAGE) { // Yukon and later, table names are returned as multi-part SQL identifiers. if (!Identifier.canDecodeAndSkipBytes(buffer)) { return false; } } if (encryptionSupported && typeInfo.isEncrypted()) { throw new UnsupportedOperationException("Driver does not support encryption"); } if (!buffer.isReadable()) { return false; } int length = buffer.readByte() * 2; if (length > buffer.readableBytes()) { return false; } buffer.skipBytes(length); decodeDataClassification(buffer); return true; } private static void decodeDataClassification(ByteBuf buffer) { if (buffer.readableBytes() > 1) { buffer.markReaderIndex(); byte nextToken = Decode.asByte(buffer); buffer.resetReaderIndex(); if (nextToken == TYPE_SQLDATACLASSIFICATION) { throw new UnsupportedOperationException("Driver does not support SQL Data Classification"); } } } private static Column decodeColumn(ByteBuf buffer, boolean encryptionSupported, int columnIndex) { TypeInformation typeInfo = TypeInformation.decode(buffer, true); Identifier tableName = null; if (typeInfo.getServerType() == SqlServerType.TEXT || typeInfo.getServerType() == SqlServerType.NTEXT || typeInfo.getServerType() == SqlServerType.IMAGE) { // Yukon and later, table names are returned as multi-part SQL identifiers. tableName = Identifier.decode(buffer); } if (encryptionSupported && typeInfo.isEncrypted()) { throw new UnsupportedOperationException("Driver does not support encryption"); } String name = Decode.unicodeBString(buffer); return new Column(columnIndex, name, typeInfo, tableName); } public Column[] getColumns() { return this.columns; } public boolean hasColumns() { return this.columns.length != 0; } @Override public String getName() { return "COLMETADATA"; } public Map toMap() { return this.namedColumns; } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [columns=").append(Arrays.toString(this.columns)); sb.append(']'); return sb.toString(); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/DataToken.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.r2dbc.mssql.message.Message; /** * Data token. * * @author Mark Paluch */ public interface DataToken extends Message { /** * @return the token type. */ byte getType(); /** * @return symbolic name of the {@link DataToken}. */ String getName(); } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/DoneInProcToken.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.Message; import io.r2dbc.mssql.message.tds.Decode; /** * Done in Proc token. * * @author Mark Paluch */ public final class DoneInProcToken extends AbstractDoneToken { public static final byte TYPE = (byte) 0xFF; private static final DoneInProcToken[] INTERMEDIATE = new DoneInProcToken[CACHE_SIZE]; private static final DoneInProcToken[] MORE_WITH_COUNT_CACHE = new DoneInProcToken[CACHE_SIZE]; private static final DoneInProcToken[] DONE_WITH_COUNT_CACHE = new DoneInProcToken[CACHE_SIZE]; private static final DoneInProcToken[] MORE_CACHE = new DoneInProcToken[CACHE_SIZE]; private static final int DONE_WITH_COUNT = DONE_FINAL | DONE_COUNT; private static final int MORE_WITH_COUNT = DONE_MORE | DONE_COUNT; private static final int MORE = DONE_MORE; static { for (int i = 0; i < INTERMEDIATE.length; i++) { INTERMEDIATE[i] = new DoneInProcToken(0, 0, i); DONE_WITH_COUNT_CACHE[i] = new DoneInProcToken(DONE_WITH_COUNT, 0, i); MORE_WITH_COUNT_CACHE[i] = new DoneInProcToken(MORE_WITH_COUNT, 0, i); MORE_CACHE[i] = new DoneInProcToken(MORE, 0, i); } } /** * Creates a new {@link DoneInProcToken}. * * @param status status flags, see {@link AbstractDoneToken} constants. * @param currentCommand the current command counter. * @param rowCount number of columns if {@link #hasCount()} is set. */ private DoneInProcToken(int status, int currentCommand, long rowCount) { super(TYPE, status, currentCommand, rowCount); } /** * Creates a new {@link DoneInProcToken} indicating a final packet and {@code rowCount}. * * @param rowCount the row count. * @return the {@link DoneInProcToken}. */ public static DoneInProcToken create(long rowCount) { return create0(DONE_WITH_COUNT, 0, rowCount); } /** * Check whether the the {@link Message} represents a finished {@link DoneInProcToken}. * * @param message the message to inspect. * @return {@literal true} if the {@link Message} represents a finished {@link DoneInProcToken}. */ public static boolean isDone(Message message) { if (message instanceof DoneInProcToken) { return ((DoneInProcToken) message).isDone(); } return false; } /** * Decode the {@link DoneInProcToken} response from a {@link ByteBuf}. * * @param buffer must not be null. * @return the decoded {@link DoneInProcToken}. */ public static DoneInProcToken decode(ByteBuf buffer) { int status = Decode.uShort(buffer); int currentCommand = Decode.uShort(buffer); long rowCount = Decode.uLongLong(buffer); return create0(status, currentCommand, rowCount); } private static DoneInProcToken create0(int status, int currentCommand, long rowCount) { if (rowCount >= 0 && rowCount < CACHE_SIZE) { switch (status) { case 0: return INTERMEDIATE[(int) rowCount]; case DONE_WITH_COUNT: return DONE_WITH_COUNT_CACHE[(int) rowCount]; case MORE_WITH_COUNT: return MORE_WITH_COUNT_CACHE[(int) rowCount]; case MORE: return MORE_CACHE[(int) rowCount]; } } return new DoneInProcToken(status, currentCommand, rowCount); } @Override public String getName() { return "DONEINPROC"; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/DoneProcToken.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.tds.Decode; /** * Done Proc token. * * @author Mark Paluch */ public final class DoneProcToken extends AbstractDoneToken { public static final byte TYPE = (byte) 0xFE; private static final DoneProcToken[] INTERMEDIATE = new DoneProcToken[CACHE_SIZE]; private static final DoneProcToken[] MORE_WITH_COUNT_CACHE = new DoneProcToken[CACHE_SIZE]; private static final DoneProcToken[] DONE_WITH_COUNT_CACHE = new DoneProcToken[CACHE_SIZE]; private static final DoneProcToken[] MORE_CACHE = new DoneProcToken[CACHE_SIZE]; private static final int DONE_WITH_COUNT = DONE_FINAL | DONE_COUNT; private static final int MORE_WITH_COUNT = DONE_MORE | DONE_COUNT; private static final int MORE = DONE_MORE; static { for (int i = 0; i < INTERMEDIATE.length; i++) { INTERMEDIATE[i] = new DoneProcToken(0, 0, i); DONE_WITH_COUNT_CACHE[i] = new DoneProcToken(DONE_WITH_COUNT, 0, i); MORE_WITH_COUNT_CACHE[i] = new DoneProcToken(MORE_WITH_COUNT, 0, i); MORE_CACHE[i] = new DoneProcToken(MORE, 0, i); } } /** * Creates a new {@link DoneProcToken}. * * @param status status flags, see {@link AbstractDoneToken} constants. * @param currentCommand the current command counter. * @param rowCount number of columns if {@link #hasCount()} is set. */ private DoneProcToken(int status, int currentCommand, long rowCount) { super(TYPE, status, currentCommand, rowCount); } /** * Creates a new {@link DoneProcToken} indicating a final packet and {@code rowCount}. * * @param rowCount the row count. * @return the {@link DoneProcToken}. */ public static DoneProcToken create(long rowCount) { return create0(DONE_FINAL | DONE_COUNT, 0, rowCount); } /** * Decode the {@link DoneProcToken} response from a {@link ByteBuf}. * * @param buffer must not be null. * @return the decoded {@link DoneProcToken}. */ public static DoneProcToken decode(ByteBuf buffer) { int status = Decode.uShort(buffer); int currentCommand = Decode.uShort(buffer); long rowCount = Decode.uLongLong(buffer); return create0(status, currentCommand, rowCount); } private static DoneProcToken create0(int status, int currentCommand, long rowCount) { if (rowCount >= 0 && rowCount < CACHE_SIZE) { switch (status) { case 0: return INTERMEDIATE[(int) rowCount]; case DONE_WITH_COUNT: return DONE_WITH_COUNT_CACHE[(int) rowCount]; case MORE_WITH_COUNT: return MORE_WITH_COUNT_CACHE[(int) rowCount]; case MORE: return MORE_CACHE[(int) rowCount]; } } return new DoneProcToken(status, currentCommand, rowCount); } @Override public String getName() { return "DONEPROC"; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/DoneToken.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.Message; import io.r2dbc.mssql.message.tds.Decode; /** * Done token. * * @author Mark Paluch */ public final class DoneToken extends AbstractDoneToken { public static final byte TYPE = (byte) 0xFD; private static final DoneToken[] INTERMEDIATE = new DoneToken[CACHE_SIZE]; private static final DoneToken[] MORE_WITH_COUNT_CACHE = new DoneToken[CACHE_SIZE]; private static final DoneToken[] DONE_WITH_COUNT_CACHE = new DoneToken[CACHE_SIZE]; private static final DoneToken[] MORE_CACHE = new DoneToken[CACHE_SIZE]; private static final int DONE_WITH_COUNT = DONE_FINAL | DONE_COUNT; private static final int MORE_WITH_COUNT = DONE_MORE | DONE_COUNT; private static final int MORE = DONE_MORE; static { for (int i = 0; i < INTERMEDIATE.length; i++) { INTERMEDIATE[i] = new DoneToken(0, 0, i); DONE_WITH_COUNT_CACHE[i] = new DoneToken(DONE_WITH_COUNT, 0, i); MORE_WITH_COUNT_CACHE[i] = new DoneToken(MORE_WITH_COUNT, 0, i); MORE_CACHE[i] = new DoneToken(MORE, 0, i); } } /** * Creates a new {@link DoneToken}. * * @param status status flags, see {@link AbstractDoneToken} constants. * @param currentCommand the current command counter. * @param rowCount number of columns if {@link #hasCount()} is set. */ private DoneToken(int status, int currentCommand, long rowCount) { super(TYPE, status, currentCommand, rowCount); } /** * Creates a new {@link DoneToken} indicating a final packet and {@code rowCount}. * * @param rowCount the row count. * @return the {@link DoneToken}. * @see #isDone() * @see #getRowCount() * @see #hasCount() */ public static DoneToken create(long rowCount) { return create0(DONE_WITH_COUNT, 0, rowCount); } /** * Creates a new {@link DoneToken} with just a {@code rowCount}. * * @param rowCount the row count. * @return the {@link DoneToken}. * @see #getRowCount() * @see #hasCount() */ public static DoneToken count(long rowCount) { return create0(DONE_COUNT, 0, rowCount); } /** * Creates a new {@link DoneToken} with just a {@code rowCount}. * * @param rowCount the row count. * @return the {@link DoneToken}. * @see #getRowCount() * @see #hasCount() */ public static DoneToken more(long rowCount) { return create0(DONE_MORE | DONE_COUNT, 0, rowCount); } /** * Check whether the the {@link Message} represents a finished {@link DoneToken}. * * @param message the message to inspect. * @return {@literal true} if the {@link Message} represents a finished {@link DoneToken}. */ public static boolean isDone(Message message) { if (message instanceof DoneToken) { return ((AbstractDoneToken) message).isDone(); } return false; } /** * Decode the {@link DoneToken} response from a {@link ByteBuf}. * * @param buffer must not be null. * @return the decoded {@link DoneToken}. */ public static DoneToken decode(ByteBuf buffer) { int status = Decode.uShort(buffer); int currentCommand = Decode.uShort(buffer); long rowCount = Decode.uLongLong(buffer); return create0(status, currentCommand, rowCount); } private static DoneToken create0(int status, int currentCommand, long rowCount) { if (rowCount >= 0 && rowCount < CACHE_SIZE) { switch (status) { case 0: return INTERMEDIATE[(int) rowCount]; case DONE_WITH_COUNT: return DONE_WITH_COUNT_CACHE[(int) rowCount]; case MORE_WITH_COUNT: return MORE_WITH_COUNT_CACHE[(int) rowCount]; case MORE: return MORE_CACHE[(int) rowCount]; } } return new DoneToken(status, currentCommand, rowCount); } @Override public String getName() { return "DONE"; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/EnvChangeToken.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.tds.Decode; import io.r2dbc.mssql.message.tds.ServerCharset; import io.r2dbc.mssql.util.Assert; import reactor.util.annotation.Nullable; /** * A notification of an environment change (for example, database, language, and so on). * * @author Mark Paluch * @author Lars Haatveit * @see EnvChangeType */ public final class EnvChangeToken extends AbstractDataToken { public static final byte TYPE = (byte) 0xE3; /** * The total length of the ENVCHANGE data stream (EnvValueData). */ private final int length; /** * The type of environment change. */ private final EnvChangeType changeType; private final byte[] newValue; private final byte[] oldValue; public EnvChangeToken(int length, EnvChangeType changeType, byte[] newValue, @Nullable byte[] oldValue) { super(TYPE); Assert.requireNonNull(changeType, "EnvChangeType must not be null"); Assert.requireNonNull(newValue, "New value must not be null"); this.length = length; this.changeType = changeType; this.newValue = newValue; this.oldValue = oldValue; } /** * Decode a {@link EnvChangeToken}. * * @param buffer the data buffer. * @return the decoded {@link EnvChangeToken} */ public static EnvChangeToken decode(ByteBuf buffer) { int length = Decode.uShort(buffer); byte type = Decode.asByte(buffer); EnvChangeType envChangeType = EnvChangeType.valueOf(type); byte[] newValue; byte[] oldValue; // The routing message contains structured data, while the other environment change tokens contain old/new value pairs prefixed with data length. if (envChangeType == EnvChangeType.Routing) { newValue = new byte[length - 1]; buffer.readBytes(newValue); oldValue = null; } else { int newValueLen = envChangeType.toByteLength(Decode.asByte(buffer)); newValue = new byte[newValueLen]; buffer.readBytes(newValue); int oldValueLen = envChangeType.toByteLength(Decode.asByte(buffer)); oldValue = new byte[oldValueLen]; buffer.readBytes(oldValue); } return new EnvChangeToken(length, envChangeType, newValue, oldValue); } /** * Check whether the {@link ByteBuf} can be decoded into an entire {@link EnvChangeType}. * * @param buffer the data buffer. * @return {@code true} if the buffer contains sufficient data to entirely decode a {@link EnvChangeType}. */ public static boolean canDecode(ByteBuf buffer) { Assert.requireNonNull(buffer, "Data buffer must not be null"); Integer requiredLength = Decode.peekUShort(buffer); return requiredLength != null && buffer.readableBytes() >= (requiredLength + /* length field */ 2); } @Override public String getName() { return "ENVCHANGE_TOKEN"; } public int getLength() { return this.length; } public EnvChangeType getChangeType() { return this.changeType; } public byte[] getNewValue() { return this.newValue; } @Nullable public byte[] getOldValue() { return this.oldValue; } public String getOldValueString() { return new String(this.oldValue, 0, this.oldValue.length, ServerCharset.UNICODE.charset()); } public String getNewValueString() { return new String(this.newValue, 0, this.newValue.length, ServerCharset.UNICODE.charset()); } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getName()); sb.append(" [length=").append(this.length); sb.append(", changeType=").append(this.changeType); sb.append(", newValue=").append(getNewValueString()); sb.append(']'); return sb.toString(); } /** * Environment change payload type (type of environment change). */ public enum EnvChangeType { Database(1) { @Override public int toByteLength(byte dataLength) { return super.toByteLength(dataLength) * 2; } }, Language(2) { @Override public int toByteLength(byte dataLength) { return super.toByteLength(dataLength) * 2; } }, Charset(3) { @Override public int toByteLength(byte dataLength) { return super.toByteLength(dataLength) * 2; } }, Packetsize(4) { @Override public int toByteLength(byte dataLength) { return super.toByteLength(dataLength) * 2; } }, UnicodeLCID(5) { @Override public int toByteLength(byte dataLength) { return super.toByteLength(dataLength) * 2; } }, UnicodeSortingComparison(6) { @Override public int toByteLength(byte dataLength) { return super.toByteLength(dataLength) * 2; } }, SQLCollation(7), BeginTx(8), CommitTx(9), RollbackTx(10), EnlistDTC(11), DefectTx(12), RealtimeLogShipping(13) { @Override public int toByteLength(byte dataLength) { return super.toByteLength(dataLength) * 2; } }, PromoteTx(15), TXMgrAddress(16), TxEnd(17), RSETACK(18), UserInstance(19) { @Override public int toByteLength(byte dataLength) { return super.toByteLength(dataLength) * 2; } }, // Routing is used for redirections to a different server Routing(20); private final byte type; EnvChangeType(int type) { this.type = (byte) type; } public byte getType() { return this.type; } /** * Lookup {@link EnvChangeType} by its by {@code value}. * * @param value the env change type byte value. * @return the resolved {@link EnvChangeType}. * @throws IllegalArgumentException if the {@code value} cannot be resolved to a {@link EnvChangeType}. */ public static EnvChangeType valueOf(int value) { for (EnvChangeType envChangeType : values()) { if (envChangeType.getType() == (byte) value) { return envChangeType; } } throw new IllegalArgumentException(String.format("Invalid env change type 0x%01X", value)); } public int toByteLength(byte dataLength) { return dataLength; } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/ErrorToken.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.tds.Decode; /** * Error token. * * @author Mark Paluch */ public final class ErrorToken extends AbstractInfoToken { public static final byte TYPE = (byte) 0xAA; public ErrorToken(long length, long number, int state, int infoClass, String message, String serverName, String procName, long lineNumber) { super(TYPE, length, number, (byte) state, (byte) infoClass, message, serverName, procName, lineNumber); } public ErrorToken(long length, long number, byte state, byte infoClass, String message, String serverName, String procName, long lineNumber) { super(TYPE, length, number, state, infoClass, message, serverName, procName, lineNumber); } /** * Decode the {@link ErrorToken}. * * @param buffer the data buffer. * @return the decoded {@link ErrorToken} */ public static ErrorToken decode(ByteBuf buffer) { int length = Decode.uShort(buffer); long number = Decode.asLong(buffer); byte state = Decode.asByte(buffer); byte infoClass = Decode.asByte(buffer); String msgText = Decode.unicodeUString(buffer); String serverName = Decode.unicodeBString(buffer); String procName = Decode.unicodeBString(buffer); long lineNumber = buffer.readUnsignedInt(); return new ErrorToken(length, number, state, infoClass, msgText, serverName, procName, lineNumber); } @Override public String getName() { return "ERROR"; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/FeatureExtAckToken.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.tds.Decode; import io.r2dbc.mssql.message.tds.ProtocolException; import io.r2dbc.mssql.util.Assert; import java.util.ArrayList; import java.util.List; /** * Feature Extension Acknowledgement token. * * @author Mark Paluch */ public final class FeatureExtAckToken extends AbstractDataToken { public static final byte TYPE = (byte) 0xAE; public static final byte TERMINATOR = (byte) 0xFF; private final List featureTokens; private FeatureExtAckToken(List featureTokens) { super(TYPE); this.featureTokens = featureTokens; } /** * Decode the {@link FeatureExtAckToken}. * * @param buffer the data buffer. * @return the decoded {@link FeatureExtAckToken}. */ public static FeatureExtAckToken decode(ByteBuf buffer) { Assert.requireNonNull(buffer, "Buffer must not be null"); List featureTokens = new ArrayList<>(); while (true) { byte featureId = buffer.readByte(); if (featureId == TERMINATOR) { break; } if (featureId == ColumnEncryption.FEATURE_ID) { featureTokens.add(ColumnEncryption.decode(buffer)); continue; } featureTokens.add(UnknownFeature.decode(featureId, buffer)); } return new FeatureExtAckToken(featureTokens); } public List getFeatureTokens() { return this.featureTokens; } @Override public String getName() { return "FEATUREEXTACK"; } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [featureTokens=").append(this.featureTokens); sb.append(']'); return sb.toString(); } /** * Acknowledged feature token. */ public abstract static class FeatureToken { /** * The unique identifier number of a feature. */ private final byte featureId; /** * The length of FeatureAckData, in bytes. */ private final long length; public FeatureToken(byte featureId, long length) { this.featureId = featureId; this.length = length; } } /** * Column encryption. */ public final static class ColumnEncryption extends FeatureToken { public static final byte FEATURE_ID = 0x04; /** * Supported table column encryption version. */ private final byte tceVersion; public ColumnEncryption(long length, byte tceVersion) { super(FEATURE_ID, length); this.tceVersion = tceVersion; } /** * Decode an unknown feature. * * @param buffer the data buffer. * @return the decoded {@link ColumnEncryption}. */ public static ColumnEncryption decode(ByteBuf buffer) { Assert.requireNonNull(buffer, "Buffer must not be null"); long length = Decode.dword(buffer); if (length != 1) { throw ProtocolException.unsupported("Unknown version number for AE"); } byte tceVersion = buffer.readByte(); if (tceVersion != 1) { throw ProtocolException.unsupported("Unsupported version number for AE"); } return new ColumnEncryption(length, tceVersion); } public byte getTceVersion() { return this.tceVersion; } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [tceVersion=").append(this.tceVersion); sb.append(']'); return sb.toString(); } } /** * Placeholder for unknown features. */ public final static class UnknownFeature extends FeatureToken { private final byte[] data; public UnknownFeature(byte featureId, long length, byte[] data) { super(featureId, length); this.data = data; } /** * Decode an unknown feature. * * @param featureId the passed-through feature Id. * @param buffer the data buffer. * @return the decoded {@link UnknownFeature}. */ public static UnknownFeature decode(byte featureId, ByteBuf buffer) { Assert.requireNonNull(buffer, "Buffer must not be null"); long length = Decode.dword(buffer); byte[] bytes = new byte[Math.toIntExact(length)]; buffer.readBytes(bytes); return new UnknownFeature(featureId, length, bytes); } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/Identifier.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.tds.Decode; import io.r2dbc.mssql.message.tds.ProtocolException; import io.r2dbc.mssql.util.Assert; import io.r2dbc.mssql.util.StringUtils; import reactor.util.annotation.Nullable; import java.util.Objects; /** * Identifier for an object, typically a type or a table name. * * @author Mark Paluch */ public final class Identifier { @Nullable private final String serverName; @Nullable private final String databaseName; @Nullable private final String schemaName; private final String objectName; private Identifier(@Nullable String serverName, @Nullable String databaseName, @Nullable String schemaName, String objectName) { this.serverName = serverName; this.databaseName = databaseName; this.schemaName = schemaName; this.objectName = Assert.requireNonNull(objectName, "Object name must not be null"); } /** * Create a new {@link Identifier} given {@code objectName}. * * @param objectName the object name. * @return the {@link Identifier} for {@code objectName}. * @throws IllegalArgumentException when {@code objectName} is {@code null}. */ public static Identifier objectName(String objectName) { return new Identifier(null, null, null, objectName); } /** * Creates a new {@link Builder} to build {@link Identifier}. * * @return a new {@link Builder}. */ public static Builder builder() { return new Builder(); } /** * Decode the identifier. * * @param buffer the data buffer. * @return the decoded {@link Identifier}. * @throws IllegalArgumentException when {@link ByteBuf} is {@code null}. */ public static Identifier decode(ByteBuf buffer) { Assert.requireNonNull(buffer, "Buffer must not be null"); // Multi-part names should have between 1 and 4 parts int parts = Decode.uByte(buffer); if (!(1 <= parts && parts <= 4)) { throw ProtocolException.invalidTds(String.format("Identifier must contain one to four parts, got: %d", parts)); } // Each part is a length-prefixed Unicode string String[] nameParts = new String[parts]; for (int i = 0; i < parts; i++) { nameParts[i] = Decode.unicodeUString(buffer); } String serverName = null; String databaseName = null; String schemaName = null; String objectName = nameParts[parts - 1]; if (parts >= 2) { schemaName = nameParts[parts - 2]; } if (parts >= 3) { databaseName = nameParts[parts - 3]; } if (parts == 4) { serverName = nameParts[parts - 4]; } return new Identifier(serverName, databaseName, schemaName, objectName); } /** * Check whether the {@link ByteBuf} can be decoded into an entire {@link ColumnMetadataToken}. Advances the {@link ByteBuf#readerIndex()}. * * @param buffer the data buffer. * @return {@literal true} if the {@link Identifier} can be decoded. * @throws IllegalArgumentException when {@link ByteBuf} is {@code null}. */ static boolean canDecodeAndSkipBytes(ByteBuf buffer) { Assert.requireNonNull(buffer, "Buffer must not be null"); if (!buffer.isReadable()) { return false; } // Multi-part names should have between 1 and 4 parts int parts = Decode.uByte(buffer); // Each part is a length-prefixed Unicode string String[] nameParts = new String[parts]; for (int i = 0; i < parts; i++) { if (1 > buffer.readableBytes()) { return false; } int length = buffer.readUnsignedShortLE() * 2; if (length > buffer.readableBytes()) { return false; } buffer.skipBytes(length); } return true; } @Nullable public String getServerName() { return this.serverName; } @Nullable public String getDatabaseName() { return this.databaseName; } @Nullable public String getSchemaName() { return this.schemaName; } public String getObjectName() { return this.objectName; } public String asEscapedString() { StringBuilder fullName = new StringBuilder(256); if (StringUtils.hasText(this.serverName)) { fullName.append("[" + this.serverName + "]."); } if (StringUtils.hasText(this.databaseName)) { fullName.append("[" + this.databaseName + "]."); } else { Assert.state(StringUtils.isEmpty(this.serverName), "Server name must be empty"); } if (StringUtils.hasText(this.schemaName)) { fullName.append("[" + this.schemaName + "]."); } else if (StringUtils.hasText(this.databaseName)) { fullName.append('.'); } fullName.append("[" + this.objectName + "]"); return fullName.toString(); } @Override public String toString() { return asEscapedString(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof Identifier)) { return false; } Identifier that = (Identifier) o; return Objects.equals(this.serverName, that.serverName) && Objects.equals(this.databaseName, that.databaseName) && Objects.equals(this.schemaName, that.schemaName) && Objects.equals(this.objectName, that.objectName); } @Override public int hashCode() { return Objects.hash(this.serverName, this.databaseName, this.schemaName, this.objectName); } /** * Builder for {@link Identifier}. */ public static class Builder { @Nullable private String serverName; @Nullable private String databaseName; @Nullable private String schemaName; @Nullable private String objectName; private Builder() { } /** * Configure an {@code objectName}. * * @param objectName the object name, must not be {@code null}. * @return {@code this} {@link Builder}. * @throws IllegalArgumentException when {@code objectName} is {@code null}. */ public Builder objectName(String objectName) { this.objectName = Assert.requireNonNull(objectName, "Object name must not be null"); return this; } /** * Configure a {@code schemaName}. * * @param schemaName the schema name, must not be {@code null}. * @return {@code this} {@link Builder}. * @throws IllegalArgumentException when {@code schemaName} is {@code null}. */ public Builder schemaName(String schemaName) { this.schemaName = Assert.requireNonNull(schemaName, "Schema name must not be null"); return this; } /** * Configure a {@code databaseName}. * * @param databaseName the database name, must not be {@code null}. * @return {@code this} {@link Builder}. * @throws IllegalArgumentException when {@link ByteBuf} is {@code null}. */ public Builder databaseName(String databaseName) { this.databaseName = Assert.requireNonNull(databaseName, "Database name must not be null"); return this; } /** * Configure a {@code serverName}. Requires the {@link #databaseName(String)} be set if the server name is not empty. * * @param serverName the server name, must not be {@code null}. * @return {@code this} {@link Builder}. * @throws IllegalArgumentException when {@code serverName} is {@code null}. */ public Builder serverName(String serverName) { this.serverName = Assert.requireNonNull(serverName, "Server name must not be null"); return this; } /** * Build the {@link Identifier}. * * @return the {@link Identifier} */ public Identifier build() { Assert.notNull(this.objectName, "Object name must not be null"); Assert.state(StringUtils.isEmpty(this.serverName) || !StringUtils.isEmpty(this.databaseName), "Server name must be either null or both, server name and database name must " + "be provided"); return new Identifier(this.serverName, this.databaseName, this.schemaName, this.objectName); } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/InfoToken.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.tds.Decode; /** * Info token. * * @author Mark Paluch */ public final class InfoToken extends AbstractInfoToken { public static final byte TYPE = (byte) 0xAB; public InfoToken(long length, long number, int state, int infoClass, String message, String serverName, String procName, long lineNumber) { super(TYPE, length, number, (byte) state, (byte) infoClass, message, serverName, procName, lineNumber); } public InfoToken(long length, long number, byte state, byte infoClass, String message, String serverName, String procName, long lineNumber) { super(TYPE, length, number, state, infoClass, message, serverName, procName, lineNumber); } /** * Decode the {@link InfoToken}. * * @param buffer the data buffer. * @return the decoded {@link InfoToken} */ public static InfoToken decode(ByteBuf buffer) { int length = Decode.uShort(buffer); long number = Decode.asLong(buffer); byte state = Decode.asByte(buffer); byte infoClass = Decode.asByte(buffer); String msgText = Decode.unicodeUString(buffer); String serverName = Decode.unicodeBString(buffer); String procName = Decode.unicodeBString(buffer); long lineNumber = buffer.readUnsignedInt(); return new InfoToken(length, number, state, infoClass, msgText, serverName, procName, lineNumber); } @Override public String getName() { return "INFO"; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/Login7.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.message.ClientMessage; import io.r2dbc.mssql.message.TDSVersion; import io.r2dbc.mssql.message.header.Header; import io.r2dbc.mssql.message.header.HeaderOptions; import io.r2dbc.mssql.message.header.Status; import io.r2dbc.mssql.message.header.Type; import io.r2dbc.mssql.message.tds.TdsFragment; import io.r2dbc.mssql.message.tds.TdsPackets; import io.r2dbc.mssql.util.Assert; import io.r2dbc.mssql.util.DriverVersion; import io.r2dbc.mssql.util.Version; import reactor.util.annotation.Nullable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.EnumSet; import java.util.List; import java.util.function.Function; /** * Login 7 message. * * @author Mark Paluch */ public final class Login7 implements TokenStream, ClientMessage { private static final short TDS_LOGIN_REQUEST_BASE_LEN = 94; private static final HeaderOptions header = HeaderOptions.create(Type.TDS7_LOGIN, Status.empty()); private final int estimatedPacketLength; private final int baseLength; /** * The highest TDS version being used by the client (for example, 0x00000071 for TDS 7.1). *

    * If the TDSVersion value sent by the client is greater than the value that the server recognizes, the server MUST * use the highest TDS version that it can use. This provides a mechanism for clients to discover the server TDS by * sending a standard LOGIN7 message. If the TDSVersion value sent by the client is lower than the highest TDS version * the server recognizes, the server MUST use the TDS version sent by the client */ private final TDSVersion tdsVersion; /** * The packet size being requested by the client. */ private final int packetSize; /** * The version of the interface library (for example, ODBC or OLEDB) being used by the client, 4 byte. */ private final byte[] clientProgVer; /** * The process ID of the client application. */ private final int clientPid; /** * The connection ID of the primary Server. Used when connecting to an "Always Up" backup server. */ private final int connectionId; private final OptionFlags1 optionFlags1; private final OptionFlags2 optionFlags2; private final TypeFlags typeFlags; private final OptionFlags3 optionFlags3; private final Collection tokens; private final byte[] clientId; private final ConditionalProtocolSegment passwordChange; private Login7(TDSVersion tdsVersion, int packetSize, byte[] clientProgVer, int clientPid, int connectionId, OptionFlags1 optionFlags1, OptionFlags2 optionFlags2, TypeFlags typeFlags, OptionFlags3 optionFlags3, Collection tokens, byte[] clientId) { this.tdsVersion = tdsVersion; this.packetSize = packetSize; this.clientProgVer = clientProgVer; this.clientPid = clientPid; this.connectionId = connectionId; this.optionFlags1 = optionFlags1; this.optionFlags2 = optionFlags2; this.typeFlags = typeFlags; this.optionFlags3 = optionFlags3; this.tokens = tokens; this.clientId = clientId; int baseLength = TDS_LOGIN_REQUEST_BASE_LEN; EnumSet lengthRelevant = EnumSet.of(TokenType.Hostname, TokenType.AppName, TokenType.Servername, TokenType.IntName, TokenType.Database, TokenType.Username); for (LoginRequestToken token : tokens) { if (lengthRelevant.contains(token.getTokenType())) { baseLength += token.getValue().length; } } baseLength += getToken(TokenType.Password).getEncrypted().length; ConditionalProtocolSegment passwordChange = Conditionals.DISABLED; if (tdsVersion.isGreateOrEqualsTo(TDSVersion.VER_YUKON)) { passwordChange = Conditionals.PASSWORD_CHANGE; } this.passwordChange = passwordChange; this.baseLength = baseLength + 4 /* AE */; this.estimatedPacketLength = this.baseLength + Header.LENGTH + 2 + passwordChange.length() + 1; } /** * @return a builder for {@link Login7}. */ public static Builder builder() { return new Builder(); } @Override public String getName() { return "LOGIN7"; } @Override public TdsFragment encode(ByteBufAllocator allocator, int packetSize) { ByteBuf buffer = allocator.buffer(this.estimatedPacketLength); encode(buffer); return TdsPackets.create(header, buffer); } void encode(ByteBuf buffer) { int len = this.baseLength; int aeoffset = len; buffer.writeIntLE(this.baseLength + 6 + 1); buffer.writeIntLE(this.tdsVersion.getVersion()); buffer.writeIntLE(this.packetSize); buffer.writeBytes(this.clientProgVer); buffer.writeIntLE(this.clientPid); buffer.writeInt(0); // Primary server connection ID buffer.writeByte(this.optionFlags1.getValue()); buffer.writeByte(this.optionFlags2.getValue()); buffer.writeByte(this.typeFlags.getValue()); buffer.writeByte(this.optionFlags3.getValue()); buffer.writeIntLE(0); // Client time zone buffer.writeIntLE(0); // Client LCID int dataLen = 0; LoginRequestToken hostname = getToken(TokenType.Hostname); LoginRequestToken username = getToken(TokenType.Username); LoginRequestToken password = getToken(TokenType.Password); LoginRequestToken appName = getToken(TokenType.AppName); LoginRequestToken serverName = getToken(TokenType.Servername); LoginRequestToken intName = getToken(TokenType.IntName); LoginRequestToken database = getToken(TokenType.Database); // Hostname position + length dataLen = writeToken(dataLen, buffer, hostname, LoginRequestToken::getValue); // Credentials position + length dataLen = writeToken(dataLen, buffer, username, LoginRequestToken::getValue); dataLen = writeToken(dataLen, buffer, password, LoginRequestToken::getEncrypted); // AppName position + length dataLen = writeToken(dataLen, buffer, appName, LoginRequestToken::getValue); // Server name position + length dataLen = writeToken(dataLen, buffer, serverName, LoginRequestToken::getValue); // Unused // AE is always ON buffer.writeShortLE(TDS_LOGIN_REQUEST_BASE_LEN + dataLen); buffer.writeShortLE(4); dataLen += 4; // Interface library name position + length dataLen = writeToken(dataLen, buffer, intName, LoginRequestToken::getValue); // Language buffer.writeShort(0); buffer.writeShort(0); // Database name position + length dataLen = writeToken(dataLen, buffer, database, LoginRequestToken::getValue); buffer.writeBytes(this.clientId); // SSPI/Integrated security disabled. buffer.writeShort(0); buffer.writeShort(0); // Database to attach during connection process buffer.writeShort(0); buffer.writeShort(0); // TDS 7.2: Password change this.passwordChange.encode(buffer); buffer.writeBytes(hostname.getValue()); buffer.writeBytes(username.getValue()); buffer.writeBytes(password.getEncrypted()); buffer.writeBytes(appName.getValue()); buffer.writeBytes(serverName.getValue()); // Extension disabled buffer.writeIntLE(aeoffset); buffer.writeBytes(intName.getValue()); buffer.writeBytes(database.getValue()); // AE buffer.writeByte(4); buffer.writeIntLE(1); buffer.writeByte(1); // AE Terminator buffer.writeByte(-1); } private int writeToken(int dataLength, ByteBuf buffer, LoginRequestToken token, Function valueFunction) { buffer.writeShortLE(TDS_LOGIN_REQUEST_BASE_LEN + dataLength); buffer.writeShortLE(token.getLength()); return dataLength + valueFunction.apply(token).length; } private LoginRequestToken getToken(TokenType tokenType) { for (LoginRequestToken token : this.tokens) { if (token.getTokenType() == tokenType) { return token; } } return new LoginRequestToken(TokenType.Unknown, ""); } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [header=").append(header); sb.append(", tdsVersion=").append(this.tdsVersion); sb.append(", packetSize=").append(this.packetSize); sb.append(", clientPid=").append(this.clientPid); sb.append(", connectionId=").append(this.connectionId); sb.append(", tokens=").append(this.tokens); sb.append(']'); return sb.toString(); } /** * Builder for {@link Login7} requests. Pre-initializes driver version, driver name, app name, and hostname. */ public static class Builder { private TDSVersion tdsVersion; /** * The packet size being requested by the client. */ private int packetSize = 8000; /** * The version of the interface library (for example, ODBC or OLEDB) being used by the client. */ private Version clientLibraryVersion = DriverVersion.getVersion(); /** * The process ID of the client application. */ private int clientPid; /** * The client Id. */ private byte[] clientId = new byte[6]; /** * The connection ID of the primary Server. Used when connecting to an "Always Up" backup server. */ private int connectionId; private OptionFlags1 optionFlags1 = OptionFlags1.empty().byteOrderX86().charSetAscii().floatIeee754().dumpLoadOn() .useDbOff().initDatabaseFailFatal().enableLang(); private OptionFlags2 optionFlags2 = OptionFlags2.empty().setInitLangFailFatal().enableOdbc() .disableIntegratedSecurity(); private TypeFlags typeFlags = TypeFlags.empty().defaultSqlType(); private OptionFlags3 optionFlags3 = OptionFlags3.empty().enableUnknownCollationHandling().enableExtensions(); @Nullable private CharSequence username; @Nullable private CharSequence password; @Nullable private CharSequence applicationName; @Nullable private CharSequence hostname; private CharSequence clientLibraryName; @Nullable private CharSequence databaseName; @Nullable private CharSequence serverName; private Builder() { String clientLibraryName = "R2DBC Driver for Microsoft SQL Server v"; if (this.clientLibraryVersion != null) { clientLibraryName += this.clientLibraryVersion.toString(); } this.clientLibraryName = clientLibraryName; this.applicationName = clientLibraryName; } /** * Set the TDS version. * * @param tdsVersion the TDS protocol version. * @return {@code this} {@link Builder}. * @throws IllegalArgumentException when {@link TDSVersion} is {@code null}. */ public Builder tdsVersion(TDSVersion tdsVersion) { this.tdsVersion = Assert.requireNonNull(tdsVersion, "TDS version must not be null"); return this; } /** * Set the requested packet size. * * @param packetSize the requested packet size. * @return {@code this} {@link Builder}. */ public Builder packetSize(int packetSize) { this.packetSize = packetSize; return this; } /** * Configure {@link OptionFlags1}. * * @param optionFlags1 option 1 flags. * @return {@code this} {@link Builder}. * @throws IllegalArgumentException when {@link OptionFlags1} is {@code null}. */ public Builder optionFlags1(OptionFlags1 optionFlags1) { this.optionFlags1 = Assert.requireNonNull(optionFlags1, "Option flags 1 must not be null"); return this; } /** * Configure {@link OptionFlags2}. * * @param optionFlags2 option 2 flags. * @return {@code this} {@link Builder}. * @throws IllegalArgumentException when {@link OptionFlags2} is {@code null}. */ public Builder optionFlags2(OptionFlags2 optionFlags2) { this.optionFlags2 = Assert.requireNonNull(optionFlags2, "Option flags 2 must not be null"); return this; } /** * Configure {@link OptionFlags3}. * * @param optionFlags3 option 3 flags. * @return {@code this} {@link Builder}. * @throws IllegalArgumentException when {@link OptionFlags3} is {@code null}. */ public Builder optionFlags3(OptionFlags3 optionFlags3) { this.optionFlags3 = Assert.requireNonNull(optionFlags3, "Option flags 3 must not be null"); return this; } /** * Configure {@link TypeFlags}. * * @param typeFlags the type flags. * @return {@code this} {@link Builder}. * @throws IllegalArgumentException when {@link TypeFlags} is {@code null}. */ public Builder typeFlags(TypeFlags typeFlags) { this.typeFlags = Assert.requireNonNull(typeFlags, "Type flags must not be null"); return this; } /** * Configure the user name. Must not exceed 127 chars. * * @param username login username. * @return {@code this} {@link Builder}. * @throws IllegalArgumentException when {@code username} is {@code null}. */ public Builder username(CharSequence username) { this.username = Assert.requireNonNull(username, "Username must not be null"); return this; } /** * Configure the password. Must not exceed 128 chars. * * @param password login password. * @return {@code this} {@link Builder}. * @throws IllegalArgumentException when {@code password} is {@code null}. */ public Builder password(CharSequence password) { Assert.requireNonNull(password, "Password must not be null"); Assert.isTrue(password.length() < 128, "Password name must be shorter than 128 chars"); this.password = password; return this; } /** * Configure the application name. Must not exceed 128 chars. * * @param applicationName the application name. * @return {@code this} {@link Builder}. * @throws IllegalArgumentException when {@code applicationName} is {@code null}. */ public Builder applicationName(CharSequence applicationName) { Assert.requireNonNull(applicationName, "App name must not be null"); Assert.isTrue(applicationName.length() < 128, "Application name must be shorter than 128 chars"); this.applicationName = applicationName; return this; } /** * Configure the client library name. Must not exceed 128 chars. * * @param clientLibraryName driver name. * @return {@code this} {@link Builder}. * @throws IllegalArgumentException when {@code clientLibraryName} is {@code null}. */ public Builder clientLibraryName(CharSequence clientLibraryName) { Assert.requireNonNull(clientLibraryName, "Client library name must not be null"); Assert.isTrue(clientLibraryName.length() < 128, "Client library name must be shorter than 128 chars"); this.clientLibraryName = clientLibraryName; return this; } /** * Configure the client library version. Must not exceed 128 chars. * * @param clientLibraryVersion driver version. * @return {@code this} {@link Builder}. * @throws IllegalArgumentException when {@link Version} is {@code null}. */ public Builder clientLibraryVersion(Version clientLibraryVersion) { this.clientLibraryVersion = Assert.requireNonNull(clientLibraryVersion, "Client library version must not be null"); return this; } /** * Configure the client host name. Must not exceed 128 chars. * * @param hostname the client hostname. * @return {@code this} {@link Builder}. * @throws IllegalArgumentException when {@code hostname} is {@code null}. */ public Builder hostName(CharSequence hostname) { Assert.requireNonNull(hostname, "Hostname must not be null"); Assert.isTrue(hostname.length() < 128, "Hostname name must be shorter than 128 chars"); this.hostname = hostname; return this; } /** * Configure the database name. Must not exceed 128 chars. * * @param databaseName the initial database name. * @return {@code this} {@link Builder}. * @throws IllegalArgumentException when {@code databaseName} is {@code null}. */ public Builder database(CharSequence databaseName) { Assert.requireNonNull(databaseName, "Database name must not be null"); Assert.isTrue(databaseName.length() < 128, "Database name must be shorter than 128 chars"); this.databaseName = databaseName; return this; } /** * Configure the client server name. Must not exceed 128 chars. * * @param serverName the remote server name. * @return {@code this} {@link Builder}. * @throws IllegalArgumentException when {@code serverName} is {@code null}. */ public Builder serverName(CharSequence serverName) { Assert.requireNonNull(serverName, "Server name must not be null"); Assert.isTrue(serverName.length() < 128, "Server name must be shorter than 128 chars"); this.serverName = serverName; return this; } /** * Configure the client Id. Must be exactly 6 bytes. * * @param clientId the client Id. * @return {@code this} {@link Builder}. * @throws IllegalArgumentException when {@code clientId} is {@code null}. */ public Builder clientId(byte[] clientId) { Assert.requireNonNull(clientId, "Client name must not be null"); Assert.isTrue(clientId.length == 6, "Client Id must be exactly 6 chars"); this.clientId = Arrays.copyOf(clientId, 6); return this; } /** * Configure the client process Id. * * @param processId the client process Id. * @return {@code this} {@link Builder}. */ public Builder clientProcessId(int processId) { this.clientPid = processId; return this; } /** * Build a new {@link Login7} message. * * @return a new {@link Login7} message. * @throws IllegalStateException if {@code username}, {@code password}, or {@code databaseName} is {@code null} (unconfigured). */ public Login7 build() { Assert.state(this.username != null, "Username must not be null"); Assert.state(this.password != null, "Password must not be null"); Assert.state(this.databaseName != null, "Database must not be null"); List requestTokens = new ArrayList<>(); requestTokens.add(new LoginRequestToken(TokenType.Hostname, this.hostname)); requestTokens.add(new LoginRequestToken(TokenType.Username, this.username)); requestTokens.add(new LoginRequestToken(TokenType.Password, this.password)); requestTokens.add(new LoginRequestToken(TokenType.AppName, this.applicationName)); requestTokens.add(new LoginRequestToken(TokenType.Servername, this.serverName)); requestTokens.add(new LoginRequestToken(TokenType.IntName, this.clientLibraryName)); requestTokens.add(new LoginRequestToken(TokenType.Database, this.databaseName)); byte[] interfaceLibVersion = new byte[4]; if (this.clientLibraryVersion != null) { interfaceLibVersion = new byte[]{(byte) 0, (byte) this.clientLibraryVersion.getBugfix(), (byte) this.clientLibraryVersion.getMinor(), (byte) this.clientLibraryVersion.getMajor()}; } return new Login7(this.tdsVersion, this.packetSize, interfaceLibVersion, this.clientPid, this.connectionId, this.optionFlags1, this.optionFlags2, this.typeFlags, this.optionFlags3, requestTokens, this.clientId); } } /** * First option byte. */ public final static class OptionFlags1 { /** * fByteOrder: The byte order used by client for numeric and datetime data types. */ public static final byte LOGIN_OPTION1_ORDER_X86 = 0x00; public static final byte LOGIN_OPTION1_ORDER_68000 = 0x01; /** * fChar: The character set used on the client. */ public static final byte LOGIN_OPTION1_CHARSET_ASCII = 0x00; public static final byte LOGIN_OPTION1_CHARSET_EBCDIC = 0x02; /** * fFLoat: The type of floating point representation used by the client. */ public static final byte LOGIN_OPTION1_FLOAT_IEEE_754 = 0x00; public static final byte LOGIN_OPTION1_FLOAT_VAX = 0x04; public static final byte LOGIN_OPTION1_FLOAT_ND5000 = 0x08; /** * fDumpLoad: Set is dump/load or BCP capabilities are needed by the client. */ public static final byte LOGIN_OPTION1_DUMPLOAD_ON = 0x00; public static final byte LOGIN_OPTION1_DUMPLOAD_OFF = 0x10; /** * UseDB: Set if the client requires warning messages on execution of the USE SQL statement. If this flag is not * set, the server MUST NOT inform the client when the database changes, and therefore the client will be unaware of * any accompanying collation changes. */ public static final byte LOGIN_OPTION1_USE_DB_ON = 0x00; public static final byte LOGIN_OPTION1_USE_DB_OFF = 0x20; /** * fDatabase: Set if the change to initial database needs to succeed if the connection is to succeed. */ public static final byte LOGIN_OPTION1_INIT_DB_WARN = 0x00; public static final byte LOGIN_OPTION1_INIT_DB_FATAL = 0x40; /** * fSetLang: Set if the client requires warning messages on execution of a language change statement. */ public static final byte LOGIN_OPTION1_SET_LANG_OFF = 0x00; public static final byte LOGIN_OPTION1_SET_LANG_ON = (byte) 0x80; private final int optionByte; private OptionFlags1(int optionByte) { this.optionByte = optionByte; } /** * Creates an empty {@link OptionFlags1}. * * @return a new {@link OptionFlags1}. */ public static OptionFlags1 empty() { return new OptionFlags1((byte) 0x00); } /** * Enable x68 byte order. * * @return new {@link OptionFlags1} with the option applied. */ public OptionFlags1 byteOrderX86() { return new OptionFlags1(this.optionByte | LOGIN_OPTION1_ORDER_X86); } /** * Enable 68000 byte order. * * @return new {@link OptionFlags1} with the option applied. */ public OptionFlags1 byteOrder6800() { return new OptionFlags1(this.optionByte | LOGIN_OPTION1_ORDER_68000); } /** * Enable ASCII charset use. * * @return new {@link OptionFlags1} with the option applied. */ public OptionFlags1 charSetAscii() { return new OptionFlags1(this.optionByte | LOGIN_OPTION1_CHARSET_ASCII); } /** * Enable EBCDIC charset use. * * @return new {@link OptionFlags1} with the option applied. */ public OptionFlags1 charSetEbcdic() { return new OptionFlags1(this.optionByte | LOGIN_OPTION1_CHARSET_EBCDIC); } /** * Represent floating point numbers using IEE 754. * * @return new {@link OptionFlags1} with the option applied. */ public OptionFlags1 floatIeee754() { return new OptionFlags1(this.optionByte | LOGIN_OPTION1_FLOAT_IEEE_754); } /** * Represent floating point numbers using VAX representation. * * @return new {@link OptionFlags1} with the option applied. */ public OptionFlags1 floatVax() { return new OptionFlags1(this.optionByte | LOGIN_OPTION1_FLOAT_VAX); } /** * Represent floating point numbers using ND500. * * @return new {@link OptionFlags1} with the option applied. */ public OptionFlags1 floatNd500() { return new OptionFlags1(this.optionByte | LOGIN_OPTION1_FLOAT_ND5000); } /** * Enable dump (BCP) loading capabilities. * * @return new {@link OptionFlags1} with the option applied. */ public OptionFlags1 dumpLoadOn() { return new OptionFlags1(this.optionByte | LOGIN_OPTION1_DUMPLOAD_ON); } /** * Disable dump (BCP) loading capabilities. * * @return new {@link OptionFlags1} with the option applied. */ public OptionFlags1 dumpLoadOff() { return new OptionFlags1(this.optionByte | LOGIN_OPTION1_DUMPLOAD_OFF); } /** * Disable {@code USE } usage. * * @return new {@link OptionFlags1} with the option applied. */ public OptionFlags1 useDbOff() { return new OptionFlags1(this.optionByte | LOGIN_OPTION1_USE_DB_OFF); } /** * Enable {@code USE } usage. * * @return new {@link OptionFlags1} with the option applied. */ public OptionFlags1 useDbOn() { return new OptionFlags1(this.optionByte | LOGIN_OPTION1_USE_DB_ON); } /** * Warn if the initial database cannot be used (selected). * * @return new {@link OptionFlags1} with the option applied. */ public OptionFlags1 initDatabaseFailWarn() { return new OptionFlags1(this.optionByte | LOGIN_OPTION1_INIT_DB_WARN); } /** * Fail if the initial database cannot be used (selected). * * @return new {@link OptionFlags1} with the option applied. */ public OptionFlags1 initDatabaseFailFatal() { return new OptionFlags1(this.optionByte | LOGIN_OPTION1_INIT_DB_FATAL); } /** * Disable language change. * * @return new {@link OptionFlags1} with the option applied. */ public OptionFlags1 disableLang() { return new OptionFlags1(this.optionByte | LOGIN_OPTION1_SET_LANG_OFF); } /** * Enable language change. * * @return new {@link OptionFlags1} with the option applied. */ public OptionFlags1 enableLang() { return new OptionFlags1(this.optionByte | LOGIN_OPTION1_SET_LANG_ON); } /** * @return the combined option byte. */ public byte getValue() { return (byte) this.optionByte; } } /** * Second option byte. */ public final static class OptionFlags2 { /** * fLanguage: Set if the change to initial language needs to succeed if the connect is to succeed. */ public static final byte LOGIN_OPTION2_INIT_LANG_WARN = 0x00; public static final byte LOGIN_OPTION2_INIT_LANG_FATAL = 0x01; /** * fODBC: Set if the client is the ODBC driver. This causes the server to set ANSI_DEFAULTS to ON, * CURSOR_CLOSE_ON_COMMIT and IMPLICIT_TRANSACTIONS to OFF, TEXTSIZE to 0x7FFFFFFF (2GB) (TDS 7.2 and earlier), * TEXTSIZE to infinite (introduced in TDS 7.3), and ROWCOUNT to infinite. */ public static final byte LOGIN_OPTION2_ODBC_OFF = 0x00; public static final byte LOGIN_OPTION2_ODBC_ON = 0x02; public static final byte LOGIN_OPTION2_TRAN_BOUNDARY_OFF = 0x00; public static final byte LOGIN_OPTION2_TRAN_BOUNDARY_ON = 0x04; public static final byte LOGIN_OPTION2_CACHE_CONNECTION_OFF = 0x00; public static final byte LOGIN_OPTION2_CACHE_CONNECTION_ON = 0x08; /** * fUserType: The type of user connecting to the server. */ public static final byte LOGIN_OPTION2_USER_NORMAL = 0x00; public static final byte LOGIN_OPTION2_USER_SERVER = 0x10; public static final byte LOGIN_OPTION2_USER_REMUSER = 0x20; public static final byte LOGIN_OPTION2_USER_SQLREPL = 0x30; /** * fIntSecurity: The type of security required by the client. */ public static final byte LOGIN_OPTION2_INTEGRATED_SECURITY_OFF = 0x00; public static final byte LOGIN_OPTION2_INTEGRATED_SECURITY_ON = (byte) 0x80; private final int optionByte; private OptionFlags2(int optionByte) { this.optionByte = optionByte; } /** * Creates an empty {@link OptionFlags2}. * * @return new {@link OptionFlags2}. */ public static OptionFlags2 empty() { return new OptionFlags2((byte) 0x00); } /** * Warn if initial language cannot be selected. * * @return new {@link OptionFlags2} with the option applied. */ public OptionFlags2 setInitLangFailWarn() { return new OptionFlags2(this.optionByte | LOGIN_OPTION2_INIT_LANG_WARN); } /** * Fail if initial language cannot be selected. * * @return new {@link OptionFlags2} with the option applied. */ public OptionFlags2 setInitLangFailFatal() { return new OptionFlags2(this.optionByte | LOGIN_OPTION2_INIT_LANG_FATAL); } /** * Enable ODBC use. * * @return new {@link OptionFlags2} with the option applied. */ public OptionFlags2 disableOdbc() { return new OptionFlags2(this.optionByte | LOGIN_OPTION2_ODBC_OFF); } /** * Enable ODBC use. * * @return new {@link OptionFlags2} with the option applied. */ public OptionFlags2 enableOdbc() { return new OptionFlags2(this.optionByte | LOGIN_OPTION2_ODBC_ON); } /** * Use regular login. * * @return new {@link OptionFlags2} with the option applied. */ public OptionFlags2 normalUserType() { return new OptionFlags2(this.optionByte | LOGIN_OPTION2_USER_NORMAL); } /** * (reserved). * * @return new {@link OptionFlags2} with the option applied. */ public OptionFlags2 serverUserType() { return new OptionFlags2(this.optionByte | LOGIN_OPTION2_USER_SERVER); } /** * Use remote (distributed query login) login. * * @return new {@link OptionFlags2} with the option applied. */ public OptionFlags2 remoteUserType() { return new OptionFlags2(this.optionByte | LOGIN_OPTION2_USER_REMUSER); } /** * Use replication login. * * @return new {@link OptionFlags2} with the option applied. */ public OptionFlags2 sqlreplUserType() { return new OptionFlags2(this.optionByte | LOGIN_OPTION2_USER_SQLREPL); } /** * Disable integrated security. * * @return new {@link OptionFlags2} with the option applied. */ public OptionFlags2 disableIntegratedSecurity() { return new OptionFlags2(this.optionByte | LOGIN_OPTION2_INTEGRATED_SECURITY_OFF); } /** * Enable integrated security. * * @return new {@link OptionFlags2} with the option applied. */ public OptionFlags2 enableIntegratedSecurity() { return new OptionFlags2(this.optionByte | LOGIN_OPTION2_INTEGRATED_SECURITY_ON); } /** * @return the combined option byte. */ public byte getValue() { return (byte) this.optionByte; } } /** * Type flags. */ public final static class TypeFlags { /** * fSQLType: The type of SQL the client sends to the server. */ static final byte LOGIN_SQLTYPE_DEFAULT = 0x00; static final byte LOGIN_SQLTYPE_TSQL = 0x01; /** * fOLEDB: Set if the client is the OLEDB driver. This causes the server to set ANSI_DEFAULTS to ON, * CURSOR_CLOSE_ON_COMMIT and IMPLICIT_TRANSACTIONS to OFF, TEXTSIZE to 0x7FFFFFFF (2GB) (TDS 7.2 and earlier), * TEXTSIZE to infinite (introduced in TDS 7.3), and ROWCOUNT to infinite. */ static final byte LOGIN_OLEDB_OFF = 0x00; static final byte LOGIN_OLEDB_ON = 0x10; /** * fReadOnlyIntent: This bit was introduced in TDS 7.4; however, TDS 7.1, 7.2, and 7.3 clients can also use this bit * in LOGIN7 to specify that the application intent of the connection is read-only. The server SHOULD ignore this * bit if the highest TDS version supported by the server is lower than TDS 7.4. */ static final byte LOGIN_READ_ONLY_INTENT = 0x20; static final byte LOGIN_READ_WRITE_INTENT = 0x00; private final int optionByte; private TypeFlags(int optionByte) { this.optionByte = optionByte; } /** * Creates an empty {@link TypeFlags}. * * @return new {@link TypeFlags}. */ public static TypeFlags empty() { return new TypeFlags((byte) 0x00); } /** * Use the default SQL type. * * @return new {@link TypeFlags} with the option applied. */ public TypeFlags defaultSqlType() { return new TypeFlags(this.optionByte | LOGIN_SQLTYPE_DEFAULT); } /** * Use T-SQL. * * @return new {@link TypeFlags} with the option applied. */ public TypeFlags tsqlType() { return new TypeFlags(this.optionByte | LOGIN_SQLTYPE_TSQL); } /** * Disable OLEDB defaults. * * @return new {@link TypeFlags} with the option applied. */ public TypeFlags disableOledb() { return new TypeFlags(this.optionByte | LOGIN_OLEDB_OFF); } /** * Enable OLEDB defaults. * * @return new {@link TypeFlags} with the option applied. */ public TypeFlags enableOledb() { return new TypeFlags(this.optionByte | LOGIN_OLEDB_ON); } /** * @return the combined option byte. */ public byte getValue() { return (byte) this.optionByte; } } /** * Third option byte. */ public final static class OptionFlags3 { /** * fChangePassword: Specifies whether the login request SHOULD change password. */ static final byte LOGIN_OPTION3_DEFAULT = 0x00; static final byte LOGIN_OPTION3_CHANGE_PASSWORD = 0x01; /** * fSendYukonBinaryXML: 1 if XML data type instances are returned as binary XML. */ static final byte LOGIN_OPTION3_SEND_YUKON_BINARY_XML = 0x02; /** * fUserInstance: 1 if client is requesting separate process to be spawned as user instance. */ static final byte LOGIN_OPTION3_USER_INSTANCE = 0x04; /** * fUnknownCollationHandling: This bit is used by the server to determine if a client is able to properly handle * collations introduced after TDS 7.2. TDS 7.2 and earlier clients are encouraged to use this login packet bit. * Servers MUST ignore this bit when it is sent by TDS 7.3 or 7.4 clients. See [MSDN-SQLCollation] and [MS-LCID] * documents for the complete list of collations for a database server that supports SQL and LCIDs. */ static final byte LOGIN_OPTION3_UNKNOWN_COLLATION_HANDLING = 0x08; /** * fExtension: Specifies whether ibExtension/cbExtension fields are used. */ static final byte LOGIN_OPTION3_FEATURE_EXTENSION = 0x10; private final int optionByte; private OptionFlags3(int optionByte) { this.optionByte = optionByte; } /** * Creates an empty {@link OptionFlags3}. * * @return new {@link OptionFlags3}. */ public static OptionFlags3 empty() { return new OptionFlags3((byte) 0x00); } /** * Request to change the password with the login. * * @return new {@link OptionFlags3} with the option applied. */ public OptionFlags3 changePassword() { return new OptionFlags3(this.optionByte | LOGIN_OPTION3_CHANGE_PASSWORD); } /** * Fail if initial language cannot be selected. * * @return new {@link OptionFlags3} with the option applied. */ public OptionFlags3 enableUserInstance() { return new OptionFlags3(this.optionByte | LOGIN_OPTION3_USER_INSTANCE); } /** * Enable ODBC use. * * @return new {@link OptionFlags3} with the option applied. */ public OptionFlags3 enableUnknownCollationHandling() { return new OptionFlags3(this.optionByte | LOGIN_OPTION3_UNKNOWN_COLLATION_HANDLING); } /** * Enable Feature Extensions. * * @return new {@link OptionFlags3} with the option applied. */ public OptionFlags3 enableExtensions() { return new OptionFlags3(this.optionByte | LOGIN_OPTION3_FEATURE_EXTENSION); } /** * @return the combined option byte. */ public byte getValue() { return (byte) this.optionByte; } } public final static class LoginRequestToken { private final TokenType tokenType; private final int length; private final byte[] value; private final byte[] encrypted; LoginRequestToken(TokenType tokenType, @Nullable CharSequence value) { this.tokenType = tokenType; this.length = value != null ? value.length() : 0; this.value = toUCS16(value); this.encrypted = encryptPassword(value); } public TokenType getTokenType() { return this.tokenType; } public byte[] getValue() { return this.value; } public byte[] getEncrypted() { return this.encrypted; } public int getLength() { return this.length; } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [tokenType=").append(this.tokenType); sb.append(']'); return sb.toString(); } /** * Convert to a String UCS16 encoding. * * @param s the string * @return the encoded data */ private static byte[] toUCS16(@Nullable CharSequence s) { if (s == null) { return new byte[0]; } int l = s.length(); byte[] data = new byte[l * 2]; int offset = 0; for (int i = 0; i < l; i++) { int c = s.charAt(i); byte b1 = (byte) (c & 0xFF); data[offset++] = b1; data[offset++] = (byte) ((c >> 8) & 0xFF); // Unicode MSB } return data; } /** * Encrypt a password for the SQL Server logon. * * @param pwd the password * @return the encryption */ private byte[] encryptPassword(@Nullable CharSequence pwd) { // Changed to handle non ascii passwords if (pwd == null) { pwd = ""; } int len = pwd.length(); byte[] data = new byte[len * 2]; for (int i1 = 0; i1 < len; i1++) { int j1 = pwd.charAt(i1) ^ 0x5a5a; j1 = (j1 & 0xf) << 4 | (j1 & 0xf0) >> 4 | (j1 & 0xf00) << 4 | (j1 & 0xf000) >> 4; byte b1 = (byte) ((j1 & 0xFF00) >> 8); data[(i1 * 2) + 1] = b1; byte b2 = (byte) ((j1 & 0x00FF)); data[(i1 * 2) + 0] = b2; } return data; } } enum TokenType { Hostname, Username, Password, AppName, Servername, IntName, Language, Database, Unknown } /** * Conditional protocol segment. */ interface ConditionalProtocolSegment { /** * @return length in bytes. */ int length(); /** * Encode the segment onto the {@link ByteBuf}. * * @param buffer the target {@link ByteBuf}. */ void encode(ByteBuf buffer); } /** * Collection of {@link ConditionalProtocolSegment}s. */ enum Conditionals implements ConditionalProtocolSegment { DISABLED, /** * @since TDS 7.2 */ PASSWORD_CHANGE { @Override public int length() { return 8; } @Override public void encode(ByteBuf buffer) { buffer.writeShort((short) 0); buffer.writeShort((short) 0); buffer.writeInt((short) 0); } }; @Override public int length() { return 0; } @Override public void encode(ByteBuf buffer) { } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/LoginAckToken.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.tds.Decode; import io.r2dbc.mssql.util.Version; /** * Info token. * * @author Mark Paluch */ public class LoginAckToken extends AbstractDataToken { public static final byte TYPE = (byte) 0xAD; public static final byte CLIENT_INTEFACE_DEFAULT = 0; public static final byte CLIENT_INTEFACE_TSQL = 1; /* * The total length, in bytes, of the following fields: Interface, TDSVersion, Progname, and ProgVersion. */ private final long length; /** * The type of interface with which the server will accept client requests: *

      *
    • SQL_DFLT (server confirms that whatever is sent by the client is acceptable. If the client requested SQL_DFLT, * SQL_TSQL will be used)
    • *
    • SQL_TSQL (TSQL is accepted)
    • *
    */ private final byte clientInterface; /** * The TDS version being used by the server. */ private final int tdsVersion; /** * The name of the server. */ private final String progrName; /** * Server version. */ private final Version version; public LoginAckToken(long length, byte clientInterface, int tdsVersion, String progrName, Version version) { super(TYPE); this.length = length; this.clientInterface = clientInterface; this.tdsVersion = tdsVersion; this.progrName = progrName; this.version = version; } /** * Decode the {@link LoginAckToken}. * * @param buffer the data buffer. * @return the decoded {@link LoginAckToken}. */ public static LoginAckToken decode(ByteBuf buffer) { int length = Decode.uShort(buffer); byte clientInterface = Decode.asByte(buffer); int tdsVersion = Decode.intBigEndian(buffer); String progName = Decode.unicodeBString(buffer); int major = Decode.asByte(buffer); int minor = Decode.asByte(buffer); int build = buffer.readShort(); Version serverVersion = new Version(major, minor, build); return new LoginAckToken(length, clientInterface, tdsVersion, progName, serverVersion); } public byte getClientInterface() { return this.clientInterface; } public int getTdsVersion() { return this.tdsVersion; } public String getProgrName() { return this.progrName; } public Version getVersion() { return this.version; } @Override public String getName() { return "LOGINACK"; } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [clientInterface=").append(this.clientInterface); sb.append(", tdsVersion=").append(this.tdsVersion); sb.append(", progrName='").append(this.progrName).append('\"'); sb.append(", version=").append(this.version); sb.append(']'); return sb.toString(); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/NbcRowToken.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.r2dbc.mssql.message.tds.Decode; import io.r2dbc.mssql.util.Assert; import java.util.Arrays; /** * NBC Row (Null-bitmap compressed row). Expresses nullability through a bitmap. *

    Note: PLP values are aggregated in a single {@link ByteBuf} and not yet streamed. This is to be fixed. * * @author Mark Paluch */ public final class NbcRowToken extends RowToken { public static final byte TYPE = (byte) 0xD2; private final boolean[] nullMarker; /** * Creates a {@link NbcRowToken}. * * @param data the row data. * @param nullMarker {@code null} bitmap. */ private NbcRowToken(ByteBuf[] data, boolean[] nullMarker) { super(data); this.nullMarker = nullMarker; } /** * Decode a {@link NbcRowToken}. * * @param buffer the data buffer. * @param columns column descriptors. * @return the {@link RowToken}. */ public static NbcRowToken decode(ByteBuf buffer, Column[] columns) { Assert.requireNonNull(buffer, "Data buffer must not be null"); Assert.requireNonNull(columns, "List of Columns must not be null"); return doDecode(buffer, columns); } /** * Check whether the {@link ByteBuf} can be decoded into an entire {@link NbcRowToken}. * * @param buffer the data buffer. * @param columns column descriptors. * @return {@code true} if the buffer contains sufficient data to entirely decode a row. */ public static boolean canDecode(ByteBuf buffer, Column[] columns) { Assert.requireNonNull(buffer, "Data buffer must not be null"); Assert.requireNonNull(columns, "List of Columns must not be null"); int readerIndex = buffer.readerIndex(); int nullBitmapSize = getNullBitmapSize(columns); try { if (buffer.readableBytes() < nullBitmapSize) { return false; } boolean[] nullBitmap = getNullBitmap(buffer, columns); for (int i = 0; i < columns.length; i++) { Column column = columns[i]; if (nullBitmap[i]) { continue; } if (!canDecodeColumn(buffer, column)) { return false; } } return true; } finally { buffer.readerIndex(readerIndex); } } @Override public ByteBuf getColumnData(int index) { return this.nullMarker[index] ? null : super.getColumnData(index); } private static NbcRowToken doDecode(ByteBuf buffer, Column[] columns) { ByteBuf[] data = new ByteBuf[columns.length]; boolean[] nullMarkers = getNullBitmap(buffer, columns); for (int i = 0; i < columns.length; i++) { Column column = columns[i]; if (nullMarkers[i]) { data[i] = Unpooled.EMPTY_BUFFER; } else { data[i] = decodeColumnData(buffer, column); } } return new NbcRowToken(data, nullMarkers); } private static boolean[] getNullBitmap(ByteBuf buffer, Column[] columns) { int nullBitmapSize = getNullBitmapSize(columns); boolean[] nullMarkers = new boolean[columns.length]; int column = 0; for (int byteNo = 0; byteNo < nullBitmapSize; byteNo++) { byte byteValue = Decode.asByte(buffer); // if this byte is 0, skip to the next byte // and increment the column number by 8(no of bits) if (byteValue == 0) { column = column + 8; continue; } for (int bitNo = 0; bitNo < 8 && column < columns.length; bitNo++) { if ((byteValue & (1 << bitNo)) != 0) { nullMarkers[column] = true; } column++; } } return nullMarkers; } private static int getNullBitmapSize(Column[] columns) { return ((columns.length - 1) >> 3) + 1; } @Override public String getName() { return "NBCROW"; } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getName()); sb.append(" [nullMarker=").append(Arrays.toString(this.nullMarker)); sb.append(']'); return sb.toString(); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/OrderToken.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.tds.Decode; import io.r2dbc.mssql.util.Assert; import java.util.ArrayList; import java.util.List; /** * Order token to inform the client by which columns the data is ordered. * * @author Mark Paluch */ class OrderToken extends AbstractDataToken { public static final byte TYPE = (byte) 0xA9; private final List orderByColumns; /** * Create a new {@link OrderToken} given {@link List} of column indexed by which data is ordered. * * @param orderByColumns must not be null. */ private OrderToken(List orderByColumns) { super(TYPE); this.orderByColumns = orderByColumns; } /** * Decode a {@link OrderToken}. * * @param buffer the data buffer. * @return the {@link OrderToken}. */ public static OrderToken decode(ByteBuf buffer) { Assert.requireNonNull(buffer, "Data buffer must not be null"); int length = Decode.uShort(buffer); int readerIndex = buffer.readerIndex(); List columns = new ArrayList<>(); while (buffer.readerIndex() - readerIndex < length) { int column = Decode.uShort(buffer); columns.add(column); } return new OrderToken(columns); } /** * Check whether the {@link ByteBuf} can be decoded into an entire {@link OrderToken}. * * @param buffer the data buffer. * @return {@code true} if the buffer contains sufficient data to entirely decode {@link OrderToken} */ public static boolean canDecode(ByteBuf buffer) { Assert.requireNonNull(buffer, "Data buffer must not be null"); Integer length = Decode.peekUShort(buffer); return length != null && buffer.readableBytes() >= length + /* length field */ 2; } public List getOrderByColumns() { return this.orderByColumns; } @Override public String getName() { return "ORDER"; } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [orderByColumns=").append(this.orderByColumns); sb.append(']'); return sb.toString(); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/Prelogin.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.message.ClientMessage; import io.r2dbc.mssql.message.Message; import io.r2dbc.mssql.message.header.Header; import io.r2dbc.mssql.message.header.HeaderOptions; import io.r2dbc.mssql.message.header.Status; import io.r2dbc.mssql.message.header.Status.StatusBit; import io.r2dbc.mssql.message.header.Type; import io.r2dbc.mssql.message.tds.ContextualTdsFragment; import io.r2dbc.mssql.message.tds.Decode; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.message.tds.ProtocolException; import io.r2dbc.mssql.message.tds.TdsFragment; import io.r2dbc.mssql.util.Assert; import reactor.util.annotation.Nullable; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; import java.util.UUID; /** * Stream structure for {@code PRELOGIN}. * * @author Mark Paluch * @see Token */ public final class Prelogin implements TokenStream, ClientMessage { private static final HeaderOptions HEADER_OPTIONS = HeaderOptions.create(Type.PRE_LOGIN, Status.of(StatusBit.EOM)); private final List tokens; /** * Create a new {@link Prelogin} given {@link List} of {@link Token}. * * @param tokens must not be null. */ public Prelogin(List tokens) { Assert.requireNonNull(tokens, "Tokens must not be null"); this.tokens = tokens; } /** * @return a new builder for {@link Prelogin}. */ public static Builder builder() { return new Builder(); } /** * Decode the {@link Prelogin} response from a {@link ByteBuf}. * * @param buffer must not be null. * @return the decoded {@link Prelogin} response {@link Message}. */ public static Prelogin decode(ByteBuf buffer) { Assert.requireNonNull(buffer, "ByteBuf must not be null"); List decodedTokens = new ArrayList<>(); Prelogin prelogin = new Prelogin(decodedTokens); TokenDecodingState decodingState = TokenDecodingState.create(buffer); while (true) { if (!decodingState.canDecode()) { break; } byte type = Decode.asByte(buffer); if (type == Terminator.TYPE) { decodedTokens.add(Terminator.INSTANCE); break; } if (type == Version.TYPE) { decodedTokens.add(Version.decode(decodingState)); continue; } if (type == Encryption.TYPE) { decodedTokens.add(Encryption.decode(decodingState)); continue; } if (type == InstanceValidation.TYPE) { decodedTokens.add(InstanceValidation.decode(decodingState)); continue; } decodedTokens.add(UnknownToken.decode(type, decodingState)); } // ignore remaining bytes of PreLogin response buffer.skipBytes(buffer.readableBytes()); return prelogin; } /** * @return the tokens. */ public List getTokens() { return this.tokens; } /** * Resolve a {@link Token} given its {@link Class type}. * * @param tokenType the type to filter. * @param token type. * @return {@link Optional} containing the potentially found {@code tokenType}. */ public Optional getToken(Class tokenType) { Assert.requireNonNull(tokenType, "Token type must not be null"); for (Token token : this.tokens) { if (tokenType.isInstance(token)) { return Optional.of(tokenType.cast(token)); } } return Optional.empty(); } /** * Resolve a {@link Token} given its {@link Class type}. * * @param tokenType the type to filter. * @param token type. * @return the token of type {@code tokenType}. * @throws NoSuchElementException if the token could not be found. */ public T getRequiredToken(Class tokenType) { Assert.requireNonNull(tokenType, "Token type must not be null"); return getToken(tokenType).orElseThrow( () -> new NoSuchElementException(String.format("No token of type [%s] available", tokenType.getName()))); } @Override public String getName() { return "PRELOGIN"; } @Override public TdsFragment encode(ByteBufAllocator allocator, int packetSize) { Assert.requireNonNull(allocator, "ByteBufAllocator must not be null"); ByteBuf buffer = allocator.buffer(getSize(this.tokens)); encode(buffer); return new ContextualTdsFragment(HEADER_OPTIONS, buffer); } /** * Encode the {@link Prelogin} request message. * * @param buffer the data buffer to write to. */ void encode(ByteBuf buffer) { int tokenHeaderLength = 0; for (Token token : this.tokens) { tokenHeaderLength += token.getTokenHeaderLength(); } int position = tokenHeaderLength; for (Token token : this.tokens) { token.encodeToken(buffer, position); position += token.getDataLength(); } for (Token token : this.tokens) { token.encodeStream(buffer); position += token.getDataLength(); } } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof Prelogin)) { return false; } Prelogin prelogin = (Prelogin) o; return Objects.equals(this.tokens, prelogin.tokens); } @Override public int hashCode() { return Objects.hash(this.tokens); } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getName()); sb.append(" [tokens=").append(this.tokens); sb.append(']'); return sb.toString(); } private static int getSize(List tokens) { int size = Header.LENGTH; for (Token token : tokens) { size += token.getTotalLength(); } return size; } /** * Builder for {@link Prelogin}. * * @author Mark Paluch */ public static class Builder { /** * Client application thread id; */ private Integer threadId; /** * Client application trace id (for debugging purposes). */ @Nullable private UUID connectionId; /** * Client application activity id (for debugging purposes). */ @Nullable private UUID activityId; /** * Client application activity sequence (for debugging purposes). */ private int activitySequence; private byte encryption = Encryption.ENCRYPT_OFF; private String instanceName = InstanceValidation.MSSQLSERVER_VALUE; private Builder() { } /** * Configure the client-side connection {@link UUID}. Typically used for tracing. * * @param connectionId the connection UUID. * @return {@code this} {@link Builder}. */ public Builder withConnectionId(UUID connectionId) { Assert.requireNonNull(connectionId, "ConnectionID must not be null"); this.connectionId = connectionId; return this; } /** * Configure the client-side activity {@link UUID}. Typically used for tracing. * * @param activityId the activity UUID. * @return {@code this} {@link Builder}. */ public Builder withActivityId(UUID activityId) { Assert.requireNonNull(activityId, "Activity ID must not be null"); this.activityId = activityId; return this; } /** * Configure the client-side activity sequence. Typically used for tracing. * * @param activitySequence the activity sequence. * @return {@code this} {@link Builder}. */ public Builder withActivitySequence(int activitySequence) { this.activitySequence = activitySequence; return this; } /** * Configure the client-side Thread Id. Typically used for tracing. * * @param threadId the Thread Id. * @return {@code this} {@link Builder}. */ public Builder withThreadId(int threadId) { this.threadId = threadId; return this; } /** * Disable encryption. * * @return {@code this} {@link Builder}. */ public Builder withEncryptionDisabled() { this.encryption = Encryption.ENCRYPT_OFF; return this; } /** * Enable encryption. * * @return {@code this} {@link Builder}. */ public Builder withEncryptionEnabled() { this.encryption = Encryption.ENCRYPT_ON; return this; } /** * Disable encryption by indicating encryption not supported. * * @return {@code this} {@link Builder}. */ public Builder withEncryptionNotSupported() { this.encryption = Encryption.ENCRYPT_NOT_SUP; return this; } public Builder withInstanceName(String instanceName) { Assert.requireNonNull(instanceName, "Instance name must not be null"); this.instanceName = instanceName; return this; } /** * Build the {@link Prelogin} message. * * @return the {@link Prelogin} message. */ public Prelogin build() { List tokens = new ArrayList<>(); tokens.add(new Version(0, 0)); tokens.add(new Encryption(this.encryption)); tokens.add(new InstanceValidation(this.instanceName)); if (this.threadId != null) { tokens.add(new ThreadId(this.threadId)); } if (this.connectionId != null) { tokens.add(new TraceId(this.connectionId, this.activityId, this.activitySequence)); } tokens.add(Terminator.INSTANCE); return new Prelogin(tokens); } } /** * Pre-Login Token. */ public abstract static class Token { private final byte type; private final int length; Token(int type, int length) { if (type > Byte.MAX_VALUE) { throw new IllegalArgumentException("Type " + type + " exceeds byte value"); } this.type = (byte) type; this.length = length; } /** * Apply functional token decoding. * * @param toDecode the decoding state. * @param validator validator for * @param decoder token decode function. * @return the decoded token. */ static T decode(TokenDecodingState toDecode, LengthValidator validator, DecodeFunction decoder) { Assert.requireNonNull(toDecode, "TokenDecodingState must not be null"); Assert.requireNonNull(validator, "LengthValidator must not be null"); Assert.requireNonNull(decoder, "DecodeFunction must not be null"); ByteBuf buffer = toDecode.buffer; short position = buffer.readShort(); short length = buffer.readShort(); validator.validate(length); buffer.markReaderIndex(); ByteBuf data = toDecode.readBody(position, length); T result = decoder.decode(length, data); data.release(); buffer.resetReaderIndex(); toDecode.afterTokenDecoded(); return result; } public void encodeToken(ByteBuf buffer, int position) { Encode.asByte(buffer, this.type); Encode.uShortBE(buffer, position); Encode.uShortBE(buffer, this.length); } /** * Returns the token type. * * @return the token type. */ byte getType() { return this.type; } /** * Returns the token length. * * @return the token data length. */ int getLength() { return this.length; } public abstract void encodeStream(ByteBuf buffer); /** * @return total length in bytes (including token header). */ final int getTotalLength() { return getDataLength() + getTokenHeaderLength(); } /** * @return token header length in bytes. */ int getTokenHeaderLength() { return 5; } /** * @return length of data bytes. */ int getDataLength() { return this.length; } } /** * Terminating token indicating the end of prelogin tokens. */ public static class Terminator extends Token { public static final Terminator INSTANCE = new Terminator(); public static final byte TYPE = (byte) 0xFF; Terminator() { super(TYPE, 0); } @Override public void encodeToken(ByteBuf buffer, int position) { buffer.writeByte(getType()); } @Override public int getTokenHeaderLength() { return 1; } @Override public void encodeStream(ByteBuf buffer) { } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" []"); return sb.toString(); } } /** * Version information representing the SQL server version. */ public static class Version extends Token { public static final byte TYPE = 0x00; /** * version of the sender. */ private final int version; /** * sub-build number of the sender */ private final short subbuild; /** * Creates a new {@link Version} given major {@code version} and the {@code subbuild}. * * @param version the server major version. * @param subbuild the server subbuild. */ public Version(int version, int subbuild) { this(version, (byte) subbuild); } /** * Creates a new {@link Version} given major {@code version} and the {@code subbuild}. * * @param version the server major version. * @param subbuild the server subbuild. */ public Version(int version, short subbuild) { super(TYPE, 6); this.version = version; this.subbuild = subbuild; } /** * Decode the {@link Version} token. * * @param toDecode the current decoding state. * @return the decoded {@link Version} token. */ public static Version decode(TokenDecodingState toDecode) { return decode(toDecode, length -> { if (length != 6) { throw ProtocolException.invalidTds(String.format("Invalid version length: %s", length)); } }, (length, body) -> { int major = Decode.asByte(body); int minor = Decode.asByte(body); short build = body.readShort(); return new Version(major, build); }); } public int getVersion() { return this.version; } public short getSubbuild() { return this.subbuild; } @Override public void encodeStream(ByteBuf buffer) { Encode.dword(buffer, this.version); Encode.shortBE(buffer, this.subbuild); } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [version=").append(this.version); sb.append(", subbuild=").append(this.subbuild); sb.append(']'); return sb.toString(); } } /** * Allows validating a remote SQL server instance. */ public static class InstanceValidation extends Token { static final String MSSQLSERVER_VALUE = "MSSQLServer"; public static final byte TYPE = 0x02; /** * Instance name for validation */ private final byte[] instanceName; /** * Request SQL server instance name validation. * * @param instanceName the requested instance name. */ public InstanceValidation(String instanceName) { this(toBytes(instanceName)); } /** * Request SQL server instance name validation. * * @param instanceName the requested instance name. */ private InstanceValidation(byte[] instanceName) { super(TYPE, Assert.requireNonNull(instanceName, "Instance name must not be null").length); this.instanceName = instanceName; } /** * Decode the {@link InstanceValidation} token. * * @param toDecode the current decoding state. * @return the decoded {@link InstanceValidation}. */ public static InstanceValidation decode(TokenDecodingState toDecode) { return decode(toDecode, LengthValidator.ignore(), (length, body) -> { byte[] validation = new byte[length]; body.readBytes(validation, 0, length); return new InstanceValidation(validation); }); } @Override public void encodeStream(ByteBuf buffer) { buffer.writeBytes(this.instanceName); } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [instanceName=").append(this.instanceName == null ? "null" : new String(this.instanceName)); sb.append(']'); return sb.toString(); } // TODO: find an approach to use Encode.as(…) private static byte[] toBytes(String instanceName) { Assert.requireNonNull(instanceName, "Instance name must not be null"); byte[] name = instanceName.getBytes(StandardCharsets.UTF_8); byte[] result = new byte[name.length + 1]; System.arraycopy(name, 0, result, 0, name.length); return result; } } /** * Allows negotiation of the used encryption (Transport-Level Encryption via SSL) mode. */ public static class Encryption extends Token { public static final byte TYPE = 0x01; /** * Disabled encryption but enabled/required for login with credentials. */ public static final byte ENCRYPT_OFF = 0x00; /** * Encryption enabled. */ public static final byte ENCRYPT_ON = 0x01; /** * Encryption not supported. */ public static final byte ENCRYPT_NOT_SUP = 0x02; /** * Encryption required. */ public static final byte ENCRYPT_REQ = 0x03; private final byte encryption; /** * Create a new {@link Encryption} token. * * @param encryption encryption capability flag. */ public Encryption(byte encryption) { super(TYPE, 1); this.encryption = encryption; } /** * Decode the {@link Encryption} token. * * @param toDecode the state to decode. * @return the decoded {@link Encryption}. */ public static Encryption decode(TokenDecodingState toDecode) { return decode(toDecode, length -> { if (length != 1) { throw ProtocolException.invalidTds(String.format("Invalid encryption length: %s", length)); } }, (length, body) -> { byte encryption = Decode.asByte(body); return new Encryption(encryption); }); } public byte getEncryption() { return this.encryption; } @Override public void encodeStream(ByteBuf buffer) { Encode.asByte(buffer, this.encryption); } /** * Returns {@code true} if the subsequent communication requires a SSL handshake. * * @return {@code true} if the subsequent communication requires a SSL handshake. */ public boolean requiresSslHandshake() { return getEncryption() == Prelogin.Encryption.ENCRYPT_REQ || getEncryption() == Prelogin.Encryption.ENCRYPT_OFF || getEncryption() == Prelogin.Encryption.ENCRYPT_ON; } /** * Returns {@code true} if a SSL handshake is required to enable SSL for sending the Login packet only. * * @return {@code true} if a SSL handshake is required to enable SSL for sending the Login packet only. */ public boolean requiresLoginSslHandshake() { return getEncryption() == Prelogin.Encryption.ENCRYPT_OFF; } /** * Returns {@code true} if a SSL handshake is required to enable SSL for the entire connection. * * @return {@code true} if a SSL handshake is required to enable SSL for the entire connection. */ public boolean requiresConnectionSslHandshake() { return getEncryption() == Encryption.ENCRYPT_ON || getEncryption() == Encryption.ENCRYPT_REQ; } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [encryption=").append(this.encryption); sb.append(']'); return sb.toString(); } } /** * Token that allows associating a client application Thread Id with the connection. */ public static class ThreadId extends Token { public static final byte TYPE = 0x03; /** * Client application thread id; */ private final int threadId; /** * Create a new {@link ThreadId} given the application {@code threadId}. * * @param threadId application thread Id. */ public ThreadId(int threadId) { super(TYPE, 4); this.threadId = threadId; } @Override public void encodeStream(ByteBuf buffer) { buffer.writeInt(this.threadId); } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [threadId=").append(this.threadId); sb.append(']'); return sb.toString(); } } /** * TraceId token that allows associating a connectionId/activityId with the connection. */ public static class TraceId extends Token { /** * Client application trace id (for debugging purposes). */ @Nullable private final UUID connectionId; /** * Client application activity id (for debugging purposes). */ @Nullable private final UUID activityId; /** * Client application activity sequence (for debugging purposes). */ private final int activitySequence; /** * Create a {@link TraceId} to associate trace Ids with the connection. * * @param connectionId can be {@code null}. * @param activityId can be {@code null}. * @param activitySequence can be {@code null}. */ public TraceId(@Nullable UUID connectionId, @Nullable UUID activityId, int activitySequence) { super(0x05, 36); this.connectionId = connectionId; this.activityId = activityId; this.activitySequence = activitySequence; } @Override public void encodeStream(ByteBuf buffer) { if (this.connectionId != null) { buffer.writeLong(this.connectionId.getMostSignificantBits()); buffer.writeLong(this.connectionId.getLeastSignificantBits()); } else { buffer.writeLong(0); buffer.writeLong(0); } if (this.activityId != null) { buffer.writeLong(this.activityId.getMostSignificantBits()); buffer.writeLong(this.activityId.getLeastSignificantBits()); } else { buffer.writeLong(0); buffer.writeLong(0); } Encode.intBigEndian(buffer, this.activitySequence); } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [connectionId=").append(this.connectionId); sb.append(", activityId=").append(this.activityId); sb.append(", activitySequence=").append(this.activitySequence); sb.append(']'); return sb.toString(); } } /** * Token placeholder that consumes unknown tokens. */ public static class UnknownToken extends Token { UnknownToken(int type, int length) { super(type, length); } /** * Decode the unknown token. * * @param type feature type. * @param toDecode decoding state. * @return the {@link UnknownToken}. */ public static UnknownToken decode(byte type, TokenDecodingState toDecode) { return decode(toDecode, LengthValidator.ignore(), (length, body) -> { return new UnknownToken(type, length); }); } @Override public void encodeStream(ByteBuf buffer) { } @Override public String toString() { return getClass().getSimpleName(); } } /** * Function to apply decoding. * * @param */ @FunctionalInterface interface DecodeFunction { T decode(short length, ByteBuf buffer); } /** * Length validator. * * @param */ @FunctionalInterface interface LengthValidator { LengthValidator IGNORE = length -> { }; /** * Validate the token data {@code length}. * * @param length token length. * @throws ProtocolException if the length is invalid. */ void validate(short length); /** * Returns a {@link LengthValidator} that ignores the length. * * @return {@link LengthValidator} that ignores the length. */ static LengthValidator ignore() { return IGNORE; } } /** * Decoding state for Token Stream decoding using positional data length and positional index data reading. */ static class TokenDecodingState { ByteBuf buffer; int initialReaderIndex; int readPositionOffset; TokenDecodingState(ByteBuf buffer) { this.initialReaderIndex = buffer.readerIndex(); this.buffer = buffer; } public static TokenDecodingState create(ByteBuf byteBuf) { return new TokenDecodingState(byteBuf); } public boolean canDecode() { return this.buffer.readableBytes() > 0; } /** * Callback to update the state after reading. */ void afterTokenDecoded() { this.readPositionOffset = this.buffer.readerIndex() - this.initialReaderIndex; } /** * Read data at {@code position} of {@code length}. * * @param position position index within the entire buffer. * @param length bytes to read. * @return the data bytes. */ ByteBuf readBody(int position, short length) { this.buffer.skipBytes(position - 5 /* type 1 byte, position 2 byte, length 2 byte */ - this.readPositionOffset); ByteBuf data = this.buffer.alloc().buffer(length); this.buffer.readBytes(data, length); return data; } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/ReturnStatus.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.tds.Decode; import io.r2dbc.mssql.util.Assert; /** * Return Status token. * * @author Mark Paluch */ public class ReturnStatus extends AbstractDataToken { private static final ReturnStatus[] CACHED = new ReturnStatus[128]; static { for (int i = 0; i < CACHED.length; i++) { CACHED[i] = new ReturnStatus(i); } } public static final byte TYPE = (byte) 0x79; private final int status; private ReturnStatus(int status) { super(TYPE); this.status = status; } /** * Creates a new {@link ReturnStatus} * * @param status the status value. * @return the {@link ReturnStatus}. */ public static ReturnStatus create(int status) { if (status >= 0 && status < CACHED.length) { return CACHED[status]; } return new ReturnStatus(status); } /** * Decode a {@link ReturnStatus}. * * @param buffer the data buffer. * @return the decoded {@link ReturnStatus}. */ public static ReturnStatus decode(ByteBuf buffer) { Assert.requireNonNull(buffer, "Data buffer must not be null"); return ReturnStatus.create(Decode.asLong(buffer)); } /** * Check whether the {@link ByteBuf} can be decoded into a {@link ReturnStatus}. * * @param buffer the data buffer. * @return {@code true} if the buffer contains sufficient data to entirely decode a {@link ReturnStatus}. */ public static boolean canDecode(ByteBuf buffer) { Assert.requireNonNull(buffer, "Data buffer must not be null"); return buffer.readableBytes() >= 5; } public int getStatus() { return this.status; } @Override public byte getType() { return TYPE; } @Override public String getName() { return "RETURNSTATUS"; } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [status=").append(this.status); sb.append(']'); return sb.toString(); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/ReturnValue.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.netty.util.AbstractReferenceCounted; import io.netty.util.ReferenceCounted; import io.r2dbc.mssql.codec.Codecs; import io.r2dbc.mssql.codec.Decodable; import io.r2dbc.mssql.codec.RpcDirection; import io.r2dbc.mssql.message.Message; import io.r2dbc.mssql.message.tds.Decode; import io.r2dbc.mssql.message.type.Length; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.Assert; import reactor.util.annotation.Nullable; /** * A returned value from a RPC call. * * @author Mark Paluch * @see RpcDirection#OUT */ public class ReturnValue extends AbstractReferenceCounted implements DataToken { public static final byte TYPE = (byte) 0xAC; /** * Indicates the ordinal position of the output parameter in the original RPC call. Large Object output parameters are reordered to appear at the end of the stream. First the group of small * parameters is sent, followed by the group of large output parameters. There is no reordering within the groups. */ private final int ordinal; /** * The parameter name. */ @Nullable private final String parameterName; private final byte status; private final TypeInformation type; private final ByteBuf value; /** * Creates a new {@link ReturnValue}. * * @param ordinal the ordinal position of the output parameter in the original RPC call. * @param parameterName the parameter name. * @param status indicator whether the value is a {@literal OUT} value or a UDF. * @param type type descriptor of this value. * @param value the actual value. */ public ReturnValue(int ordinal, @Nullable String parameterName, int status, TypeInformation type, ByteBuf value) { this(ordinal, parameterName, (byte) status, type, value); } /** * Creates a new {@link ReturnValue}. * * @param ordinal the ordinal position of the output parameter in the original RPC call. * @param parameterName the parameter name. * @param status indicator whether the value is a {@literal OUT} value or a UDF. * @param type type descriptor of this value. * @param value the actual value. */ public ReturnValue(int ordinal, @Nullable String parameterName, byte status, TypeInformation type, ByteBuf value) { super(); this.ordinal = ordinal; this.parameterName = parameterName; this.status = status; this.type = type; this.value = value; } /** * Decode a {@link RowToken}. * * @param buffer the data buffer. * @param encryptionSupported whether encryption is supported. * @return the {@link ReturnValue}. */ public static ReturnValue decode(ByteBuf buffer, boolean encryptionSupported) { Assert.requireNonNull(buffer, "Data buffer must not be null"); int ordinal = Decode.uShort(buffer); String name = Decode.unicodeBString(buffer); byte status = Decode.asByte(buffer); TypeInformation type = TypeInformation.decode(buffer, true); // Preserve length for Codecs int beforeLengthDescriptor = buffer.readerIndex(); Length length = Length.decode(buffer, type); int descriptorLength = buffer.readerIndex() - beforeLengthDescriptor; buffer.readerIndex(beforeLengthDescriptor); ByteBuf value = buffer.readRetainedSlice(descriptorLength + length.getLength()); return new ReturnValue(ordinal, name, status, type, value); } /** * Check whether the {@link ByteBuf} can be decoded into an entire {@link ReturnValue}. * * @param buffer the data buffer. * @param encryptionSupported whether encryption is supported. * @return {@code true} if the buffer contains sufficient data to entirely decode a {@link ReturnValue}. */ public static boolean canDecode(ByteBuf buffer, boolean encryptionSupported) { Assert.requireNonNull(buffer, "Data buffer must not be null"); int readerIndex = buffer.readerIndex(); try { int requiredLength = 3; if (buffer.readableBytes() >= requiredLength) { buffer.skipBytes(2); int nameLength = Decode.asByte(buffer); if (buffer.readableBytes() < (nameLength * 2) + /* status */ 1) { return false; } buffer.skipBytes((nameLength * 2) + 1); if (!TypeInformation.canDecode(buffer, true)) { return false; } TypeInformation type = TypeInformation.decode(buffer, true); if (!Length.canDecode(buffer, type)) { return false; } Length length = Length.decode(buffer, type); if (buffer.readableBytes() >= length.getLength()) { return true; } } } finally { buffer.readerIndex(readerIndex); } return false; } /** * Check whether the {@link Message} is a {@link ReturnValue} that matches the parameter {@literal name}. * * @param message the message. * @param name the parameter name. * @return {@code true} if the {@link Message} is a {@link ReturnValue} that matches the parameter {@literal name}. */ public static boolean matches(Message message, String name) { Assert.requireNonNull(message, "Message must not be null"); Assert.requireNonNull(name, "Name must not be null"); return message instanceof ReturnValue && name.equals(((ReturnValue) message).getParameterName()); } /** * Check whether the {@link Message} is a {@link ReturnValue} that matches the parameter {@literal ordinal}. * * @param message the message. * @param ordinal the parameter ordinal. * @return {@code true} if the {@link Message} is a {@link ReturnValue} that matches the parameter {@literal ordinal}. */ public static boolean matches(Message message, int ordinal) { Assert.requireNonNull(message, "Message must not be null"); return message instanceof ReturnValue && ordinal == ((ReturnValue) message).getOrdinal(); } public int getOrdinal() { return this.ordinal; } @Nullable public String getParameterName() { return this.parameterName; } public TypeInformation getValueType() { return this.type; } public byte getStatus() { return this.status; } public ByteBuf getValue() { return this.value; } /** * Create a {@link Decodable} from this {@link ReturnValue} to allow decoding. * * @return the {@link Decodable}. * @see Codecs#decode(ByteBuf, Decodable, Class) */ public Decodable asDecodable() { return new Decodable() { @Override public TypeInformation getType() { return getValueType(); } @Override public String getName() { return getParameterName() == null ? "" : getParameterName(); } }; } @Override public byte getType() { return TYPE; } @Override public String getName() { return "RETURNVALUE"; } @Override public ReferenceCounted touch(Object hint) { this.value.touch(hint); return this; } @Override protected void deallocate() { this.value.release(); } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [ordinal=").append(this.ordinal); sb.append(", parameterName='").append(this.parameterName).append('\''); sb.append(", value=").append(this.value); sb.append(", type=").append(this.type); sb.append(']'); return sb.toString(); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/RowToken.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.netty.buffer.CompositeByteBuf; import io.netty.util.AbstractReferenceCounted; import io.netty.util.ReferenceCountUtil; import io.r2dbc.mssql.message.type.Length; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.PlpLength; import io.r2dbc.mssql.util.Assert; import reactor.util.annotation.Nullable; import java.nio.Buffer; /** * Row token message containing row bytes. * Extends {@link AbstractReferenceCounted} to release associated {@link ByteBuf}s once the row is de-allocated. * *

    Note: PLP values are aggregated in a single {@link ByteBuf} and not yet streamed. This is to be fixed. * * @author Mark Paluch */ public class RowToken extends AbstractReferenceCounted implements DataToken { public static final byte TYPE = (byte) 0xD1; private final ByteBuf[] data; /** * Creates a {@link RowToken}. * * @param data the row data. */ RowToken(ByteBuf[] data) { this.data = data; } /** * Decode a {@link RowToken}. * * @param buffer the data buffer. * @param columns column descriptors. * @return the {@link RowToken}. */ public static RowToken decode(ByteBuf buffer, Column[] columns) { Assert.requireNonNull(buffer, "Data buffer must not be null"); Assert.requireNonNull(columns, "List of Columns must not be null"); return doDecode(buffer, columns); } /** * Check whether the {@link ByteBuf} can be decoded into an entire {@link RowToken}. * * @param buffer the data buffer. * @param columns column descriptors. * @return {@code true} if the buffer contains sufficient data to entirely decode a row. */ public static boolean canDecode(ByteBuf buffer, Column[] columns) { Assert.requireNonNull(buffer, "Data buffer must not be null"); Assert.requireNonNull(columns, "List of Columns must not be null"); int readerIndex = buffer.readerIndex(); try { for (Column column : columns) { if (!canDecodeColumn(buffer, column)) { return false; } } return true; } finally { buffer.readerIndex(readerIndex); } } static boolean canDecodeColumn(ByteBuf buffer, Column column) { if (column.getType().getLengthStrategy() == LengthStrategy.PARTLENTYPE) { return canDecodePlp(buffer, column); } return doCanDecode(buffer, column); } /** * Returns whether the {@link Buffer} with a scalar size can be decoded. * * @param buffer * @param column * @return */ private static boolean doCanDecode(ByteBuf buffer, Column column) { if (!Length.canDecode(buffer, column.getType())) { return false; } int startRead = buffer.readerIndex(); Length length = Length.decode(buffer, column.getType()); int endRead = buffer.readerIndex(); int descriptorLength = endRead - startRead; int dataLength = descriptorLength + length.getLength(); int adjusted = dataLength - descriptorLength; if (buffer.readableBytes() >= adjusted) { buffer.skipBytes(adjusted); return true; } return false; } /** * Returns whether a PLP stream can be decoded where can be decoded means that we have received at least the PLP length header. * * @param buffer data buffer. * @param column the related column. * @return {@code true} if the PLP sream can be decoded. * @see LengthStrategy#PARTLENTYPE */ private static boolean canDecodePlp(ByteBuf buffer, Column column) { if (!PlpLength.canDecode(buffer, column.getType())) { return false; } PlpLength totalLength = PlpLength.decode(buffer, column.getType()); if (totalLength.isNull()) { return true; } while (true) { if (!Length.canDecode(buffer, column.getType())) { return false; } Length chunkLength = Length.decode(buffer, column.getType()); if (chunkLength.getLength() == 0) { return true; } if (buffer.readableBytes() >= chunkLength.getLength()) { buffer.skipBytes(chunkLength.getLength()); } else { return false; } } } private static RowToken doDecode(ByteBuf buffer, Column[] columns) { ByteBuf[] data = new ByteBuf[columns.length]; for (int i = 0; i < columns.length; i++) { data[i] = decodeColumnData(buffer, columns[i]); } return new RowToken(data); } /** * Decode a {@link ByteBuf data buffer} for a single {@link Column}. * * @param buffer the data buffer. * @param column the column. * @return */ @Nullable static ByteBuf decodeColumnData(ByteBuf buffer, Column column) { if (column.getType().getLengthStrategy() == LengthStrategy.PARTLENTYPE) { buffer.markReaderIndex(); return doDecodePlp(buffer, column); } else { return doDecode(buffer, column); } } /** * Decode a scalar length value. Returns {@code null} if {@link Length#isNull()}. * * @param buffer the data buffer. * @param column the column. * @return */ @Nullable private static ByteBuf doDecode(ByteBuf buffer, Column column) { int startRead = buffer.readerIndex(); Length length = Length.decode(buffer, column.getType()); if (length.isNull()) { return null; } int endRead = buffer.readerIndex(); int descriptorLength = endRead - startRead; buffer.readerIndex(startRead); return buffer.readRetainedSlice(descriptorLength + length.getLength()); } /** * Decode a PLP stream value. Returns {@code null} if {@link Length#isNull()}. The decoded value contains an entire PLP token stream with chunk headers. * * @param buffer the data buffer. * @param column the column. * @return */ @Nullable private static ByteBuf doDecodePlp(ByteBuf buffer, Column column) { PlpLength totalLength = PlpLength.decode(buffer, column.getType()); if (totalLength.isNull()) { return null; } CompositeByteBuf plpData = buffer.alloc().compositeBuffer(); ByteBuf length = buffer.alloc().buffer(8); totalLength.encode(length); plpData.addComponent(true, length); while (true) { Length chunkLength = Length.decode(buffer, column.getType()); if (chunkLength.getLength() == 0) { break; } length = buffer.alloc().buffer(4); chunkLength.encode(length, column.getType()); plpData.addComponent(true, length); plpData.addComponent(true, buffer.readRetainedSlice(chunkLength.getLength())); } return plpData; } /** * Returns the {@link ByteBuf data} for the column at {@code index}. * * @param index the column {@code index}. * @return the data buffer. Can be {@code null} if indicated by null-bit compression. */ @Nullable public ByteBuf getColumnData(int index) { return this.data[index]; } @Override public byte getType() { return TYPE; } @Override public String getName() { return "ROW"; } @Override public RowToken touch(Object hint) { return this; } @Override protected void deallocate() { for (ByteBuf datum : this.data) { ReferenceCountUtil.release(datum); } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/RpcRequest.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.Unpooled; import io.r2dbc.mssql.codec.Encoded; import io.r2dbc.mssql.codec.PlpEncoded; import io.r2dbc.mssql.codec.RpcDirection; import io.r2dbc.mssql.codec.RpcEncoding; import io.r2dbc.mssql.message.ClientMessage; import io.r2dbc.mssql.message.TransactionDescriptor; import io.r2dbc.mssql.message.header.HeaderOptions; import io.r2dbc.mssql.message.header.Status; import io.r2dbc.mssql.message.header.Type; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.message.tds.TdsFragment; import io.r2dbc.mssql.message.tds.TdsPackets; import io.r2dbc.mssql.message.type.Collation; import io.r2dbc.mssql.util.Assert; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.annotation.Nullable; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; /** * RPC request to invoke stored procedures. * * @author Mark Paluch */ public final class RpcRequest implements ClientMessage, TokenStream { static final HeaderOptions HEADER = HeaderOptions.create(Type.RPC, Status.empty()); /** * Requests positioned updates. This procedure performs operations on one or more rows within a cursor's fetch buffer. */ public static final short Sp_Cursor = 1; /** * Opens a cursor. sp_cursoropen defines the SQL statement associated with the cursor and cursor options, and then populates the cursor. */ public static final short Sp_CursorOpen = 2; /** * Compiles the cursor statement or batch into an execution plan, but does not create the cursor. The compiled statement can later be used by {@link #Sp_CursorExecute}. */ public static final short Sp_CursorPrepare = 3; /** * Creates and populates a cursor based upon the execution plan created by {@link #Sp_CursorPrepare}. This procedure, coupled with {@link #Sp_CursorPrepare}, has the same function as * {@link #Sp_CursorOpen}, but is split into two phases. */ public static final short Sp_CursorExecute = 4; /** * Compiles a plan for the submitted cursor statement or batch, then creates and populates the cursor. sp_cursorprepexec combines the functions of {@link #Sp_CursorPrepare} and * {@link #Sp_CursorExecute}. */ public static final short Sp_CursorPrepExec = 5; /** * Discards the execution plan developed in the sp_cursorprepare stored procedure. */ public static final short Sp_CursorUnprepare = 6; /** * Fetches a buffer of one or more rows from the database. The group of rows in this buffer is called the cursor's fetch buffer. */ public static final short Sp_CursorFetch = 7; /** * Sets cursor options or returns cursor information created by the {@link #Sp_CursorOpen} stored procedure. */ public static final short Sp_CursorOption = 8; /** * Closes and de-allocates the cursor, as well as releases all associated resources; that is, it drops the temporary table used in support of {@literal KEYSET} or {@literal STATIC} cursor. */ public static final short Sp_CursorClose = 9; /** * Executes a Transact-SQL statement or batch that can be reused many times, or one that has been built dynamically. */ public static final short Sp_ExecuteSql = 10; /** * Prepares a parameterized Transact-SQL statement and returns a statement handle for execution. */ public static final short Sp_Prepare = 11; /** * Executes a prepared Transact-SQL statement using a specified handle and optional parameter value. */ public static final short Sp_Execute = 12; /** * Prepares and executes a parameterized Transact-SQL statement. {@link #Sp_PrepExec} combines the functions of {@link #Sp_Prepare} and {@link #Sp_Execute}. */ public static final short Sp_PrepExec = 13; /** * Prepares and executes a parameterized stored procedure call that has been specified using an RPC identifier. */ public static final short Sp_PrepExecRpc = 14; /** * Discards the execution plan created by the sp_prepare stored procedure. */ public static final short Sp_Unprepare = 15; private static final short PROC_ID_SWITCH = (short) 0xFFFF; private final AllHeaders allHeaders; @Nullable private final String procName; private final Integer procId; private final OptionFlags optionFlags; private final byte statusFlags; private final List parameterDescriptors; private RpcRequest(AllHeaders allHeaders, @Nullable String procName, @Nullable Integer procId, OptionFlags optionFlags, byte statusFlags, List parameterDescriptors) { this.allHeaders = Assert.requireNonNull(allHeaders, "AllHeaders must not be null"); this.procName = procName; this.procId = procId; this.optionFlags = Assert.requireNonNull(optionFlags, "Option flags must not be null"); this.statusFlags = statusFlags; this.parameterDescriptors = parameterDescriptors; } /** * Creates a new {@link Builder} to build a {@link RpcRequest}. * * @return a new {@link Builder} to build a {@link RpcRequest}. */ public static Builder builder() { return new Builder(); } @Override public Publisher encode(ByteBufAllocator allocator, int packetSize) { Assert.requireNonNull(allocator, "ByteBufAllocator must not be null"); return Flux.defer(() -> { int name = 2 + (this.procName != null ? this.procName.length() * 2 : 0); int length = 4 + name + this.allHeaders.getLength(); for (ParameterDescriptor descriptor : this.parameterDescriptors) { length += descriptor.estimateLength(); } ByteBuf scalarBuffer = allocator.buffer(length); encodeHeader(scalarBuffer); boolean hasPlpSegments = false; for (ParameterDescriptor descriptor : this.parameterDescriptors) { if (descriptor instanceof EncodedRpcParameter) { if (((EncodedRpcParameter) descriptor).getValue() instanceof PlpEncoded) { hasPlpSegments = true; } } } if (!hasPlpSegments) { for (ParameterDescriptor descriptor : this.parameterDescriptors) { descriptor.encode(scalarBuffer); } return Flux.just(TdsPackets.create(HEADER, scalarBuffer)); } AtomicReference firstBufferHolder = new AtomicReference<>(scalarBuffer); AtomicBoolean first = new AtomicBoolean(true); return Flux.fromIterable(this.parameterDescriptors).concatMap(it -> { ByteBuf buffer = getByteBuf(firstBufferHolder, allocator, it); if (it instanceof EncodedRpcParameter && ((EncodedRpcParameter) it).getValue() instanceof PlpEncoded) { EncodedRpcParameter parameter = (EncodedRpcParameter) it; PlpEncoded encoded = (PlpEncoded) parameter.getValue(); parameter.encodeHeader(buffer); encoded.encodeHeader(buffer); AtomicReference firstChunk = new AtomicReference<>(buffer); Flux tdsFragments = encoded.chunked(() -> packetSize * 4, true).map(chunk -> { if (firstChunk.compareAndSet(buffer, null)) { CompositeByteBuf withInitialBuffer = allocator.compositeBuffer(); withInitialBuffer.addComponent(true, buffer); withInitialBuffer.addComponent(true, chunk); return withInitialBuffer; } return chunk; }); return tdsFragments.concatWith(Mono.create(sink -> { ByteBuf terminator = allocator.buffer(); Encode.asInt(terminator, 0); sink.success(terminator); })); } it.encode(buffer); return Mono.just(buffer); }, 1).map(buf -> { if (first.compareAndSet(true, false)) { return TdsPackets.first(HEADER, buf); } return TdsPackets.create(buf); }).concatWith(Mono.create(sink -> { ByteBuf firstBuffer = firstBufferHolder.getAndSet(null); if (firstBuffer != null) { sink.success(TdsPackets.last(firstBuffer)); return; } sink.success(TdsPackets.last(Unpooled.EMPTY_BUFFER)); })); }); } private ByteBuf getByteBuf(AtomicReference firstBufferHolder, ByteBufAllocator allocator, ParameterDescriptor it) { ByteBuf firstBuffer = firstBufferHolder.getAndSet(null); if (firstBuffer != null) { return firstBuffer; } int estimatedLength = it.estimateLength(); return estimatedLength > 0 ? allocator.buffer(estimatedLength) : allocator.buffer(); } private void encodeHeader(ByteBuf buffer) { this.allHeaders.encode(buffer); if (this.procId != null) { Encode.uShort(buffer, PROC_ID_SWITCH); Encode.uShort(buffer, this.procId); } else { Assert.state(this.procName != null, "ProcName must not be null if ProcId is not set."); Encode.unicodeStream(buffer, this.procName); } Encode.asByte(buffer, this.optionFlags.getValue()); Encode.asByte(buffer, this.statusFlags); } @Nullable public String getProcName() { return this.procName; } public Integer getProcId() { return this.procId; } @Override public String getName() { return "RPCRequest"; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof RpcRequest)) { return false; } RpcRequest that = (RpcRequest) o; return this.statusFlags == that.statusFlags && Objects.equals(this.allHeaders, that.allHeaders) && Objects.equals(this.procName, that.procName) && Objects.equals(this.procId, that.procId) && Objects.equals(this.optionFlags, that.optionFlags) && Objects.equals(this.parameterDescriptors, that.parameterDescriptors); } @Override public int hashCode() { return Objects.hash(this.allHeaders, this.procName, this.procId, this.optionFlags, this.statusFlags, this.parameterDescriptors); } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getName()); sb.append(" [procName='").append(this.procName).append('\''); sb.append(", procId=").append(this.procId); sb.append(", optionFlags=").append(this.optionFlags); sb.append(", statusFlags=").append(this.statusFlags); sb.append(", parameterDescriptors=").append(this.parameterDescriptors); sb.append(']'); return sb.toString(); } /** * Builder for {@link RpcRequest}. */ public final static class Builder { @Nullable private String procName; private Integer procId; private OptionFlags optionFlags = OptionFlags.empty(); private byte statusFlags; private TransactionDescriptor transactionDescriptor; private final List parameterDescriptors = new ArrayList<>(); /** * Configure a procedure name. * * @param procName the name of the stored procedure to call. * @return {@code this} {@link Builder}. */ public Builder withProcName(String procName) { Assert.requireNonNull(procName, "ProcName must not be null"); this.procId = null; this.procName = procName; return this; } /** * Configure a procedureId to call a pre-defined stored procedure. * * @param id the stored procedure Id. See {@link RpcRequest#Sp_Cursor} and other {@literal Sp_} constants. * @return {@code this} {@link Builder}. */ public Builder withProcId(int id) { this.procName = null; this.procId = id; return this; } /** * Add a {@link String} parameter to this RPC call. * * @param direction RPC parameter direction (in/out). * @param collation parameter encoding. * @param value the parameter value, can be {@code null}. * @return {@code this} {@link Builder}. * @throws IllegalArgumentException when {@link RpcDirection} or {@link Collation} is {@code null}. */ public Builder withParameter(RpcDirection direction, Collation collation, @Nullable String value) { Assert.requireNonNull(direction, "RPC direction (in/out) must not be null"); Assert.requireNonNull(collation, "Collation must not be null"); this.parameterDescriptors.add(new RpcString(direction, null, collation, value)); return this; } /** * Add a {@link Integer} parameter to this RPC call. * * @param direction RPC parameter direction (in/out). * @param value the parameter value, can be {@code null}. * @return {@code this} {@link Builder}. * @throws IllegalArgumentException when {@link RpcDirection} is {@code null}. */ public Builder withParameter(RpcDirection direction, @Nullable Integer value) { Assert.requireNonNull(direction, "RPC direction (in/out) must not be null"); this.parameterDescriptors.add(new RpcInt(direction, null, value)); return this; } /** * Add an {@link Encoded} parameter to this RPC call. * * @param direction RPC parameter direction (in/out). * @param value the parameter value. * @return {@code this} {@link Builder}. * @throws IllegalArgumentException when {@link RpcDirection} or {@link Encoded} is {@code null}. */ public Builder withParameter(RpcDirection direction, Encoded value) { Assert.requireNonNull(direction, "RPC direction (in/out) must not be null"); Assert.requireNonNull(value, "Encoded parameter name must not be null"); this.parameterDescriptors.add(new EncodedRpcParameter(direction, null, value)); return this; } /** * Add a {@link String} parameter to this RPC call. * * @param direction RPC parameter direction (in/out). * @param name the parameter name * @param collation parameter encoding. * @param value the parameter value, can be {@code null}. * @return {@code this} {@link Builder}. * @throws IllegalArgumentException when {@link RpcDirection}, {@code name} or {@link Collation} is {@code null}. */ public Builder withNamedParameter(RpcDirection direction, String name, Collation collation, @Nullable String value) { Assert.requireNonNull(direction, "RPC direction (in/out) must not be null"); Assert.requireNonNull(name, "Parameter name must not be null"); Assert.requireNonNull(collation, "Collation must not be null"); this.parameterDescriptors.add(new RpcString(direction, name, collation, value)); return this; } /** * Add a {@link Integer} parameter to this RPC call. * * @param direction RPC parameter direction (in/out). * @param name the parameter name * @param value the parameter value, can be {@code null}. * @return {@code this} {@link Builder}. * @throws IllegalArgumentException when {@link RpcDirection} or {@code name} is {@code null}. */ public Builder withNamedParameter(RpcDirection direction, String name, @Nullable Integer value) { Assert.requireNonNull(direction, "RPC direction (in/out) must not be null"); Assert.requireNonNull(name, "Parameter name must not be null"); this.parameterDescriptors.add(new RpcInt(direction, name, value)); return this; } /** * Add an {@link Encoded} parameter to this RPC call. * * @param direction RPC parameter direction (in/out). * @param name the parameter name * @param value the parameter value. * @return {@code this} {@link Builder}. * @throws IllegalArgumentException when {@link RpcDirection}, {@code name}, or {@link Encoded} is {@code null}. */ public Builder withNamedParameter(RpcDirection direction, String name, Encoded value) { Assert.requireNonNull(direction, "RPC direction (in/out) must not be null"); Assert.requireNonNull(name, "Parameter name must not be null"); Assert.requireNonNull(value, "Encoded parameter name must not be null"); this.parameterDescriptors.add(new EncodedRpcParameter(direction, name, value)); return this; } /** * Configure a {@link TransactionDescriptor}. * * @param transactionDescriptor the transaction descriptor. * @return {@code this} {@link Builder}. * @throws IllegalArgumentException when {@link TransactionDescriptor} is {@code null}. */ public Builder withTransactionDescriptor(TransactionDescriptor transactionDescriptor) { this.transactionDescriptor = Assert.requireNonNull(transactionDescriptor, "TransactionDescriptor must not be null"); return this; } /** * Configure the {@link OptionFlags}. * * @param optionFlags the option flags to use. * @return {@code this} {@link Builder}. * @throws IllegalArgumentException when {@link OptionFlags} is {@code null}. */ public Builder withOptionFlags(OptionFlags optionFlags) { this.optionFlags = Assert.requireNonNull(optionFlags, "OptionFlags must not be null"); return this; } /** * Build a {@link RpcRequest}. * * @return a new {@link RpcRequest}. * @throws IllegalStateException when {@link TransactionDescriptor} or procedure name/id are not configured. */ public RpcRequest build() { Assert.state(this.transactionDescriptor != null, "TransactionDescriptor is not configured"); Assert.state(this.procName != null || this.procId != null, "Either procedure name or procedure Id required"); return new RpcRequest(AllHeaders.transactional(this.transactionDescriptor.toBytes(), 1), this.procName, this.procId, this.optionFlags, this.statusFlags, new ArrayList<>(this.parameterDescriptors)); } } /** * RPC option flags. */ public final static class OptionFlags { private static final OptionFlags EMPTY = new OptionFlags(0x00); /** * Recompile the called procedure. */ static final byte RPC_OPTION_RECOMPILE = (byte) 0x01; /** * The server sends No Meta Data only if fNoMetadata is set to 1 in the request (i.e. suppressing Column Metadata). */ static final byte RPC_OPTION_NO_METADATA = (byte) 0x02; private final int optionByte; private OptionFlags(int optionByte) { this.optionByte = optionByte; } /** * Creates an empty {@link OptionFlags}. * * @return a new {@link OptionFlags}. */ public static OptionFlags empty() { return EMPTY; } /** * Enable procedure recompilation. * * @return new {@link OptionFlags} with the option applied. */ public OptionFlags enableRecompile() { return new OptionFlags(this.optionByte | RPC_OPTION_RECOMPILE); } /** * Disable metadata. * * @return new {@link OptionFlags} with the option applied. */ public OptionFlags disableMetadata() { return new OptionFlags(this.optionByte | RPC_OPTION_NO_METADATA); } /** * @return the combined option byte. */ public byte getValue() { return (byte) this.optionByte; } } /** * Abstract base class for RPC parameter implementations. */ abstract static class ParameterDescriptor { private final RpcDirection direction; @Nullable private final String name; ParameterDescriptor(RpcDirection direction, @Nullable String name) { this.direction = Assert.requireNonNull(direction, "Direction must not be null"); this.name = name; } /** * Encode the parameter value. * * @param buffer the data buffer to use as encode target. */ abstract void encode(ByteBuf buffer); /** * Estimate the encoded parameter length. * * @return the estimated parameter length in bytes. */ abstract int estimateLength(); public RpcDirection getDirection() { return this.direction; } @Nullable public String getName() { return this.name; } } /** * String RPC parameter. */ static class RpcString extends ParameterDescriptor { private final Collation collation; @Nullable private final String value; RpcString(RpcDirection direction, @Nullable String name, Collation collation, @Nullable String value) { super(direction, name); this.value = value; this.collation = collation; } @Override void encode(ByteBuf buffer) { RpcEncoding.encodeString(buffer, getName(), getDirection(), this.collation, this.value); } @Override int estimateLength() { return 16 + (this.value != null ? this.value.length() * 2 : 0); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof RpcString)) { return false; } RpcString rpcString = (RpcString) o; return Objects.equals(this.collation, rpcString.collation) && Objects.equals(this.value, rpcString.value); } @Override public int hashCode() { return Objects.hash(this.collation, this.value); } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [name='").append(getName()).append('\''); sb.append(", value=").append(this.value); sb.append(']'); return sb.toString(); } } /** * Integer RPC parameter. */ static class RpcInt extends ParameterDescriptor { @Nullable private final Integer value; RpcInt(RpcDirection direction, @Nullable String name, @Nullable Integer value) { super(direction, name); this.value = value; } @Override void encode(ByteBuf buffer) { RpcEncoding.encodeInteger(buffer, getName(), getDirection(), this.value); } @Override int estimateLength() { return this.value != null ? 5 : 0; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof RpcInt)) { return false; } RpcInt rpcInt = (RpcInt) o; return Objects.equals(this.value, rpcInt.value); } @Override public int hashCode() { return Objects.hash(this.value); } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [name='").append(getName()).append('\''); sb.append(", value=").append(this.value); sb.append(']'); return sb.toString(); } } /** * RPC parameter. */ static class EncodedRpcParameter extends ParameterDescriptor { private final Encoded value; EncodedRpcParameter(RpcDirection direction, @Nullable String name, Encoded value) { super(direction, name); this.value = value; } public Encoded getValue() { return this.value; } @Override void encode(ByteBuf buffer) { encodeHeader(buffer); ByteBuf value = this.value.getValue(); buffer.writeBytes(value); value.release(); } void encodeHeader(ByteBuf buffer) { RpcEncoding.encodeHeader(buffer, getName(), getDirection(), this.value.getDataType()); } @Override int estimateLength() { int estimate = 2 + (getName() != null ? (getName().length() + 1) * 2 : 0); estimate += this.value.estimateLength(); return estimate; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof EncodedRpcParameter)) { return false; } EncodedRpcParameter encodedRpcParameter = (EncodedRpcParameter) o; return Objects.equals(this.value, encodedRpcParameter.value); } @Override public int hashCode() { return Objects.hash(this.value); } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [name='").append(getName()).append('\''); sb.append(", value=").append(this.value); sb.append(']'); return sb.toString(); } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/SqlBatch.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.message.ClientMessage; import io.r2dbc.mssql.message.TransactionDescriptor; import io.r2dbc.mssql.message.header.HeaderOptions; import io.r2dbc.mssql.message.header.Status; import io.r2dbc.mssql.message.header.Type; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.message.tds.TdsFragment; import io.r2dbc.mssql.message.tds.TdsPackets; import io.r2dbc.mssql.util.Assert; import java.util.Objects; /** * SQL batch token to execute simple SQL. * * @author Mark Paluch */ public final class SqlBatch implements ClientMessage, TokenStream { private final HeaderOptions header; private final AllHeaders allHeaders; private final String sql; /** * Creates a new {@link SqlBatch} token. * * @param outstandingRequests the number of outstanding requests. * @param transactionDescriptor the transaction descriptor (8 byte). * @param sql the SQL string. */ private SqlBatch(int outstandingRequests, byte[] transactionDescriptor, String sql) { Assert.requireNonNull(transactionDescriptor, "Transaction descriptor must not be null"); Assert.requireNonNull(sql, "SQL must not be null"); this.header = HeaderOptions.create(Type.SQL_BATCH, Status.empty()); this.allHeaders = AllHeaders.transactional(transactionDescriptor, outstandingRequests); this.sql = sql; } /** * Creates a new {@link SqlBatch}. * * @param outstandingRequests the number of outstanding requests. * @param transactionDescriptor the transaction descriptor * @param sql the SQL string. * @return the {@link SqlBatch}. */ public static SqlBatch create(int outstandingRequests, TransactionDescriptor transactionDescriptor, String sql) { Assert.requireNonNull(transactionDescriptor, "Transaction descriptor must not be null"); Assert.requireNonNull(sql, "SQL must not be null"); return new SqlBatch(outstandingRequests, transactionDescriptor.toBytes(), sql); } @Override public TdsFragment encode(ByteBufAllocator allocator, int packetSize) { Assert.requireNonNull(allocator, "ByteBufAllocator must not be null"); int length = this.allHeaders.getLength() + (this.sql.length() * 2); ByteBuf buffer = allocator.buffer(length); encode(buffer); return TdsPackets.create(this.header, buffer); } void encode(ByteBuf buffer) { this.allHeaders.encode(buffer); Encode.unicodeStream(buffer, this.sql); } public String getSql() { return this.sql; } @Override public String getName() { return "SQLBatch"; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof SqlBatch)) { return false; } SqlBatch batch = (SqlBatch) o; return Objects.equals(this.allHeaders, batch.allHeaders) && Objects.equals(this.sql, batch.sql); } @Override public int hashCode() { return Objects.hash(this.allHeaders, this.sql); } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getName()); sb.append(" [sql=\"").append(this.sql).append('\"'); sb.append(']'); return sb.toString(); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/TabnameToken.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.tds.Decode; import io.r2dbc.mssql.util.Assert; import java.util.ArrayList; import java.util.List; /** * Table name token. Used to send the table name to the client only when in browser mode or from cursors. * * @author Mark Paluch */ public class TabnameToken extends AbstractDataToken { public static final byte TYPE = (byte) 0xA4; private final List tableNames; private TabnameToken(List tableNames) { super(TYPE); this.tableNames = tableNames; } /** * Decode a {@link TabnameToken}. * * @param buffer the data buffer. * @return the {@link TabnameToken}. */ public static TabnameToken decode(ByteBuf buffer) { Assert.requireNonNull(buffer, "Data buffer must not be null"); int length = Decode.uShort(buffer); int readerIndex = buffer.readerIndex(); List tableNames = new ArrayList<>(); while (buffer.readerIndex() - readerIndex < length) { tableNames.add(Identifier.decode(buffer)); } return new TabnameToken(tableNames); } /** * Check whether the {@link ByteBuf} can be decoded into an entire {@link TabnameToken}. * * @param buffer the data buffer. * @return {@code true} if the buffer contains sufficient data to entirely decode a {@link TabnameToken}. */ public static boolean canDecode(ByteBuf buffer) { Assert.requireNonNull(buffer, "Data buffer must not be null"); if (buffer.readableBytes() >= 5) { Integer requiredLength = Decode.peekUShort(buffer); return requiredLength != null && buffer.readableBytes() >= (requiredLength + /* length field */ 2); } return false; } public List getTableNames() { return this.tableNames; } @Override public String getName() { return "TABNAME"; } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getName()); sb.append(" [names=").append(this.tableNames); sb.append(']'); return sb.toString(); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/Tabular.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.Message; import io.r2dbc.mssql.message.header.Type; import io.r2dbc.mssql.message.tds.Decode; import io.r2dbc.mssql.message.tds.ProtocolException; import io.r2dbc.mssql.util.Assert; import reactor.core.publisher.SynchronousSink; import reactor.util.annotation.Nullable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Predicate; /** * Tabular response. * * @author Mark Paluch * @see Type#TABULAR_RESULT */ public final class Tabular implements Message { private final List tokens; private Tabular(List tokens) { this.tokens = tokens; } /** * Creates a new {@link Tabular}. * * @param tokens the tokens. * @return the tabular token. */ public static Tabular create(DataToken... tokens) { Assert.requireNonNull(tokens, "Data tokens must not be null"); return new Tabular(Arrays.asList(tokens)); } /** * Decode the {@link Tabular} response from a {@link ByteBuf}. * * @param buffer must not be null. * @param encryptionSupported whether encryption is supported. * @return the decoded {@link Tabular} response {@link Message}. */ public static Tabular decode(ByteBuf buffer, boolean encryptionSupported) { Assert.requireNonNull(buffer, "Buffer must not be null"); return new Tabular(new TabularDecoder(encryptionSupported).decode(buffer)); } /** * Creates a new {@link TabularDecoder}. * * @param encryptionSupported {@code true} if table column encryption is supported. * @return the decoder. */ public static TabularDecoder createDecoder(boolean encryptionSupported) { return new TabularDecoder(encryptionSupported); } /** * Creates a new, stateful {@link DecodeFunction}. * * @param encryptionSupported {@code true} if table column encryption is supported. * @return the decoder. */ private static DecodeFunction decodeFunction(boolean encryptionSupported) { AtomicReference columns = new AtomicReference<>(); return (type, buffer) -> { if (type == EnvChangeToken.TYPE) { return EnvChangeToken.canDecode(buffer) ? EnvChangeToken.decode(buffer) : DecodeFinished.UNABLE_TO_DECODE; } if (type == FeatureExtAckToken.TYPE) { return FeatureExtAckToken.decode(buffer); } if (type == InfoToken.TYPE) { return InfoToken.canDecode(buffer) ? InfoToken.decode(buffer) : DecodeFinished.UNABLE_TO_DECODE; } if (type == ErrorToken.TYPE) { return ErrorToken.canDecode(buffer) ? ErrorToken.decode(buffer) : DecodeFinished.UNABLE_TO_DECODE; } if (type == LoginAckToken.TYPE) { return LoginAckToken.decode(buffer); } if (type == ColumnMetadataToken.TYPE) { if (!ColumnMetadataToken.canDecode(buffer, encryptionSupported)) { return DecodeFinished.UNABLE_TO_DECODE; } ColumnMetadataToken colMetadataToken = ColumnMetadataToken.decode(buffer, encryptionSupported); if (columns.get() == null || colMetadataToken.hasColumns()) { columns.set(colMetadataToken); } return colMetadataToken; } if (type == ColInfoToken.TYPE) { return ColInfoToken.canDecode(buffer) ? ColInfoToken.decode(buffer) : DecodeFinished.UNABLE_TO_DECODE; } if (type == TabnameToken.TYPE) { return TabnameToken.canDecode(buffer) ? TabnameToken.decode(buffer) : DecodeFinished.UNABLE_TO_DECODE; } if (type == DoneToken.TYPE) { if (DoneToken.canDecode(buffer)) { DoneToken decode = DoneToken.decode(buffer); if (!decode.hasMore()) { columns.set(null); } return decode; } return DecodeFinished.UNABLE_TO_DECODE; } if (type == DoneInProcToken.TYPE) { if (DoneInProcToken.canDecode(buffer)) { return DoneInProcToken.decode(buffer); } return DecodeFinished.UNABLE_TO_DECODE; } if (type == DoneProcToken.TYPE) { if (DoneProcToken.canDecode(buffer)) { return DoneProcToken.decode(buffer); } return DecodeFinished.UNABLE_TO_DECODE; } if (type == OrderToken.TYPE) { if (!OrderToken.canDecode(buffer)) { return DecodeFinished.UNABLE_TO_DECODE; } return OrderToken.decode(buffer); } if (type == RowToken.TYPE) { ColumnMetadataToken colMetadataToken = columns.get(); if (!RowToken.canDecode(buffer, colMetadataToken.getColumns())) { return DecodeFinished.UNABLE_TO_DECODE; } return RowToken.decode(buffer, colMetadataToken.getColumns()); } if (type == NbcRowToken.TYPE) { ColumnMetadataToken colMetadataToken = columns.get(); if (!NbcRowToken.canDecode(buffer, colMetadataToken.getColumns())) { return DecodeFinished.UNABLE_TO_DECODE; } return NbcRowToken.decode(buffer, colMetadataToken.getColumns()); } if (type == ReturnStatus.TYPE) { return ReturnStatus.canDecode(buffer) ? ReturnStatus.decode(buffer) : DecodeFinished.UNABLE_TO_DECODE; } if (type == ReturnValue.TYPE) { return ReturnValue.canDecode(buffer, encryptionSupported) ? ReturnValue.decode(buffer, encryptionSupported) : DecodeFinished.UNABLE_TO_DECODE; } throw ProtocolException.invalidTds(String.format("Unable to decode unknown token type 0x%02X", type)); }; } /** * @return the tokens. */ public List getTokens() { return this.tokens; } /** * Resolve a {@link Prelogin.Token} given its {@link Class type}. * * @param filter filter that the desired {@link DataToken} must match. * @return the lookup result or {@code null} if no {@link DataToken} matches. */ @Nullable private DataToken findToken(Predicate filter) { Assert.requireNonNull(filter, "Filter must not be null"); for (DataToken token : this.tokens) { if (filter.test(token)) { return token; } } return null; } /** * Find a {@link DataToken} by its {@link Class type} and a {@link Predicate}. * * @param tokenType type of the desired {@link DataToken}. * @return the lookup result. */ Optional getToken(Class tokenType) { Assert.requireNonNull(tokenType, "Token type must not be null"); return Optional.ofNullable(findToken(tokenType::isInstance)).map(tokenType::cast); } /** * Find a {@link DataToken} by its {@link Class type} and a {@link Predicate}. * * @param tokenType type of the desired {@link DataToken}. * @param filter filter that the desired {@link DataToken} must match. * @return the lookup result. */ Optional getToken(Class tokenType, Predicate filter) { Assert.requireNonNull(tokenType, "Token type must not be null"); Assert.requireNonNull(filter, "Filter must not be null"); Predicate predicate = tokenType::isInstance; return Optional.ofNullable(findToken(predicate.and(dataToken -> filter.test(tokenType.cast(dataToken))))) .map(tokenType::cast); } /** * Find a {@link DataToken} by its {@link Class type} and a {@link Predicate}. * * @param tokenType type of the desired {@link DataToken}. * @return * @throws IllegalArgumentException if no token was found. */ T getRequiredToken(Class tokenType) { return getToken(tokenType).orElseThrow( () -> new IllegalArgumentException(String.format("No token of type [%s] available", tokenType.getName()))); } /** * Find a {@link DataToken} by its {@link Class type} and a {@link Predicate}. * * @param tokenType type of the desired {@link DataToken}. * @param filter filter that the desired {@link DataToken} must match. * @return * @throws IllegalArgumentException if no token was found. */ T getRequiredToken(Class tokenType, Predicate filter) { return getToken(tokenType, filter).orElseThrow( () -> new IllegalArgumentException(String.format("No token of type [%s] available", tokenType.getName()))); } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [tokens=").append(this.tokens); sb.append(']'); return sb.toString(); } /** * Marker {@link DataToken} when decoding is finished. */ enum DecodeFinished implements DataToken { /** * Decoding is finished. */ FINISHED, /** * The {@link DecodeFunction} is not able to decode a {@link DataToken} from the given data buffer. */ UNABLE_TO_DECODE; @Override public byte getType() { return (byte) 0xFF; } @Override public String getName() { return "DECODE_FINISHED"; } } /** * A stateful {@link TabularDecoder}. State is required to decode response chunks in multiple attempts/calls to a {@link DecodeFunction}. Typically, state is a previous * {@link ColumnMetadataToken column description} for row results. * * @author Mark Paluch */ public static class TabularDecoder { private final DecodeFunction decodeFunction; /** * @param encryptionSupported whether encryption is supported. */ TabularDecoder(boolean encryptionSupported) { this.decodeFunction = Tabular.decodeFunction(encryptionSupported); } /** * Decode the {@link Tabular} response from a {@link ByteBuf}. * * @param buffer must not be null. * @return the decoded {@link Tabular} response {@link Message}. */ public List decode(ByteBuf buffer) { Assert.requireNonNull(buffer, "Buffer must not be null"); List tokens = new ArrayList<>(); while (true) { if (buffer.readableBytes() == 0) { break; } int readerIndex = buffer.readerIndex(); byte type = Decode.asByte(buffer); DataToken message = this.decodeFunction.tryDecode(type, buffer); if (message == DecodeFinished.UNABLE_TO_DECODE) { buffer.readerIndex(readerIndex); break; } if (message == DecodeFinished.FINISHED) { break; } tokens.add(message); } return tokens; } /** * Decode the {@link Tabular} response from a {@link ByteBuf}. * * @param buffer must not be null. * @param messageConsumer sink to consume decoded frames. * @return the decoded {@link Tabular} response {@link Message}. */ public boolean decode(ByteBuf buffer, SynchronousSink messageConsumer) { Assert.requireNonNull(buffer, "Buffer must not be null"); boolean hasMessages = false; while (true) { if (buffer.readableBytes() == 0) { break; } int readerIndex = buffer.readerIndex(); byte type = Decode.asByte(buffer); DataToken message = this.decodeFunction.tryDecode(type, buffer); if (message == DecodeFinished.UNABLE_TO_DECODE) { buffer.readerIndex(readerIndex); break; } if (message == DecodeFinished.FINISHED) { break; } messageConsumer.next(message); hasMessages = true; } return hasMessages; } } /** * Decode function for {@link Tabular} streams. Can be called incrementally until the {@link #tryDecode(byte, ByteBuf) decode method} returns {@link DecodeFinished}. */ @FunctionalInterface interface DecodeFunction { /** * Try to decode a {@link DataToken} from the {@link ByteBuf data buffer}. * * @param type token type. * @param buffer the data buffer. * @return a decoded {@link DataToken} or a {@link DecodeFinished} marker. */ DataToken tryDecode(byte type, ByteBuf buffer); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/TokenStream.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; /** * A token stream message. */ public interface TokenStream { /** * @return symbolic name of the {@link TokenStream}. */ String getName(); } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/token/package-info.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * The infrastructure for exchanging messages with the server. *

    * Token stream message structures. *

    * Token stream message structures. *

    * Token stream message structures. */ /** * Token stream message structures. */ @NonNullApi package io.r2dbc.mssql.message.token; import reactor.util.annotation.NonNullApi; ================================================ FILE: src/main/java/io/r2dbc/mssql/message/type/AbstractTypeDecoderStrategy.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.type; import io.netty.buffer.ByteBuf; /** * Support class for {@link TypeDecoderStrategy} implementations. * * @author Mark Paluch */ abstract class AbstractTypeDecoderStrategy implements TypeDecoderStrategy { private final int typeDescriptorLength; /** * Creates a new {@link AbstractTypeDecoderStrategy}. * * @param typeDescriptorLength length in bytes. */ AbstractTypeDecoderStrategy(int typeDescriptorLength) { this.typeDescriptorLength = typeDescriptorLength; } @Override public final boolean canDecode(ByteBuf buffer) { return buffer.readableBytes() >= this.typeDescriptorLength; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/type/Collation.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.type; import io.netty.buffer.ByteBuf; import io.netty.util.collection.LongObjectHashMap; import io.netty.util.collection.LongObjectMap; import io.r2dbc.mssql.message.tds.Decode; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.message.tds.ProtocolException; import io.r2dbc.mssql.message.tds.ServerCharset; import io.r2dbc.mssql.util.Assert; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.util.HashMap; import java.util.Map; import java.util.Objects; /** * A collation represents encoding and collation used for character data types. Collation is in the following BNF format * (see TDS spec for full details): *

      *
    • LCID := 20 BIT
    • *
    • fIgnoreCase := BIT
    • *
    • fIgnoreAccent := BIT
    • *
    • fIgnoreWidth := BIT
    • *
    • fIgnoreKana := BIT
    • *
    • fBinary := BIT
    • *
    • ColFlags := fIgnoreCase, fIgnoreAccent, fIgnoreWidth, fIgnoreKana, fBinary, FRESERVEDBIT, FRESERVEDBIT, FRESERVEDBIT
    • *
    • Version := 4 BIT
    • *
    • SortId := BYTE
    • *
    * * @author Mark Paluch * @see ServerCharset */ @SuppressWarnings("unused") public final class Collation { private static final LongObjectMap COLLATIONS = new LongObjectHashMap<>(); public static final Collation RAW = Collation.from(0, 0); private static final int UTF8_IN_TDSCOLLATION = 0x4000000; // Index from of windows locales by their LangIDs for fast lookup // of encodings associated with various SQL collations private static final Map localeCache; private static final Map sortOrderCache; static { // Populate the windows locale and sort order indices localeCache = new HashMap<>(); for (WindowsLocale locale : WindowsLocale.values()) { localeCache.put(locale.langID, locale); } sortOrderCache = new HashMap<>(); for (SortOrder sortOrder : SortOrder.values()) { sortOrderCache.put(sortOrder.sortId, sortOrder); } } private final int lcid; // First 4 bytes of TDS collation. private final int sortId; // 5th byte of TDS collation. private final ServerCharset serverCharset; private Collation(int lcid, int sortId) throws UnsupportedEncodingException { this.lcid = lcid; this.sortId = sortId; if (lcid != 0 || sortId != 0) { if (UTF8_IN_TDSCOLLATION == (lcid & UTF8_IN_TDSCOLLATION)) { this.serverCharset = ServerCharset.UTF8; } else { // For a SortId==0 collation, the LCID bits correspond to a LocaleId this.serverCharset = (0 == sortId) ? getEncodingFromLCID() : getEncodingFromSortId(); } } else { this.serverCharset = ServerCharset.CP1252; } } /** * Create a new {@link Collation} from LCID and {@code sortId}. * * @param lcid locale Id * @param sortId sort Id * @return the {@link Collation}. */ public static Collation from(int lcid, int sortId) { Collation collation; long cacheKey = lcid | (sortId << 4); synchronized (COLLATIONS) { collation = COLLATIONS.get(cacheKey); if (collation == null) { try { collation = new Collation(lcid, sortId); } catch (UnsupportedEncodingException e) { throw ProtocolException.unsupported(e); } COLLATIONS.put(cacheKey, collation); } return collation; } } /** * Decode the {@link Collation}. * * @param buffer the buffer. * @return the decoded Collation. */ public static Collation decode(ByteBuf buffer) { Assert.requireNonNull(buffer, "Buffer must not be null"); int info = Decode.asInt(buffer); // 4 bytes, contains: LCID ColFlags Version int sortId = Decode.uByte(buffer); // 1 byte, contains: SortId return Collation.from(info, sortId); } static int getLength() { return 5; } // Length of collation in TDS (in bytes) /** * Encode the collation. * * @param buffer the data buffer. */ public void encode(ByteBuf buffer) { Assert.requireNonNull(buffer, "Data buffer must not be null"); Encode.asInt(buffer, this.lcid); Encode.asByte(buffer, (byte) this.sortId); } // Utility methods for getting details of this collation's encoding public Charset getCharset() { return this.serverCharset.charset(); } /** * Returns the collation info. * * @return the LCID (collation info). */ int getLCID() { return this.lcid; } /** * Returns the sort Id * * @return the sort Od. */ int getSortId() { return this.sortId; } /** * Returns whether the underlying encoding supports ASCII conversion. * * @return {@code true} if the underlying encoding supports ASCII conversion. */ boolean supportsAsciiConversion() { return this.serverCharset.supportsAsciiConversion(); } /** * Returns whether the underlying encoding allows fast-path ASCII conversion by filtering lower ASCII chars. * * @return {@code true} the underlying encoding allows fast-path ASCII. */ boolean hasAsciiCompatibleSBCS() { return this.serverCharset.hasAsciiCompatibleSBCS(); } private int getLanguageId() { return this.lcid & 0x0000FFFF; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof Collation)) { return false; } Collation collation = (Collation) o; return this.lcid == collation.lcid && this.sortId == collation.sortId; } @Override public int hashCode() { return Objects.hash(this.lcid, this.sortId); } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [encoding=").append(this.serverCharset); sb.append(']'); return sb.toString(); } private ServerCharset getEncodingFromLCID() throws UnsupportedEncodingException { WindowsLocale locale = localeCache.get(getLanguageId()); if (locale == null) { throw new UnsupportedEncodingException(String.format("Windows collation not supported: %s", Integer.toHexString(getLanguageId()).toUpperCase())); } try { return locale.getServerCharset(); } catch (RuntimeException inner) { UnsupportedEncodingException e = new UnsupportedEncodingException(String.format("Windows collation not supported: %s", Integer.toHexString(getLanguageId()).toUpperCase())); e.initCause(inner); throw e; } } private ServerCharset getEncodingFromSortId() throws UnsupportedEncodingException { SortOrder sortOrder = sortOrderCache.get(this.sortId); if (sortOrder == null) { throw new UnsupportedEncodingException(String.format("SQL Server collation is not supported: %d", this.sortId)); } try { return sortOrder.getServerCharset(); } catch (RuntimeException inner) { UnsupportedEncodingException e = new UnsupportedEncodingException(String.format("SQL Server collation is not supported: %d", this.sortId)); e.initCause(inner); throw e; } } /** * Enumeration of Windows locales recognized by SQL Server. *

    * For our purposes in the driver, locales are only described by their LangID and character encodings. * The set of locales is derived from the following resources: * https://download.microsoft.com/download/9/5/e/95ef66af-9026-4bb0-a41d-a4f81802d92c/[MS-LCID].pdf Lists LCID values * and their corresponding meanings (in RFC 3066 format). Used to derive the names for the various enumeration * constants. *

    * Collectively, these two * tables provide a mapping of collation-version specific encodings for every locale supported by SQL Server. Lang * IDs are derived from locales' LCIDs. */ enum WindowsLocale { ar_SA(0x0401, ServerCharset.CP1256), bg_BG(0x0402, ServerCharset.CP1251), ca_ES(0x0403, ServerCharset.CP1252), zh_TW(0x0404, ServerCharset.CP950), cs_CZ(0x0405, ServerCharset.CP1250), da_DK(0x0406, ServerCharset.CP1252), de_DE(0x0407, ServerCharset.CP1252), el_GR(0x0408, ServerCharset.CP1253), en_US(0x0409, ServerCharset.CP1252), es_ES_tradnl(0x040a, ServerCharset.CP1252), fi_FI(0x040b, ServerCharset.CP1252), fr_FR(0x040c, ServerCharset.CP1252), he_IL(0x040d, ServerCharset.CP1255), hu_HU(0x040e, ServerCharset.CP1250), is_IS(0x040f, ServerCharset.CP1252), it_IT(0x0410, ServerCharset.CP1252), ja_JP(0x0411, ServerCharset.CP932), ko_KR(0x0412, ServerCharset.CP949), nl_NL(0x0413, ServerCharset.CP1252), nb_NO(0x0414, ServerCharset.CP1252), pl_PL(0x0415, ServerCharset.CP1250), pt_BR(0x0416, ServerCharset.CP1252), rm_CH(0x0417, ServerCharset.CP1252), ro_RO(0x0418, ServerCharset.CP1250), ru_RU(0x0419, ServerCharset.CP1251), hr_HR(0x041a, ServerCharset.CP1250), sk_SK(0x041b, ServerCharset.CP1250), sq_AL(0x041c, ServerCharset.CP1250), sv_SE(0x041d, ServerCharset.CP1252), th_TH(0x041e, ServerCharset.CP874), tr_TR(0x041f, ServerCharset.CP1254), ur_PK(0x0420, ServerCharset.CP1256), id_ID(0x0421, ServerCharset.CP1252), uk_UA(0x0422, ServerCharset.CP1251), be_BY(0x0423, ServerCharset.CP1251), sl_SI(0x0424, ServerCharset.CP1250), et_EE(0x0425, ServerCharset.CP1257), lv_LV(0x0426, ServerCharset.CP1257), lt_LT(0x0427, ServerCharset.CP1257), tg_Cyrl_TJ(0x0428, ServerCharset.CP1251), fa_IR(0x0429, ServerCharset.CP1256), vi_VN(0x042a, ServerCharset.CP1258), hy_AM(0x042b, ServerCharset.CP1252), az_Latn_AZ(0x042c, ServerCharset.CP1254), eu_ES(0x042d, ServerCharset.CP1252), wen_DE(0x042e, ServerCharset.CP1252), mk_MK(0x042f, ServerCharset.CP1251), tn_ZA(0x0432, ServerCharset.CP1252), xh_ZA(0x0434, ServerCharset.CP1252), zu_ZA(0x0435, ServerCharset.CP1252), Af_ZA(0x0436, ServerCharset.CP1252), ka_GE(0x0437, ServerCharset.CP1252), fo_FO(0x0438, ServerCharset.CP1252), hi_IN(0x0439, ServerCharset.UNICODE), mt_MT(0x043a, ServerCharset.UNICODE), se_NO(0x043b, ServerCharset.CP1252), ms_MY(0x043e, ServerCharset.CP1252), kk_KZ(0x043f, ServerCharset.CP1251), ky_KG(0x0440, ServerCharset.CP1251), sw_KE(0x0441, ServerCharset.CP1252), tk_TM(0x0442, ServerCharset.CP1250), uz_Latn_UZ(0x0443, ServerCharset.CP1254), tt_RU(0x0444, ServerCharset.CP1251), bn_IN(0x0445, ServerCharset.UNICODE), pa_IN(0x0446, ServerCharset.UNICODE), gu_IN(0x0447, ServerCharset.UNICODE), or_IN(0x0448, ServerCharset.UNICODE), ta_IN(0x0449, ServerCharset.UNICODE), te_IN(0x044a, ServerCharset.UNICODE), kn_IN(0x044b, ServerCharset.UNICODE), ml_IN(0x044c, ServerCharset.UNICODE), as_IN(0x044d, ServerCharset.UNICODE), mr_IN(0x044e, ServerCharset.UNICODE), sa_IN(0x044f, ServerCharset.UNICODE), mn_MN(0x0450, ServerCharset.CP1251), bo_CN(0x0451, ServerCharset.UNICODE), cy_GB(0x0452, ServerCharset.CP1252), km_KH(0x0453, ServerCharset.UNICODE), lo_LA(0x0454, ServerCharset.UNICODE), gl_ES(0x0456, ServerCharset.CP1252), kok_IN(0x0457, ServerCharset.UNICODE), syr_SY(0x045a, ServerCharset.UNICODE), si_LK(0x045b, ServerCharset.UNICODE), iu_Cans_CA(0x045d, ServerCharset.CP1252), am_ET(0x045e, ServerCharset.CP1252), ne_NP(0x0461, ServerCharset.UNICODE), fy_NL(0x0462, ServerCharset.CP1252), ps_AF(0x0463, ServerCharset.UNICODE), fil_PH(0x0464, ServerCharset.CP1252), dv_MV(0x0465, ServerCharset.UNICODE), ha_Latn_NG(0x0468, ServerCharset.CP1252), yo_NG(0x046a, ServerCharset.CP1252), quz_BO(0x046b, ServerCharset.CP1252), nso_ZA(0x046c, ServerCharset.CP1252), ba_RU(0x046d, ServerCharset.CP1251), lb_LU(0x046e, ServerCharset.CP1252), kl_GL(0x046f, ServerCharset.CP1252), ig_NG(0x0470, ServerCharset.CP1252), ii_CN(0x0478, ServerCharset.CP1252), arn_CL(0x047a, ServerCharset.CP1252), moh_CA(0x047c, ServerCharset.CP1252), br_FR(0x047e, ServerCharset.CP1252), ug_CN(0x0480, ServerCharset.CP1256), mi_NZ(0x0481, ServerCharset.UNICODE), oc_FR(0x0482, ServerCharset.CP1252), co_FR(0x0483, ServerCharset.CP1252), gsw_FR(0x0484, ServerCharset.CP1252), sah_RU(0x0485, ServerCharset.CP1251), qut_GT(0x0486, ServerCharset.CP1252), rw_RW(0x0487, ServerCharset.CP1252), wo_SN(0x0488, ServerCharset.CP1252), prs_AF(0x048c, ServerCharset.CP1256), ar_IQ(0x0801, ServerCharset.CP1256), zh_CN(0x0804, ServerCharset.CP936), de_CH(0x0807, ServerCharset.CP1252), en_GB(0x0809, ServerCharset.CP1252), es_MX(0x080a, ServerCharset.CP1252), fr_BE(0x080c, ServerCharset.CP1252), it_CH(0x0810, ServerCharset.CP1252), nl_BE(0x0813, ServerCharset.CP1252), nn_NO(0x0814, ServerCharset.CP1252), pt_PT(0x0816, ServerCharset.CP1252), sr_Latn_CS(0x081a, ServerCharset.CP1250), sv_FI(0x081d, ServerCharset.CP1252), Lithuanian_Classic(0x0827, ServerCharset.CP1257), az_Cyrl_AZ(0x082c, ServerCharset.CP1251), dsb_DE(0x082e, ServerCharset.CP1252), se_SE(0x083b, ServerCharset.CP1252), ga_IE(0x083c, ServerCharset.CP1252), ms_BN(0x083e, ServerCharset.CP1252), uz_Cyrl_UZ(0x0843, ServerCharset.CP1251), bn_BD(0x0845, ServerCharset.UNICODE), mn_Mong_CN(0x0850, ServerCharset.CP1251), iu_Latn_CA(0x085d, ServerCharset.CP1252), tzm_Latn_DZ(0x085f, ServerCharset.CP1252), quz_EC(0x086b, ServerCharset.CP1252), ar_EG(0x0c01, ServerCharset.CP1256), zh_HK(0x0c04, ServerCharset.CP950), de_AT(0x0c07, ServerCharset.CP1252), en_AU(0x0c09, ServerCharset.CP1252), es_ES(0x0c0a, ServerCharset.CP1252), fr_CA(0x0c0c, ServerCharset.CP1252), sr_Cyrl_CS(0x0c1a, ServerCharset.CP1251), se_FI(0x0c3b, ServerCharset.CP1252), quz_PE(0x0c6b, ServerCharset.CP1252), ar_LY(0x1001, ServerCharset.CP1256), zh_SG(0x1004, ServerCharset.CP936), de_LU(0x1007, ServerCharset.CP1252), en_CA(0x1009, ServerCharset.CP1252), es_GT(0x100a, ServerCharset.CP1252), fr_CH(0x100c, ServerCharset.CP1252), hr_BA(0x101a, ServerCharset.CP1250), smj_NO(0x103b, ServerCharset.CP1252), ar_DZ(0x1401, ServerCharset.CP1256), zh_MO(0x1404, ServerCharset.CP950), de_LI(0x1407, ServerCharset.CP1252), en_NZ(0x1409, ServerCharset.CP1252), es_CR(0x140a, ServerCharset.CP1252), fr_LU(0x140c, ServerCharset.CP1252), bs_Latn_BA(0x141a, ServerCharset.CP1250), smj_SE(0x143b, ServerCharset.CP1252), ar_MA(0x1801, ServerCharset.CP1256), en_IE(0x1809, ServerCharset.CP1252), es_PA(0x180a, ServerCharset.CP1252), fr_MC(0x180c, ServerCharset.CP1252), sr_Latn_BA(0x181a, ServerCharset.CP1250), sma_NO(0x183b, ServerCharset.CP1252), ar_TN(0x1c01, ServerCharset.CP1256), en_ZA(0x1c09, ServerCharset.CP1252), es_DO(0x1c0a, ServerCharset.CP1252), sr_Cyrl_BA(0x1c1a, ServerCharset.CP1251), sma_SB(0x1c3b, ServerCharset.CP1252), ar_OM(0x2001, ServerCharset.CP1256), en_JM(0x2009, ServerCharset.CP1252), es_VE(0x200a, ServerCharset.CP1252), bs_Cyrl_BA(0x201a, ServerCharset.CP1251), sms_FI(0x203b, ServerCharset.CP1252), ar_YE(0x2401, ServerCharset.CP1256), en_CB(0x2409, ServerCharset.CP1252), es_CO(0x240a, ServerCharset.CP1252), smn_FI(0x243b, ServerCharset.CP1252), ar_SY(0x2801, ServerCharset.CP1256), en_BZ(0x2809, ServerCharset.CP1252), es_PE(0x280a, ServerCharset.CP1252), ar_JO(0x2c01, ServerCharset.CP1256), en_TT(0x2c09, ServerCharset.CP1252), es_AR(0x2c0a, ServerCharset.CP1252), ar_LB(0x3001, ServerCharset.CP1256), en_ZW(0x3009, ServerCharset.CP1252), es_EC(0x300a, ServerCharset.CP1252), ar_KW(0x3401, ServerCharset.CP1256), en_PH(0x3409, ServerCharset.CP1252), es_CL(0x340a, ServerCharset.CP1252), ar_AE(0x3801, ServerCharset.CP1256), es_UY(0x380a, ServerCharset.CP1252), ar_BH(0x3c01, ServerCharset.CP1256), es_PY(0x3c0a, ServerCharset.CP1252), ar_QA(0x4001, ServerCharset.CP1256), en_IN(0x4009, ServerCharset.CP1252), es_BO(0x400a, ServerCharset.CP1252), en_MY(0x4409, ServerCharset.CP1252), es_SV(0x440a, ServerCharset.CP1252), en_SG(0x4809, ServerCharset.CP1252), es_HN(0x480a, ServerCharset.CP1252), es_NI(0x4c0a, ServerCharset.CP1252), es_PR(0x500a, ServerCharset.CP1252), es_US(0x540a, ServerCharset.CP1252); private final int langID; private final ServerCharset serverCharset; WindowsLocale(int langID, ServerCharset serverCharset) { this.langID = langID; this.serverCharset = serverCharset; } ServerCharset getServerCharset() { this.serverCharset.charset(); return this.serverCharset; } } /** * Enumeration of original SQL Server sort orders recognized by SQL Server. */ enum SortOrder { BIN_CP437(30, "SQL_Latin1_General_CP437_BIN", ServerCharset.CP437), DICTIONARY_437(31, "SQL_Latin1_General_CP437_CS_AS", ServerCharset.CP437), NOCASE_437(32, "SQL_Latin1_General_CP437_CI_AS", ServerCharset.CP437), NOCASEPREF_437(33, "SQL_Latin1_General_Pref_CP437_CI_AS", ServerCharset.CP437), NOACCENTS_437(34, "SQL_Latin1_General_CP437_CI_AI", ServerCharset.CP437), BIN2_CP437(35, "SQL_Latin1_General_CP437_BIN2", ServerCharset.CP437), BIN_CP850(40, "SQL_Latin1_General_CP850_BIN", ServerCharset.CP850), DICTIONARY_850(41, "SQL_Latin1_General_CP850_CS_AS", ServerCharset.CP850), NOCASE_850(42, "SQL_Latin1_General_CP850_CI_AS", ServerCharset.CP850), NOCASEPREF_850(43, "SQL_Latin1_General_Pref_CP850_CI_AS", ServerCharset.CP850), NOACCENTS_850(44, "SQL_Latin1_General_CP850_CI_AI", ServerCharset.CP850), BIN2_CP850(45, "SQL_Latin1_General_CP850_BIN2", ServerCharset.CP850), CASELESS_34(49, "SQL_1xCompat_CP850_CI_AS", ServerCharset.CP850), BIN_ISO_1(50, "bin_iso_1", ServerCharset.CP1252), DICTIONARY_ISO(51, "SQL_Latin1_General_CP1_CS_AS", ServerCharset.CP1252), NOCASE_ISO(52, "SQL_Latin1_General_CP1_CI_AS", ServerCharset.CP1252), NOCASEPREF_ISO(53, "SQL_Latin1_General_Pref_CP1_CI_AS", ServerCharset.CP1252), NOACCENTS_ISO(54, "SQL_Latin1_General_CP1_CI_AI", ServerCharset.CP1252), ALT_DICTIONARY(55, "SQL_AltDiction_CP850_CS_AS", ServerCharset.CP850), ALT_NOCASEPREF(56, "SQL_AltDiction_Pref_CP850_CI_AS", ServerCharset.CP850), ALT_NOACCENTS(57, "SQL_AltDiction_CP850_CI_AI", ServerCharset.CP850), SCAND_NOCASEPREF(58, "SQL_Scandinavian_Pref_CP850_CI_AS", ServerCharset.CP850), SCAND_DICTIONARY(59, "SQL_Scandinavian_CP850_CS_AS", ServerCharset.CP850), SCAND_NOCASE(60, "SQL_Scandinavian_CP850_CI_AS", ServerCharset.CP850), ALT_NOCASE(61, "SQL_AltDiction_CP850_CI_AS", ServerCharset.CP850), DICTIONARY_1252(71, "dictionary_1252", ServerCharset.CP1252), NOCASE_1252(72, "nocase_1252", ServerCharset.CP1252), DNK_NOR_DICTIONARY(73, "dnk_nor_dictionary", ServerCharset.CP1252), FIN_SWE_DICTIONARY(74, "fin_swe_dictionary", ServerCharset.CP1252), ISL_DICTIONARY(75, "isl_dictionary", ServerCharset.CP1252), BIN_CP1250(80, "bin_cp1250", ServerCharset.CP1250), DICTIONARY_1250(81, "SQL_Latin1_General_CP1250_CS_AS", ServerCharset.CP1250), NOCASE_1250(82, "SQL_Latin1_General_CP1250_CI_AS", ServerCharset.CP1250), CSYDIC(83, "SQL_Czech_CP1250_CS_AS", ServerCharset.CP1250), CSYNC(84, "SQL_Czech_CP1250_CI_AS", ServerCharset.CP1250), HUNDIC(85, "SQL_Hungarian_CP1250_CS_AS", ServerCharset.CP1250), HUNNC(86, "SQL_Hungarian_CP1250_CI_AS", ServerCharset.CP1250), PLKDIC(87, "SQL_Polish_CP1250_CS_AS", ServerCharset.CP1250), PLKNC(88, "SQL_Polish_CP1250_CI_AS", ServerCharset.CP1250), ROMDIC(89, "SQL_Romanian_CP1250_CS_AS", ServerCharset.CP1250), ROMNC(90, "SQL_Romanian_CP1250_CI_AS", ServerCharset.CP1250), SHLDIC(91, "SQL_Croatian_CP1250_CS_AS", ServerCharset.CP1250), SHLNC(92, "SQL_Croatian_CP1250_CI_AS", ServerCharset.CP1250), SKYDIC(93, "SQL_Slovak_CP1250_CS_AS", ServerCharset.CP1250), SKYNC(94, "SQL_Slovak_CP1250_CI_AS", ServerCharset.CP1250), SLVDIC(95, "SQL_Slovenian_CP1250_CS_AS", ServerCharset.CP1250), SLVNC(96, "SQL_Slovenian_CP1250_CI_AS", ServerCharset.CP1250), POLISH_CS(97, "polish_cs", ServerCharset.CP1250), POLISH_CI(98, "polish_ci", ServerCharset.CP1250), BIN_CP1251(104, "bin_cp1251", ServerCharset.CP1251), DICTIONARY_1251(105, "SQL_Latin1_General_CP1251_CS_AS", ServerCharset.CP1251), NOCASE_1251(106, "SQL_Latin1_General_CP1251_CI_AS", ServerCharset.CP1251), UKRDIC(107, "SQL_Ukrainian_CP1251_CS_AS", ServerCharset.CP1251), UKRNC(108, "SQL_Ukrainian_CP1251_CI_AS", ServerCharset.CP1251), BIN_CP1253(112, "bin_cp1253", ServerCharset.CP1253), DICTIONARY_1253(113, "SQL_Latin1_General_CP1253_CS_AS", ServerCharset.CP1253), NOCASE_1253(114, "SQL_Latin1_General_CP1253_CI_AS", ServerCharset.CP1253), GREEK_MIXEDDICTIONARY(120, "SQL_MixDiction_CP1253_CS_AS", ServerCharset.CP1253), GREEK_ALTDICTIONARY(121, "SQL_AltDiction_CP1253_CS_AS", ServerCharset.CP1253), GREEK_ALTDICTIONARY2(122, "SQL_AltDiction2_CP1253_CS_AS", ServerCharset.CP1253), GREEK_NOCASEDICT(124, "SQL_Latin1_General_CP1253_CI_AI", ServerCharset.CP1253), BIN_CP1254(128, "bin_cp1254", ServerCharset.CP1254), DICTIONARY_1254(129, "SQL_Latin1_General_CP1254_CS_AS", ServerCharset.CP1254), NOCASE_1254(130, "SQL_Latin1_General_CP1254_CI_AS", ServerCharset.CP1254), BIN_CP1255(136, "bin_cp1255", ServerCharset.CP1255), DICTIONARY_1255(137, "SQL_Latin1_General_CP1255_CS_AS", ServerCharset.CP1255), NOCASE_1255(138, "SQL_Latin1_General_CP1255_CI_AS", ServerCharset.CP1255), BIN_CP1256(144, "bin_cp1256", ServerCharset.CP1256), DICTIONARY_1256(145, "SQL_Latin1_General_CP1256_CS_AS", ServerCharset.CP1256), NOCASE_1256(146, "SQL_Latin1_General_CP1256_CI_AS", ServerCharset.CP1256), BIN_CP1257(152, "bin_cp1257", ServerCharset.CP1257), DICTIONARY_1257(153, "SQL_Latin1_General_CP1257_CS_AS", ServerCharset.CP1257), NOCASE_1257(154, "SQL_Latin1_General_CP1257_CI_AS", ServerCharset.CP1257), ETIDIC(155, "SQL_Estonian_CP1257_CS_AS", ServerCharset.CP1257), ETINC(156, "SQL_Estonian_CP1257_CI_AS", ServerCharset.CP1257), LVIDIC(157, "SQL_Latvian_CP1257_CS_AS", ServerCharset.CP1257), LVINC(158, "SQL_Latvian_CP1257_CI_AS", ServerCharset.CP1257), LTHDIC(159, "SQL_Lithuanian_CP1257_CS_AS", ServerCharset.CP1257), LTHNC(160, "SQL_Lithuanian_CP1257_CI_AS", ServerCharset.CP1257), DANNO_NOCASEPREF(183, "SQL_Danish_Pref_CP1_CI_AS", ServerCharset.CP1252), SVFI1_NOCASEPREF(184, "SQL_SwedishPhone_Pref_CP1_CI_AS", ServerCharset.CP1252), SVFI2_NOCASEPREF(185, "SQL_SwedishStd_Pref_CP1_CI_AS", ServerCharset.CP1252), ISLAN_NOCASEPREF(186, "SQL_Icelandic_Pref_CP1_CI_AS", ServerCharset.CP1252), BIN_CP932(192, "bin_cp932", ServerCharset.CP932), NLS_CP932(193, "nls_cp932", ServerCharset.CP932), BIN_CP949(194, "bin_cp949", ServerCharset.CP949), NLS_CP949(195, "nls_cp949", ServerCharset.CP949), BIN_CP950(196, "bin_cp950", ServerCharset.CP950), NLS_CP950(197, "nls_cp950", ServerCharset.CP950), BIN_CP936(198, "bin_cp936", ServerCharset.CP936), NLS_CP936(199, "nls_cp936", ServerCharset.CP936), NLS_CP932_CS(200, "nls_cp932_cs", ServerCharset.CP932), NLS_CP949_CS(201, "nls_cp949_cs", ServerCharset.CP949), NLS_CP950_CS(202, "nls_cp950_cs", ServerCharset.CP950), NLS_CP936_CS(203, "nls_cp936_cs", ServerCharset.CP936), BIN_CP874(204, "bin_cp874", ServerCharset.CP874), NLS_CP874(205, "nls_cp874", ServerCharset.CP874), NLS_CP874_CS(206, "nls_cp874_cs", ServerCharset.CP874), EBCDIC_037(210, "SQL_EBCDIC037_CP1_CS_AS", ServerCharset.CP1252), EBCDIC_273(211, "SQL_EBCDIC273_CP1_CS_AS", ServerCharset.CP1252), EBCDIC_277(212, "SQL_EBCDIC277_CP1_CS_AS", ServerCharset.CP1252), EBCDIC_278(213, "SQL_EBCDIC278_CP1_CS_AS", ServerCharset.CP1252), EBCDIC_280(214, "SQL_EBCDIC280_CP1_CS_AS", ServerCharset.CP1252), EBCDIC_284(215, "SQL_EBCDIC284_CP1_CS_AS", ServerCharset.CP1252), EBCDIC_285(216, "SQL_EBCDIC285_CP1_CS_AS", ServerCharset.CP1252), EBCDIC_297(217, "SQL_EBCDIC297_CP1_CS_AS", ServerCharset.CP1252); private final int sortId; private final String name; private final ServerCharset serverCharset; SortOrder(int sortId, String name, ServerCharset serverCharset) { this.sortId = sortId; this.name = name; this.serverCharset = serverCharset; } ServerCharset getServerCharset() { this.serverCharset.charset(); return this.serverCharset; } public final String toString() { return this.name; } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/type/Length.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.type; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.tds.Decode; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.message.tds.ProtocolException; /** * Descriptor for data length in row results. * Use {@link Length} to encode/decode length headers of a PLP chunk and {@link PlpLength} to * encode/decode the the total PLP stream length. * * @author Mark Paluch * @see PlpLength */ public final class Length { public static final int USHORT_NULL = 65535; public static final int UNKNOWN_STREAM_LENGTH = -1; private static final int CACHE_ENTRIES = 1024; private static final Length NULL; private static final Length[] CACHE; private static final Length UNKNOWN_NULL; private static final Length UNKNOWN; static { CACHE = new Length[CACHE_ENTRIES]; for (int i = 0; i < CACHE_ENTRIES; i++) { CACHE[i] = new Length(i, false); } NULL = new Length(0, true); UNKNOWN = new Length(UNKNOWN_STREAM_LENGTH, false); UNKNOWN_NULL = new Length(UNKNOWN_STREAM_LENGTH, true); } private final int length; private final boolean isNull; private Length(int length, boolean isNull) { this.length = length; this.isNull = isNull; } /** * Creates a {@link Length} that indicates the value is {@code null}. * * @return a {@link Length} for {@code null}. */ public static Length nullLength() { return of(0, true); } /** * Creates a {@link Length} with a given {@code length}. * * @param length value length. * @return a {@link Length} for a non-{@code null} value of the given {@code length}. */ public static Length of(int length) { return of(length, false); } /** * Creates a {@link Length}. * * @param length value length. * @param isNull {@code true} if the value is {@code null}. * @return the {@link Length}. */ public static Length of(int length, boolean isNull) { if (length == UNKNOWN_STREAM_LENGTH) { return isNull ? UNKNOWN_NULL : UNKNOWN; } if (isNull) { return NULL; } if (length < 0) { throw new IllegalArgumentException("length must be greater or equal to zero"); } if (length > (CACHE_ENTRIES - 1)) { return new Length(length, isNull); } return CACHE[length]; } /** * Decode a {@link Length} for a {@link TypeInformation}. * * @param buffer the data buffer. * @param type {@link TypeInformation}. * @return the {@link Length}. */ public static Length decode(ByteBuf buffer, TypeInformation type) { switch (type.getLengthStrategy()) { case PARTLENTYPE: { int length = Decode.asInt(buffer); return Length.of(length, false); } case FIXEDLENTYPE: return Length.of(type.getMaxLength(), type.getMaxLength() == 0); case BYTELENTYPE: { int length = Decode.uByte(buffer); return Length.of(length, length == 0); } case USHORTLENTYPE: { int length = Decode.uShort(buffer); return Length.of(length == USHORT_NULL ? 0 : length, length == USHORT_NULL); } case LONGLENTYPE: { SqlServerType serverType = type.getServerType(); if (serverType == SqlServerType.TEXT || serverType == SqlServerType.IMAGE || serverType == SqlServerType.NTEXT) { int nullMarker = Decode.uByte(buffer); if (nullMarker == 0) { return new Length(0, true); } // skip(24) is to skip the textptr and timestamp fields buffer.skipBytes(24); int valueLength = Decode.asLong(buffer); return Length.of(valueLength, false); } if (serverType == SqlServerType.SQL_VARIANT) { int valueLength = Decode.asInt(buffer); return Length.of(valueLength, valueLength == 0); } int length = Decode.uShort(buffer); return Length.of(length == USHORT_NULL ? 0 : length, length == USHORT_NULL); } } throw ProtocolException.invalidTds("Cannot parse value LengthDescriptor"); } /** * Check whether the {@link ByteBuf} can be decoded into an {@link Length}. * * @param buffer the data buffer. * @param type {@link TypeInformation}. * @return {@code true} if the buffer contains sufficient data to decode a {@link Length}. */ public static boolean canDecode(ByteBuf buffer, TypeInformation type) { int readerIndex = buffer.readerIndex(); try { return doCanDecode(buffer, type); } finally { buffer.readerIndex(readerIndex); } } private static boolean doCanDecode(ByteBuf buffer, TypeInformation type) { switch (type.getLengthStrategy()) { case PARTLENTYPE: return buffer.readableBytes() >= 4; case FIXEDLENTYPE: return true; case BYTELENTYPE: case USHORTLENTYPE: return buffer.readableBytes() >= 2; case LONGLENTYPE: { SqlServerType serverType = type.getServerType(); if (serverType == SqlServerType.TEXT || serverType == SqlServerType.IMAGE || serverType == SqlServerType.NTEXT) { if (buffer.readableBytes() == 0) { return false; } int nullMarker = Decode.uByte(buffer); if (nullMarker == 0) { return true; } // skip(24) is to skip the textptr and timestamp fields return buffer.readableBytes() >= 24 + /* int */ 4; } if (serverType == SqlServerType.SQL_VARIANT) { return buffer.readableBytes() >= 4; } return buffer.readableBytes() >= 2; } } throw ProtocolException.invalidTds("Cannot parse value LengthDescriptor"); } public void encode(ByteBuf buffer, TypeInformation type) { LengthStrategy lengthStrategy = type.getLengthStrategy(); if (lengthStrategy == LengthStrategy.LONGLENTYPE) { SqlServerType serverType = type.getServerType(); if (serverType == SqlServerType.TEXT || serverType == SqlServerType.IMAGE || serverType == SqlServerType.NTEXT) { if (isNull()) { Encode.asByte(buffer, (byte) 0); return; } // skip(24) is to skip the textptr and timestamp fields buffer.skipBytes(24); buffer.writeLong(getLength()); return; } if (serverType == SqlServerType.SQL_VARIANT) { Encode.intBigEndian(buffer, getLength()); return; } if (isNull()) { Encode.uShortBE(buffer, USHORT_NULL); } else { Encode.uShortBE(buffer, getLength()); } return; } encode(buffer, lengthStrategy); } public void encode(ByteBuf buffer, LengthStrategy lengthStrategy) { switch (lengthStrategy) { case PARTLENTYPE: Encode.asInt(buffer, getLength()); return; case FIXEDLENTYPE: return; case BYTELENTYPE: if (isNull()) { Encode.asByte(buffer, (byte) 0); } else { Encode.asByte(buffer, (byte) getLength()); } return; case USHORTLENTYPE: if (isNull()) { Encode.uShort(buffer, USHORT_NULL); } else { Encode.uShort(buffer, getLength()); } return; } throw ProtocolException.invalidTds("Cannot encode value LengthDescriptor for " + lengthStrategy); } public int getLength() { return this.length; } public boolean isNull() { return this.isNull; } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [length=").append(this.length); sb.append(", isNull=").append(this.isNull); sb.append(']'); return sb.toString(); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/type/LengthStrategy.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.type; /** * SQL Server length strategies. */ public enum LengthStrategy { /** * Fixed-length type such as {@code NULL}, {@code INTn}, {@code MONEY}. */ FIXEDLENTYPE, /** * Variable length type such as {@code NUMERICN} using a single {@code byte} as length * descriptor (0-255). */ BYTELENTYPE, /** * Variable length type such as {@code VARCHAR}, {@code VARBINARY} (2 bytes) as length * descriptor (0-65534), {@code -1} represents {@code null} */ USHORTLENTYPE, /** * Variable length type such as {@code TEXT} and {@code IMAGE} using a {@code long} (4 bytes) as length * descriptor (0-2GB), {@code -1} represents {@code null}. */ LONGLENTYPE, /** * Partially length type such as {@code BIGVARCHARTYPE}, {@code UDTTYYPE}, {@code NVARCHARTYPE} using a {@code short} as length * descriptor (0-8000). */ PARTLENTYPE } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/type/MutableTypeInformation.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.type; import reactor.util.annotation.Nullable; import java.nio.charset.Charset; /** * Mutable {@link TypeInformation} information for a column. * * @author Mark Paluch */ final class MutableTypeInformation implements TypeInformation { int maxLength; LengthStrategy lengthStrategy; // Length type (FIXEDLENTYPE, PARTLENTYPE, etc.) int precision; int displaySize; int scale; int flags; SqlServerType serverType; int userType; @Nullable String udtTypeName; // Collation (will be null for non-textual types). @Nullable Collation collation; @Nullable Charset charset; @Override public int getMaxLength() { return this.maxLength; } @Override public LengthStrategy getLengthStrategy() { return this.lengthStrategy; } @Override public int getPrecision() { return this.precision; } @Override public int getDisplaySize() { return this.displaySize; } @Override public int getScale() { return this.scale; } @Override public SqlServerType getServerType() { return this.serverType; } @Override public int getUserType() { return this.userType; } @Override @Nullable public String getUdtTypeName() { return this.udtTypeName; } @Override @Nullable public Collation getCollation() { return this.collation; } @Override @Nullable public Charset getCharset() { return this.charset; } @Override public String getServerTypeName() { return (SqlServerType.UDT == this.serverType) ? this.udtTypeName : this.serverType.toString(); } @Override public boolean isNullable() { return 0x0001 == (this.flags & 0x0001); } @Override public boolean isCaseSensitive() { return 0x0002 == (this.flags & 0x0002); } @Override public boolean isSparseColumnSet() { return 0x0400 == (this.flags & 0x0400); } @Override public boolean isEncrypted() { return 0x0800 == (this.flags & 0x0800); } @Override public Updatability getUpdatability() { int value = (this.flags >> 2) & 0x0003; if (value == 0) { return Updatability.READ_ONLY; } if (value == 1) { return Updatability.READ_WRITE; } return Updatability.UNKNOWN; } @Override public boolean isIdentity() { return 0x0010 == (this.flags & 0x0010); } private byte[] getFlags() { byte[] f = new byte[2]; f[0] = (byte) (this.flags & 0xFF); f[1] = (byte) ((this.flags >> 8) & 0xFF); return f; } /** * Returns true if this type is a textual type with a single-byte character set that is compatible with the 7-bit * US-ASCII character set. */ public boolean supportsFastAsciiConversion() { switch (this.serverType) { case CHAR: case VARCHAR: case VARCHARMAX: case TEXT: return this.collation != null && this.collation.hasAsciiCompatibleSBCS(); default: return false; } } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [maxLength=").append(this.maxLength); sb.append(", lengthStrategy=").append(this.lengthStrategy); sb.append(", precision=").append(this.precision); sb.append(", displaySize=").append(this.displaySize); sb.append(", scale=").append(this.scale); sb.append(", flags=").append(this.flags); sb.append(", serverType=").append(this.serverType); sb.append(", userType=").append(this.userType); sb.append(", udtTypeName=\"").append(this.udtTypeName).append('\"'); sb.append(", collation=").append(this.collation); sb.append(", charset=").append(this.charset); sb.append(']'); return sb.toString(); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/type/PlpLength.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.type; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.tds.Decode; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.message.tds.ProtocolException; /** * Descriptor for PLP data length in row results. * This class encapsulates the total length of a value using a 8 byte unsigned long counter. * Use {@link Length} to encode/decode length headers of a PLP chunk and {@link PlpLength} to * encode/decode the the total PLP stream length * * @author Mark Paluch * @see Length */ public final class PlpLength { public static final long PLP_NULL = 0xFF_FF_FF_FF_FF_FF_FF_FFL; public static final long UNKNOWN_PLP_LEN = 0xFF_FF_FF_FF_FF_FF_FF_FEL; private final long length; private final boolean isNull; private PlpLength(long length, boolean isNull) { this.length = length; this.isNull = isNull; } /** * Creates a {@link PlpLength} that indicates the value length is unknown. * * @return a {@link PlpLength} with unknown length. */ public static PlpLength unknown() { return of(UNKNOWN_PLP_LEN, false); } /** * Creates a {@link PlpLength} that indicates the value is {@code null}. * * @return a {@link PlpLength} for {@code null}. */ public static PlpLength nullLength() { return of(0, true); } /** * Creates a {@link PlpLength} with a given {@code length}. * * @param length value length. * @return a {@link PlpLength} for a non-{@code null} value of the given {@code length}. */ public static PlpLength of(long length) { return of(length, false); } /** * Creates a {@link PlpLength}. * * @param length value length. * @param isNull {@code true} if the value is {@code null}. * @return the {@link PlpLength}. */ public static PlpLength of(long length, boolean isNull) { return new PlpLength(length, isNull); } /** * Decode a {@link PlpLength} for a {@link TypeInformation}. * * @param buffer the data buffer. * @param type {@link TypeInformation}. * @return the {@link PlpLength}. */ public static PlpLength decode(ByteBuf buffer, TypeInformation type) { if (type.getLengthStrategy() == LengthStrategy.PARTLENTYPE) { long length = Decode.uLongLong(buffer); return PlpLength.of(length == PLP_NULL ? 0 : length, length == PLP_NULL); } throw ProtocolException.invalidTds("Cannot parse using " + type.getLengthStrategy()); } /** * Check whether the {@link ByteBuf} can be decoded into an {@link PlpLength}. * * @param buffer the data buffer. * @param type {@link TypeInformation}. * @return {@code true} if the buffer contains sufficient data to decode a {@link PlpLength}. */ public static boolean canDecode(ByteBuf buffer, TypeInformation type) { int readerIndex = buffer.readerIndex(); try { return doCanDecode(buffer, type); } finally { buffer.readerIndex(readerIndex); } } /** * Encode length or PLP_NULL. * * @param buffer the data buffer. */ public void encode(ByteBuf buffer) { if (isNull()) { Encode.uLongLong(buffer, PLP_NULL); } else { Encode.uLongLong(buffer, getLength()); } } private static boolean doCanDecode(ByteBuf buffer, TypeInformation type) { if (type.getLengthStrategy() == LengthStrategy.PARTLENTYPE) { return buffer.readableBytes() >= 8; } throw ProtocolException.invalidTds("Cannot parse value LengthDescriptor"); } public long getLength() { return this.length; } public boolean isNull() { return this.isNull; } public boolean isUnknown() { return this.length == UNKNOWN_PLP_LEN; } @Override public String toString() { final StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [length=").append(this.length); sb.append(", isNull=").append(this.isNull); sb.append(']'); return sb.toString(); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/type/SqlServerType.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.type; import io.r2dbc.mssql.util.Assert; import io.r2dbc.spi.Clob; import io.r2dbc.spi.R2dbcType; import io.r2dbc.spi.Type; import reactor.util.annotation.Nullable; import java.math.BigDecimal; import java.nio.ByteBuffer; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.OffsetDateTime; import java.util.UUID; /** * Enumeration of SQL server data types. */ public enum SqlServerType implements Type { // @formatter:off UNKNOWN(Category.UNKNOWN, Object.class, "unknown"), TINYINT(Category.NUMERIC, Byte.class, "tinyint", 1, TdsDataType.INTN, TdsDataType.INT1), BIT(Category.NUMERIC, Byte.class, "bit", 1, TdsDataType.INTN, TdsDataType.INT1), SMALLINT(Category.NUMERIC, Short.class, "smallint", 2, TdsDataType.INTN, TdsDataType.INT2), INTEGER(Category.NUMERIC, Integer.class, "int", 4, TdsDataType.INTN, TdsDataType.INT4), BIGINT(Category.NUMERIC, Long.class, "bigint", 8, TdsDataType.INTN, TdsDataType.INT8), FLOAT(Category.NUMERIC, Double.class, "float", 8, TdsDataType.FLOATN, TdsDataType.FLOAT8), REAL(Category.NUMERIC, Float.class, "real", 4, TdsDataType.FLOATN, TdsDataType.FLOAT4), SMALLDATETIME(Category.DATETIME, LocalDateTime.class, "smalldatetime", 4, TdsDataType.DATETIMEN, TdsDataType.DATETIME4), DATETIME(Category.DATETIME, LocalDateTime.class, "datetime", 8, TdsDataType.DATETIMEN, TdsDataType.DATETIME8), DATE(Category.DATE, LocalDate.class, "date", 3, TdsDataType.DATEN), TIME(Category.TIME, LocalTime.class, "time", 7, TdsDataType.TIMEN), DATETIME2(Category.DATETIME2, LocalDateTime.class, "datetime2", 7, TdsDataType.DATETIME2N), DATETIMEOFFSET(Category.DATETIMEOFFSET, OffsetDateTime.class, "datetimeoffset", 7, TdsDataType.DATETIMEOFFSETN), SMALLMONEY(Category.NUMERIC, BigDecimal.class, "smallmoney", 4, TdsDataType.MONEYN, TdsDataType.MONEY4), MONEY(Category.NUMERIC, BigDecimal.class, "money", 8, TdsDataType.MONEYN, TdsDataType.MONEY8), CHAR(Category.CHARACTER, String.class, "char"), VARCHAR(Category.CHARACTER, String.class, "varchar", 8000, TdsDataType.BIGVARCHAR), VARCHARMAX(Category.LONG_CHARACTER, String.class, "varchar", TdsDataType.BIGVARCHAR), TEXT(Category.LONG_CHARACTER, Clob.class, "text", TdsDataType.TEXT), NCHAR(Category.NCHARACTER, String.class, "nchar"), NVARCHAR(Category.NCHARACTER, String.class, "nvarchar", 4000, TdsDataType.NVARCHAR), NVARCHARMAX(Category.LONG_NCHARACTER, String.class, "nvarchar", TdsDataType.NVARCHAR), NTEXT(Category.LONG_NCHARACTER, String.class, "ntext", TdsDataType.NTEXT), BINARY(Category.BINARY, ByteBuffer.class,"binary"), VARBINARY(Category.BINARY, ByteBuffer.class,"varbinary", 8000, TdsDataType.BIGVARBINARY), VARBINARYMAX(Category.LONG_BINARY, ByteBuffer.class,"varbinary", TdsDataType.BIGVARBINARY), IMAGE(Category.LONG_BINARY, ByteBuffer.class,"image", TdsDataType.IMAGE), DECIMAL(Category.NUMERIC, BigDecimal.class,"decimal", 38, TdsDataType.DECIMALN), NUMERIC(Category.NUMERIC, BigDecimal.class,"numeric", 38, TdsDataType.NUMERICN), GUID(Category.GUID, UUID.class, "uniqueidentifier", 16, TdsDataType.GUID), SQL_VARIANT(Category.SQL_VARIANT, Object.class, "sql_variant", TdsDataType.SQL_VARIANT), UDT(Category.UDT, Object.class, "udt"), XML(Category.XML, Object.class, "xml"), TIMESTAMP(Category.TIMESTAMP, byte[].class, "timestamp", 8, TdsDataType.BIGBINARY), GEOMETRY(Category.UDT, Object.class, "geometry"), GEOGRAPHY(Category.UDT, Object.class, "geography"); // @formatter:on private final Category category; private final Class defaultJavaType; private final String name; private final int maxLength; @Nullable private final TdsDataType nullableType; private final TdsDataType[] fixedTypes; /** * @param category type category. * @param defaultJavaType default Java type. * @param name SQL server type name. * @param nullableType the nullable {@link TdsDataType}. * @param fixedTypes zero or many fixed-length {@link TdsDataType}s. */ SqlServerType(Category category, Class defaultJavaType, String name, TdsDataType nullableType, TdsDataType... fixedTypes) { this(category, defaultJavaType, name, 0, nullableType, fixedTypes); } /** * @param category type category. * @param defaultJavaType default Java type. * @param name SQL server type name. * @param maxLength maximal type length. * @param nullableType the nullable {@link TdsDataType}. * @param fixedTypes zero or many fixed-length {@link TdsDataType}s. */ SqlServerType(Category category, Class defaultJavaType, String name, int maxLength, TdsDataType nullableType, TdsDataType... fixedTypes) { Assert.isTrue(nullableType.getLengthStrategy() != LengthStrategy.FIXEDLENTYPE, String.format("Type [%s] specified a fixed-length strategy in its nullable type", name)); for (TdsDataType fixedType : fixedTypes) { Assert.isTrue(fixedType.getLengthStrategy() == LengthStrategy.FIXEDLENTYPE, String.format("Type [%s] specified [%s] in its fixed length type [%s] ", name, fixedType.getLengthStrategy(), fixedType)); } this.category = category; this.defaultJavaType = defaultJavaType; this.name = name; this.maxLength = maxLength; this.nullableType = nullableType; this.fixedTypes = fixedTypes; } SqlServerType(Category category, Class defaultJavaType, String name) { this.category = category; this.defaultJavaType = defaultJavaType; this.name = name; this.maxLength = 0; this.nullableType = null; this.fixedTypes = new TdsDataType[0]; } /** * Resolve a {@link SqlServerType} by its {@code typeName}. Name comparison is case-insensitive. * * @param typeName the type name. * @return the resolved {@link SqlServerType}. * @throws IllegalArgumentException if the type name cannot be resolved to a {@link SqlServerType} */ public static SqlServerType of(String typeName) { for (SqlServerType type : SqlServerType.values()) if (type.name.equalsIgnoreCase(typeName)) { return type; } throw new IllegalArgumentException(String.format("Unknown type: %s", typeName)); } /** * Resolve a {@link SqlServerType} by its {@link R2dbcType}. * * @param type the R2DBC type. * @return the resolved {@link SqlServerType}. * @throws IllegalArgumentException if the type name cannot be resolved to a {@link SqlServerType} */ public static SqlServerType of(R2dbcType type) { switch (type) { case CHAR: return SqlServerType.CHAR; case VARCHAR: return SqlServerType.VARCHAR; case NCHAR: return SqlServerType.NCHAR; case NVARCHAR: return SqlServerType.NVARCHAR; case CLOB: return SqlServerType.TEXT; case NCLOB: return SqlServerType.NTEXT; case BOOLEAN: return SqlServerType.TINYINT; case BINARY: return SqlServerType.BINARY; case VARBINARY: return SqlServerType.VARBINARY; case BLOB: return SqlServerType.IMAGE; case INTEGER: return SqlServerType.INTEGER; case TINYINT: return SqlServerType.TINYINT; case SMALLINT: return SqlServerType.SMALLINT; case BIGINT: return SqlServerType.BIGINT; case NUMERIC: return SqlServerType.NUMERIC; case DECIMAL: return SqlServerType.DECIMAL; case FLOAT: case DOUBLE: return SqlServerType.FLOAT; case REAL: return SqlServerType.REAL; case DATE: return SqlServerType.DATE; case TIME: return SqlServerType.TIME; case TIMESTAMP: return SqlServerType.TIMESTAMP; } throw new IllegalArgumentException(String.format("Unsupported type: %s", type)); } public Category getCategory() { return this.category; } @Override public Class getJavaType() { return this.defaultJavaType; } public int getMaxLength() { return this.maxLength; } @Override public String getName() { return this.name; } @Nullable public TdsDataType getNullableType() { return this.nullableType; } public TdsDataType[] getFixedTypes() { return this.fixedTypes; } /** * Returns the type name. * * @return the type name. */ @Override public String toString() { return this.name; } /** * Type categories. */ public enum Category { // @formatter:off BINARY, CHARACTER, DATE, DATETIME, DATETIME2, DATETIMEOFFSET, GUID, LONG_BINARY, LONG_CHARACTER, LONG_NCHARACTER, NCHARACTER, NUMERIC, UNKNOWN, TIME, TIMESTAMP, UDT, SQL_VARIANT, XML // @formatter:on } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/type/TdsDataType.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.type; /** * SQL Server data types as represented within TDS. */ public enum TdsDataType { // @formatter:off // FIXEDLEN types BIT1(0x32, LengthStrategy.FIXEDLENTYPE), // 50 INT8(0x7F, LengthStrategy.FIXEDLENTYPE), // 127 INT4(0x38, LengthStrategy.FIXEDLENTYPE), // 56 INT2(0x34, LengthStrategy.FIXEDLENTYPE), // 52 INT1(0x30, LengthStrategy.FIXEDLENTYPE), // 48 FLOAT4(0x3B, LengthStrategy.FIXEDLENTYPE), // 59 FLOAT8(0x3E, LengthStrategy.FIXEDLENTYPE), // 62 DATETIME4(0x3A, LengthStrategy.FIXEDLENTYPE), // 58 DATETIME8(0x3D, LengthStrategy.FIXEDLENTYPE), // 61 MONEY4(0x7A, LengthStrategy.FIXEDLENTYPE), // 122 MONEY8(0x3C, LengthStrategy.FIXEDLENTYPE), // 60 // BYTELEN types BITN(0x68, LengthStrategy.BYTELENTYPE), // 104 INTN(0x26, LengthStrategy.BYTELENTYPE), // 38 DECIMALN(0x6A, LengthStrategy.BYTELENTYPE), // 106 NUMERICN(0x6C, LengthStrategy.BYTELENTYPE), // 108 FLOATN(0x6D, LengthStrategy.BYTELENTYPE), // 109 MONEYN(0x6E, LengthStrategy.BYTELENTYPE), // 110 DATETIMEN(0x6F, LengthStrategy.BYTELENTYPE), // 111 GUID(0x24, LengthStrategy.BYTELENTYPE), // 36 DATEN(0x28, LengthStrategy.BYTELENTYPE), // 40 TIMEN(0x29, LengthStrategy.BYTELENTYPE), // 41 DATETIME2N(0x2a, LengthStrategy.BYTELENTYPE), // 42 DATETIMEOFFSETN(0x2b, LengthStrategy.BYTELENTYPE), // 43 // USHORTLEN type BIGCHAR(0xAF, LengthStrategy.USHORTLENTYPE), // -81 BIGVARCHAR(0xA7, LengthStrategy.USHORTLENTYPE), // -89 BIGBINARY(0xAD, LengthStrategy.USHORTLENTYPE), // -83 BIGVARBINARY(0xA5, LengthStrategy.USHORTLENTYPE), // -91 NCHAR(0xEF, LengthStrategy.USHORTLENTYPE), // -17 NVARCHAR(0xE7, LengthStrategy.USHORTLENTYPE), // -15 // PARTLEN types IMAGE(0x22, LengthStrategy.PARTLENTYPE), // 34 TEXT(0x23, LengthStrategy.PARTLENTYPE), // 35 NTEXT(0x63, LengthStrategy.PARTLENTYPE), // 99 UDT(0xF0, LengthStrategy.PARTLENTYPE), // -16 XML(0xF1, LengthStrategy.PARTLENTYPE), // -15 // LONGLEN types SQL_VARIANT(0x62, LengthStrategy.LONGLENTYPE); // 98 // @formatter:on private static final int MAXELEMENTS = 256; private static final TdsDataType[] cache = new TdsDataType[MAXELEMENTS]; private final int value; private final LengthStrategy lengthStrategy; static { for (TdsDataType s : values()) cache[s.value] = s; } TdsDataType(int value, LengthStrategy lengthStrategy) { this.value = value; this.lengthStrategy = lengthStrategy; } public byte getValue() { return (byte) this.value; } public LengthStrategy getLengthStrategy() { return this.lengthStrategy; } /** * Return the {@link TdsDataType} by looking it up using the given type value. * * @param value the data type. * @return the {@link TdsDataType}. */ static TdsDataType valueOf(int value) { if (value > MAXELEMENTS || value < 0 || cache[value] == null) { throw new IllegalArgumentException(String.format("Invalid TDS type: %d", value)); } return cache[value]; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/type/TypeBuilder.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.type; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.tds.Decode; import io.r2dbc.mssql.message.tds.ProtocolException; import io.r2dbc.mssql.message.tds.ServerCharset; import io.r2dbc.mssql.util.Assert; import java.util.EnumMap; import java.util.Map; /** * Builders to parse {@link TypeInformation}. * * @author Mark Paluch */ @SuppressWarnings("unused") enum TypeBuilder { BIT(TdsDataType.BIT1, TypeDecoderStrategies.create(SqlServerType.BIT, 1, // TDS length (bytes) 1, // precision (max numeric precision, in decimal digits) "1".length(), // column display size 0) // scale ), BIGINT(TdsDataType.INT8, TypeDecoderStrategies.create(SqlServerType.BIGINT, 8, // TDS length (bytes) Long.toString(Long.MAX_VALUE).length(), // precision (max numeric precision, in decimal digits) ("-" + Long.MAX_VALUE).length(), // column display size (includes sign) 0) // scale ), INTEGER(TdsDataType.INT4, TypeDecoderStrategies.create(SqlServerType.INTEGER, 4, // TDS length (bytes) Integer.toString(Integer.MAX_VALUE).length(), // precision (max numeric precision, in decimal digits) ("-" + Integer.MAX_VALUE).length(), // column display size (includes sign) 0) // scale ), SMALLINT(TdsDataType.INT2, TypeDecoderStrategies.create(SqlServerType.SMALLINT, 2, // TDS length (bytes) Short.toString(Short.MAX_VALUE).length(), // precision (max numeric precision, in decimal digits) ("-" + Short.MAX_VALUE).length(), // column display size (includes sign) 0) // scale ), TINYINT(TdsDataType.INT1, TypeDecoderStrategies.create(SqlServerType.TINYINT, 1, // TDS length (bytes) Byte.toString(Byte.MAX_VALUE).length(), // precision (max numeric precision, in decimal digits) Byte.toString(Byte.MAX_VALUE).length(), // column display size (no sign - TINYINT is unsigned) 0) // scale ), REAL(TdsDataType.FLOAT4, TypeDecoderStrategies.create(SqlServerType.REAL, 4, // TDS length (bytes) 7, // precision (max numeric precision, in bits) 13, // column display size 0) // scale ), FLOAT(TdsDataType.FLOAT8, TypeDecoderStrategies.create(SqlServerType.FLOAT, 8, // TDS length (bytes) 15, // precision (max numeric precision, in bits) 22, // column display size 0) // scale ), SMALLDATETIME(TdsDataType.DATETIME4, TypeDecoderStrategies.create(SqlServerType.SMALLDATETIME, 4, // TDS length (bytes) "yyyy-mm-dd hh:mm".length(), // precision (formatted length, in characters, assuming max fractional // seconds precision (0)) "yyyy-mm-dd hh:mm".length(), // column display size 0) // scale ), DATETIME(TdsDataType.DATETIME8, TypeDecoderStrategies.create(SqlServerType.DATETIME, 8, // TDS length (bytes) "yyyy-mm-dd hh:mm:ss.fff".length(), // precision (formatted length, in characters, assuming max // fractional seconds precision) "yyyy-mm-dd hh:mm:ss.fff".length(), // column display size 3) // scale ), SMALLMONEY(TdsDataType.MONEY4, TypeDecoderStrategies.create(SqlServerType.SMALLMONEY, 4, // TDS length (bytes) Integer.toString(Integer.MAX_VALUE).length(), // precision (max unscaled numeric precision, in decimal // digits) ("-" + "." + Integer.MAX_VALUE).length(), // column display size (includes sign and // decimal for scale) 4) // scale ), MONEY(TdsDataType.MONEY8, TypeDecoderStrategies.create(SqlServerType.MONEY, 8, // TDS length (bytes) Long.toString(Long.MAX_VALUE).length(), // precision (max unscaled numeric precision, in decimal digits) ("-" + "." + Long.MAX_VALUE).length(), // column display size (includes sign and decimal // for scale) 4) // scale ), BITN(TdsDataType.BITN, new AbstractTypeDecoderStrategy(1) { @Override public void decode(MutableTypeInformation typeInfo, ByteBuf buffer) { if (1 != Decode.uByte(buffer)) { throw ProtocolException.invalidTds("Invalid mutability for BITN"); } BIT.build(typeInfo, buffer); typeInfo.lengthStrategy = LengthStrategy.BYTELENTYPE; } } ), INTN(TdsDataType.INTN, new AbstractTypeDecoderStrategy(1) { @Override public void decode(MutableTypeInformation typeInfo, ByteBuf buffer) { int intType = Decode.uByte(buffer); switch (intType) { case 8: BIGINT.build(typeInfo, buffer); break; case 4: INTEGER.build(typeInfo, buffer); break; case 2: SMALLINT.build(typeInfo, buffer); break; case 1: TINYINT.build(typeInfo, buffer); break; default: throw ProtocolException.invalidTds(String.format("Unsupported INTN type %s", intType)); } typeInfo.lengthStrategy = LengthStrategy.BYTELENTYPE; } } ), DECIMAL(TdsDataType.DECIMALN, TypeDecoderStrategies.decimalNumeric(SqlServerType.DECIMAL)), NUMERIC(TdsDataType.NUMERICN, TypeDecoderStrategies.decimalNumeric(SqlServerType.NUMERIC)), FLOATN(TdsDataType.FLOATN, TypeDecoderStrategies.bigOrSmall(FLOAT, REAL)), MONEYN(TdsDataType.MONEYN, TypeDecoderStrategies.bigOrSmall(MONEY, SMALLMONEY)), DATETIMEN(TdsDataType.DATETIMEN, TypeDecoderStrategies.bigOrSmall(DATETIME, SMALLDATETIME)), TIME(TdsDataType.TIMEN, TypeDecoderStrategies.temporal(SqlServerType.TIME)), DATETIME2(TdsDataType.DATETIME2N, TypeDecoderStrategies.temporal(SqlServerType.DATETIME2)), DATETIMEOFFSET(TdsDataType.DATETIMEOFFSETN, TypeDecoderStrategies.temporal(SqlServerType.DATETIMEOFFSET)), DATE(TdsDataType.DATEN, new AbstractTypeDecoderStrategy(0) { @Override public void decode(MutableTypeInformation typeInfo, ByteBuf buffer) { typeInfo.serverType = SqlServerType.DATE; typeInfo.lengthStrategy = LengthStrategy.BYTELENTYPE; typeInfo.maxLength = 3; typeInfo.displaySize = typeInfo.precision = "yyyy-mm-dd".length(); } }), BIGBINARY(TdsDataType.BIGBINARY, new AbstractTypeDecoderStrategy(2) { @Override public void decode(MutableTypeInformation typeInfo, ByteBuf buffer) { typeInfo.lengthStrategy = LengthStrategy.USHORTLENTYPE; typeInfo.maxLength = Decode.uShort(buffer); if (typeInfo.maxLength > TypeUtils.SHORT_VARTYPE_MAX_BYTES) { throw ProtocolException.invalidTds("Max length exceeds short VARBINARY/VARCHAR type"); } typeInfo.precision = typeInfo.maxLength; typeInfo.displaySize = 2 * typeInfo.maxLength; typeInfo.serverType = (TypeUtils.UDT_TIMESTAMP == typeInfo.userType) ? SqlServerType.TIMESTAMP : SqlServerType.BINARY; } }), BIGVARBINARY(TdsDataType.BIGVARBINARY, new AbstractTypeDecoderStrategy(2) { @Override public void decode(MutableTypeInformation typeInfo, ByteBuf buffer) { typeInfo.maxLength = Decode.uShort(buffer); if (TypeUtils.MAXTYPE_LENGTH == typeInfo.maxLength)// for PLP types { typeInfo.lengthStrategy = LengthStrategy.PARTLENTYPE; typeInfo.serverType = SqlServerType.VARBINARYMAX; typeInfo.displaySize = typeInfo.precision = TypeUtils.MAX_VARTYPE_MAX_BYTES; } else if (typeInfo.maxLength <= TypeUtils.SHORT_VARTYPE_MAX_BYTES)// for non-PLP types { typeInfo.lengthStrategy = LengthStrategy.USHORTLENTYPE; typeInfo.serverType = SqlServerType.VARBINARY; typeInfo.precision = typeInfo.maxLength; typeInfo.displaySize = 2 * typeInfo.maxLength; } else { throw ProtocolException.invalidTds("Cannot parse BIGVARBINARY type info"); } } }), IMAGE(TdsDataType.IMAGE, new AbstractTypeDecoderStrategy(4) { @Override public void decode(MutableTypeInformation typeInfo, ByteBuf buffer) { typeInfo.lengthStrategy = LengthStrategy.LONGLENTYPE; typeInfo.maxLength = Decode.asLong(buffer); if (typeInfo.maxLength < 0) { throw ProtocolException.invalidTds("Negative IMAGE type length"); } typeInfo.serverType = SqlServerType.IMAGE; typeInfo.displaySize = typeInfo.precision = Integer.MAX_VALUE; } }), BIGCHAR(TdsDataType.BIGCHAR, new AbstractTypeDecoderStrategy(7) { @Override public void decode(MutableTypeInformation typeInfo, ByteBuf buffer) { typeInfo.lengthStrategy = LengthStrategy.USHORTLENTYPE; typeInfo.maxLength = Decode.uShort(buffer); if (typeInfo.maxLength > TypeUtils.SHORT_VARTYPE_MAX_BYTES) { throw ProtocolException.invalidTds(String.format("BIGCHAR max length exceeded: %d", typeInfo.maxLength)); } typeInfo.displaySize = typeInfo.precision = typeInfo.maxLength; typeInfo.serverType = SqlServerType.CHAR; typeInfo.collation = Collation.decode(buffer); typeInfo.charset = typeInfo.collation.getCharset(); } }), BIGVARCHAR(TdsDataType.BIGVARCHAR, new AbstractTypeDecoderStrategy(7) { @Override public void decode(MutableTypeInformation typeInfo, ByteBuf buffer) { typeInfo.maxLength = Decode.uShort(buffer); if (TypeUtils.MAXTYPE_LENGTH == typeInfo.maxLength)// for PLP types { typeInfo.lengthStrategy = LengthStrategy.PARTLENTYPE; typeInfo.serverType = SqlServerType.VARCHARMAX; typeInfo.displaySize = typeInfo.precision = TypeUtils.MAX_VARTYPE_MAX_BYTES; } else if (typeInfo.maxLength <= TypeUtils.SHORT_VARTYPE_MAX_BYTES)// for non-PLP types { typeInfo.lengthStrategy = LengthStrategy.USHORTLENTYPE; typeInfo.serverType = SqlServerType.VARCHAR; typeInfo.displaySize = typeInfo.precision = typeInfo.maxLength; } else { throw ProtocolException.invalidTds("Cannot parse BIGVARCHAR type info"); } typeInfo.collation = Collation.decode(buffer); typeInfo.charset = typeInfo.collation.getCharset(); } }), TEXT(TdsDataType.TEXT, new AbstractTypeDecoderStrategy(9) { @Override public void decode(MutableTypeInformation typeInfo, ByteBuf buffer) { typeInfo.lengthStrategy = LengthStrategy.LONGLENTYPE; typeInfo.maxLength = Decode.asLong(buffer); if (typeInfo.maxLength < 0) { throw ProtocolException.invalidTds("Negative TEXT type length"); } typeInfo.serverType = SqlServerType.TEXT; typeInfo.displaySize = typeInfo.precision = Integer.MAX_VALUE; typeInfo.collation = Collation.decode(buffer); typeInfo.charset = typeInfo.collation.getCharset(); } }), NCHAR(TdsDataType.NCHAR, new AbstractTypeDecoderStrategy(7) { @Override public void decode(MutableTypeInformation typeInfo, ByteBuf buffer) { typeInfo.lengthStrategy = LengthStrategy.USHORTLENTYPE; typeInfo.maxLength = Decode.uShort(buffer); if (typeInfo.maxLength > TypeUtils.SHORT_VARTYPE_MAX_BYTES || 0 != typeInfo.maxLength % 2) { throw ProtocolException.invalidTds("Invalid NCHAR length"); } typeInfo.displaySize = typeInfo.precision = typeInfo.maxLength / 2; typeInfo.serverType = SqlServerType.NCHAR; typeInfo.collation = Collation.decode(buffer); typeInfo.charset = ServerCharset.UNICODE.charset(); } }), NVARCHAR(TdsDataType.NVARCHAR, new AbstractTypeDecoderStrategy(7) { @Override public void decode(MutableTypeInformation typeInfo, ByteBuf buffer) { typeInfo.maxLength = Decode.uShort(buffer); if (TypeUtils.MAXTYPE_LENGTH == typeInfo.maxLength)// for PLP types { typeInfo.lengthStrategy = LengthStrategy.PARTLENTYPE; typeInfo.serverType = SqlServerType.NVARCHARMAX; typeInfo.displaySize = typeInfo.precision = TypeUtils.MAX_VARTYPE_MAX_CHARS; } else if (typeInfo.maxLength <= TypeUtils.SHORT_VARTYPE_MAX_BYTES && 0 == typeInfo.maxLength % 2)// for // non-PLP // types { typeInfo.lengthStrategy = LengthStrategy.USHORTLENTYPE; typeInfo.serverType = SqlServerType.NVARCHAR; typeInfo.displaySize = typeInfo.precision = typeInfo.maxLength / 2; } else { throw ProtocolException.invalidTds("Invalid NVARCHAR length"); } typeInfo.collation = Collation.decode(buffer); typeInfo.charset = ServerCharset.UNICODE.charset(); } }), NTEXT(TdsDataType.NTEXT, new AbstractTypeDecoderStrategy(9) { @Override public void decode(MutableTypeInformation typeInfo, ByteBuf buffer) { typeInfo.lengthStrategy = LengthStrategy.LONGLENTYPE; typeInfo.maxLength = Decode.asLong(buffer); if (typeInfo.maxLength < 0) { throw ProtocolException.invalidTds("Negative TEXT type length"); } typeInfo.serverType = SqlServerType.NTEXT; typeInfo.displaySize = typeInfo.precision = Integer.MAX_VALUE / 2; typeInfo.collation = Collation.decode(buffer); typeInfo.charset = ServerCharset.UNICODE.charset(); } }), GUID(TdsDataType.GUID, new AbstractTypeDecoderStrategy(1) { @Override public void decode(MutableTypeInformation typeInfo, ByteBuf buffer) { int maxLength = Decode.uByte(buffer); if (maxLength != 16 && maxLength != 0) { throw ProtocolException.invalidTds("Negative GUID type length"); } typeInfo.lengthStrategy = LengthStrategy.BYTELENTYPE; typeInfo.serverType = SqlServerType.GUID; typeInfo.maxLength = maxLength; typeInfo.displaySize = 36; typeInfo.precision = 36; } }), // TODO: UDT, XML SQL_VARIANT(TdsDataType.SQL_VARIANT, new AbstractTypeDecoderStrategy(4) { @Override public void decode(MutableTypeInformation typeInfo, ByteBuf buffer) { typeInfo.lengthStrategy = LengthStrategy.LONGLENTYPE; // sql_variant type should be LONGLENTYPE length. typeInfo.maxLength = Decode.asLong(buffer); typeInfo.serverType = SqlServerType.SQL_VARIANT; } }); private final TdsDataType tdsType; private final TypeDecoderStrategy strategy; private static final Map builderMap = new EnumMap<>(TdsDataType.class); static { for (TypeBuilder builder : TypeBuilder.values()) { builderMap.put(builder.getTdsDataType(), builder); } } TypeBuilder(TdsDataType tdsType, TypeDecoderStrategy strategy) { this.tdsType = tdsType; this.strategy = strategy; } /** * Decode {@link TypeInformation} from the {@code ByteBuf}. * * @param buffer the data {@link ByteBuf buffer}. * @param readFlags {@code true} to decode {@code flags} (typically used when not using encryption). * @return the decoded {@link TypeInformation}. */ static TypeInformation decode(ByteBuf buffer, boolean readFlags) { MutableTypeInformation typeInfo = new MutableTypeInformation(); // UserType is USHORT in TDS 7.1 and earlier; ULONG in TDS 7.2 and later. typeInfo.userType = Decode.intBigEndian(buffer); if (readFlags) { // Flags (2 bytes) typeInfo.flags = Decode.uShort(buffer); } TdsDataType tdsType = TdsDataType.valueOf(Decode.uByte(buffer)); TypeBuilder typeBuilder = builderMap.get(tdsType); if (typeBuilder == null) { throw new IllegalStateException("TypeBuilder for " + tdsType + " not available"); } return typeBuilder.build(typeInfo, buffer); } /** * Check whether the {@link ByteBuf} contains sufficient readable bytes to decode the {@link TypeInformation}. * * @param buffer the data buffer. * @param boolean {@code true} to parse type flags. * @return {@code true} if the data buffer contains sufficient readable bytes to decode the {@link TypeInformation}. */ static boolean canDecode(ByteBuf buffer, boolean readFlags) { int length = 4 /* user type */ + (readFlags ? 2 : 0); int readerIndex = buffer.readerIndex(); try { if (buffer.readableBytes() >= length + 1) { buffer.skipBytes(length); TdsDataType tdsType = TdsDataType.valueOf(Decode.uByte(buffer)); TypeBuilder typeBuilder = builderMap.get(tdsType); return typeBuilder.strategy.canDecode(buffer); } return false; } finally { buffer.readerIndex(readerIndex); } } TdsDataType getTdsDataType() { return this.tdsType; } /** * Build the {@link TypeInformation} by parsing details from {@link ByteBuf}. * * @param typeInfo the type builder. * @param buffer the data buffer. * @return the built {@link TypeInformation}. * @throws ProtocolException */ MutableTypeInformation build(MutableTypeInformation typeInfo, ByteBuf buffer) throws ProtocolException { this.strategy.decode(typeInfo, buffer); // Postcondition: SqlServerType and SqlServerLength are initialized Assert.state(typeInfo.serverType != null, "Server type must not be null"); Assert.state(typeInfo.lengthStrategy != null, "Length type must not be null"); return typeInfo; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/type/TypeDecoderStrategies.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.type; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.tds.Decode; import io.r2dbc.mssql.message.tds.ProtocolException; /** * Typical type parsing strategies. * * @author Mark Paluch */ interface TypeDecoderStrategies { /** * Strategy using a byte length ({@code 8} or {@code 4}) for big or small type parsing. * * @param big big type. * @param small small type. * @return the decoder strategy. */ static TypeDecoderStrategy bigOrSmall(TypeBuilder big, TypeBuilder small) { return new BigOrSmallByteLenStrategy(big, small); } /** * Strategy for {@link LengthStrategy#FIXEDLENTYPE}. * * @param serverType the server data type. * @param maxLength actual length of the type. * @param precision the precision. * @param displaySize * @param scale * @return the decoder strategy. */ static TypeDecoderStrategy create(SqlServerType serverType, int maxLength, int precision, int displaySize, int scale) { return new FixedLenStrategy(serverType, maxLength, precision, displaySize, scale); } /** * Strategy for decimal numbers using {@link LengthStrategy#BYTELENTYPE}. * * @param serverType the server data type. * @return the decoder strategy. */ static TypeDecoderStrategy decimalNumeric(SqlServerType serverType) { return new DecimalNumericStrategy(serverType); } /** * Strategy for temporal types. * * @param serverType the server data type. * @return the decoder strategy. */ static TypeDecoderStrategy temporal(SqlServerType serverType) { return new KatmaiScaledTemporalStrategy(serverType); } /** * Strategy for {@link LengthStrategy#FIXEDLENTYPE}. */ class FixedLenStrategy extends AbstractTypeDecoderStrategy { private final SqlServerType serverType; private final int maxLength; private final int precision; private final int displaySize; private final int scale; FixedLenStrategy(SqlServerType serverType, int maxLength, int precision, int displaySize, int scale) { super(0); this.serverType = serverType; this.maxLength = maxLength; this.precision = precision; this.displaySize = displaySize; this.scale = scale; } @Override public void decode(MutableTypeInformation typeInfo, ByteBuf buffer) { typeInfo.lengthStrategy = LengthStrategy.FIXEDLENTYPE; typeInfo.serverType = this.serverType; typeInfo.maxLength = this.maxLength; typeInfo.precision = this.precision; typeInfo.displaySize = this.displaySize; typeInfo.scale = this.scale; } } /** * Strategy for decimal numbers using {@link LengthStrategy#BYTELENTYPE}. */ class DecimalNumericStrategy extends AbstractTypeDecoderStrategy { private final SqlServerType serverType; DecimalNumericStrategy(SqlServerType serverType) { super(3); this.serverType = serverType; } @Override public void decode(MutableTypeInformation typeInfo, ByteBuf buffer) { int maxLength = Decode.uByte(buffer); int precision = Decode.uByte(buffer); int scale = Decode.uByte(buffer); if (maxLength > 17) { throw ProtocolException.invalidTds(String.format("Invalid maximal length for decimal number type: %d", maxLength)); } typeInfo.lengthStrategy = LengthStrategy.BYTELENTYPE; typeInfo.serverType = this.serverType; typeInfo.maxLength = maxLength; typeInfo.precision = precision; typeInfo.displaySize = precision + 2; typeInfo.scale = scale; } } /** * Strategy using a byte length ({@code 8} or {@code 4}) for big or small type parsing. */ class BigOrSmallByteLenStrategy extends AbstractTypeDecoderStrategy { private final TypeBuilder bigBuilder; private final TypeBuilder smallBuilder; BigOrSmallByteLenStrategy(TypeBuilder bigBuilder, TypeBuilder smallBuilder) { super(1); this.bigBuilder = bigBuilder; this.smallBuilder = smallBuilder; } @Override public void decode(MutableTypeInformation typeInfo, ByteBuf buffer) { int length = Decode.uByte(buffer); switch (length) // maxLength { case 8: this.bigBuilder.build(typeInfo, buffer); break; case 4: this.smallBuilder.build(typeInfo, buffer); break; default: throw ProtocolException.invalidTds(String.format("Unsupported length for Big/Small strategy: %d", length)); } typeInfo.lengthStrategy = LengthStrategy.BYTELENTYPE; } } /** * Strategy for temporal types. */ class KatmaiScaledTemporalStrategy extends AbstractTypeDecoderStrategy { private final SqlServerType serverType; KatmaiScaledTemporalStrategy(SqlServerType serverType) { super(1); this.serverType = serverType; } private int getPrecision(String baseFormat, int scale) { // For 0-scale temporal, there is no '.' after the seconds component because there are no sub-seconds. // Example: 12:34:56.12134 includes a '.', but 12:34:56 doesn't return baseFormat.length() + ((scale > 0) ? (1 + scale) : 0); } @Override public void decode(MutableTypeInformation typeInfo, ByteBuf buffer) { typeInfo.scale = Decode.uByte(buffer); if (typeInfo.scale > TypeUtils.MAX_FRACTIONAL_SECONDS_SCALE) { throw ProtocolException.invalidTds(String.format("Unsupported temporal scale: %d", typeInfo.scale)); } switch (this.serverType) { case TIME: typeInfo.precision = getPrecision("hh:mm:ss", typeInfo.scale); typeInfo.maxLength = TypeUtils.getTimeValueLength(typeInfo.scale); break; case DATETIME2: typeInfo.precision = getPrecision("yyyy-mm-dd hh:mm:ss", typeInfo.scale); typeInfo.maxLength = TypeUtils.getDateTimeValueLength(typeInfo.scale); break; case DATETIMEOFFSET: typeInfo.precision = getPrecision("yyyy-mm-dd hh:mm:ss +HH:MM", typeInfo.scale); typeInfo.maxLength = TypeUtils.getDatetimeoffsetValueLength(typeInfo.scale); break; default: throw ProtocolException.invalidTds(String.format("Unexpected SQL Server type: %s", this.serverType)); } typeInfo.lengthStrategy = LengthStrategy.BYTELENTYPE; typeInfo.serverType = this.serverType; typeInfo.displaySize = typeInfo.precision; } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/type/TypeDecoderStrategy.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.type; import io.netty.buffer.ByteBuf; /** * Interface typically implemented by type decoder implementations that can decode a {@link TypeInformation}. */ public interface TypeDecoderStrategy { /** * Check whether the {@link ByteBuf} contains sufficient readable bytes to decode the {@link TypeInformation}. * * @param buffer the data buffer. * @return {@code true} if the data buffer contains sufficient readable bytes to decode the {@link TypeInformation}. */ boolean canDecode(ByteBuf buffer); /** * Decode the type information and enhance {@link MutableTypeInformation}. * * @param typeInfo the mutable {@link TypeInformation}. * @param buffer the data buffer. */ void decode(MutableTypeInformation typeInfo, ByteBuf buffer); } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/type/TypeInformation.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.type; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.util.Assert; import reactor.util.annotation.Nullable; import java.nio.charset.Charset; /** * Type information for a column following the {@code TYPE_INFO} rule * * @author Mark Paluch * @see Collation * @see SqlServerType * @see Updatability */ public interface TypeInformation { /** * Decode {@link TypeInformation} from the {@code ByteBuf}. * * @param buffer the data {@link ByteBuf buffer}. * @param readFlags {@code true} to decode {@code flags} (typically used when not using encryption). * @return the decoded {@link TypeInformation}. */ static TypeInformation decode(ByteBuf buffer, boolean readFlags) { return TypeBuilder.decode(buffer, readFlags); } /** * Check whether the {@link ByteBuf} contains sufficient readable bytes to decode the {@link TypeInformation}. * * @param buffer the data buffer. * @param readFlags {@code true} to parse type flags. * @return {@code true} if the data buffer contains sufficient readable bytes to decode the {@link TypeInformation}. */ static boolean canDecode(ByteBuf buffer, boolean readFlags) { return TypeBuilder.canDecode(buffer, readFlags); } /** * Returns the maximal length. * * @return the maximal length. */ int getMaxLength(); /** * Returns the length {@link LengthStrategy strategy}. * * @return the length {@link LengthStrategy strategy}. */ LengthStrategy getLengthStrategy(); /** * Returns the precision. * * @return the precision. */ int getPrecision(); /** * Returns the display size. * * @return the display size. */ int getDisplaySize(); /** * Returns the scale. * * @return the scale. */ int getScale(); /** * Returns the {@link SqlServerType} base type. * * @return the {@link SqlServerType} base type */ SqlServerType getServerType(); /** * Returns the user type. * * @return the user type. */ int getUserType(); /** * Returns the user type name. Can be {@code null} if this type information is not related to a user type. * * @return the user type name. */ @Nullable String getUdtTypeName(); /** * Returns the {@link Collation}. Can be {@code null} if this type information has no collation details. * * @return the {@link Collation}. */ @Nullable Collation getCollation(); /** * Returns the {@link Charset}. Can be {@code null} if this type information has no collation details. * * @return the {@link Charset}. * @see #getCollation() */ @Nullable Charset getCharset(); /** * Returns the server type name. * * @return the server type name. */ String getServerTypeName(); /** * Returns whether the type is nullable. * * @return {@code true} if the type is nullable. */ boolean isNullable(); /** * Returns whether the type is case-sensitive. * * @return {@code true} if the type is case-sensitive. */ boolean isCaseSensitive(); boolean isSparseColumnSet(); /** * Returns whether the type is encrypted. * * @return {@code true} if the type is encrypted. */ boolean isEncrypted(); /** * Returns the type {@link Updatability}. * * @return the type {@link Updatability}. */ Updatability getUpdatability(); /** * Returns whether the type is an identity type. * * @return {@code true} if the type is an identity type. */ boolean isIdentity(); /** * Creates a {@link Builder} for {@link TypeInformation}. * * @return a new {@link Builder} to build {@link TypeInformation}. */ static Builder builder() { return new Builder(); } /** * Builder for {@link TypeInformation}. */ final class Builder { private Charset charset; private Collation collation; private int displaySize; private int flags; private LengthStrategy lengthStrategy; private int maxLength; private int precision; private int scale; private SqlServerType serverType; private String udtTypeName; private int userType; private Builder() { } /** * Configure the {@link Charset}. * * @param charset the charset to use. * @return {@code this} {@link Builder}. */ public Builder withCharset(Charset charset) { this.charset = Assert.requireNonNull(charset, "Charset must not be null"); return this; } /** * Configure the {@link Collation}. * * @param collation the collation to use. * @return {@code this} {@link Builder}. */ public Builder withCollation(Collation collation) { this.collation = Assert.requireNonNull(collation, "Collation must not be null"); return this; } /** * Configure the display size. * * @param displaySize the display size. * @return {@code this} {@link Builder}. */ public Builder withDisplaySize(int displaySize) { this.displaySize = displaySize; return this; } /** * Configure flags. * * @param flags type flags. * @return {@code this} {@link Builder}. */ public Builder withFlags(int flags) { this.flags = flags; return this; } /** * Configure the {@link LengthStrategy}. * * @param lengthStrategy the display size. * @return {@code this} {@link Builder}. */ public Builder withLengthStrategy(LengthStrategy lengthStrategy) { this.lengthStrategy = Assert.requireNonNull(lengthStrategy, "LengthStrategy must not be null"); return this; } /** * Configure the maximal maxLength. * * @param maxLength max length. * @return {@code this} {@link Builder}. */ public Builder withMaxLength(int maxLength) { this.maxLength = maxLength; return this; } /** * Configure the precision. * * @param precision the precision. * @return {@code this} {@link Builder}. */ public Builder withPrecision(int precision) { this.precision = precision; return this; } /** * Configure the scale. * * @param scale the scale. * @return {@code this} {@link Builder}. */ public Builder withScale(int scale) { this.scale = scale; return this; } /** * Configure the {@link SqlServerType}. * * @param serverType the server type. * @return {@code this} {@link Builder}. */ public Builder withServerType(SqlServerType serverType) { this.serverType = Assert.requireNonNull(serverType, "SqlServerType must not be null"); return this; } /** * Build a new {@link TypeInformation}. * * @return a new {@link TypeInformation}. */ public TypeInformation build() { MutableTypeInformation mutableTypeInformation = new MutableTypeInformation(); mutableTypeInformation.lengthStrategy = this.lengthStrategy; mutableTypeInformation.serverType = this.serverType; mutableTypeInformation.flags = this.flags; mutableTypeInformation.maxLength = this.maxLength; mutableTypeInformation.charset = this.charset; mutableTypeInformation.scale = this.scale; mutableTypeInformation.userType = this.userType; mutableTypeInformation.precision = this.precision; mutableTypeInformation.displaySize = this.displaySize; mutableTypeInformation.udtTypeName = this.udtTypeName; mutableTypeInformation.collation = this.collation; return mutableTypeInformation; } } /** * Enumeration of updatability constants. */ enum Updatability { READ_ONLY(0), READ_WRITE(1), UNKNOWN(2); private final byte value; Updatability(int value) { this.value = (byte) value; } public byte getValue() { return this.value; } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/type/TypeUtils.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.type; import io.r2dbc.mssql.util.Assert; /** * Helper methods for Type-specific calculations. * * @author Mark Paluch */ public final class TypeUtils { /* System defined UDTs */ static final int UDT_TIMESTAMP = 0x0050; /** * Max length in Unicode characters allowed by the "short" NVARCHAR type. Values longer than this must use * NVARCHAR(max) (Yukon or later) or NTEXT (Shiloh) */ static final int SHORT_VARTYPE_MAX_CHARS = 4000; /** * Max length in bytes allowed by the "short" VARBINARY/VARCHAR types. Values longer than this must use * VARBINARY(max)/VARCHAR(max) (Yukon or later) or IMAGE/TEXT (Shiloh) */ public static final int SHORT_VARTYPE_MAX_BYTES = 8000; /** * A type with unlimited max size, known as varchar(max), varbinary(max) and nvarchar(max), which has a max size of * 0xFFFF, defined by PARTLENTYPE. */ static final int SQL_USHORTVARMAXLEN = 65535; // 0xFFFF /** * From SQL Server 2005 Books Online : ntext, text, and image (Transact-SQL) * https://msdn.microsoft.com/en-us/library/ms187993.aspx *

    * image "... through 2^31 - 1 (2,147,483,687) bytes." *

    * text "... maximum length of 2^31 - 1 (2,147,483,687) characters." *

    * ntext "... maximum length of 2^30 - 1 (1,073,741,823) characters." */ static final int NTEXT_MAX_CHARS = 0x3FFFFFFF; public static final int IMAGE_TEXT_MAX_BYTES = 0x7FFFFFFF; /** * Transact-SQL Data Types: https://msdn.microsoft.com/en-us/library/ms179910.aspx *

    * {@literal varbinary(max)} "max indicates that the maximum storage size is 231 - 1 bytes. The storage size is the actual * length of the data entered + 2 bytes." *

    * {@literal varchar(max)} "max indicates that the maximum storage size is 231 - 1 bytes. The storage size is the actual * length of the data entered + 2 bytes." *

    * {@literal nvarchar(max)} "max indicates that the maximum storage size is 231 - 1 bytes. The storage size, in bytes, is two * times the number of characters entered + 2 bytes." *

    * Normally, that would mean that the maximum length of {@literal nvarchar(max)} data is 0x3FFFFFFE characters and that the * maximum length of {@literal varchar(max)} or {@literal varbinary(max)} data is 0x3FFFFFFD bytes. Despite the documentation, * SQL Server returns 230 - 1 and 231 - 1 respectively as the PRECISION of these types, so use that instead. */ static final int MAX_VARTYPE_MAX_CHARS = 0x3FFFFFFF; static final int MAX_VARTYPE_MAX_BYTES = 0x7FFFFFFF; // Special length indicator for varchar(max), nvarchar(max) and varbinary(max). static final int MAXTYPE_LENGTH = 0xFFFF; public static final int UNKNOWN_STREAM_LENGTH = -1; public static final int MAX_FRACTIONAL_SECONDS_SCALE = 7; public static final int DAYS_INTO_CE_LENGTH = 3; public static final int MINUTES_OFFSET_LENGTH = 2; // Number of days in a "normal" (non-leap) year according to SQL Server. static final int DAYS_PER_YEAR = 365; private static final int[] SCALED_TIME_LENGTHS = new int[]{3, 3, 3, 4, 4, 5, 5, 5}; /** * Returns the length of time values using {@code scale}. * * @param scale the time scale. * @return length of a time value. */ public static int getTimeValueLength(int scale) { return getNanosSinceMidnightLength(scale); } /** * Returns the length of Date-Time2 values using {@code scale}. * * @param scale the time scale. * @return length of a time value. */ public static int getDateTimeValueLength(int scale) { return DAYS_INTO_CE_LENGTH + getNanosSinceMidnightLength(scale); } /** * Returns the length of Date-Time offset values using {@code scale}. * * @param scale the scale. * @return length of Date-Time offset values. */ static int getDatetimeoffsetValueLength(int scale) { return DAYS_INTO_CE_LENGTH + MINUTES_OFFSET_LENGTH + getNanosSinceMidnightLength(scale); } /** * Returns the length of Time offset values using {@code scale}. * * @param scale the scale. * @return */ private static int getNanosSinceMidnightLength(int scale) { Assert.isTrue(scale >= 0 && scale <= MAX_FRACTIONAL_SECONDS_SCALE, "Scale must be between 0 and 7"); return SCALED_TIME_LENGTHS[scale]; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/message/type/package-info.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Classes to read and build type information. */ @NonNullApi package io.r2dbc.mssql.message.type; import reactor.util.annotation.NonNullApi; ================================================ FILE: src/main/java/io/r2dbc/mssql/package-info.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * An implementation of the Reactive Relational Database Connection API for Microsoft SQL Servers. */ @NonNullApi package io.r2dbc.mssql; import reactor.util.annotation.NonNullApi; ================================================ FILE: src/main/java/io/r2dbc/mssql/util/Assert.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.util; import reactor.util.annotation.Nullable; import java.util.function.Supplier; /** * Assertion utility class that assists in validating arguments. *

    * Useful for identifying programmer errors early and clearly at runtime. *

    * For example, if the contract of a public method states it does not allow {@code null} arguments, {@code Assert} can * be used to validate that contract. Doing this clearly indicates a contract violation when it occurs and protects the * class's invariants. *

    * Typically used to validate method arguments rather than configuration properties, to check for cases that are usually * programmer errors rather than configuration errors. In contrast to configuration initialization code, there is * usually no point in falling back to defaults in such methods. *

    * This class is similar to JUnit's assertion library. If an argument value is deemed invalid, an * {@link IllegalArgumentException} is thrown (typically). For example: * *

     * Assert.notNull(clazz, "The class must not be null");
     * Assert.isTrue(i > 0, "The value must be greater than zero");
     * 
    *

    * Mainly for internal use within the framework; consider * Apache's Commons Lang for a more comprehensive suite of * {@code String} utilities. * * @author Mark Paluch */ public final class Assert { /** * Assert a boolean expression, throwing an {@code IllegalStateException} if the expression evaluates to * {@code false}. *

    * Call {@link #isTrue} if you wish to throw an {@code IllegalArgumentException} on an assertion failure. * *

         * Assert.state(id == null, "The id property must not already be initialized");
         * 
    * * @param expression a boolean expression * @param message the exception message to use if the assertion fails * @throws IllegalStateException if {@code expression} is {@code false} */ public static void state(boolean expression, String message) { if (!expression) { throw new IllegalStateException(message); } } /** * Assert a boolean expression, throwing an {@code IllegalStateException} if the expression evaluates to * {@code false}. *

    * Call {@link #isTrue} if you wish to throw an {@code IllegalArgumentException} on an assertion failure. * *

         * Assert.state(id == null, () -> "ID for " + entity.getName() + " must not already be initialized");
         * 
    * * @param expression a boolean expression * @param messageSupplier a supplier for the exception message to use if the assertion fails * @throws IllegalStateException if {@code expression} is {@code false} */ public static void state(boolean expression, Supplier messageSupplier) { if (!expression) { throw new IllegalStateException(nullSafeGet(messageSupplier)); } } /** * Assert a boolean expression, throwing an {@code IllegalArgumentException} if the expression evaluates to * {@code false}. * *
         * Assert.isTrue(i > 0, "The value must be greater than zero");
         * 
    * * @param expression a boolean expression * @param message the exception message to use if the assertion fails * @throws IllegalArgumentException if {@code expression} is {@code false} */ public static void isTrue(boolean expression, String message) { if (!expression) { throw new IllegalArgumentException(message); } } /** * Assert a boolean expression, throwing an {@code IllegalArgumentException} if the expression evaluates to * {@code false}. * *
         * Assert.isTrue(i > 0, () -> "The value '" + i + "' must be greater than zero");
         * 
    * * @param expression a boolean expression * @param messageSupplier a supplier for the exception message to use if the assertion fails * @throws IllegalArgumentException if {@code expression} is {@code false} */ public static void isTrue(boolean expression, Supplier messageSupplier) { if (!expression) { throw new IllegalArgumentException(nullSafeGet(messageSupplier)); } } /** * Assert that an object is {@code null}. * *
         * Assert.isNull(value, "The value must be null");
         * 
    * * @param object the object to check * @param message the exception message to use if the assertion fails * @throws IllegalArgumentException if the object is not {@code null} */ public static void isNull(@Nullable Object object, String message) { if (object != null) { throw new IllegalArgumentException(message); } } /** * Assert that an object is {@code null}. * *
         * Assert.isNull(value, () -> "The value '" + value + "' must be null");
         * 
    * * @param object the object to check * @param messageSupplier a supplier for the exception message to use if the assertion fails * @throws IllegalArgumentException if the object is not {@code null} */ public static void isNull(@Nullable Object object, Supplier messageSupplier) { if (object != null) { throw new IllegalArgumentException(nullSafeGet(messageSupplier)); } } /** * Assert that an object is not {@code null}. * *
         * Assert.notNull(clazz, "The class must not be null");
         * 
    * * @param object the object to check * @param message the exception message to use if the assertion fails * @throws IllegalArgumentException if the object is {@code null} */ public static void notNull(@Nullable Object object, String message) { if (object == null) { throw new IllegalArgumentException(message); } } /** * Assert that an object is not {@code null}. * *
         * Assert.notNull(clazz, () -> "The class '" + clazz.getName() + "' must not be null");
         * 
    * * @param object the object to check * @param messageSupplier a supplier for the exception message to use if the assertion fails * @throws IllegalArgumentException if the object is {@code null} */ public static void notNull(@Nullable Object object, Supplier messageSupplier) { if (object == null) { throw new IllegalArgumentException(nullSafeGet(messageSupplier)); } } /** * Assert that an object is not {@code null} and return the non-null instance. * *
         * Class<?> nonNullObject = Assert.requireNonNull(clazz, "The class must not be null");
         * 
    * * @param object the object to check * @param message the exception message to use if the assertion fails * @return the non-null {@code object} * @throws IllegalArgumentException if the object is {@code null} */ public static T requireNonNull(@Nullable T object, String message) { notNull(object, message); return object; } /** * Assert that an object is not {@code null} and return the non-null instance. * *
         * Class<?> nonNullObject = Assert.requireNonNull(clazz, () -> "The class '" + clazz.getName() + "' must not be null");
         * 
    * * @param object the object to check * @param messageSupplier a supplier for the exception message to use if the assertion fails * @return the non-null {@code object} * @throws IllegalArgumentException if the object is {@code null} */ public static T requireNonNull(@Nullable T object, Supplier messageSupplier) { notNull(object, messageSupplier); return object; } /** * Assert that the provided object is an instance of the provided class. *
    Assert.instanceOf(Foo.class, foo, "Foo expected");
    * * @param type the type to check against * @param obj the object to check * @param message a message which will be prepended to provide further context. * If it is empty or ends in ":" or ";" or "," or ".", a full exception message * will be appended. If it ends in a space, the name of the offending object's * type will be appended. In any other case, a ":" with a space and the name * of the offending object's type will be appended. * @throws IllegalArgumentException if the object is not an instance of type */ public static void isInstanceOf(Class type, @Nullable Object obj, String message) { notNull(type, "Type to check against must not be null"); if (!type.isInstance(obj)) { instanceCheckFailed(type, obj, message); } } /** * Assert that the provided object is an instance of the provided class. *
         * Assert.instanceOf(Foo.class, foo, () -> "Processing " + Foo.class.getSimpleName() + ":");
         * 
    * * @param type the type to check against * @param obj the object to check * @param messageSupplier a supplier for the exception message to use if the * assertion fails. See {@link #isInstanceOf(Class, Object, String)} for details. * @throws IllegalArgumentException if the object is not an instance of type */ public static void isInstanceOf(Class type, @Nullable Object obj, Supplier messageSupplier) { notNull(type, "Type to check against must not be null"); if (!type.isInstance(obj)) { instanceCheckFailed(type, obj, nullSafeGet(messageSupplier)); } } /** * Assert that the provided object is an instance of the provided class. *
    Assert.instanceOf(Foo.class, foo);
    * * @param type the type to check against * @param obj the object to check * @throws IllegalArgumentException if the object is not an instance of type */ public static void isInstanceOf(Class type, @Nullable Object obj) { isInstanceOf(type, obj, ""); } /** * Assert that {@code superType.isAssignableFrom(subType)} is {@code true}. *
    Assert.isAssignable(Number.class, myClass, "Number expected");
    * * @param superType the super type to check against * @param subType the sub type to check * @param message a message which will be prepended to provide further context. * If it is empty or ends in ":" or ";" or "," or ".", a full exception message * will be appended. If it ends in a space, the name of the offending sub type * will be appended. In any other case, a ":" with a space and the name of the * offending sub type will be appended. * @throws IllegalArgumentException if the classes are not assignable */ public static void isAssignable(Class superType, @Nullable Class subType, String message) { notNull(superType, "Super type to check against must not be null"); if (subType == null || !superType.isAssignableFrom(subType)) { assignableCheckFailed(superType, subType, message); } } /** * Assert that {@code superType.isAssignableFrom(subType)} is {@code true}. *
         * Assert.isAssignable(Number.class, myClass, () -> "Processing " + myAttributeName + ":");
         * 
    * * @param superType the super type to check against * @param subType the sub type to check * @param messageSupplier a supplier for the exception message to use if the * assertion fails. See {@link #isAssignable(Class, Class, String)} for details. * @throws IllegalArgumentException if the classes are not assignable */ public static void isAssignable(Class superType, @Nullable Class subType, Supplier messageSupplier) { notNull(superType, "Super type to check against must not be null"); if (subType == null || !superType.isAssignableFrom(subType)) { assignableCheckFailed(superType, subType, nullSafeGet(messageSupplier)); } } /** * Assert that {@code superType.isAssignableFrom(subType)} is {@code true}. *
    Assert.isAssignable(Number.class, myClass);
    * * @param superType the super type to check * @param subType the sub type to check * @throws IllegalArgumentException if the classes are not assignable */ public static void isAssignable(Class superType, Class subType) { isAssignable(superType, subType, ""); } private static void instanceCheckFailed(Class type, @Nullable Object obj, @Nullable String msg) { String className = (obj != null ? obj.getClass().getName() : "null"); String result = ""; boolean defaultMessage = true; if (StringUtils.hasLength(msg)) { if (endsWithSeparator(msg)) { result = msg + " "; } else { result = messageWithTypeName(msg, className); defaultMessage = false; } } if (defaultMessage) { result = result + ("Object of class [" + className + "] must be an instance of " + type); } throw new IllegalArgumentException(result); } private static void assignableCheckFailed(Class superType, @Nullable Class subType, @Nullable String msg) { String result = ""; boolean defaultMessage = true; if (StringUtils.hasLength(msg)) { if (endsWithSeparator(msg)) { result = msg + " "; } else { result = messageWithTypeName(msg, subType); defaultMessage = false; } } if (defaultMessage) { result = result + (subType + " is not assignable to " + superType); } throw new IllegalArgumentException(result); } private static boolean endsWithSeparator(String msg) { return (msg.endsWith(":") || msg.endsWith(";") || msg.endsWith(",") || msg.endsWith(".")); } private static String messageWithTypeName(String msg, @Nullable Object typeName) { return msg + (msg.endsWith(" ") ? "" : ": ") + typeName; } @Nullable private static String nullSafeGet(@Nullable Supplier messageSupplier) { return (messageSupplier != null ? messageSupplier.get() : null); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/util/DriverVersion.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.util; import reactor.util.annotation.Nullable; /** * Class that exposes the driver version. Fetches the "Implementation-Version" manifest attribute from the jar file. *

    * Note that some ClassLoaders do not expose the package metadata, hence this class might not be able to determine the * driver version in all environments. */ public final class DriverVersion { private DriverVersion() { } /** * Return the full version string of the present Spring codebase, or {@code null} if it cannot be determined. * * @see Package#getImplementationVersion() */ @Nullable public static Version getVersion() { Package pkg = DriverVersion.class.getPackage(); return (pkg != null && pkg.getImplementationVersion() != null ? Version.parse(pkg.getImplementationVersion()) : null); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/util/FluxDiscardOnCancel.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.util; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.publisher.Flux; import reactor.core.publisher.FluxOperator; import reactor.core.publisher.Operators; import reactor.util.Logger; import reactor.util.Loggers; import reactor.util.context.Context; import java.util.concurrent.atomic.AtomicBoolean; /** * A decorating operator that replays signals from its source to a {@link Subscriber} and drains the source upon {@link Subscription#cancel() cancel} and drops data signals until termination. * Draining data is required to complete a particular request/response window and clear the protocol state as client code expects to start a request/response conversation without any previous * response state. * * @param produced type * @author Mark Paluch */ class FluxDiscardOnCancel extends FluxOperator { private static final Logger logger = Loggers.getLogger(FluxDiscardOnCancel.class); private final Runnable cancelConsumer; FluxDiscardOnCancel(Flux source, Runnable cancelConsumer) { super(source); this.cancelConsumer = cancelConsumer; } @Override public void subscribe(CoreSubscriber actual) { this.source.subscribe(new FluxDiscardOnCancelSubscriber<>(actual, this.cancelConsumer)); } static class FluxDiscardOnCancelSubscriber extends AtomicBoolean implements CoreSubscriber, Subscription { final CoreSubscriber actual; final Context ctx; final Runnable cancelConsumer; Subscription s; FluxDiscardOnCancelSubscriber(CoreSubscriber actual, Runnable cancelConsumer) { this.actual = actual; this.ctx = actual.currentContext(); this.cancelConsumer = cancelConsumer; } @Override public void onSubscribe(Subscription s) { if (Operators.validate(this.s, s)) { this.s = s; this.actual.onSubscribe(this); } } @Override public Context currentContext() { return this.ctx; } @Override public void onNext(T t) { if (this.get()) { Operators.onDiscard(t, this.ctx); return; } this.actual.onNext(t); } @Override public void onError(Throwable t) { if (this.get()) { Operators.onErrorDropped(t, this.ctx); } else { this.actual.onError(t); } } @Override public void onComplete() { if (!this.get()) { this.actual.onComplete(); } } @Override public void request(long n) { this.s.request(n); } @Override public void cancel() { if (compareAndSet(false, true)) { if (logger.isDebugEnabled()) { logger.debug("received cancel signal"); } try { this.cancelConsumer.run(); } catch (Exception e) { Operators.onErrorDropped(e, this.ctx); } this.s.request(Long.MAX_VALUE); } } } } ================================================ FILE: src/main/java/io/r2dbc/mssql/util/Operators.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.util; import org.reactivestreams.Subscription; import reactor.core.publisher.Flux; /** * Operator utility. * * @author Mark Paluch */ public final class Operators { private Operators() { } /** * Replay signals from {@link Flux the source} until cancellation. Drains the source for data signals if the subscriber cancels the subscription. *

    * Draining data is required to complete a particular request/response window and clear the protocol state as client code expects to start a request/response conversation without leaving * previous frames on the stack. * * @param source the source to decorate. * @param The type of values in both source and output sequences. * @return decorated {@link Flux}. */ public static Flux discardOnCancel(Flux source) { return new FluxDiscardOnCancel<>(source, () -> { }); } /** * Replay signals from {@link Flux the source} until cancellation. Drains the source for data signals if the subscriber cancels the subscription. *

    * Draining data is required to complete a particular request/response window and clear the protocol state as client code expects to start a request/response conversation without leaving * previous frames on the stack. *

    Propagates the {@link Subscription#cancel()} signal to a {@link Runnable consumer}. * * @param source the source to decorate. * @param cancelConsumer {@link Runnable} notified when the resulting {@link Flux} receives a {@link Subscription#cancel() cancel} signal. * @param The type of values in both source and output sequences. * @return decorated {@link Flux}. */ public static Flux discardOnCancel(Flux source, Runnable cancelConsumer) { return new FluxDiscardOnCancel<>(source, cancelConsumer); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/util/PredicateUtils.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.util; import java.util.Arrays; import java.util.function.Predicate; /** * Utilities for working with {@link Predicate}s. */ public final class PredicateUtils { private PredicateUtils() { } /** * Negates a {@link Predicate}. Exists primarily to enable negation of method references that are {@link Predicate}s. * * @param t the predicate to negate * @param the type of element being tested * @return a negated predicate * @throws IllegalArgumentException when {@link Predicate} is {@code null}. * @see Predicate#negate() */ public static Predicate not(Predicate t) { Assert.requireNonNull(t, "t must not be null"); return t.negate(); } /** * Logical OR a collection of {@link Predicate}s. Exists primarily to enable the logical OR of method references that are {@link Predicate}s. * * @param ts the predicates to logical OR * @param the type of element being tested * @return a local ORd collection of predicates * @throws IllegalArgumentException when {@link Predicate predicates} is {@code null}. */ @SafeVarargs @SuppressWarnings("varargs") public static Predicate or(Predicate... ts) { Assert.requireNonNull(ts, "ts must not be null"); return Arrays.stream(ts).reduce(Predicate::or).orElseThrow(() -> new IllegalStateException("Unable to combine predicates together via logical OR")); } /** * Logical AND a collection of {@link Predicate}s. Exists primarily to enable the logical AND of method references that are {@link Predicate}s. * * @param ts the predicates to logical AND * @param the type of element being tested * @return a local ANDd collection of predicates * @throws IllegalArgumentException when {@link Predicate predicates} is {@code null}. */ @SafeVarargs @SuppressWarnings("varargs") public static Predicate and(Predicate... ts) { Assert.requireNonNull(ts, "ts must not be null"); return Arrays.stream(ts).reduce(Predicate::and).orElseThrow(() -> new IllegalStateException("Unable to combine predicates together via logical AND")); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/util/ReferenceCountUtil.java ================================================ /* * Copyright 2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.util; import io.netty.util.ReferenceCounted; import reactor.util.annotation.Nullable; /** * Collection of methods to handle objects that may implement {@link ReferenceCounted}. * * @author Mark Paluch * @since 1.0.3 */ public class ReferenceCountUtil { /** * Try to call {@link ReferenceCounted#release()} if the specified object implements {@link ReferenceCounted} and its reference count is greater than zero. * If the specified message doesn't implement {@link ReferenceCounted}, this method does nothing. */ public static void maybeRelease(@Nullable Object obj) { if (obj instanceof ReferenceCounted && ((ReferenceCounted) obj).refCnt() > 0) { ((ReferenceCounted) obj).release(); } } /** * Try to call {@link ReferenceCounted#release()} if the specified object implements {@link ReferenceCounted} and its reference count is greater than zero. * If the specified message doesn't implement {@link ReferenceCounted}, this method does nothing. */ public static void maybeSafeRelease(@Nullable Object obj) { if (obj instanceof ReferenceCounted && ((ReferenceCounted) obj).refCnt() > 0) { io.netty.util.ReferenceCountUtil.safeRelease(obj); } } // Utility constructor private ReferenceCountUtil() { } } ================================================ FILE: src/main/java/io/r2dbc/mssql/util/StringUtils.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.util; import reactor.util.annotation.Nullable; /** * Miscellaneous {@link String} utility methods. * * @author Mark Paluch */ public class StringUtils { //--------------------------------------------------------------------- // General convenience methods for working with Strings //--------------------------------------------------------------------- /** * Check whether the given {@code String} is empty. *

    This method accepts any Object as an argument, comparing it to * {@code null} and the empty String. As a consequence, this method * will never return {@code true} for a non-null non-String object. *

    The Object signature is useful for general attribute handling code * that commonly deals with Strings but generally has to iterate over * Objects since attributes may e.g. be primitive value objects as well. * * @param str the candidate String */ public static boolean isEmpty(@Nullable Object str) { return (str == null || "".equals(str)); } /** * Check that the given {@code CharSequence} is neither {@code null} nor * of length 0. *

    Note: this method returns {@code true} for a {@code CharSequence} * that purely consists of whitespace. *

         * StringUtils.hasLength(null) = false
         * StringUtils.hasLength("") = false
         * StringUtils.hasLength(" ") = true
         * StringUtils.hasLength("Hello") = true
         * 
    * * @param str the {@code CharSequence} to check (may be {@code null}) * @return {@code true} if the {@code CharSequence} is not {@code null} and has length * @see #hasText(String) */ public static boolean hasLength(@Nullable CharSequence str) { return (str != null && str.length() > 0); } /** * Check that the given {@code String} is neither {@code null} nor of length 0. *

    Note: this method returns {@code true} for a {@code String} that * purely consists of whitespace. * * @param str the {@code String} to check (may be {@code null}) * @return {@code true} if the {@code String} is not {@code null} and has length * @see #hasLength(CharSequence) * @see #hasText(String) */ public static boolean hasLength(@Nullable String str) { return (str != null && !str.isEmpty()); } /** * Check whether the given {@code CharSequence} contains actual text. *

    More specifically, this method returns {@code true} if the * {@code CharSequence} is not {@code null}, its length is greater than * 0, and it contains at least one non-whitespace character. *

         * StringUtils.hasText(null) = false
         * StringUtils.hasText("") = false
         * StringUtils.hasText(" ") = false
         * StringUtils.hasText("12345") = true
         * StringUtils.hasText(" 12345 ") = true
         * 
    * * @param str the {@code CharSequence} to check (may be {@code null}) * @return {@code true} if the {@code CharSequence} is not {@code null}, * its length is greater than 0, and it does not contain whitespace only * @see Character#isWhitespace */ public static boolean hasText(@Nullable CharSequence str) { return (str != null && str.length() > 0 && containsText(str)); } /** * Check whether the given {@code String} contains actual text. *

    More specifically, this method returns {@code true} if the * {@code String} is not {@code null}, its length is greater than 0, * and it contains at least one non-whitespace character. * * @param str the {@code String} to check (may be {@code null}) * @return {@code true} if the {@code String} is not {@code null}, its * length is greater than 0, and it does not contain whitespace only * @see #hasText(CharSequence) */ public static boolean hasText(@Nullable String str) { return (str != null && !str.isEmpty() && containsText(str)); } private static boolean containsText(CharSequence str) { int strLen = str.length(); for (int i = 0; i < strLen; i++) { if (!Character.isWhitespace(str.charAt(i))) { return true; } } return false; } } ================================================ FILE: src/main/java/io/r2dbc/mssql/util/Version.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.util; /** * Value object representing Version consisting of major, minor and bugfix part. */ public class Version implements Comparable { private static final String VERSION_PARSE_ERROR = "Invalid version string! Could not parse segment [%s] within [%s]."; private final int major; private final int minor; private final int bugfix; private final int build; /** * Creates a new {@link Version} from the given integer values. At least one value has to be given but a maximum of 4. * * @param parts must not be {@code null} or empty. */ public Version(int... parts) { Assert.notNull(parts, "Parts must not be null!"); Assert.isTrue(parts.length > 0 && parts.length < 5, "Parts must contain 1 to 5 segments!"); this.major = parts[0]; this.minor = parts.length > 1 ? parts[1] : 0; this.bugfix = parts.length > 2 ? parts[2] : 0; this.build = parts.length > 3 ? parts[3] : 0; Assert.isTrue(this.major >= 0, "Major version must be greater or equal zero!"); Assert.isTrue(this.minor >= 0, "Minor version must be greater or equal zero!"); Assert.isTrue(this.bugfix >= 0, "Bugfix version must be greater or equal zero!"); Assert.isTrue(this.build >= 0, "Build version must be greater or equal zero!"); } /** * Parses the given string representation of a version into a {@link Version} object. * * @param version must not be {@code null} or empty. * @return */ public static Version parse(String version) { String[] parts = version.trim().split("\\."); int[] intParts = new int[parts.length]; for (int i = 0; i < parts.length; i++) { String input = i == parts.length - 1 ? parts[i].replaceAll("\\D.*", "") : parts[i]; if (!input.isEmpty()) { try { intParts[i] = Integer.parseInt(input); } catch (IllegalArgumentException o_O) { throw new IllegalArgumentException(String.format(VERSION_PARSE_ERROR, input, version), o_O); } } } return new Version(intParts); } public int getMajor() { return this.major; } public int getMinor() { return this.minor; } public int getBugfix() { return this.bugfix; } /** * Returns whether the current {@link Version} is greater (newer) than the given one. * * @param version * @return */ public boolean isGreaterThan(Version version) { return compareTo(version) > 0; } /** * Returns whether the current {@link Version} is greater (newer) or the same as the given one. * * @param version * @return */ public boolean isGreaterThanOrEqualTo(Version version) { return compareTo(version) >= 0; } /** * Returns whether the current {@link Version} is the same as the given one. * * @param version * @return */ public boolean is(Version version) { return equals(version); } /** * Returns whether the current {@link Version} is less (older) than the given one. * * @param version * @return */ public boolean isLessThan(Version version) { return compareTo(version) < 0; } /** * Returns whether the current {@link Version} is less (older) or equal to the current one. * * @param version * @return */ public boolean isLessThanOrEqualTo(Version version) { return compareTo(version) <= 0; } /* * (non-Javadoc) * * @see java.lang.Comparable#compareTo(java.lang.Object) */ public int compareTo(Version that) { if (that == null) { return 1; } if (this.major != that.major) { return this.major - that.major; } if (this.minor != that.minor) { return this.minor - that.minor; } if (this.bugfix != that.bugfix) { return this.bugfix - that.bugfix; } if (this.build != that.build) { return this.build - that.build; } return 0; } /* * (non-Javadoc) * * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof Version)) { return false; } Version that = (Version) obj; return this.major == that.major && this.minor == that.minor && this.bugfix == that.bugfix && this.build == that.build; } /* * (non-Javadoc) * * @see java.lang.Object#hashCode() */ @Override public int hashCode() { int result = 17; result += 31 * this.major; result += 31 * this.minor; result += 31 * this.bugfix; result += 31 * this.build; return result; } /* * (non-Javadoc) * * @see java.lang.Object#toString() */ @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append(this.major).append(".").append(((this.minor <= 9) ? "0" : "")).append(this.minor); if (this.build != 0 || this.bugfix != 0) { builder.append('.').append(this.bugfix); } if (this.build != 0) { builder.append('.').append(this.build); } return builder.toString(); } } ================================================ FILE: src/main/java/io/r2dbc/mssql/util/package-info.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Utility code used throughout the project. */ @NonNullApi package io.r2dbc.mssql.util; import reactor.util.annotation.NonNullApi; ================================================ FILE: src/main/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider ================================================ io.r2dbc.mssql.MssqlConnectionFactoryProvider ================================================ FILE: src/test/java/io/r2dbc/mssql/BindingUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.r2dbc.mssql.codec.Encoded; import io.r2dbc.mssql.codec.RpcDirection; import io.r2dbc.mssql.message.type.TdsDataType; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link Binding}. * * @author Mark Paluch */ class BindingUnitTests { @Test void shouldReportFormalParameters() { Binding binding = new Binding(); binding.add("foo", RpcDirection.IN, Encoded.of(TdsDataType.INT8, Unpooled.EMPTY_BUFFER)); assertThat(binding.getFormalParameters()).isEqualTo("@foo bigint"); } @Test void shouldReportMultipleFormalParameters() { Binding binding = new Binding(); binding.add("foo", RpcDirection.IN, Encoded.of(TdsDataType.INT8, Unpooled.EMPTY_BUFFER)); binding.add("bar", RpcDirection.IN, Encoded.of(TdsDataType.MONEY8, Unpooled.EMPTY_BUFFER)); assertThat(binding.getFormalParameters()).isEqualTo("@foo bigint,@bar money"); } @Test void shouldReportCorrectIntermediateFormalParameters() { Binding binding = new Binding(); binding.add("foo", RpcDirection.IN, Encoded.of(TdsDataType.INT8, Unpooled.EMPTY_BUFFER)); assertThat(binding.getFormalParameters()).isEqualTo("@foo bigint"); binding.add("bar", RpcDirection.IN, Encoded.of(TdsDataType.MONEY8, Unpooled.EMPTY_BUFFER)); assertThat(binding.getFormalParameters()).isEqualTo("@foo bigint,@bar money"); } @Test void shouldBindParameters() { Binding binding = new Binding(); binding.add("foo", RpcDirection.IN, Encoded.of(TdsDataType.INT8, Unpooled.EMPTY_BUFFER)); assertThat(binding.getParameters()).isNotEmpty(); assertThat(binding.isEmpty()).isFalse(); assertThat(binding.size()).isEqualTo(1); } @Test void shouldClearBindings() { Binding binding = new Binding(); ByteBuf buffer = Unpooled.buffer(); binding.add("foo", RpcDirection.IN, Encoded.of(TdsDataType.INT8, buffer)); binding.clear(); assertThat(binding.isEmpty()).isTrue(); assertThat(buffer.refCnt()).isZero(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/CodecIntegrationTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.codec.DefaultCodecs; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.util.IntegrationTestSupport; import io.r2dbc.spi.Blob; import io.r2dbc.spi.Clob; import io.r2dbc.spi.ConnectionFactories; import io.r2dbc.spi.ConnectionFactoryOptions; import io.r2dbc.spi.Parameters; import io.r2dbc.spi.R2dbcType; import io.r2dbc.spi.Result; import io.r2dbc.spi.Type; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Hooks; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import reactor.util.annotation.Nullable; import java.math.BigDecimal; import java.math.BigInteger; import java.nio.ByteBuffer; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.ZonedDateTime; import java.util.Optional; import java.util.UUID; import java.util.function.Consumer; import java.util.stream.IntStream; import static org.assertj.core.api.Assertions.assertThat; /** * Integration tests for {@link DefaultCodecs} testing all known codecs with pre-defined values and {@code null} values. * * @author Mark Paluch */ class CodecIntegrationTests extends IntegrationTestSupport { static { Hooks.onOperatorDebug(); } @Test void shouldEncodeBooleanAsBit() { testType(connection, "BIT", true); } @Test void shouldEncodeBooleanAsTinyint() { testType(connection, "TINYINT", true, Boolean.class, (byte) 1); } @Test void shouldEncodeByteAsTinyint() { testType(connection, "TINYINT", (byte) 0x42); } @Test void shouldEncodeShortAsSmallint() { testType(connection, "SMALLINT", Short.MAX_VALUE); } @Test void shouldEncodeIntegerAInt() { testType(connection, "INT", Integer.MAX_VALUE); } @Test void shouldEncodeLongABigint() { testType(connection, "BIGINT", Long.MAX_VALUE); } @Test void shouldEncodeFloatAsReal() { testType(connection, "REAL", Float.MAX_VALUE); } @Test void shouldEncodeDoubleAsFloat() { testType(connection, "FLOAT", Double.MAX_VALUE); } @Test void shouldEncodeDoubleAsNumeric() { testType(connection, "NUMERIC(38,5)", new BigDecimal("12345.12345")); } @Test void shouldEncodeBigIntegerAsNumeric() { testType(connection, "NUMERIC(38,0)", new BigInteger("12345"), BigInteger.class, new BigDecimal("12345")); } @Test void shouldEncodeDoubleAsDecimal() { testType(connection, "DECIMAL(38,5)", new BigDecimal("12345.12345")); } @Test void shouldEncodeDoubleAsDecimal1() { testType(connection, "DECIMAL(38,0)", new BigDecimal("12345")); } @Test void shouldEncodeDate() { testType(connection, "DATE", LocalDate.parse("2018-11-08")); } @Test void shouldEncodeTime() { testType(connection, "TIME", LocalTime.parse("11:08:27.1")); } @Test void shouldEncodeDateTime() { testType(connection, "DATETIME", LocalDateTime.parse("2018-11-08T11:08:28.2")); } @Test void shouldEncodeDateTime2() { testType(connection, "DATETIME2", LocalDateTime.parse("2018-11-08T11:08:28.2")); } @Test void shouldEncodeZonedDateTimeAsDatetimeoffset() { testType(connection, "DATETIMEOFFSET", ZonedDateTime.parse("2018-08-27T17:41:14.890+00:45"), ZonedDateTime.class, OffsetDateTime.parse("2018-08-27T17:41:14.890+00:45")); testType(connection, "DATETIMEOFFSET", ZonedDateTime.parse("2018-08-27T17:41:14.890-01:45"), ZonedDateTime.class, OffsetDateTime.parse("2018-08-27T17:41:14.890-01:45")); } @Test void shouldEncodeOffsetDateTimeAsDatetimeoffset() { testType(connection, "DATETIMEOFFSET", OffsetDateTime.parse("2018-08-27T17:41:14.890+00:45")); } @Test void shouldEncodeGuid() { testType(connection, "uniqueidentifier", UUID.randomUUID()); } @Test void shouldEncodeStringAsVarchar() { testType(connection, "VARCHAR(255)", "Hello, World!"); testType(connection, "VARCHAR(255)", "Hello, World!", R2dbcType.VARCHAR); testType(connection, "VARCHAR(255)", "Hello, World!", R2dbcType.NVARCHAR); testType(connection, "VARCHAR(255)", "Hello, World!", SqlServerType.VARCHAR); testType(connection, "VARCHAR(255)", "Hello, World!", SqlServerType.NVARCHAR); } @Test void shouldEncodeStringAsNVarchar() { testType(connection, "NVARCHAR(255)", "Hello, World! äöü"); testType(connection, "NVARCHAR(255)", "Hello, World! äöü", R2dbcType.NVARCHAR); testType(connection, "NVARCHAR(255)", "Hello, World!äöü", SqlServerType.NVARCHAR); } @Test void shouldEncodeStringAsVarcharSendingCharsAsNatl() { ConnectionFactoryOptions options = builder().option(MssqlConnectionFactoryProvider.SEND_STRING_PARAMETERS_AS_UNICODE, false).build(); MssqlConnection natlConnection = Mono.from(ConnectionFactories.get(options).create()).cast(MssqlConnection.class).block(); testType(natlConnection, "VARCHAR(255)", "Hello, World!"); natlConnection.close().block(); } @Test void shouldEncodeStringAsVarcharMax() { testType(connection, "VARCHAR(MAX)", "Hello, World!"); } @Test void shouldEncodeStringAsVarcharMaxWithBigString() { String template = UUID.randomUUID().toString(); StringBuilder builder = new StringBuilder(); IntStream.range(0, 1900).forEach(ignore -> builder.append(template)); assertThat(builder).hasSize(68400); testType(connection, "VARCHAR(MAX)", builder.toString()); } @Test void shouldEncodeStringAsNVarcharMax() { testType(connection, "NVARCHAR(MAX)", "Hello, World! äöü"); testType(connection, "NVARCHAR(MAX)", "Hello, World! äöü", R2dbcType.NVARCHAR); testType(connection, "NVARCHAR(MAX)", "Hello, World! äöü", SqlServerType.NVARCHARMAX); testType(connection, "NVARCHAR(MAX)", "Hello, World! äöü", R2dbcType.VARCHAR); } @Test void shouldEncodeClobAsNVarcharMax() { testType(connection, "NVARCHAR(MAX)", Clob.from(Mono.just("Hello, World! äöü")), Clob.class, actual -> { assertThat(actual).isInstanceOf(Clob.class); Flux.from(((Clob) actual).stream()).as(StepVerifier::create).expectNext("Hello, World! äöü").verifyComplete(); }, actual -> { assertThat(actual).isEqualTo("Hello, World! äöü"); }, null); } @Test void shouldEncodeClobAsVarcharMaxAsNatl() { ConnectionFactoryOptions options = builder().option(MssqlConnectionFactoryProvider.SEND_STRING_PARAMETERS_AS_UNICODE, false).build(); MssqlConnection natlConnection = Mono.from(ConnectionFactories.get(options).create()).cast(MssqlConnection.class).block(); testType(natlConnection, "VARCHAR(MAX)", Clob.from(Mono.just("Hello, World!")), Clob.class, actual -> { assertThat(actual).isInstanceOf(Clob.class); Flux.from(((Clob) actual).stream()).as(StepVerifier::create).expectNext("Hello, World!").verifyComplete(); }, actual -> { assertThat(actual).isEqualTo("Hello, World!"); }, null); natlConnection.close().block(); } @Test void shouldEncodeClobAsVarcharMaxAsUnicode() { testType(connection, "VARCHAR(MAX)", Clob.from(Mono.just("Hello, World!")), Clob.class, actual -> { assertThat(actual).isInstanceOf(Clob.class); Flux.from(((Clob) actual).stream()).as(StepVerifier::create).expectNext("Hello, World!").verifyComplete(); }, actual -> { assertThat(actual).isEqualTo("Hello, World!"); }, null); } @Test void shouldEncodeStringAsText() { testType(connection, "TEXT", "Hello, World!"); } @Test void shouldEncodeStringAsNText() { testType(connection, "NTEXT", "Hello, World! äöü"); } @Test void shouldEncodeByteBufferAsBinary() { testType(connection, "BINARY(9)", ByteBuffer.wrap("foobarbaz".getBytes())); } @Test void shouldEncodeByteBufferAsVarBinary() { testType(connection, "VARBINARY(9)", ByteBuffer.wrap("foobarbaz".getBytes())); } @Test void shouldEncodeByteArrayAsVarBinaryMax() { testType(connection, "VARBINARY(MAX)", "foobarbaz".getBytes(), byte[].class, actual -> assertThat(actual).isEqualTo(ByteBuffer.wrap("foobarbaz".getBytes()))); testType(connection, "VARBINARY(MAX)", new byte[8000], byte[].class, actual -> assertThat(actual).isEqualTo(ByteBuffer.wrap(new byte[8000]))); testType(connection, "VARBINARY(MAX)", new byte[8001], byte[].class, actual -> assertThat(actual).isEqualTo(ByteBuffer.wrap(new byte[8001]))); testType(connection, "VARBINARY(MAX)", new byte[65534], byte[].class, actual -> assertThat(actual).isEqualTo(ByteBuffer.wrap(new byte[65534]))); testType(connection, "VARBINARY(MAX)", new byte[65535], byte[].class, actual -> assertThat(actual).isEqualTo(ByteBuffer.wrap(new byte[65535]))); testType(connection, "VARBINARY(MAX)", new byte[65536], byte[].class, actual -> assertThat(actual).isEqualTo(ByteBuffer.wrap(new byte[65536]))); } @Test void shouldEncodeBlobAsVarBinaryMax() { testType(connection, "VARBINARY(MAX)", Blob.from(Mono.just(ByteBuffer.wrap("foobarbaz".getBytes()))), Blob.class, actual -> { assertThat(actual).isInstanceOf(Blob.class); Mono.from(((Blob) actual).discard()).subscribe(); }, actual -> assertThat(actual).isEqualTo(ByteBuffer.wrap("foobarbaz".getBytes())), null); } @Test void shouldEncodeByteArrayAsImage() { testType(connection, "IMAGE", "foobarbaz".getBytes(), byte[].class, actual -> assertThat(actual).isEqualTo(ByteBuffer.wrap("foobarbaz".getBytes()))); } @Test void shouldEncodeByteArrayAsBinary() { testType(connection, "BINARY(9)", "foobarbaz".getBytes(), byte[].class, ByteBuffer.wrap("foobarbaz".getBytes())); } @Test void shouldEncodeByteArrayAsVarBinary() { testType(connection, "VARBINARY(9)", "foobarbaz".getBytes(), byte[].class, ByteBuffer.wrap("foobarbaz".getBytes())); } @Test void shouldEncodeByteBufferAsVarBinaryMax() { testType(connection, "VARBINARY(MAX)", ByteBuffer.wrap("foobarbaz".getBytes()), ByteBuffer.class, actual -> { assertThat(actual).isInstanceOf(ByteBuffer.class).isEqualTo(ByteBuffer.wrap("foobarbaz".getBytes())); }); } @Test void shouldEncodeByteBufferAsImage() { testType(connection, "IMAGE", ByteBuffer.wrap("foobarbaz".getBytes()), ByteBuffer.class, actual -> { assertThat(actual).isInstanceOf(ByteBuffer.class).isEqualTo(ByteBuffer.wrap("foobarbaz".getBytes())); }); } private void testType(MssqlConnection connection, String columnType, Object value) { testType(connection, columnType, value, value.getClass(), value, null); } private void testType(MssqlConnection connection, String columnType, Object value, @Nullable Type parameterValueType) { testType(connection, columnType, value, value.getClass(), value, parameterValueType); } private void testType(MssqlConnection connection, String columnType, Object value, Class valueClass, Object expectedGetObjectValue) { testType(connection, columnType, value, valueClass, actual -> assertThat(actual).isEqualTo(value), actual -> assertThat(actual).isEqualTo(expectedGetObjectValue), null); } private void testType(MssqlConnection connection, String columnType, Object value, Class valueClass, Object expectedGetObjectValue, @Nullable Type parameterValueType) { testType(connection, columnType, value, valueClass, actual -> assertThat(actual).isEqualTo(value), actual -> assertThat(actual).isEqualTo(expectedGetObjectValue), parameterValueType); } private void testType(MssqlConnection connection, String columnType, Object value, Class valueClass, Consumer nativeValueConsumer) { testType(connection, columnType, value, valueClass, actual -> assertThat(actual).isEqualTo(value), nativeValueConsumer, null); } private void testType(MssqlConnection connection, String columnType, Object value, Class valueClass, Consumer expectedValueConsumer, Consumer nativeValueConsumer, @Nullable Type parameterValueType) { createTable(connection, columnType); if (parameterValueType == null) { Flux.from(connection.createStatement("INSERT INTO codec_test values(@P0)") .bind("P0", value) .execute()) .flatMap(Result::getRowsUpdated) .as(StepVerifier::create) .expectNext(1L) .verifyComplete(); } else { Flux.from(connection.createStatement("INSERT INTO codec_test values(@P0)") .bind("P0", Parameters.in(parameterValueType, value)) .execute()) .flatMap(Result::getRowsUpdated) .as(StepVerifier::create) .expectNext(1L) .verifyComplete(); } if (value instanceof ByteBuffer) { ((ByteBuffer) value).rewind(); } connection.createStatement("SELECT my_col FROM codec_test") .execute() .flatMap(it -> it.map((row, rowMetadata) -> (Object) row.get("my_col", valueClass))) .as(StepVerifier::create) .consumeNextWith(expectedValueConsumer) .verifyComplete(); connection.createStatement("SELECT my_col FROM codec_test") .execute() .flatMap(it -> it.map((row, rowMetadata) -> row.get("my_col"))) .as(StepVerifier::create) .consumeNextWith(nativeValueConsumer) .verifyComplete(); if (parameterValueType == null) { Flux.from(connection.createStatement("UPDATE codec_test SET my_col = @P0") .bindNull("P0", value.getClass()) .execute()) .flatMap(Result::getRowsUpdated) .as(StepVerifier::create) .expectNext(1L) .verifyComplete(); connection.createStatement("SELECT my_col FROM codec_test") .execute() .flatMap(it -> it.map((row, rowMetadata) -> Optional.ofNullable((Object) row.get("my_col", valueClass)))) .as(StepVerifier::create) .expectNext(Optional.empty()) .verifyComplete(); Flux.from(connection.createStatement("UPDATE codec_test SET my_col = @P0") .bind("P0", Parameters.in(value.getClass())) .execute()) .flatMap(Result::getRowsUpdated) .as(StepVerifier::create) .expectNext(1L) .verifyComplete(); connection.createStatement("SELECT my_col FROM codec_test") .execute() .flatMap(it -> it.map((row, rowMetadata) -> Optional.ofNullable((Object) row.get("my_col", valueClass)))) .as(StepVerifier::create) .expectNext(Optional.empty()) .verifyComplete(); } else { Flux.from(connection.createStatement("UPDATE codec_test SET my_col = @P0") .bind("P0", Parameters.in(parameterValueType)) .execute()) .flatMap(Result::getRowsUpdated) .as(StepVerifier::create) .expectNext(1L) .verifyComplete(); connection.createStatement("SELECT my_col FROM codec_test") .execute() .flatMap(it -> it.map((row, rowMetadata) -> Optional.ofNullable((Object) row.get("my_col", valueClass)))) .as(StepVerifier::create) .expectNext(Optional.empty()) .verifyComplete(); } } private void createTable(MssqlConnection connection, String columnType) { connection.createStatement("DROP TABLE codec_test").execute() .flatMap(MssqlResult::getRowsUpdated) .onErrorResume(e -> Mono.empty()) .thenMany(connection.createStatement("CREATE TABLE codec_test (my_col " + columnType + ")") .execute().flatMap(MssqlResult::getRowsUpdated)) .as(StepVerifier::create) .verifyComplete(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/ColumnMetadataIntegrationTests.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.util.IntegrationTestSupport; import io.r2dbc.spi.Nullability; import io.r2dbc.spi.Result; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.ZonedDateTime; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; /** * Integration tests for {@link MssqlColumnMetadata} testing all known types. This is an integration test because we want the server to produce type information. * * @author Mark Paluch */ class ColumnMetadataIntegrationTests extends IntegrationTestSupport { @Test void shouldReportBit() { testType("BIT", true, new Expectation(1, 0, Boolean.class)); } @Test void shouldReportTinyint() { testType("TINYINT", 0x42, new Expectation(3, 0, Byte.class)); } @Test void shouldReportSmallint() { testType("SMALLINT", Short.MAX_VALUE, new Expectation(5, 0, Short.class)); } @Test void shouldReportInt() { testType("INT", Integer.MAX_VALUE, new Expectation(10, 0, Integer.class)); } @Test void shouldReportBigint() { testType("BIGINT", Long.MAX_VALUE, new Expectation(19, 0, Long.class)); } @Test void shouldReportReal() { testType("REAL", Float.MAX_VALUE, new Expectation(7, 0, Float.class)); } @Test void shouldReportFloat() { testType("FLOAT", Float.MAX_VALUE, new Expectation(15, 0, Double.class)); testType("FLOAT(48)", Float.MAX_VALUE, new Expectation(15, 0, Double.class)); } @Test void shouldReportNumeric() { testType("NUMERIC(38,5)", new BigDecimal("12345.12345"), new Expectation(38, 5, BigDecimal.class)); } @Test void shouldReportDecimal() { testType("DECIMAL(38,5)", new BigDecimal("12345.12345"), new Expectation(38, 5, BigDecimal.class)); } @Test void shouldReportDate() { testType("DATE", LocalDate.parse("2018-11-08"), new Expectation(10, 0, LocalDate.class)); } @Test void shouldReportTime() { testType("TIME", LocalTime.parse("11:08:27.1"), new Expectation(16, 7, LocalTime.class)); } @Test void shouldReportDateTime() { testType("DATETIME", LocalDateTime.parse("2018-11-08T11:08:28.2"), new Expectation(23, 3, LocalDateTime.class)); } @Test void shouldReportDateTime2() { testType("DATETIME2", LocalDateTime.parse("2018-11-08T11:08:28.2"), new Expectation(27, 7, LocalDateTime.class)); } @Test void shouldReportDatetimeoffset() { testType("DATETIMEOFFSET", ZonedDateTime.parse("2018-08-27T17:41:14.890+00:45"), new Expectation(34, 7, OffsetDateTime.class)); } @Test void shouldReportUuid() { testType("uniqueidentifier", UUID.randomUUID(), new Expectation(36, 0, UUID.class)); } @Test void shouldReportVarchar() { testType("VARCHAR(255)", "Hello, World!", new Expectation(255, 0, String.class)); } @Test void shouldEncodeStringAsNVarchar() { testType("NVARCHAR(200)", "Hello, World!", new Expectation(200, 0, String.class)); } private void testType(String columnType, Object value, Expectation expectation) { createTable(connection, columnType); Flux.from(connection.createStatement("INSERT INTO metadata_test values(@P0, @P0)") .bind("P0", value) .execute()) .flatMap(Result::getRowsUpdated) .as(StepVerifier::create) .expectNext(1L) .verifyComplete(); connection.createStatement("SELECT non_nullable_col FROM metadata_test") .execute() .flatMap(it -> it.map((row, rowMetadata) -> rowMetadata.getColumnMetadata("non_nullable_col"))) .as(StepVerifier::create) .consumeNextWith(it -> { assertThat(it.getNullability()).isEqualTo(Nullability.NON_NULL); assertThat(it.getPrecision()).isEqualTo(expectation.precision); assertThat(it.getScale()).isEqualTo(expectation.scale); assertThat(it.getJavaType()).isEqualTo(expectation.javaClass); }) .verifyComplete(); connection.createStatement("SELECT nullable_col FROM metadata_test") .execute() .flatMap(it -> it.map((row, rowMetadata) -> rowMetadata.getColumnMetadata("nullable_col"))) .as(StepVerifier::create) .consumeNextWith(it -> { assertThat(it.getNullability()).isEqualTo(Nullability.NULLABLE); assertThat(it.getPrecision()).isEqualTo(expectation.precision); assertThat(it.getScale()).isEqualTo(expectation.scale); assertThat(it.getJavaType()).isEqualTo(expectation.javaClass); }) .verifyComplete(); } private void createTable(MssqlConnection connection, String columnType) { connection.createStatement("DROP TABLE metadata_test").execute() .flatMap(MssqlResult::getRowsUpdated) .onErrorResume(e -> Mono.empty()) .thenMany(connection.createStatement("CREATE TABLE metadata_test (nullable_col " + columnType + " NULL, non_nullable_col " + columnType + " NOT NULL)") .execute().flatMap(MssqlResult::getRowsUpdated)) .as(StepVerifier::create) .verifyComplete(); } /** * Expectations wrapper. */ static class Expectation { final int precision; final int scale; final Class javaClass; Expectation(int precision, int scale, Class javaClass) { this.precision = precision; this.scale = scale; this.javaClass = javaClass; } } } ================================================ FILE: src/test/java/io/r2dbc/mssql/ConcurrentAccessIntegrationTests.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.util.IntegrationTestSupport; import io.r2dbc.spi.ColumnMetadata; import io.r2dbc.spi.Result; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.util.LinkedHashMap; import java.util.Map; /** * Integration tests for multiple active subscriptions. * * @author Mark Paluch */ class ConcurrentAccessIntegrationTests extends IntegrationTestSupport { @Test void shouldSerializeMultipleActiveSubscriptions() { createTable(connection); connection.beginTransaction().as(StepVerifier::create).verifyComplete(); Flux insertOne = insertRecord(connection, 1); Flux insertTwo = insertRecord(connection, 2); Flux insertThree = insertRecord(connection, 3); Flux.merge(insertOne, insertTwo, insertThree).as(StepVerifier::create).expectNextCount(3).verifyComplete(); connection.commitTransaction().as(StepVerifier::create).verifyComplete(); connection.createStatement("SELECT * FROM r2dbc_example ORDER BY first_name") .execute() .flatMap(it -> it.map((row, rowMetadata) -> { Map values = new LinkedHashMap<>(); for (ColumnMetadata column : rowMetadata.getColumnMetadatas()) { values.put(column.getName(), row.get(column.getName())); } return values; })) .as(StepVerifier::create) .expectNextCount(3) .verifyComplete(); } private void createTable(MssqlConnection connection) { connection.createStatement("DROP TABLE r2dbc_example").execute() .flatMap(MssqlResult::getRowsUpdated) .onErrorResume(e -> Mono.empty()) .thenMany(connection.createStatement("CREATE TABLE r2dbc_example (" + "id int PRIMARY KEY, " + "first_name varchar(255), " + "last_name varchar(255))") .execute().flatMap(MssqlResult::getRowsUpdated).then()) .as(StepVerifier::create) .verifyComplete(); } private Flux insertRecord(MssqlConnection connection, int id) { return connection.createStatement("INSERT INTO r2dbc_example VALUES(@id, @firstname, @lastname)") .bind("id", id) .bind("firstname", "Walter") .bind("lastname", "White") .execute() .flatMap(Result::getRowsUpdated); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/EscapeAwareNameMatcherUnitTests.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import org.junit.jupiter.api.Test; import java.util.Arrays; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link EscapeAwareNameMatcher}. * * @author Mark Paluch */ class EscapeAwareNameMatcherUnitTests { List columns = Arrays.asList("one", "two", "three", "one"); @Test void containsConsidersNamingRules() { assertThat(EscapeAwareNameMatcher.find("one", this.columns)).isNotNull(); assertThat(EscapeAwareNameMatcher.find("[one]", this.columns)).isNotNull(); assertThat(EscapeAwareNameMatcher.find("[one", this.columns)).isNull(); assertThat(EscapeAwareNameMatcher.find("one]", this.columns)).isNull(); assertThat(EscapeAwareNameMatcher.find("[One]", this.columns)).isNull(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/ExceptionFactoryUnitTests.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.message.token.InfoToken; import io.r2dbc.spi.R2dbcBadGrammarException; import io.r2dbc.spi.R2dbcDataIntegrityViolationException; import io.r2dbc.spi.R2dbcException; import io.r2dbc.spi.R2dbcNonTransientException; import io.r2dbc.spi.R2dbcPermissionDeniedException; import io.r2dbc.spi.R2dbcRollbackException; import io.r2dbc.spi.R2dbcTransientException; import io.r2dbc.spi.R2dbcTransientResourceException; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link ExceptionFactory}. * * @author Mark Paluch */ class ExceptionFactoryUnitTests { @Test void shouldTranslateWellKnownGrammarErrors() { R2dbcException exception = ExceptionFactory.createException(new InfoToken(0, 130, 0x00, 0x10, "err", "", "", 0), ""); assertThat(exception).isInstanceOf(R2dbcBadGrammarException.class); } @Test void shouldTranslateWellKnownIntegrityViolation() { R2dbcException exception = ExceptionFactory.createException(new InfoToken(0, 2601, 0x00, 0x0E, "err", "", "", 0), ""); assertThat(exception).isInstanceOf(R2dbcDataIntegrityViolationException.class); } @Test void shouldTranslateGeneralPermissionErrors() { R2dbcException exception = ExceptionFactory.createException(new InfoToken(0, 999, 0x00, 0x0E, "err", "", "", 0), ""); assertThat(exception).isInstanceOf(R2dbcPermissionDeniedException.class); } @Test void shouldTranslateWellKnownTransientError() { R2dbcException exception = ExceptionFactory.createException(new InfoToken(0, 701, 0x00, 0x13, "err", "", "", 0), ""); assertThat(exception).isInstanceOf(R2dbcTransientException.class); } @Test void shouldTranslateGeneralGrammarErrors() { R2dbcException exception = ExceptionFactory.createException(new InfoToken(0, 999, 0x00, 0x0B, "err", "", "", 0), ""); assertThat(exception).isInstanceOf(R2dbcBadGrammarException.class); exception = ExceptionFactory.createException(new InfoToken(0, 999, 0x00, 0x0F, "err", "", "", 0), ""); assertThat(exception).isInstanceOf(R2dbcBadGrammarException.class); } @Test void shouldTranslateGeneralIntegrityViolation() { R2dbcException exception = ExceptionFactory.createException(new InfoToken(0, 999, 0x00, 0x0C, "err", "", "", 0), ""); assertThat(exception).isInstanceOf(R2dbcDataIntegrityViolationException.class); } @Test void shouldTranslateGeneralRollbackException() { R2dbcException exception = ExceptionFactory.createException(new InfoToken(0, 999, 0x00, 0x0D, "err", "", "", 0), ""); assertThat(exception).isInstanceOf(R2dbcRollbackException.class); } @Test void shouldTranslateGeneralError() { R2dbcException exception = ExceptionFactory.createException(new InfoToken(0, 999, 0x00, 0x10, "err", "", "", 0), ""); assertThat(exception).isInstanceOf(R2dbcNonTransientException.class); } @Test void shouldTranslateGeneralResourceException() { R2dbcException exception = ExceptionFactory.createException(new InfoToken(0, 999, 0x00, 0x11, "err", "", "", 0), ""); assertThat(exception).isInstanceOf(R2dbcTransientResourceException.class); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/GeneratedValuesUnitTests.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.client.ssl.SslState; import io.r2dbc.mssql.message.token.DoneToken; import io.r2dbc.mssql.message.token.ReturnStatus; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.test.StepVerifier; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Unit tests for {@link GeneratedValues}. * * @author Mark Paluch */ class GeneratedValuesUnitTests { @Test void shouldRejectNullColumns() { assertThatThrownBy(() -> GeneratedValues.getGeneratedKeysClause((String[]) null)).isInstanceOf(IllegalArgumentException.class); assertThatThrownBy(() -> GeneratedValues.getGeneratedKeysClause(new String[]{null})).isInstanceOf(IllegalArgumentException.class); } @Test void shouldRejectMultipleColumns() { assertThatThrownBy(() -> GeneratedValues.getGeneratedKeysClause("foo", "bar")).isInstanceOf(UnsupportedOperationException.class); } @Test void shouldAugmentQuery() { assertThat(GeneratedValues.augmentQuery("foo", new String[]{"bar"})).isEqualTo("foo SELECT SCOPE_IDENTITY() AS bar"); } @Test void shouldReportGeneratedKeysExpectation() { assertThat(GeneratedValues.shouldExpectGeneratedKeys(null)).isFalse(); assertThat(GeneratedValues.shouldExpectGeneratedKeys(new String[0])).isTrue(); } @Test void shouldGenerateDefaultClause() { assertThat(GeneratedValues.getGeneratedKeysClause()).isEqualTo("SELECT SCOPE_IDENTITY() AS GENERATED_KEYS"); } @Test void shouldGenerateCustomizedClause() { assertThat(GeneratedValues.getGeneratedKeysClause("foo")).isEqualTo("SELECT SCOPE_IDENTITY() AS foo"); } @Test void shouldReorderMessageFlowForGeneratedKeys() { ReturnStatus status = ReturnStatus.create(2); DoneToken count = DoneToken.count(10); ReturnStatus finalSegment = ReturnStatus.create(2); Flux.just(status, count, SslState.CONNECTION, DoneToken.create(1), finalSegment) // .transform(GeneratedValues::reduceToSingleCountDoneToken) // .as(StepVerifier::create) // .expectNext(status, SslState.CONNECTION, count, finalSegment) // .verifyComplete(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/IndefinitePreparedStatementCacheUnitTests.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import org.junit.jupiter.api.Test; import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link IndefinitePreparedStatementCache}. * * @author Mark Paluch */ class IndefinitePreparedStatementCacheUnitTests { @Test void shouldReuseCachedStatements() { AtomicInteger parseCounter = new AtomicInteger(); IndefinitePreparedStatementCache cache = new IndefinitePreparedStatementCache(); cache.getParsedSql("statement", s -> { parseCounter.incrementAndGet(); return Optional.of(s); }); cache.getParsedSql("statement", s -> { parseCounter.incrementAndGet(); return Optional.of(s); }); assertThat(parseCounter).hasValue(1); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/JsonIntegrationTests.java ================================================ /* * Copyright 2021-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.util.IntegrationTestSupport; import org.junit.jupiter.api.Test; import reactor.test.StepVerifier; /** * Integration tests using JSON as return type. * * @author Mark Paluch */ class JsonIntegrationTests extends IntegrationTestSupport { @Test void shouldExecuteForJsonSimple() { connection.createStatement("select 1 as a for json path").execute() .flatMap(result -> result.map((row, rowMetadata) -> row.get(0))) .as(StepVerifier::create) .expectNext("[{\"a\":1}]") .verifyComplete(); } @Test void shouldExecuteForJsonParametrized() { connection.createStatement("select 1 as a where @P0 = @P0 for json path").bind("@P0", true).fetchSize(0).execute() .flatMap(result -> result.map((row, rowMetadata) -> row.get(0))) .as(StepVerifier::create) .expectNext("[{\"a\":1}]") .verifyComplete(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/LobIntegrationTests.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.codec.DefaultCodecs; import io.r2dbc.mssql.util.IntegrationTestSupport; import io.r2dbc.spi.Blob; import io.r2dbc.spi.Clob; import io.r2dbc.spi.Result; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.IntStream; import static org.assertj.core.api.Assertions.assertThat; /** * Integration tests for {@link DefaultCodecs} testing all known codecs with pre-defined values and {@code null} values. * * @author Mark Paluch */ class LobIntegrationTests extends IntegrationTestSupport { static byte[] ALL_BYTES = new byte[-(-128) + 127]; static { for (int i = -128; i < 127; i++) { ALL_BYTES[-(-128) + i] = (byte) i; } } @Test void testNullBlob() { createTable(connection, "IMAGE"); Flux.from(connection.createStatement("INSERT INTO lob_test values(@P0)") .bindNull("P0", Blob.class) .execute()) .flatMap(Result::getRowsUpdated) .as(StepVerifier::create) .expectNext(1L) .verifyComplete(); connection.createStatement("SELECT my_col FROM lob_test") .execute() .flatMap(it -> it.map((row, rowMetadata) -> Optional.ofNullable(row.get("my_col", Blob.class)))) .as(StepVerifier::create) .consumeNextWith(actual -> assertThat(actual).isEqualTo(Optional.empty())) .verifyComplete(); } @Test void testSmallBlob() { createTable(connection, "IMAGE"); Flux.from(connection.createStatement("INSERT INTO lob_test values(@P0)") .bind("P0", Blob.from(Mono.just("foo".getBytes()).map(ByteBuffer::wrap))) .execute()) .flatMap(Result::getRowsUpdated) .as(StepVerifier::create) .expectNext(1L) .verifyComplete(); connection.createStatement("SELECT my_col FROM lob_test") .execute() .flatMap(it -> it.map((row, rowMetadata) -> row.get("my_col", Blob.class))) .flatMap(Blob::stream) .as(StepVerifier::create) .consumeNextWith(actual -> assertThat(actual).isEqualTo(ByteBuffer.wrap("foo".getBytes()))) .verifyComplete(); } @Test void testBigBlob() { int count = 1500; // ~ 382kb createTable(connection, "VARBINARY(MAX)"); Flux.from(connection.createStatement("INSERT INTO lob_test values(@P0)") .bind("P0", Blob.from(Flux.range(0, count).map(it -> ByteBuffer.wrap(ALL_BYTES)))) .execute()) .flatMap(Result::getRowsUpdated) .as(StepVerifier::create) .expectNext(1L) .verifyComplete(); connection.createStatement("SELECT my_col FROM lob_test") .execute() .flatMap(it -> it.map((row, rowMetadata) -> row.get("my_col", Blob.class))) .flatMap(Blob::stream) .map(Buffer::remaining) .collect(Collectors.summingInt(value -> value)) .as(StepVerifier::create) .expectNext(count * ALL_BYTES.length) .verifyComplete(); } @Test void testBigBlobs() { int count = 512000; createTable(connection, "NVARCHAR(MAX)"); CharBuffer chars = CharBuffer.allocate(count); IntStream.range(0, count).forEach(i -> chars.put((char) ((i % 26) + 'a'))); chars.flip(); String data = chars.toString(); for (int i = 0; i < 30; i++) { Flux.from(connection.createStatement("INSERT INTO lob_test values(@P0)") .bind("P0", data) .execute()) .flatMap(Result::getRowsUpdated) .as(StepVerifier::create) .expectNext(1L) .verifyComplete(); } connection.createStatement("SELECT my_col FROM lob_test") .execute() .concatMap(it -> it.map((row, rowMetadata) -> row.get("my_col"))) .as(StepVerifier::create) .expectNextCount(30) .verifyComplete(); } @Test void testByteArrayBlob() { int count = 1500; // ~ 382kb createTable(connection, "VARBINARY(MAX)"); byte[] bytes = new byte[count * ALL_BYTES.length]; for (int i = 0; i < count; i++) { System.arraycopy(ALL_BYTES, 0, bytes, i * ALL_BYTES.length, ALL_BYTES.length); } Flux.from(connection.createStatement("INSERT INTO lob_test values(@P0)") .bind("P0", bytes) .execute()) .flatMap(Result::getRowsUpdated) .as(StepVerifier::create) .expectNext(1L) .verifyComplete(); connection.createStatement("SELECT my_col FROM lob_test") .execute() .flatMap(it -> it.map((row, rowMetadata) -> row.get("my_col", Blob.class))) .flatMap(Blob::stream) .map(Buffer::remaining) .collect(Collectors.summingInt(value -> value)) .as(StepVerifier::create) .expectNext(count * ALL_BYTES.length) .verifyComplete(); connection.createStatement("SELECT my_col FROM lob_test") .execute() .flatMap(it -> it.map((row, rowMetadata) -> row.get("my_col", byte[].class))) .as(StepVerifier::create) .consumeNextWith(actual -> assertThat(actual).isEqualTo(bytes)) .verifyComplete(); } @Test void testNullClob() { createTable(connection, "NTEXT"); Flux.from(connection.createStatement("INSERT INTO lob_test values(@P0)") .bindNull("P0", Clob.class) .execute()) .flatMap(Result::getRowsUpdated) .as(StepVerifier::create) .expectNext(1L) .verifyComplete(); connection.createStatement("SELECT my_col FROM lob_test") .execute() .flatMap(it -> it.map((row, rowMetadata) -> Optional.ofNullable(row.get("my_col", Clob.class)))) .as(StepVerifier::create) .consumeNextWith(actual -> assertThat(actual).isEqualTo(Optional.empty())) .verifyComplete(); } @Test void testSmallClob() { createTable(connection, "NTEXT"); Flux.from(connection.createStatement("INSERT INTO lob_test values(@P0)") .bind("P0", Clob.from(Mono.just("foo"))) .execute()) .flatMap(Result::getRowsUpdated) .as(StepVerifier::create) .expectNext(1L) .verifyComplete(); connection.createStatement("SELECT my_col FROM lob_test") .execute() .flatMap(it -> it.map((row, rowMetadata) -> row.get("my_col", Clob.class))) .flatMap(Clob::stream) .as(StepVerifier::create) .consumeNextWith(actual -> assertThat(actual).isEqualTo("foo")) .verifyComplete(); } private void createTable(MssqlConnection connection, String columnType) { connection.createStatement("DROP TABLE lob_test").execute() .flatMap(MssqlResult::getRowsUpdated) .onErrorResume(e -> Mono.empty()) .thenMany(connection.createStatement("CREATE TABLE lob_test (my_col " + columnType + ")") .execute().flatMap(MssqlResult::getRowsUpdated)) .as(StepVerifier::create) .verifyComplete(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/LoginFlowUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.client.TestClient; import io.r2dbc.mssql.message.token.DoneToken; import io.r2dbc.mssql.message.token.ErrorToken; import io.r2dbc.mssql.message.token.Prelogin; import io.r2dbc.spi.R2dbcPermissionDeniedException; import org.junit.jupiter.api.Test; import reactor.test.StepVerifier; import java.util.ArrayList; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link LoginFlow}. * * @author Mark Paluch */ class LoginFlowUnitTests { @Test void shouldInitiateLogin() { List tokens = new ArrayList<>(); tokens.add(new Prelogin.Version(14, 0)); tokens.add(new Prelogin.Encryption(Prelogin.Encryption.ENCRYPT_NOT_SUP)); tokens.add(Prelogin.Terminator.INSTANCE); Prelogin response = new Prelogin(tokens); TestClient client = TestClient.builder() .assertNextRequestWith(actual -> assertThat(actual).isInstanceOf(Prelogin.class)) .thenRespond(response) .build(); LoginConfiguration login = new LoginConfiguration("app", null, "db", "host", "bar", "server", false, "foo"); LoginFlow.exchange(client, login) .as(StepVerifier::create) .verifyComplete(); } @Test void shouldFinishLogin() { TestClient client = TestClient.builder() .assertNextRequestWith(actual -> assertThat(actual).isInstanceOf(Prelogin.class)) .thenRespond(DoneToken.create(0)) .build(); LoginConfiguration login = new LoginConfiguration("app", null, "db", "host", "bar", "server", false, "foo"); LoginFlow.exchange(client, login) .as(StepVerifier::create) .expectNext(DoneToken.create(0)) .verifyComplete(); } @Test void shouldPropagateError() { TestClient client = TestClient.builder() .assertNextRequestWith(actual -> assertThat(actual).isInstanceOf(Prelogin.class)) .thenRespond(new ErrorToken(0, 0, (byte) 0x00, (byte) 0x0E, "some error", "", "", 0)) .expectClose() .build(); LoginConfiguration login = new LoginConfiguration("app", null, "db", "host", "bar", "server", false, "foo"); LoginFlow.exchange(client, login) .as(StepVerifier::create) .expectError(R2dbcPermissionDeniedException.class) .verify(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/MssqlBatchIntegrationTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.util.IntegrationTestSupport; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Hooks; import reactor.test.StepVerifier; import java.util.concurrent.atomic.AtomicLong; import static org.assertj.core.api.Assertions.assertThat; /** * Integration tests for {@link MssqlBatch}. * * @author Mark Paluch */ class MssqlBatchIntegrationTests extends IntegrationTestSupport { static { Hooks.onOperatorDebug(); } @Test void shouldRunBatchWithMultipleResults() { AtomicLong resultCounter = new AtomicLong(); AtomicLong firstUpdateCount = new AtomicLong(); AtomicLong rowCount = new AtomicLong(); Flux.from(connection.createBatch().add("DECLARE @t TABLE(i INT)").add("INSERT INTO @t VALUES (1),(2),(3)").add("SELECT * FROM @t") .execute()).flatMap(it -> { if (resultCounter.compareAndSet(0, 1)) { return it.getRowsUpdated().doOnNext(firstUpdateCount::set).then(); } if (resultCounter.incrementAndGet() == 2) { return it.map(((row, rowMetadata) -> { rowCount.incrementAndGet(); return new Object(); })).then(); } throw new IllegalStateException("Unexpected result"); }).as(StepVerifier::create).verifyComplete(); assertThat(resultCounter).hasValue(2); assertThat(firstUpdateCount).hasValue(3); assertThat(rowCount).hasValue(3); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/MssqlBatchUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.client.TestClient; import io.r2dbc.mssql.message.TransactionDescriptor; import io.r2dbc.mssql.message.token.DoneToken; import io.r2dbc.mssql.message.token.ErrorToken; import io.r2dbc.mssql.message.token.SqlBatch; import org.junit.jupiter.api.Test; import reactor.test.StepVerifier; /** * Unit tests for {@link MssqlBatch}. * * @author Mark Paluch */ class MssqlBatchUnitTests { @Test void shouldExecuteSingleBatch() { TestClient client = TestClient.builder() .expectRequest(SqlBatch.create(1, TransactionDescriptor.empty(), "foo")) .thenRespond(DoneToken.create(1)) .build(); new MssqlBatch(client, new TestConnectionOptions()) .add("foo") .execute() .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); } @Test void shouldExecuteMultiBatch() { TestClient client = TestClient.builder() .expectRequest(SqlBatch.create(1, TransactionDescriptor.empty(), "foo; bar")) .thenRespond(DoneToken.create(1), DoneToken.create(1)) .build(); new MssqlBatch(client, new TestConnectionOptions()) .add("foo") .add("bar") .execute() .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); } @Test void shouldFailOnExecution() { TestClient client = TestClient.builder() .expectRequest(SqlBatch.create(1, TransactionDescriptor.empty(), "foo")) .thenRespond(new ErrorToken(1, 1, (byte) 0, (byte) 0, "error", "server", "proc", 0)) .build(); new MssqlBatch(client, new TestConnectionOptions()) .add("foo") .execute() .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/MssqlCancelIntegrationTests.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.util.IntegrationTestSupport; import io.r2dbc.spi.Result; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.reactivestreams.Subscription; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; /** * Integration tests for {@link Subscription subscription cancellation} {@link MssqlConnection} and {@link MssqlStatement}. * * @author Mark Paluch */ class MssqlCancelIntegrationTests extends IntegrationTestSupport { static boolean initialized = false; @BeforeEach void setUp() { if (!initialized) { createTable(connection, "r2dbc_example"); createTable(connection, "r2dbc_empty"); for (int i = 0; i < 100; i++) { insertRecord(connection, i); } initialized = true; } } @Test void shouldCancelUnparametrizedBatch() { connection.createStatement("SELECT * FROM r2dbc_example") .fetchSize(0) .execute() .concatMap(it -> it.map((row, metadata) -> row.get("id", Integer.class))) .as(it -> StepVerifier.create(it, 0)) .thenRequest(1) .expectNextCount(1) .thenCancel() .verify(); connection.createStatement("SELECT * FROM r2dbc_empty") .execute() .flatMap(it -> it.map((row, metadata) -> row.get("id"))) .as(StepVerifier::create) .verifyComplete(); } @Test void shouldCancelUnparametrizedCursoredBatch() { connection.createStatement("SELECT * FROM r2dbc_example") .fetchSize(10) .execute() .concatMap(it -> it.map((row, metadata) -> row.get("id", Integer.class))) .as(it -> StepVerifier.create(it, 0)) .thenRequest(1) .expectNextCount(1) .thenCancel() .verify(); connection.createStatement("SELECT * FROM r2dbc_empty") .execute() .flatMap(it -> it.map((row, metadata) -> row.get("id"))) .as(StepVerifier::create) .verifyComplete(); } @Test void shouldCancelParametrizedBatch() { connection.createStatement("SELECT * FROM r2dbc_example where id != @P1") .bind("@P1", -1) .fetchSize(0) .execute() .concatMap(it -> it.map((row, metadata) -> row.get("id", Integer.class))) .as(it -> StepVerifier.create(it, 0)) .thenRequest(1) .expectNextCount(1) .thenCancel() .verify(); connection.createStatement("SELECT * FROM r2dbc_empty") .execute() .flatMap(it -> it.map((row, metadata) -> row.get("id"))) .as(StepVerifier::create) .verifyComplete(); } @Test void shouldCancelParametrizedCursoredBatch() { connection.createStatement("SELECT * FROM r2dbc_example where id != @P1") .bind("@P1", -1) .fetchSize(10) .execute() .concatMap(it -> it.map((row, metadata) -> row.get("id", Integer.class))) .as(it -> StepVerifier.create(it, 0)) .thenRequest(1) .expectNextCount(1) .thenCancel() .verify(); connection.createStatement("SELECT * FROM r2dbc_empty") .execute() .flatMap(it -> it.map((row, metadata) -> row.get("id"))) .as(StepVerifier::create) .verifyComplete(); } private void createTable(MssqlConnection connection, String table) { connection.createStatement("DROP TABLE " + table).execute() .flatMap(MssqlResult::getRowsUpdated) .onErrorResume(e -> Mono.empty()) .thenMany(connection.createStatement("CREATE TABLE " + table + " (" + "id int PRIMARY KEY, " + "first_name varchar(255), " + "last_name varchar(255))") .execute().flatMap(MssqlResult::getRowsUpdated).then()) .as(StepVerifier::create) .verifyComplete(); } private void insertRecord(MssqlConnection connection, int id) { Flux.from(connection.createStatement("INSERT INTO r2dbc_example VALUES(@id, @firstname, @lastname)") .bind("id", id) .bind("firstname", "Walter") .bind("lastname", "White") .execute()) .flatMap(Result::getRowsUpdated) .as(StepVerifier::create) .expectNext(1L) .verifyComplete(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/MssqlConnectionConfigurationUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.message.tds.Redirect; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.testcontainers.shaded.org.bouncycastle.asn1.x500.X500Name; import org.testcontainers.shaded.org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; import org.testcontainers.shaded.org.bouncycastle.cert.X509CertificateHolder; import org.testcontainers.shaded.org.bouncycastle.cert.X509v3CertificateBuilder; import org.testcontainers.shaded.org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.testcontainers.shaded.org.bouncycastle.operator.ContentSigner; import org.testcontainers.shaded.org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import reactor.netty.resources.ConnectionProvider; import java.io.File; import java.io.FileOutputStream; import java.math.BigInteger; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.KeyStore; import java.security.SecureRandom; import java.security.cert.Certificate; import java.util.Calendar; import java.util.Date; import java.util.UUID; import java.util.function.Predicate; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** * Unit tests for {@link MssqlConnectionConfiguration}. * * @author Mark Paluch * @author Paul Johe */ final class MssqlConnectionConfigurationUnitTests { @Test void builderNoApplicationName() { assertThatIllegalArgumentException().isThrownBy(() -> MssqlConnectionConfiguration.builder().applicationName(null)) .withMessage("applicationName must not be null"); } @Test void builderNoConnectionId() { assertThatIllegalArgumentException().isThrownBy(() -> MssqlConnectionConfiguration.builder().connectionId(null)) .withMessage("connectionId must not be null"); } @Test void builderNoHost() { assertThatIllegalArgumentException().isThrownBy(() -> MssqlConnectionConfiguration.builder().host(null)) .withMessage("host must not be null"); } @Test void builderNoPassword() { assertThatIllegalArgumentException().isThrownBy(() -> MssqlConnectionConfiguration.builder().password(null)) .withMessage("password must not be null"); } @Test void builderNoUsername() { assertThatIllegalArgumentException().isThrownBy(() -> MssqlConnectionConfiguration.builder().username(null)) .withMessage("username must not be null"); } @Test void configuration() { UUID connectionId = UUID.randomUUID(); Predicate TRUE = s -> true; ConnectionProvider connectionProvider = ConnectionProvider.create("test"); MssqlConnectionConfiguration configuration = MssqlConnectionConfiguration.builder() .connectionId(connectionId) .database("test-database") .host("test-host") .password("test-password") .preferCursoredExecution(TRUE) .port(100) .username("test-username") .sendStringParametersAsUnicode(false) .connectionProvider(connectionProvider) .build(); assertThat(configuration) .hasFieldOrPropertyWithValue("connectionId", connectionId) .hasFieldOrPropertyWithValue("connectionProvider", connectionProvider) .hasFieldOrPropertyWithValue("database", "test-database") .hasFieldOrPropertyWithValue("host", "test-host") .hasFieldOrPropertyWithValue("password", "test-password") .hasFieldOrPropertyWithValue("preferCursoredExecution", TRUE) .hasFieldOrPropertyWithValue("port", 100) .hasFieldOrPropertyWithValue("username", "test-username") .hasFieldOrPropertyWithValue("sendStringParametersAsUnicode", false); } @Test void configurationDefaults() { MssqlConnectionConfiguration configuration = MssqlConnectionConfiguration.builder() .applicationName("r2dbc") .database("test-database") .host("test-host") .password("test-password") .username("test-username") .build(); assertThat(configuration) .hasFieldOrPropertyWithValue("applicationName", "r2dbc") .hasFieldOrPropertyWithValue("database", "test-database") .hasFieldOrPropertyWithValue("host", "test-host") .hasFieldOrPropertyWithValue("password", "test-password") .hasFieldOrPropertyWithValue("port", 1433) .hasFieldOrPropertyWithValue("username", "test-username") .hasFieldOrPropertyWithValue("sendStringParametersAsUnicode", true); } @Test void constructorNoNoHost() { assertThatIllegalArgumentException().isThrownBy(() -> MssqlConnectionConfiguration.builder() .password("test-password") .username("test-username") .build()) .withMessage("host must not be null"); } @Test void constructorNoPassword() { assertThatIllegalArgumentException().isThrownBy(() -> MssqlConnectionConfiguration.builder() .host("test-host") .username("test-username") .build()) .withMessage("password must not be null"); } @Test void constructorNoUsername() { assertThatIllegalArgumentException().isThrownBy(() -> MssqlConnectionConfiguration.builder() .host("test-host") .password("test-password") .build()) .withMessage("username must not be null"); } @Test void constructorNoSslCustomizer() { assertThatIllegalArgumentException().isThrownBy(() -> MssqlConnectionConfiguration.builder() .sslContextBuilderCustomizer(null) .build()) .withMessage("sslContextBuilderCustomizer must not be null"); } @Test void redirect() { MssqlConnectionConfiguration configuration = MssqlConnectionConfiguration.builder() .applicationName("r2dbc") .database("test-database") .host("test-host") .password("test-password") .username("test-username") .build(); MssqlConnectionConfiguration target = configuration.withRedirect(Redirect.create("target", 1234)); assertThat(target) .hasFieldOrPropertyWithValue("applicationName", "r2dbc") .hasFieldOrPropertyWithValue("database", "test-database") .hasFieldOrPropertyWithValue("host", "target") .hasFieldOrPropertyWithValue("password", "test-password") .hasFieldOrPropertyWithValue("port", 1234) .hasFieldOrPropertyWithValue("username", "test-username") .hasFieldOrPropertyWithValue("sendStringParametersAsUnicode", true) .hasFieldOrPropertyWithValue("hostNameInCertificate", "test-host"); } @Test void redirectOtherDomain() { MssqlConnectionConfiguration configuration = MssqlConnectionConfiguration.builder() .applicationName("r2dbc") .database("test-database") .host("test-host.windows.net") .password("test-password") .username("test-username") .build(); MssqlConnectionConfiguration target = configuration.withRedirect(Redirect.create("target.other.domain", 1234)); assertThat(target) .hasFieldOrPropertyWithValue("applicationName", "r2dbc") .hasFieldOrPropertyWithValue("database", "test-database") .hasFieldOrPropertyWithValue("host", "target.other.domain") .hasFieldOrPropertyWithValue("password", "test-password") .hasFieldOrPropertyWithValue("port", 1234) .hasFieldOrPropertyWithValue("username", "test-username") .hasFieldOrPropertyWithValue("sendStringParametersAsUnicode", true) .hasFieldOrPropertyWithValue("hostNameInCertificate", "test-host.windows.net"); } @Test void redirectInDomain() { MssqlConnectionConfiguration configuration = MssqlConnectionConfiguration.builder() .applicationName("r2dbc") .database("test-database") .host("test-host.windows.net") .password("test-password") .username("test-username") .hostNameInCertificate("*.windows.net") .build(); MssqlConnectionConfiguration target = configuration.withRedirect(Redirect.create("worker.target.windows.net", 1234)); assertThat(target) .hasFieldOrPropertyWithValue("applicationName", "r2dbc") .hasFieldOrPropertyWithValue("database", "test-database") .hasFieldOrPropertyWithValue("host", "worker.target.windows.net") .hasFieldOrPropertyWithValue("password", "test-password") .hasFieldOrPropertyWithValue("port", 1234) .hasFieldOrPropertyWithValue("username", "test-username") .hasFieldOrPropertyWithValue("sendStringParametersAsUnicode", true) .hasFieldOrPropertyWithValue("hostNameInCertificate", "*.target.windows.net"); } @Test void configureKeyStore(@TempDir File tempDir) throws Exception { KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); keyGen.initialize(1024, new SecureRandom()); KeyPair keypair = keyGen.generateKeyPair(); KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(null, null); Certificate selfSignedCertificate = selfSign(keypair, "CN=dummy"); KeyStore.Entry entry = new KeyStore.PrivateKeyEntry(keypair.getPrivate(), new Certificate[]{selfSignedCertificate}); keyStore.setEntry("dummy", entry, new KeyStore.PasswordProtection("key-password".toCharArray())); File file = new File(tempDir, getClass().getName() + ".jks"); try (FileOutputStream stream = new FileOutputStream(file)) { keyStore.store(stream, "my-password".toCharArray()); } MssqlConnectionConfiguration configuration = MssqlConnectionConfiguration.builder() .database("test-database") .host("test-host.windows.net") .password("test-password") .username("test-username") .trustStore(file) .trustStorePassword("my-password".toCharArray()) .build(); MssqlConnectionConfiguration.DefaultClientConfiguration clientConfiguration = (MssqlConnectionConfiguration.DefaultClientConfiguration) configuration.toClientConfiguration(); KeyStore loaded = clientConfiguration.loadCustomTrustStore(); KeyStore.Entry loadedEntry = loaded.getEntry("dummy", new KeyStore.PasswordProtection("key-password".toCharArray())); assertThat(loadedEntry).isInstanceOf(KeyStore.PrivateKeyEntry.class); } private static Certificate selfSign(KeyPair keyPair, String subjectDN) throws Exception { Date startDate = new Date(); X500Name dnName = new X500Name(subjectDN); Calendar calendar = Calendar.getInstance(); calendar.setTime(startDate); calendar.add(Calendar.YEAR, 1); Date endDate = calendar.getTime(); SubjectPublicKeyInfo subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair .getPublic().getEncoded()); X509v3CertificateBuilder certificateBuilder = new X509v3CertificateBuilder(dnName, BigInteger.valueOf(1), startDate, endDate, dnName, subjectPublicKeyInfo); ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256WithRSA").build(keyPair.getPrivate()); X509CertificateHolder certificateHolder = certificateBuilder.build(contentSigner); return new JcaX509CertificateConverter() .getCertificate(certificateHolder); } @ParameterizedTest @ValueSource(strings = {"select", "SELECT", "sElEcT"}) void shouldAcceptQueries(String query) { assertThat(MssqlConnectionConfiguration.DefaultCursorPreference.INSTANCE).accepts(query); } @ParameterizedTest @ValueSource(strings = {" select", "sp_cursor", "INSERT"}) void shouldRejectQueries(String query) { assertThat(MssqlConnectionConfiguration.DefaultCursorPreference.INSTANCE).rejects(query); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/MssqlConnectionFactoryMetadataUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; final class MssqlConnectionFactoryMetadataUnitTests { @Test void name() { assertThat(MssqlConnectionFactoryMetadata.INSTANCE.getName()).isEqualTo(MssqlConnectionFactoryMetadata.NAME); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/MssqlConnectionFactoryProviderTest.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.client.ClientConfiguration; import io.r2dbc.spi.ConnectionFactoryOptions; import io.r2dbc.spi.Option; import org.junit.jupiter.api.Test; import java.io.File; import java.time.Duration; import java.util.function.Function; import java.util.function.Predicate; import static io.r2dbc.mssql.MssqlConnectionFactoryProvider.ALTERNATE_MSSQL_DRIVER; import static io.r2dbc.mssql.MssqlConnectionFactoryProvider.MSSQL_DRIVER; import static io.r2dbc.mssql.MssqlConnectionFactoryProvider.SSL_CONTEXT_BUILDER_CUSTOMIZER; import static io.r2dbc.mssql.MssqlConnectionFactoryProvider.SSL_TUNNEL; import static io.r2dbc.mssql.MssqlConnectionFactoryProvider.TCP_KEEPALIVE; import static io.r2dbc.mssql.MssqlConnectionFactoryProvider.TCP_NODELAY; import static io.r2dbc.mssql.MssqlConnectionFactoryProvider.TRUST_SERVER_CERTIFICATE; import static io.r2dbc.mssql.MssqlConnectionFactoryProvider.TRUST_STORE; import static io.r2dbc.mssql.MssqlConnectionFactoryProvider.TRUST_STORE_PASSWORD; import static io.r2dbc.mssql.MssqlConnectionFactoryProvider.TRUST_STORE_TYPE; import static io.r2dbc.spi.ConnectionFactoryOptions.DRIVER; import static io.r2dbc.spi.ConnectionFactoryOptions.HOST; import static io.r2dbc.spi.ConnectionFactoryOptions.LOCK_WAIT_TIMEOUT; import static io.r2dbc.spi.ConnectionFactoryOptions.PASSWORD; import static io.r2dbc.spi.ConnectionFactoryOptions.PORT; import static io.r2dbc.spi.ConnectionFactoryOptions.SSL; import static io.r2dbc.spi.ConnectionFactoryOptions.USER; import static io.r2dbc.spi.ConnectionFactoryOptions.builder; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Unit tests for {@link MssqlConnectionFactoryProvider}. * * @author Mark Paluch */ final class MssqlConnectionFactoryProviderTest { private final MssqlConnectionFactoryProvider provider = new MssqlConnectionFactoryProvider(); @Test void doesNotSupportWithWrongDriver() { assertThat(this.provider.supports(ConnectionFactoryOptions.builder() .option(DRIVER, "test-driver") .option(HOST, "test-host") .option(PASSWORD, "test-password") .option(PORT, -1) .option(USER, "test-user") .build())).isFalse(); } @Test void doesNotSupportWithoutDriver() { assertThat(this.provider.supports(ConnectionFactoryOptions.builder() .option(HOST, "test-host") .option(PASSWORD, "test-password") .option(PORT, -1) .option(USER, "test-user") .build())).isFalse(); } @Test void doesNotSupportWithoutHost() { assertThat(this.provider.supports(ConnectionFactoryOptions.builder() .option(DRIVER, MSSQL_DRIVER) .option(PASSWORD, "test-password") .option(PORT, -1) .option(USER, "test-user") .build())).isFalse(); } @Test void doesNotSupportWithoutPassword() { assertThat(this.provider.supports(ConnectionFactoryOptions.builder() .option(DRIVER, MSSQL_DRIVER) .option(HOST, "test-host") .option(PORT, -1) .option(USER, "test-user") .build())).isFalse(); } @Test void doesNotSupportWithoutUser() { assertThat(this.provider.supports(ConnectionFactoryOptions.builder() .option(DRIVER, MSSQL_DRIVER) .option(HOST, "test-host") .option(PASSWORD, "test-password") .option(PORT, -1) .build())).isFalse(); } @Test void supports() { assertThat(this.provider.supports(ConnectionFactoryOptions.builder() .option(DRIVER, MSSQL_DRIVER) .option(HOST, "test-host") .option(PASSWORD, "test-password") .option(USER, "test-user") .build())).isTrue(); } @Test void supportsAlternateDriverId() { assertThat(this.provider.supports(ConnectionFactoryOptions.builder() .option(DRIVER, ALTERNATE_MSSQL_DRIVER) .option(HOST, "test-host") .option(PASSWORD, "test-password") .option(USER, "test-user") .build())).isTrue(); } @Test void shouldConfigureWithStaticCursoredExecutionPreference() { MssqlConnectionFactory factory = this.provider.create(ConnectionFactoryOptions.builder() .option(DRIVER, MSSQL_DRIVER) .option(HOST, "test-host") .option(PASSWORD, "test-password") .option(USER, "test-user") .option(MssqlConnectionFactoryProvider.PREFER_CURSORED_EXECUTION, "true") .build()); ConnectionOptions options = factory.getConnectionOptions(); assertThat(options.prefersCursors("foo")).isTrue(); } @Test void shouldConfigureWithLockWaitTimeout() { MssqlConnectionFactory factory = this.provider.create(ConnectionFactoryOptions.builder() .option(DRIVER, MSSQL_DRIVER) .option(HOST, "test-host") .option(PASSWORD, "test-password") .option(USER, "test-user") .option(LOCK_WAIT_TIMEOUT, Duration.ofSeconds(10)) .build()); MssqlConnectionConfiguration configuration = factory.getConfiguration(); assertThat(configuration.getLockWaitTimeout()).isEqualTo(Duration.ofSeconds(10)); } @Test void shouldConfigureWithStringAsUnicode() { MssqlConnectionFactory factory = this.provider.create(ConnectionFactoryOptions.builder() .option(SSL, true) .option(DRIVER, MSSQL_DRIVER) .option(HOST, "test-host") .option(PASSWORD, "test-password") .option(USER, "test-user") .option(MssqlConnectionFactoryProvider.SEND_STRING_PARAMETERS_AS_UNICODE, false) .build()); assertThat(factory.getConnectionOptions().isSendStringParametersAsUnicode()).isFalse(); factory = this.provider.create(ConnectionFactoryOptions.builder() .option(SSL, true) .option(DRIVER, MSSQL_DRIVER) .option(HOST, "test-host") .option(PASSWORD, "test-password") .option(USER, "test-user") .option(MssqlConnectionFactoryProvider.SEND_STRING_PARAMETERS_AS_UNICODE, true) .build()); assertThat(factory.getConnectionOptions().isSendStringParametersAsUnicode()).isTrue(); } @Test void shouldConfigureWithSsl() { MssqlConnectionFactory factory = this.provider.create(ConnectionFactoryOptions.builder() .option(SSL, true) .option(DRIVER, MSSQL_DRIVER) .option(HOST, "test-host") .option(PASSWORD, "test-password") .option(USER, "test-user") .option(MssqlConnectionFactoryProvider.HOSTNAME_IN_CERTIFICATE, "*.foo") .build()); ClientConfiguration configuration = factory.getClientConfiguration(); assertThat(configuration.isSslEnabled()).isTrue(); assertThat(configuration.getSslTunnelConfiguration().isSslEnabled()).isFalse(); } @Test void shouldConfigureWithoutSsl() { MssqlConnectionFactory factory = this.provider.create(ConnectionFactoryOptions.builder() .option(SSL, false) .option(DRIVER, MSSQL_DRIVER) .option(HOST, "test-host") .option(PASSWORD, "test-password") .option(USER, "test-user") .build()); ClientConfiguration configuration = factory.getClientConfiguration(); assertThat(configuration.isSslEnabled()).isFalse(); } @Test void shouldConfigureWithSslCustomizer() { MssqlConnectionFactory factory = this.provider.create(ConnectionFactoryOptions.builder() .option(SSL, true) .option(DRIVER, MSSQL_DRIVER) .option(HOST, "test-host") .option(PASSWORD, "test-password") .option(USER, "test-user") .option(SSL_CONTEXT_BUILDER_CUSTOMIZER, sslContextBuilder -> { throw new IllegalStateException("Works!"); }) .build()); assertThatIllegalStateException().isThrownBy(() -> factory.getClientConfiguration().getSslContext()).withMessageContaining("Works!"); } @Test void shouldConfigureWithSslTunnel() { MssqlConnectionFactory factory = this.provider.create(ConnectionFactoryOptions.builder() .option(SSL, true) .option(DRIVER, MSSQL_DRIVER) .option(HOST, "test-host") .option(PASSWORD, "test-password") .option(USER, "test-user") .option(Option.valueOf("sslTunnel"), true) .build()); assertThat(factory.getClientConfiguration().getSslTunnelConfiguration().isSslEnabled()).isTrue(); factory = this.provider.create(ConnectionFactoryOptions.builder() .option(SSL, true) .option(DRIVER, MSSQL_DRIVER) .option(HOST, "test-host") .option(PASSWORD, "test-password") .option(USER, "test-user") .option(Option.valueOf("sslTunnel"), false) .build()); assertThat(factory.getClientConfiguration().getSslTunnelConfiguration().isSslEnabled()).isFalse(); } @Test void shouldConfigureWithSslTunnelCustomizer() { MssqlConnectionFactory factory = this.provider.create(ConnectionFactoryOptions.builder() .option(SSL, true) .option(DRIVER, MSSQL_DRIVER) .option(HOST, "test-host") .option(PASSWORD, "test-password") .option(USER, "test-user") .option(SSL_TUNNEL, Function.identity()) .build()); assertThat(factory.getClientConfiguration().getSslTunnelConfiguration().isSslEnabled()).isTrue(); } @Test void shouldConfigureTcpKeepAlive() { MssqlConnectionFactory factory = this.provider.create(builder() .option(DRIVER, MSSQL_DRIVER) .option(HOST, "test-host") .option(PASSWORD, "test-password") .option(USER, "test-user") .option(TCP_KEEPALIVE, true) .build()); assertThat(factory.getClientConfiguration().isTcpKeepAlive()).isTrue(); } @Test void shouldConfigureTcpNoDelay() { MssqlConnectionFactory factory = this.provider.create(builder() .option(DRIVER, MSSQL_DRIVER) .option(HOST, "test-host") .option(PASSWORD, "test-password") .option(USER, "test-user") .option(TCP_NODELAY, true) .build()); assertThat(factory.getClientConfiguration().isTcpNoDelay()).isTrue(); } @Test void shouldConfigureWithTrustServerCertificate() { MssqlConnectionFactory factory = this.provider.create(ConnectionFactoryOptions.builder() .option(SSL, true) .option(DRIVER, MSSQL_DRIVER) .option(HOST, "test-host") .option(PASSWORD, "test-password") .option(USER, "test-user") .option(TRUST_SERVER_CERTIFICATE, true) .option(TRUST_STORE_PASSWORD, "hello".toCharArray()) .option(TRUST_STORE_TYPE, "PKCS") .build()); assertThat(factory.getClientConfiguration()) .hasFieldOrPropertyWithValue("trustServerCertificate", true); } @Test void shouldConfigureWithTrustStoreCustomizer() { MssqlConnectionFactory factory = this.provider.create(ConnectionFactoryOptions.builder() .option(SSL, true) .option(DRIVER, MSSQL_DRIVER) .option(HOST, "test-host") .option(PASSWORD, "test-password") .option(USER, "test-user") .option(TRUST_STORE, new File("foo")) .option(TRUST_STORE_PASSWORD, "hello".toCharArray()) .option(TRUST_STORE_TYPE, "PKCS") .build()); assertThat(factory.getClientConfiguration()) .hasFieldOrPropertyWithValue("trustServerCertificate", false) .hasFieldOrPropertyWithValue("trustStore", new File("foo")) .hasFieldOrPropertyWithValue("trustStorePassword", "hello".toCharArray()) .hasFieldOrPropertyWithValue("trustStoreType", "PKCS"); } @Test void shouldConfigureWithStaticBooleanCursoredExecutionPreference() { MssqlConnectionFactory factory = this.provider.create(ConnectionFactoryOptions.builder() .option(DRIVER, MSSQL_DRIVER) .option(HOST, "test-host") .option(PASSWORD, "test-password") .option(USER, "test-user") .option(MssqlConnectionFactoryProvider.PREFER_CURSORED_EXECUTION, true) .build()); ConnectionOptions options = factory.getConnectionOptions(); assertThat(options.prefersCursors("foo")).isTrue(); } @Test void shouldConfigureWithClassCursoredExecutionPreference() { MssqlConnectionFactory factory = this.provider.create(ConnectionFactoryOptions.builder() .option(DRIVER, MSSQL_DRIVER) .option(HOST, "test-host") .option(PASSWORD, "test-password") .option(USER, "test-user") .option(MssqlConnectionFactoryProvider.PREFER_CURSORED_EXECUTION, MyPredicate.class.getName()) .build()); ConnectionOptions options = factory.getConnectionOptions(); assertThat(options.prefersCursors("foo")).isTrue(); assertThat(options.prefersCursors("bar")).isFalse(); } @Test void returnsDriverIdentifier() { assertThat(this.provider.getDriver()).isEqualTo(MSSQL_DRIVER); } static class MyPredicate implements Predicate { @Override public boolean test(String s) { return s.equals("foo"); } } } ================================================ FILE: src/test/java/io/r2dbc/mssql/MssqlConnectionFactoryUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.client.TestClient; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.message.tds.Redirect; import io.r2dbc.mssql.message.tds.ServerCharset; import io.r2dbc.mssql.message.token.*; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.spi.R2dbcNonTransientResourceException; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import reactor.util.annotation.Nullable; import java.nio.charset.Charset; import java.util.Arrays; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** * Unit tests for {@link MssqlConnectionFactory}. * * @author Mark Paluch */ final class MssqlConnectionFactoryUnitTests { MssqlConnectionConfiguration configuration = MssqlConnectionConfiguration.builder().host("initial").username("user").password("password").build(); static final Column[] COLUMNS = Arrays.asList( createColumn(0, "Edition", SqlServerType.NVARCHAR, 100, LengthStrategy.USHORTLENTYPE, ServerCharset.UNICODE.charset()), createColumn(1, "VersionString", SqlServerType.VARCHAR, 100, LengthStrategy.USHORTLENTYPE, ServerCharset.CP1252.charset())).toArray(new Column[0]); @Test void constructorNoClientFactory() { assertThatIllegalArgumentException().isThrownBy(() -> new MssqlConnectionFactory(null, MssqlConnectionConfiguration.builder() .host("test-host") .password("test-password") .username("test-username") .build())) .withMessage("clientFactory must not be null"); } @Test void constructorNoConfiguration() { assertThatIllegalArgumentException().isThrownBy(() -> new MssqlConnectionFactory(null)) .withMessage("configuration must not be null"); } @Test void shouldFollowRedirect() { ColumnMetadataToken columns = ColumnMetadataToken.create(COLUMNS); RowToken rowToken = RowTokenFactory.create(columns, buffer -> { Encode.uString(buffer, "Edition", ServerCharset.UNICODE.charset()); Encode.uString(buffer, "1.2.3", ServerCharset.CP1252.charset()); }); TestClient initial = TestClient.builder().expectClose().withRedirect(Redirect.create("redirect", 1234)).assertNextRequestWith(clientMessage -> { assertThat(clientMessage).isInstanceOf(Prelogin.class); }).thenRespond(DoneToken.create(0)).build(); TestClient redirect = TestClient.builder().assertNextRequestWith(clientMessage -> { assertThat(clientMessage).isInstanceOf(Prelogin.class); }).thenRespond(DoneToken.create(0)).assertNextRequestWith(clientMessage -> { assertThat(clientMessage).isInstanceOf(SqlBatch.class); }).thenRespond(columns, rowToken, DoneToken.create(1)).build(); MssqlConnectionFactory connectionFactory = new MssqlConnectionFactory(config -> { if (config.getHost().equals("initial")) { return Mono.just(initial); } return Mono.just(redirect); }, this.configuration); connectionFactory.create().as(StepVerifier::create).expectNextCount(1).verifyComplete(); assertThat(initial.isClosed()).isTrue(); assertThat(redirect.isClosed()).isFalse(); } @Test void properlyPropagatesFailures() { ErrorToken error = new ErrorToken(0, 0, (byte) 0, (byte) 0, "failure", "", "", 0); TestClient initial = TestClient.builder().expectClose().withRedirect(Redirect.create("redirect", 1234)).assertNextRequestWith(clientMessage -> { assertThat(clientMessage).isInstanceOf(Prelogin.class); }).thenRespond(error).build(); MssqlConnectionFactory connectionFactory = new MssqlConnectionFactory(config -> { return Mono.just(initial); }, this.configuration); connectionFactory.create().as(StepVerifier::create).verifyError(R2dbcNonTransientResourceException.class); assertThat(initial.isClosed()).isTrue(); } @Test void shouldFailOnMultipleRedirects() { TestClient initial = TestClient.builder().expectClose().withRedirect(Redirect.create("redirect", 1234)).assertNextRequestWith(clientMessage -> { assertThat(clientMessage).isInstanceOf(Prelogin.class); }).thenRespond(DoneToken.create(0)).build(); TestClient redirect = TestClient.builder().expectClose().withRedirect(Redirect.create("redirect", 1234)).assertNextRequestWith(clientMessage -> { assertThat(clientMessage).isInstanceOf(Prelogin.class); }).thenRespond(DoneToken.create(0)).build(); MssqlConnectionFactory connectionFactory = new MssqlConnectionFactory(config -> { if (config.getHost().equals("initial")) { return Mono.just(initial); } return Mono.just(redirect); }, this.configuration); connectionFactory.create().as(StepVerifier::create).verifyError(MssqlConnectionFactory.MssqlRoutingException.class); assertThat(initial.isClosed()).isTrue(); assertThat(redirect.isClosed()).isTrue(); } @Test void shouldCreateNewPreparedStatement() { MssqlConnectionFactory connectionFactory = new MssqlConnectionFactory(config -> Mono.empty(), this.configuration); ConnectionOptions options = connectionFactory.getConnectionOptions(); ConnectionOptions other = connectionFactory.getConnectionOptions(); options.getPreparedStatementCache().putHandle(1, "foo", new Binding()); assertThat(options.getPreparedStatementCache().getHandle("foo", new Binding())).isEqualTo(1); assertThat(other.getPreparedStatementCache().getHandle("foo", new Binding())).isEqualTo(0); } private static Column createColumn(int index, String name, SqlServerType serverType, int length, LengthStrategy lengthStrategy, @Nullable Charset charset) { TypeInformation.Builder builder = TypeInformation.builder().withServerType(serverType).withMaxLength(length).withLengthStrategy(lengthStrategy); if (charset != null) { builder.withCharset(charset); } TypeInformation type = builder.build(); return new Column(index, name, type, null); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/MssqlConnectionIntegrationTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.util.IntegrationTestSupport; import io.r2dbc.spi.ColumnMetadata; import io.r2dbc.spi.ConnectionFactories; import io.r2dbc.spi.ConnectionFactoryOptions; import io.r2dbc.spi.IsolationLevel; import io.r2dbc.spi.R2dbcPermissionDeniedException; import io.r2dbc.spi.R2dbcTimeoutException; import io.r2dbc.spi.R2dbcTransientException; import io.r2dbc.spi.Result; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.net.ConnectException; import java.nio.file.Path; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.time.Duration; import java.util.LinkedHashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import static io.r2dbc.spi.ConnectionFactoryOptions.LOCK_WAIT_TIMEOUT; import static org.assertj.core.api.Assertions.assertThat; /** * Integration tests for {@link MssqlConnection} and {@link MssqlStatement}. * * @author Mark Paluch */ class MssqlConnectionIntegrationTests extends IntegrationTestSupport { @Test void shouldFailOnConnectionRefused() { MssqlConnectionConfiguration configuration = MssqlConnectionConfiguration.builder() .host(SERVER.getHost()) .port(123) .username(SERVER.getUsername()) .password(SERVER.getPassword()) .build(); MssqlConnectionFactory connectionFactory = new MssqlConnectionFactory(configuration); connectionFactory.create() .as(StepVerifier::create) .expectError(ConnectException.class) .verify(); } @Test void shouldFailOnLoginFailedRefused() { MssqlConnectionConfiguration configuration = MssqlConnectionConfiguration.builder() .host(SERVER.getHost()) .port(SERVER.getPort()) .username(SERVER.getUsername()) .password("foobar") .build(); MssqlConnectionFactory connectionFactory = new MssqlConnectionFactory(configuration); connectionFactory.create() .as(StepVerifier::create) .expectError(R2dbcPermissionDeniedException.class) .verify(); } @Test void shouldConnectWithSelfSignedCert() { MssqlConnectionConfiguration configuration = MssqlConnectionConfiguration.builder() .host(SERVER.getHost()) .port(SERVER.getPort()) .database("master") .username(SERVER.getUsername()) .password(SERVER.getPassword()) .enableSsl() .trustServerCertificate() .build(); MssqlConnectionFactory connectionFactory = new MssqlConnectionFactory(configuration); Flux.usingWhen(connectionFactory.create(), conn -> conn.createStatement("SELECT @@VERSION").execute() .flatMap(it -> it.map((row, rowMetadata) -> row.get(0))), MssqlConnection::close) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); } @Test void shouldReportMetadata() throws Exception { try (Connection connection = SERVER.getDataSource().getConnection()) { DatabaseMetaData jdbcMetadata = connection.getMetaData(); MssqlConnectionMetadata metadata = IntegrationTestSupport.connection.getMetadata(); assertThat(metadata.getDatabaseProductName()).contains(jdbcMetadata.getDatabaseProductName()); assertThat(metadata.getDatabaseProductName()).doesNotContain("Copyright"); assertThat(metadata.getDatabaseVersion()).isEqualTo(jdbcMetadata.getDatabaseProductVersion()); } } @Test void shouldInsertAndSelectUsingMap() { createTable(connection); insertRecord(connection, 1); connection.createStatement("SELECT * FROM r2dbc_example ORDER BY first_name") .execute() .flatMap(it -> it.map((row, rowMetadata) -> { Map values = new LinkedHashMap<>(); for (ColumnMetadata column : rowMetadata.getColumnMetadatas()) { values.put(column.getName(), row.get(column.getName())); } return values; })) .as(StepVerifier::create) .consumeNextWith(actual -> { assertThat(actual) .containsEntry("id", 1) .containsEntry("first_name", "Walter") .containsEntry("last_name", "White"); }) .verifyComplete(); } @Test void shouldInsertAndSelectUsingRowCount() { createTable(connection); insertRecord(connection, 1); insertRecord(connection, 2); insertRecord(connection, 3); connection.createStatement("SELECT * FROM r2dbc_example") .execute() .flatMap(MssqlResult::getRowsUpdated) .as(StepVerifier::create) .expectNext(3L) .verifyComplete(); } @Test void shouldInsertAndSelectUsingPagingAndDirectMode() { createTable(connection); insertRecord(connection, 1); insertRecord(connection, 2); insertRecord(connection, 3); Flux.from(connection.createStatement("SELECT * FROM r2dbc_example ORDER BY id OFFSET @Offset ROWS" + " FETCH NEXT @Rows ROWS ONLY") .bind("Offset", 0) .bind("Rows", 2) .execute()) .flatMap(it -> it.map((row, rowMetadata) -> row.get("id", Integer.class))) .as(StepVerifier::create) .expectNext(1) .expectNext(2) .verifyComplete(); Flux.from(connection.createStatement("SELECT * FROM r2dbc_example ORDER BY id OFFSET @Offset ROWS" + " FETCH NEXT @Rows ROWS ONLY") .bind("Offset", 2) .bind("Rows", 2) .execute()) .flatMap(it -> it.map((row, rowMetadata) -> row.get("id", Integer.class))) .as(StepVerifier::create) .expectNext(3) .verifyComplete(); } @Test void shouldInsertAndSelectUsingPagingAndCursors() { createTable(connection); insertRecord(connection, 1); insertRecord(connection, 2); insertRecord(connection, 3); Flux.from(connection.createStatement("SELECT * FROM r2dbc_example ORDER BY id OFFSET @Offset ROWS" + " FETCH NEXT @Rows ROWS ONLY /* cursored */") .bind("Offset", 0) .bind("Rows", 2) .execute()) .flatMap(it -> it.map((row, rowMetadata) -> row.get("id", Integer.class))) .as(StepVerifier::create) .expectNext(1) .expectNext(2) .verifyComplete(); } @Test void shouldInsertAndSelectCompoundStatement() { createTable(connection); connection.createStatement("SELECT * FROM r2dbc_example;SELECT * FROM r2dbc_example") .execute() .flatMap(it -> it.map((row, rowMetadata) -> { return new Object(); // just a marker }).collectList()) .as(StepVerifier::create) .consumeNextWith(actual -> assertThat(actual).isEmpty()) .consumeNextWith(actual -> assertThat(actual).isEmpty()) .verifyComplete(); insertRecord(connection, 1); connection.createStatement("SELECT * FROM r2dbc_example;SELECT * FROM r2dbc_example") .execute() .flatMap(it -> it.map((row, rowMetadata) -> { Map values = new LinkedHashMap<>(); for (ColumnMetadata column : rowMetadata.getColumnMetadatas()) { values.put(column.getName(), row.get(column.getName())); } return values; }).collectList()) .as(StepVerifier::create) .consumeNextWith(actual -> assertThat(actual).hasSize(1)) .consumeNextWith(actual -> assertThat(actual).hasSize(1)) .verifyComplete(); } @Test void shouldConsumeSequence() { createSequence(connection); connection.createStatement("SELECT CAST(NEXT VALUE FOR integration_test AS BIGINT)") .execute() .flatMap(it -> it.map((row, rowMetadata) -> row.get(0))) .as(StepVerifier::create) .expectNext(1L) .verifyComplete(); connection.createStatement("SELECT CAST(NEXT VALUE FOR integration_test AS BIGINT)") .execute() .flatMap(it -> it.map((row, rowMetadata) -> row.get(0))) .as(StepVerifier::create) .expectNext(2L) .verifyComplete(); } @Test void shouldReusePreparedStatements() { createTable(connection); insertRecord(connection, 1); insertRecord(connection, 2); } @Test void shouldApplyLockWaitwaot() { ConnectionFactoryOptions options = builder().option(LOCK_WAIT_TIMEOUT, Duration.ofMillis(100)).build(); MssqlConnectionFactory factory = (MssqlConnectionFactory) ConnectionFactories.get(options); MssqlConnection connection1 = factory.create().block(); MssqlConnection connection2 = factory.create().block(); connection1.createStatement("SELECT @@LOCK_TIMEOUT AS [Lock Timeout]").execute() .flatMap(it -> it.map(row -> row.get("Lock Timeout"))) .as(StepVerifier::create) .expectNext(100) .verifyComplete(); createTable(connection); connection1.createStatement("INSERT INTO r2dbc_example VALUES (1, 'fn', 'ln')") .execute() .flatMap(Result::getRowsUpdated) .then() .as(StepVerifier::create) .verifyComplete(); connection1.beginTransaction(IsolationLevel.READ_COMMITTED).as(StepVerifier::create).verifyComplete(); connection2.beginTransaction(IsolationLevel.READ_COMMITTED).as(StepVerifier::create).verifyComplete(); connection1.createStatement("SELECT * FROM r2dbc_example WITH (UPDLOCK) WHERE id = 1") .execute() .flatMap(Result::getRowsUpdated) .then() .as(StepVerifier::create) .verifyComplete(); connection2.createStatement("UPDATE r2dbc_example SET first_name = 'updated' WHERE id = 1") .execute() .flatMap(Result::getRowsUpdated) .then() .as(StepVerifier::create) .verifyError(R2dbcTimeoutException.class); connection1.close().block(); connection2.close().block(); } @Test void queryAfterCancelShouldCompleteSuccessfully() { connection.cancel().as(StepVerifier::create).verifyComplete(); connection.createStatement("SELECT 1").execute().flatMap(it -> it.map(row -> row.get(0))).as(StepVerifier::create).expectNext(1).verifyComplete(); } @Test void cancelShouldTerminateOngoingDirectQuery() throws Exception { CompletableFuture future = connection.createStatement("WAITFOR DELAY '10:00'").fetchSize(0).execute().flatMap(Result::getRowsUpdated).then().toFuture(); connection.cancel().as(StepVerifier::create).verifyComplete(); try { future.get(2, TimeUnit.SECONDS); } catch (ExecutionException e) { assertThat(e.getCause()).isInstanceOf(R2dbcTransientException.class).hasMessageContaining("cancelled"); } assertThat(future).isCompletedExceptionally(); } @Test void cancelShouldTerminateOngoingCursoredQuery() throws Exception { CompletableFuture future = connection.createStatement("WAITFOR DELAY '10:00'").fetchSize(10).execute().flatMap(Result::getRowsUpdated).then().toFuture(); connection.cancel().as(StepVerifier::create).verifyComplete(); try { future.get(2, TimeUnit.SECONDS); } catch (ExecutionException e) { assertThat(e.getCause()).isInstanceOf(R2dbcTransientException.class).hasMessageContaining("cancelled"); } assertThat(future).isCompletedExceptionally(); } @Test void shouldResumeOperationsAfterCancellationOfPendingRequest() throws Exception { CompletableFuture future = connection.createStatement("WAITFOR DELAY '10:00'").execute().flatMap(Result::getRowsUpdated).then().toFuture(); connection.cancel().as(StepVerifier::create).verifyComplete(); try { future.get(2, TimeUnit.SECONDS); } catch (ExecutionException e) { assertThat(e.getCause()).isInstanceOf(R2dbcTransientException.class).hasMessageContaining("cancelled"); } connection.createStatement("SELECT 1").execute().flatMap(it -> it.map(row -> row.get(0))).as(StepVerifier::create).expectNext(1).verifyComplete(); } @Test void shouldRejectMultipleParametrizedExecutions() { createTable(connection); Flux prepared = Flux.from(connection.createStatement("SELECT * FROM r2dbc_example ORDER BY id OFFSET @Offset ROWS") .bind("Offset", 0) .execute()); prepared.flatMap(MssqlResult::getRowsUpdated) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); prepared.flatMap(MssqlResult::getRowsUpdated) .as(StepVerifier::create) .verifyError(IllegalStateException.class); } private void createTable(MssqlConnection connection) { connection.createStatement("DROP TABLE r2dbc_example").execute() .flatMap(MssqlResult::getRowsUpdated) .onErrorResume(e -> Mono.empty()) .thenMany(connection.createStatement("CREATE TABLE r2dbc_example (" + "id int PRIMARY KEY, " + "first_name varchar(255), " + "last_name varchar(255))") .execute().flatMap(MssqlResult::getRowsUpdated).then()) .as(StepVerifier::create) .verifyComplete(); } private void createSequence(MssqlConnection connection) { connection.createStatement("DROP SEQUENCE integration_test").execute() .flatMap(MssqlResult::getRowsUpdated) .onErrorResume(e -> Mono.empty()) .thenMany(connection.createStatement("CREATE SEQUENCE integration_test START WITH 1 INCREMENT BY 1") .execute().flatMap(MssqlResult::getRowsUpdated).then()) .as(StepVerifier::create) .verifyComplete(); } private String writeKeyStoreToTempFile(Path tempDir, KeyStore keyStore, String password) { final File file = new File(tempDir.toFile(), UUID.randomUUID() + ".jks"); try (OutputStream outputStream = new FileOutputStream(file)) { keyStore.store(outputStream, password.toCharArray()); return file.getAbsolutePath(); } catch (final IOException | KeyStoreException | NoSuchAlgorithmException | CertificateException e) { throw new RuntimeException("Failed to write key store to file", e); } } private void insertRecord(MssqlConnection connection, int id) { Flux.from(connection.createStatement("INSERT INTO r2dbc_example VALUES(@id, @firstname, @lastname)") .bind("id", id) .bind("firstname", "Walter") .bind("lastname", "White") .execute()) .flatMap(Result::getRowsUpdated) .as(StepVerifier::create) .expectNext(1L) .verifyComplete(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/MssqlConnectionMetadataUnitTests.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link MssqlConnectionMetadata} * * @author Mark Paluch */ class MssqlConnectionMetadataUnitTests { @Test void shouldConstructMetadata() { String edition = "Developer Edition (64-bit)"; String version = "14.0.3162.1"; String versionString = "Microsoft SQL Server 2017 (RTM-CU15) (KB4498951) - 14.0.3162.1 (X64) " + "May 15 2019 19:14:30" + "Copyright (C) 2017 Microsoft Corporation" + "Developer Edition (64-bit) on Linux (Ubuntu 16.04.6 LTS)"; MssqlConnectionMetadata metadata = MssqlConnectionMetadata.from(edition, version, versionString); assertThat(metadata.getDatabaseProductName()).isEqualTo("Microsoft SQL Server 2017 (RTM-CU15) (KB4498951) - Developer Edition (64-bit)"); assertThat(metadata.getDatabaseVersion()).isEqualTo(version); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/MssqlConnectionUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.client.Client; import io.r2dbc.mssql.client.ConnectionContext; import io.r2dbc.mssql.client.TestClient; import io.r2dbc.mssql.client.TransactionStatus; import io.r2dbc.mssql.message.TransactionDescriptor; import io.r2dbc.mssql.message.token.DoneToken; import io.r2dbc.mssql.message.token.ErrorToken; import io.r2dbc.mssql.message.token.SqlBatch; import io.r2dbc.spi.IsolationLevel; import io.r2dbc.spi.ValidationDepth; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import reactor.test.StepVerifier; import java.time.Duration; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.*; /** * Unit tests for {@link MssqlConnection}. * * @author Mark Paluch * @author Hebert Coelho * @author Nayan Hajratwala */ class MssqlConnectionUnitTests { static MssqlConnectionMetadata metadata = new MssqlConnectionMetadata("SQL Server", "1.0"); static ConnectionOptions conectionOptions = new TestConnectionOptions(); @Test void shouldBeginTransactionFromInitialState() { TestClient client = TestClient.builder().expectRequest(SqlBatch.create(1, TransactionDescriptor.empty(), "BEGIN TRANSACTION;")).thenRespond(DoneToken.create(0)).build(); MssqlConnection connection = new MssqlConnection(client, metadata, conectionOptions); connection.beginTransaction() .as(StepVerifier::create) .verifyComplete(); } @Test void shouldBeginTransactionFromExplicitState() { TestClient client = TestClient.builder().withTransactionStatus(TransactionStatus.EXPLICIT).expectRequest(SqlBatch.create(1, TransactionDescriptor.empty(), "BEGIN TRANSACTION;")).thenRespond(DoneToken.create(0)).build(); MssqlConnection connection = new MssqlConnection(client, metadata, conectionOptions); connection.beginTransaction() .as(StepVerifier::create) .verifyComplete(); } @Test void shouldNotBeginTransactionFromStartedState() { Client clientMock = mock(Client.class); when(clientMock.getContext()).thenReturn(new ConnectionContext()); when(clientMock.getTransactionStatus()).thenReturn(TransactionStatus.STARTED); MssqlConnection connection = new MssqlConnection(clientMock, metadata, conectionOptions); connection.beginTransaction() .as(StepVerifier::create) .verifyComplete(); verify(clientMock, times(2)).getTransactionStatus(); verify(clientMock, atLeast(1)).getContext(); verifyNoMoreInteractions(clientMock); } @Test void shouldCommitFromExplicitTransaction() { TestClient client = TestClient.builder().withTransactionStatus(TransactionStatus.STARTED).expectRequest(SqlBatch.create(1, TransactionDescriptor.empty(), "IF @@TRANCOUNT > 0 COMMIT TRANSACTION;")).thenRespond(DoneToken.create(0)).build(); MssqlConnection connection = new MssqlConnection(client, metadata, conectionOptions); connection.commitTransaction() .as(StepVerifier::create) .verifyComplete(); } @Test void shouldNotCommitInAutoCommitState() { Client clientMock = mock(Client.class); when(clientMock.getContext()).thenReturn(new ConnectionContext()); when(clientMock.getTransactionStatus()).thenReturn(TransactionStatus.AUTO_COMMIT); MssqlConnection connection = new MssqlConnection(clientMock, metadata, conectionOptions); connection.commitTransaction() .as(StepVerifier::create) .verifyComplete(); verify(clientMock, times(2)).getTransactionStatus(); verify(clientMock, atLeast(1)).getContext(); verifyNoMoreInteractions(clientMock); } @Test void shouldRollbackFromExplicitTransaction() { TestClient client = TestClient.builder().withTransactionStatus(TransactionStatus.STARTED).expectRequest(SqlBatch.create(1, TransactionDescriptor.empty(), "IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;")).thenRespond(DoneToken.create(0)).build(); MssqlConnection connection = new MssqlConnection(client, metadata, conectionOptions); connection.rollbackTransaction() .as(StepVerifier::create) .verifyComplete(); } @Test void shouldNotRollbackInAutoCommitState() { Client clientMock = mock(Client.class); when(clientMock.getContext()).thenReturn(new ConnectionContext()); when(clientMock.getTransactionStatus()).thenReturn(TransactionStatus.AUTO_COMMIT); MssqlConnection connection = new MssqlConnection(clientMock, metadata, conectionOptions); connection.rollbackTransaction() .as(StepVerifier::create) .verifyComplete(); verify(clientMock, times(2)).getTransactionStatus(); verify(clientMock, atLeast(1)).getContext(); verifyNoMoreInteractions(clientMock); } @Test void shouldNotSupportSavePointRelease() { Client clientMock = mock(Client.class); when(clientMock.getContext()).thenReturn(new ConnectionContext()); MssqlConnection connection = new MssqlConnection(clientMock, metadata, conectionOptions); connection.releaseSavepoint("foo").as(StepVerifier::create).verifyComplete(); } @ParameterizedTest @ValueSource(strings = {"0", "a", "A", "foo", "foo_bar"}) void shouldAllowSavepointNames(String name) { Client clientMock = mock(Client.class); when(clientMock.getContext()).thenReturn(new ConnectionContext()); MssqlConnection connection = new MssqlConnection(clientMock, metadata, conectionOptions); assertThat(connection.createSavepoint(name)).isNotNull(); } @ParameterizedTest @ValueSource(strings = {"", "@", "a'", "a\"", "a[", "a]"}) void shouldRejectSavepointNames(String name) { Client clientMock = mock(Client.class); when(clientMock.getContext()).thenReturn(new ConnectionContext()); MssqlConnection connection = new MssqlConnection(clientMock, metadata, conectionOptions); assertThatThrownBy(() -> connection.createSavepoint(name)).isInstanceOf(IllegalArgumentException.class); } @Test void shouldRollbackTransactionToSavepointFromExplicitTransaction() { TestClient client = TestClient.builder().withTransactionStatus(TransactionStatus.STARTED).expectRequest(SqlBatch.create(1, TransactionDescriptor.empty(), "ROLLBACK TRANSACTION foo")).thenRespond(DoneToken.create(0)).build(); MssqlConnection connection = new MssqlConnection(client, metadata, conectionOptions); connection.rollbackTransactionToSavepoint("foo") .as(StepVerifier::create) .verifyComplete(); } @Test void shouldNotRollbackTransactionToSavepointInAutoCommitState() { Client clientMock = mock(Client.class); when(clientMock.getContext()).thenReturn(new ConnectionContext()); when(clientMock.getTransactionStatus()).thenReturn(TransactionStatus.AUTO_COMMIT); MssqlConnection connection = new MssqlConnection(clientMock, metadata, conectionOptions); connection.rollbackTransactionToSavepoint("foo") .as(StepVerifier::create) .verifyComplete(); verify(clientMock, times(2)).getTransactionStatus(); verify(clientMock, atLeast(1)).getContext(); verifyNoMoreInteractions(clientMock); } @Test void shouldCreateSavepointFromExplicitTransaction() { TestClient client = TestClient.builder().withTransactionStatus(TransactionStatus.STARTED).expectRequest(SqlBatch.create(1, TransactionDescriptor.empty(), "SET IMPLICIT_TRANSACTIONS ON; IF @@TRANCOUNT = 0 " + "BEGIN BEGIN TRAN IF @@TRANCOUNT = 2 COMMIT TRAN END SAVE TRAN foo;")).thenRespond(DoneToken.create(0)).build(); MssqlConnection connection = new MssqlConnection(client, metadata, conectionOptions); connection.createSavepoint("foo") .as(StepVerifier::create) .verifyComplete(); } @Test void createSavepointShouldBeginTransaction() { TestClient client = TestClient.builder().withTransactionStatus(TransactionStatus.AUTO_COMMIT).expectRequest(SqlBatch.create(1, TransactionDescriptor.empty(), "SET IMPLICIT_TRANSACTIONS ON; IF @@TRANCOUNT =" + " 0 BEGIN BEGIN TRAN IF @@TRANCOUNT = 2 COMMIT TRAN END SAVE TRAN foo;")).thenRespond(DoneToken.create(0)).build(); MssqlConnection connection = new MssqlConnection(client, metadata, conectionOptions); connection.createSavepoint("foo") .as(StepVerifier::create) .verifyComplete(); } @ParameterizedTest @MethodSource("isolationLevels") void shouldSetIsolationLevel(IsolationLevel isolationLevel) { TestClient client = TestClient.builder().withTransactionStatus(TransactionStatus.EXPLICIT).expectRequest(SqlBatch.create(1, TransactionDescriptor.empty(), "SET TRANSACTION ISOLATION LEVEL " + isolationLevel.asSql().toUpperCase())).thenRespond(DoneToken.create(0)).build(); MssqlConnection connection = new MssqlConnection(client, metadata, conectionOptions); connection.setTransactionIsolationLevel(isolationLevel) .as(StepVerifier::create) .verifyComplete(); } @Test void shouldSetLockWaitTimeout() { TestClient client = TestClient.builder().withTransactionStatus(TransactionStatus.EXPLICIT).expectRequest(SqlBatch.create(1, TransactionDescriptor.empty(), "SET LOCK_TIMEOUT 10000")).thenRespond(DoneToken.create(0)).build(); MssqlConnection connection = new MssqlConnection(client, metadata, conectionOptions); connection.setLockWaitTimeout(Duration.ofSeconds(10)) .as(StepVerifier::create) .verifyComplete(); client = TestClient.builder().withTransactionStatus(TransactionStatus.EXPLICIT).expectRequest(SqlBatch.create(1, TransactionDescriptor.empty(), "SET LOCK_TIMEOUT -1")).thenRespond(DoneToken.create(0)).build(); connection = new MssqlConnection(client, metadata, conectionOptions); connection.setLockWaitTimeout(Duration.ofSeconds(-10)) .as(StepVerifier::create) .verifyComplete(); client = TestClient.builder().withTransactionStatus(TransactionStatus.EXPLICIT).expectRequest(SqlBatch.create(1, TransactionDescriptor.empty(), "SET LOCK_TIMEOUT 0")).thenRespond(DoneToken.create(0)).build(); connection = new MssqlConnection(client, metadata, conectionOptions); connection.setLockWaitTimeout(Duration.ZERO) .as(StepVerifier::create) .verifyComplete(); } @Test void localValidationShouldValidateAgainstConnectionState() { TestClient connected = TestClient.builder().withConnected(true).build(); MssqlConnection connection = new MssqlConnection(connected, metadata, conectionOptions); connection.validate(ValidationDepth.LOCAL) .as(StepVerifier::create) .expectNext(true) .verifyComplete(); TestClient disconnected = TestClient.builder().withConnected(false).build(); connection = new MssqlConnection(disconnected, metadata, conectionOptions); connection.validate(ValidationDepth.LOCAL) .as(StepVerifier::create) .expectNext(false) .verifyComplete(); } @Test void remoteValidationShouldIssueQuery() { TestClient client = TestClient.builder().withConnected(true).expectRequest(SqlBatch.create(1, TransactionDescriptor.empty(), "SELECT 1")).thenRespond(DoneToken.create(0)).build(); MssqlConnection connection = new MssqlConnection(client, metadata, conectionOptions); connection.validate(ValidationDepth.REMOTE) .as(StepVerifier::create) .expectNext(true) .verifyComplete(); } @Test void remoteValidationShouldFail() { TestClient client = TestClient.builder().withConnected(true).expectRequest(SqlBatch.create(1, TransactionDescriptor.empty(), "SELECT 1")).thenRespond(new ErrorToken(1, 1, (byte) 1, (byte) 1, "failed", "", "", 0), DoneToken.create(0)).build(); MssqlConnection connection = new MssqlConnection(client, metadata, conectionOptions); connection.validate(ValidationDepth.REMOTE) .as(StepVerifier::create) .expectNext(false) .verifyComplete(); } @Test void shouldSanitizeProperly() { assertThat(MssqlConnection.sanitize("12345", 10)).isEqualTo("12345"); assertThat(MssqlConnection.sanitize("1234567", 7)).isEqualTo("1234567"); assertThat(MssqlConnection.sanitize("1234567", 3)).isEqualTo("123"); } private static Stream isolationLevels() { return Stream.of(MssqlIsolationLevel.SERIALIZABLE, MssqlIsolationLevel.READ_COMMITTED, MssqlIsolationLevel.READ_UNCOMMITTED, MssqlIsolationLevel.REPEATABLE_READ, MssqlIsolationLevel.SNAPSHOT); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/MssqlResultUnitTests.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.client.ConnectionContext; import io.r2dbc.mssql.codec.DefaultCodecs; import io.r2dbc.mssql.message.Message; import io.r2dbc.mssql.message.token.DoneToken; import io.r2dbc.mssql.message.token.ErrorToken; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import reactor.core.publisher.Flux; import reactor.test.StepVerifier; import java.util.Arrays; import java.util.List; /** * Unit tests for {@link DefaultMssqlResult}. * * @author Mark Paluch */ class MssqlResultUnitTests { @ParameterizedTest @MethodSource("factories") void shouldEmitErrorSignalInOrder(ResultFactory factory) { ErrorToken error = new ErrorToken(0, 0, Byte.MIN_VALUE, Byte.MIN_VALUE, "foo", "", "", 0); DoneToken done = DoneToken.create(0); MssqlResult countThenError = factory.create(Flux.just(done, error)); countThenError.getRowsUpdated() .as(StepVerifier::create) .expectError() .verify(); MssqlResult errorThenCount = factory.create(Flux.just(error, done)); errorThenCount.getRowsUpdated() .as(StepVerifier::create) .expectError() .verify(); } static List factories() { return Arrays.asList(new ResultFactory() { @Override MssqlResult create(Flux messages) { return DefaultMssqlResult.toResult("", new ConnectionContext(), new DefaultCodecs(), messages, false); } @Override public String toString() { return "DefaultMssqlResult"; } }, new ResultFactory() { @Override MssqlResult create(Flux messages) { return MssqlSegmentResult.toResult("", new ConnectionContext(), new DefaultCodecs(), messages, false); } @Override public String toString() { return "MssqlSegmentResult"; } }); } static abstract class ResultFactory { abstract MssqlResult create(Flux messages); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/MssqlReturnValuesUnitTests.java ================================================ /* * Copyright 2021-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.r2dbc.mssql.codec.DefaultCodecs; import io.r2dbc.mssql.message.token.Column; import io.r2dbc.mssql.message.token.ReturnValue; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.Types; import org.junit.jupiter.api.Test; import java.util.Arrays; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link MssqlReturnValues}. * * @author Mark Paluch */ class MssqlReturnValuesUnitTests { TypeInformation integer = Types.integer(); Column column = new Column(0, "foo", this.integer, null); ByteBuf data1 = Unpooled.wrappedBuffer(new byte[]{(byte) 0x4, 0x42, 0, 0, 0}); ByteBuf data2 = Unpooled.wrappedBuffer(new byte[]{(byte) 0x4, 0x43, 0, 0, 0}); ReturnValue first = new ReturnValue(6, "@First", 0, this.integer, this.data1); ReturnValue second = new ReturnValue(17, "@Second", 0, this.integer, this.data2); MssqlReturnValues returnValues = MssqlReturnValues.toReturnValues(new DefaultCodecs(), Arrays.asList(this.first, this.second)); @Test void metadataShouldReportColumnNames() { MssqlReturnValuesMetadata metadata = this.returnValues.getMetadata(); assertThat(metadata).containsExactly("@First", "@Second"); assertThat(metadata.getParameterMetadatas().stream().map(MssqlColumnMetadata::getName).collect(Collectors.toList())).containsExactly("@First", "@Second"); } @Test void metadataShouldReportCorrectType() { MssqlReturnValuesMetadata metadata = this.returnValues.getMetadata(); assertThat(metadata.getParameterMetadata(0).getName()).isEqualTo("@First"); assertThat(metadata.getParameterMetadata(0).getType()).isEqualTo(SqlServerType.INTEGER); assertThat(metadata.getParameterMetadata("first").getName()).isEqualTo("@First"); assertThat(metadata.getParameterMetadata("@FIRST").getName()).isEqualTo("@First"); assertThat(metadata.getParameterMetadata(1).getName()).isEqualTo("@Second"); assertThat(metadata.getParameterMetadata(1).getType()).isEqualTo(SqlServerType.INTEGER); } @Test void getShouldReturnValue() { assertThat(this.returnValues.get(0)).isEqualTo(0x42); assertThat(this.returnValues.get(0)).isEqualTo(0x42); assertThat(this.returnValues.get("First")).isEqualTo(0x42); assertThat(this.returnValues.get("@First")).isEqualTo(0x42); assertThat(this.returnValues.get(1)).isEqualTo(0x43); assertThat(this.returnValues.get(1)).isEqualTo(0x43); assertThat(this.returnValues.get("Second")).isEqualTo(0x43); assertThat(this.returnValues.get("@Second")).isEqualTo(0x43); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/MssqlRowMetadataUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.r2dbc.mssql.codec.Codecs; import io.r2dbc.mssql.codec.DefaultCodecs; import io.r2dbc.mssql.message.token.Column; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.spi.Nullability; import org.junit.jupiter.api.Test; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.NoSuchElementException; import static io.r2dbc.mssql.message.type.TypeInformation.builder; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * Unit tests for {@link MssqlRowMetadata}. * * @author Mark Paluch */ class MssqlRowMetadataUnitTests { Codecs codecs = new DefaultCodecs(); TypeInformation integer = builder().withScale(5).withMaxLength(4).withPrecision(4).withLengthStrategy(LengthStrategy.FIXEDLENTYPE).withServerType(SqlServerType.INTEGER).build(); Column column = new Column(0, "foo", this.integer, null); ByteBuf data = Unpooled.wrappedBuffer(new byte[]{(byte) 0x42, 0, 0, 0}); MssqlRowMetadata rowMetadata = new MssqlRowMetadata(this.codecs, new Column[]{this.column}, Collections.singletonMap("foo", this.column)); @Test void shouldLookupMetadataByName() { MssqlColumnMetadata columnMetadata = this.rowMetadata.getColumnMetadata("foo"); assertThat(columnMetadata).isNotNull(); assertThat(columnMetadata.getName()).isEqualTo("foo"); assertThat(columnMetadata.getJavaType()).isEqualTo(Integer.class); assertThat(columnMetadata.getPrecision()).isEqualTo(4); assertThat(columnMetadata.getScale()).isEqualTo(5); assertThat(columnMetadata.getNullability()).isEqualTo(Nullability.NON_NULL); assertThat(columnMetadata.getNativeTypeMetadata()).isEqualTo(this.integer); } @Test void shouldRemoveRowstatIfLastColumn() { Column rowstat = new Column(0, "ROWSTAT", this.integer, null); MssqlRowMetadata rowMetadata1 = new MssqlRowMetadata(this.codecs, new Column[]{this.column, rowstat}, new HashMap<>()); assertThat(rowMetadata1.getCount()).isOne(); assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(() -> rowMetadata1.getColumnMetadata("ROWSTAT")); MssqlRowMetadata rowMetadata2 = new MssqlRowMetadata(this.codecs, new Column[]{rowstat}, new HashMap<>()); assertThat(rowMetadata2.getCount()).isZero(); assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(() -> rowMetadata2.getColumnMetadata("ROWSTAT")); } @Test void retainsRowstatIfNotLastColumn() { Column rowstat = new Column(0, "ROWSTAT", this.integer, null); Map nameKeyedColumns = new HashMap<>(); nameKeyedColumns.put("foo", this.column); nameKeyedColumns.put("ROWSTAT", rowstat); MssqlRowMetadata rowMetadata = new MssqlRowMetadata(this.codecs, new Column[]{rowstat, this.column}, nameKeyedColumns); assertThat(rowMetadata.getCount()).isEqualTo(2); } @Test void shouldLookupMetadataByIndex() { MssqlColumnMetadata columnMetadata = this.rowMetadata.getColumnMetadata(0); assertThat(columnMetadata).isNotNull(); assertThat(columnMetadata.getName()).isEqualTo("foo"); } @Test void shouldReturnMetadataForAllColumns() { assertThat(this.rowMetadata.getColumnMetadatas()).hasSize(1); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/MssqlRowUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.r2dbc.mssql.codec.Codecs; import io.r2dbc.mssql.codec.DefaultCodecs; import io.r2dbc.mssql.message.token.Column; import io.r2dbc.mssql.message.token.RowToken; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.Types; import org.junit.jupiter.api.Test; import java.util.Collections; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Unit tests for {@link MssqlRow}. * * @author Mark Paluch */ class MssqlRowUnitTests { Codecs codecs = new DefaultCodecs(); TypeInformation integer = Types.integer(); Column column = new Column(0, "foo", this.integer, null); ByteBuf data = Unpooled.wrappedBuffer(new byte[]{(byte) 0x4, 0x42, 0, 0, 0}); RowToken rowToken = RowToken.decode(this.data, new Column[]{this.column}); MssqlRowMetadata rowMetadata = new MssqlRowMetadata(this.codecs, new Column[]{this.column}, Collections.singletonMap("foo", this.column)); MssqlRow row = new MssqlRow(this.codecs, this.rowToken, this.rowMetadata); @Test void shouldReadRowByIndex() { assertThat(this.row.get(0)).isEqualTo(66); assertThat(this.row.get(0, Short.class)).isEqualTo((short) 66); assertThat(this.row.get(0, Integer.class)).isEqualTo(66); assertThat(this.row.get(0, Long.class)).isEqualTo(66L); } @Test void shouldReadRowByName() { assertThat(this.row.get("foo")).isEqualTo(66); assertThat(this.row.get("foo", Integer.class)).isEqualTo(66); } @Test void releaseShouldDeallocateResources() { this.row.release(); assertThatThrownBy(() -> this.row.get("foo")).isInstanceOf(IllegalStateException.class).hasMessage("Value cannot be retrieved after row has been released"); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/MssqlSegmentResultUnitTests.java ================================================ /* * Copyright 2021-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.netty.buffer.ByteBuf; import io.netty.util.ReferenceCounted; import io.r2dbc.mssql.client.ConnectionContext; import io.r2dbc.mssql.codec.DefaultCodecs; import io.r2dbc.mssql.message.token.Column; import io.r2dbc.mssql.message.token.ColumnMetadataToken; import io.r2dbc.mssql.message.token.DoneToken; import io.r2dbc.mssql.message.token.ErrorToken; import io.r2dbc.mssql.message.token.InfoToken; import io.r2dbc.mssql.message.token.NbcRowToken; import io.r2dbc.mssql.message.token.ReturnValue; import io.r2dbc.mssql.message.token.RowToken; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.Types; import io.r2dbc.spi.R2dbcNonTransientException; import io.r2dbc.spi.R2dbcNonTransientResourceException; import io.r2dbc.spi.Result; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.util.Arrays; import java.util.function.Function; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link MssqlSegmentResult}. * * @author Mark Paluch */ class MssqlSegmentResultUnitTests { ErrorToken errorToken = new ErrorToken(0, 0, 0, 0, "error message desc", "", "", 0); InfoToken infoToken = new InfoToken(0, 0, 0, 0, "error message desc", "", "", 0); TypeInformation integerType = Types.integer(); TypeInformation stringType = Types.varchar(255); Column[] columns = Arrays.asList(new Column(0, "id", this.integerType), new Column(1, "first_name", this.stringType), new Column(2, "last_name", this.stringType), new Column(3, "other", this.stringType), new Column(4, "other2", this.stringType), new Column(5, "other3", this.stringType), new Column(6, "rowstat", this.integerType)).toArray(new Column[0]); DefaultCodecs codecs = new DefaultCodecs(); private NbcRowToken getRowToken() { return NbcRowToken.decode(HexUtils.decodeToByteBuf("D2 1C 04 01 00 00 00 01 00 61 02 00 78 61 04 01 00 00 00").skipBytes(1), this.columns); } @Test void shouldApplyRowMapping() { MssqlSegmentResult result = MssqlSegmentResult.toResult("", new ConnectionContext(), this.codecs, Flux.just(ColumnMetadataToken.create(this.columns), getRowToken()), false); result.map((row, rowMetadata) -> row) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); } @Test void shouldApplyOutParameterMapping() { ByteBuf buffer = HexUtils.decodeToByteBuf("AC0000000100000000000026" + "0404F3DEBC0A").skipBytes(1); MssqlSegmentResult result = MssqlSegmentResult.toResult("", new ConnectionContext(), this.codecs, Flux.just(ReturnValue.decode(buffer, false)), true); result.map((readable) -> readable) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); } @Test void mapShouldIgnoreNotice() { MssqlSegmentResult result = MssqlSegmentResult.toResult("", new ConnectionContext(), this.codecs, Flux.just(this.infoToken), false); result.map((row, rowMetadata) -> row) .as(StepVerifier::create) .verifyComplete(); } @Test void mapShouldTerminateWithError() { MssqlSegmentResult result = MssqlSegmentResult.toResult("", new ConnectionContext(), this.codecs, Flux.just(this.errorToken), false); result.map((row, rowMetadata) -> row) .as(StepVerifier::create) .verifyError(R2dbcNonTransientException.class); } @Test void getRowsUpdatedShouldTerminateWithError() { MssqlSegmentResult result = MssqlSegmentResult.toResult("", new ConnectionContext(), this.codecs, Flux.just(this.errorToken), false); result.getRowsUpdated() .as(StepVerifier::create) .verifyError(R2dbcNonTransientException.class); } @Test void shouldConsumeRowsUpdated() { MssqlSegmentResult result = MssqlSegmentResult.toResult("", new ConnectionContext(), this.codecs, Flux.just(DoneToken.count(42)), false); result.getRowsUpdated() .as(StepVerifier::create) .expectNext(42L) .verifyComplete(); } @Test void filterShouldRetainUpdateCount() { MssqlSegmentResult result = MssqlSegmentResult.toResult("", new ConnectionContext(), this.codecs, Flux.just(DoneToken.count(42)), false); result.filter(Result.UpdateCount.class::isInstance).getRowsUpdated() .as(StepVerifier::create) .expectNext(42L) .verifyComplete(); } @Test void filterShouldSkipRowMapping() { MssqlResult result = MssqlSegmentResult.toResult("", new ConnectionContext(), this.codecs, Flux.just(ColumnMetadataToken.create(this.columns), getRowToken()), false); result = result.filter(it -> false); result.map((row, rowMetadata) -> row) .as(StepVerifier::create) .verifyComplete(); } @Test void filterShouldSkipErrorMessage() { MssqlResult result = MssqlSegmentResult.toResult("", new ConnectionContext(), this.codecs, Flux.just(this.errorToken, ColumnMetadataToken.create(this.columns), getRowToken()), false); result = result.filter(Result.RowSegment.class::isInstance); result.map((row, rowMetadata) -> row) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); } @ValueSource(booleans = {true, false}) @ParameterizedTest void mapRowShouldDeallocateRowResources(boolean expectReturnValues) { ByteBuf buffer = HexUtils.decodeToByteBuf("AC0000000100000000000026" + "0404F3DEBC0A").skipBytes(1); ReturnValue returnValue = ReturnValue.decode(buffer, false); buffer.release(); RowToken dataRow = getRowToken(); assertThat(dataRow.refCnt()).isOne(); MssqlSegmentResult result = MssqlSegmentResult.toResult("", new ConnectionContext(), this.codecs, Flux.just(ColumnMetadataToken.create(this.columns), dataRow, returnValue), expectReturnValues); result.map((row, rowMetadata) -> row) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); assertThat(dataRow.refCnt()).isZero(); assertThat(returnValue.refCnt()).isZero(); assertThat(buffer.refCnt()).isZero(); } @ValueSource(booleans = {true, false}) @ParameterizedTest void mapShouldDeallocateRowResources(boolean expectReturnValues) { ByteBuf buffer = HexUtils.decodeToByteBuf("AC0000000100000000000026" + "0404F3DEBC0A").skipBytes(1); ReturnValue returnValue = ReturnValue.decode(buffer, false); buffer.release(); RowToken dataRow = getRowToken(); assertThat(dataRow.refCnt()).isOne(); MssqlSegmentResult result = MssqlSegmentResult.toResult("", new ConnectionContext(), this.codecs, Flux.just(ColumnMetadataToken.create(this.columns), dataRow, returnValue), expectReturnValues); result.map(Function.identity()) .as(StepVerifier::create) .expectNextCount(expectReturnValues ? 2 : 1) .verifyComplete(); assertThat(dataRow.refCnt()).isZero(); assertThat(returnValue.refCnt()).isZero(); assertThat(buffer.refCnt()).isZero(); } @ValueSource(booleans = {true, false}) @ParameterizedTest void filterShouldDeallocateResources(boolean expectReturnValues) { ByteBuf buffer = HexUtils.decodeToByteBuf("AC0000000100000000000026" + "0404F3DEBC0A").skipBytes(1); ReturnValue returnValue = ReturnValue.decode(buffer, false); buffer.release(); RowToken dataRow = getRowToken(); assertThat(dataRow.refCnt()).isOne(); MssqlResult result = MssqlSegmentResult.toResult("", new ConnectionContext(), this.codecs, Flux.just(ColumnMetadataToken.create(this.columns), dataRow, returnValue), expectReturnValues); result = result.filter(it -> false); result.map((row, rowMetadata) -> row) .as(StepVerifier::create) .verifyComplete(); assertThat(dataRow.refCnt()).isZero(); assertThat(returnValue.refCnt()).isZero(); assertThat(buffer.refCnt()).isZero(); } @Test void flatMapShouldDeallocateResourcesAfterConsumption() { RowToken dataRow = getRowToken(); assertThat(dataRow.refCnt()).isOne(); MssqlResult result = MssqlSegmentResult.toResult("", new ConnectionContext(), this.codecs, Flux.just(ColumnMetadataToken.create(this.columns), dataRow), false); Flux.from(result.flatMap(Mono::just)) .map(it -> { assertThat(((ReferenceCounted) it).refCnt()).isOne(); return it; }) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); assertThat(dataRow.refCnt()).isZero(); } @Test void flatMapShouldNotTerminateWithError() { MssqlResult result = MssqlSegmentResult.toResult("", new ConnectionContext(), this.codecs, Flux.just(this.errorToken, ColumnMetadataToken.create(this.columns), getRowToken(), DoneToken.create(42)), false); Flux.from(result.flatMap(Mono::just)) .as(StepVerifier::create) .expectNextCount(3) .verifyComplete(); } @Test void emptyFlatMapShouldDeallocateResourcesAfterConsumption() { RowToken dataRow = getRowToken(); assertThat(dataRow.refCnt()).isOne(); MssqlResult result = MssqlSegmentResult.toResult("", new ConnectionContext(), this.codecs, Flux.just(ColumnMetadataToken.create(this.columns), dataRow), false); Flux.from(result.flatMap(data -> Mono.empty())) .as(StepVerifier::create) .verifyComplete(); assertThat(dataRow.refCnt()).isZero(); } @Test void flatMapShouldMapErrorResponse() { MssqlResult result = MssqlSegmentResult.toResult("", new ConnectionContext(), this.codecs, Flux.just(this.errorToken), false); Flux.from(result.flatMap(data -> { assertThat(data).isInstanceOf(Result.Message.class); Result.Message message = (Result.Message) data; assertThat(message.errorCode()).isZero(); assertThat(message.sqlState()).isEqualTo("S0000"); assertThat(message.message()).isEqualTo("error message desc"); assertThat(message.exception()).isInstanceOf(R2dbcNonTransientResourceException.class); return Mono.just(data); })) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); } @Test void flatMapShouldMapNoticeResponse() { MssqlResult result = MssqlSegmentResult.toResult("", new ConnectionContext(), this.codecs, Flux.just(this.infoToken), false); Flux.from(result.flatMap(data -> { assertThat(data).isInstanceOf(Result.Message.class); Result.Message message = (Result.Message) data; assertThat(message.errorCode()).isZero(); assertThat(message.sqlState()).isEqualTo("S0000"); assertThat(message.message()).isEqualTo("error message desc"); assertThat(message.exception()).isInstanceOf(R2dbcNonTransientResourceException.class); return Mono.just(data); })) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/MssqlTestKit.java ================================================ /* * Copyright 2017-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.util.IntegrationTestSupport; import io.r2dbc.spi.ConnectionFactories; import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.ConnectionFactoryOptions; import io.r2dbc.spi.test.TestKit; import org.junit.jupiter.api.Disabled; import org.springframework.jdbc.core.JdbcOperations; import static io.r2dbc.mssql.MssqlConnectionFactoryProvider.MSSQL_DRIVER; import static io.r2dbc.spi.ConnectionFactoryOptions.DRIVER; import static io.r2dbc.spi.ConnectionFactoryOptions.HOST; import static io.r2dbc.spi.ConnectionFactoryOptions.PASSWORD; import static io.r2dbc.spi.ConnectionFactoryOptions.PORT; import static io.r2dbc.spi.ConnectionFactoryOptions.USER; /** * TCK Test Kit for SQL Server driver. */ final class MssqlTestKit extends IntegrationTestSupport implements TestKit { private final ConnectionFactory connectionFactory = ConnectionFactories.get(ConnectionFactoryOptions.builder() .option(DRIVER, MSSQL_DRIVER) .option(HOST, SERVER.getHost()) .option(PORT, SERVER.getPort()) .option(PASSWORD, SERVER.getPassword()) .option(USER, SERVER.getUsername()) .build()); @Override public String blobType() { return "VARBINARY(MAX)"; } @Override public String clobType() { return "VARCHAR(MAX)"; } @Override public ConnectionFactory getConnectionFactory() { return this.connectionFactory; } @Override public String getIdentifier(int index) { return getPlaceholder(index); } @Override public JdbcOperations getJdbcOperations() { JdbcOperations jdbcOperations = SERVER.getJdbcOperations(); if (jdbcOperations == null) { throw new IllegalStateException("JdbcOperations not yet initialized"); } return jdbcOperations; } @Override public String expand(TestStatement statement, Object... args) { if(statement == TestStatement.CREATE_TABLE_AUTOGENERATED_KEY){ return "CREATE TABLE test (id INTEGER IDENTITY, test_value INTEGER)"; } return TestKit.super.expand(statement, args); } @Override public String getPlaceholder(int index) { return String.format("@P%d", index); } @Override @Disabled public void prepareStatementWithIncompleteBatchFails() { } @Override @Disabled public void prepareStatementWithIncompleteBindingFails() { } } ================================================ FILE: src/test/java/io/r2dbc/mssql/ParametrizedMssqlStatementIntegrationTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.util.IntegrationTestSupport; import io.r2dbc.spi.R2dbcTimeoutException; import io.r2dbc.spi.Result; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Hooks; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.time.Duration; import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import static org.assertj.core.api.Assertions.assertThat; /** * Integration tests for {@link ParametrizedMssqlStatement}. * * @author Mark Paluch */ class ParametrizedMssqlStatementIntegrationTests extends IntegrationTestSupport { static { Hooks.onOperatorDebug(); } @Test void shouldExecuteBatch() { connection.createStatement("DROP TABLE r2dbc_example").execute() .flatMap(MssqlResult::getRowsUpdated) .onErrorResume(e -> Mono.empty()) .thenMany(connection.createStatement("CREATE TABLE r2dbc_example (" + "id int PRIMARY KEY IDENTITY(1,1), " + "first_name varchar(255), " + "last_name varchar(255))") .execute().flatMap(MssqlResult::getRowsUpdated).then()) .as(StepVerifier::create) .verifyComplete(); Flux.from(connection.createStatement("INSERT INTO r2dbc_example (first_name, last_name) values (@fn, @ln)") .bind("fn", "Walter").bind("ln", "White").add() .bind("fn", "Hank").bind("@ln", "Schrader").add() .bind("fn", "Skyler").bind("@ln", "White") .execute()) .flatMap(Result::getRowsUpdated) .as(StepVerifier::create) .expectNext(1L, 1L, 1L) .verifyComplete(); } @Test void failureShouldNotLockUpConnection() { connection.createStatement("DROP TABLE r2dbc_example").execute() .flatMap(MssqlResult::getRowsUpdated) .onErrorResume(e -> Mono.empty()) .thenMany(connection.createStatement("CREATE TABLE r2dbc_example (" + "id int NOT NULL, " + "first_name varchar(255), " + "last_name varchar(255))") .execute().flatMap(MssqlResult::getRowsUpdated).then()) .as(StepVerifier::create) .verifyComplete(); for (int i = 0; i < 10; i++) { connection.createStatement("INSERT INTO r2dbc_example (id, first_name) VALUES(@P1, @P2)") .bindNull("@P1", Integer.class) .bind("@P2", "foo") .returnGeneratedValues() .execute() .flatMap(Result::getRowsUpdated) .as(StepVerifier::create) .verifyError(); } } @Test void shouldDecodeNull() { shouldExecuteBatch(); Flux.from(connection.createStatement("SELECT null, first_name FROM r2dbc_example") .execute()) .flatMap(result -> result.map((row, rowMetadata) -> { return Optional.ofNullable(row.get(0)); })) .as(StepVerifier::create) .expectNext(Optional.empty(), Optional.empty(), Optional.empty()) .verifyComplete(); } @Test void shouldRunQueryWithLocalVariableDeclarations() { Flux.from(connection.createStatement("declare @i int = 1; select @i where @x = 1") .bind("x", 1) .execute()) .flatMap(it -> it.map((r, m) -> r.get(0))) .as(StepVerifier::create).expectNextCount(1).verifyComplete(); } @Test void shouldEmitSingleResultForCursoredExecution() { shouldExecuteBatch(); AtomicInteger resultCounter = new AtomicInteger(); AtomicInteger rowCounter = new AtomicInteger(); Flux.from(connection.createStatement("SELECT first_name FROM r2dbc_example") .fetchSize(2) .execute()) .flatMap(result -> { resultCounter.incrementAndGet(); return result.map((row, rowMetadata) -> new Object()).doOnNext(it -> rowCounter.incrementAndGet()).then(); }) .as(StepVerifier::create) .verifyComplete(); assertThat(resultCounter).hasValue(1); assertThat(rowCounter).hasValue(3); } @Test void shouldRepreparePreparedStatement() { shouldExecuteBatch(); connection.createStatement("SET ANSI_NULLS ON") .execute() .flatMap(MssqlResult::getRowsUpdated) .as(StepVerifier::create) .verifyComplete(); Flux.from(connection.createStatement("SELECT first_name FROM r2dbc_example where id != @P0") .fetchSize(2) .bind("P0", 99) .execute()) .flatMap(result -> { return result.map((row, rowMetadata) -> new Object()); }) .as(StepVerifier::create) .expectNextCount(3) .verifyComplete(); connection.createStatement("SET ANSI_NULLS OFF") .execute() .flatMap(MssqlResult::getRowsUpdated) .as(StepVerifier::create) .verifyComplete(); Flux.from(connection.createStatement("SELECT first_name FROM r2dbc_example where id != @P0") .fetchSize(2) .bind("P0", 99) .execute()) .flatMap(result -> { return result.map((row, rowMetadata) -> new Object()); }) .as(StepVerifier::create) .expectNextCount(3) .verifyComplete(); } @Test void shouldRunStatementWithMultipleResults() { AtomicLong resultCounter = new AtomicLong(); AtomicLong firstUpdateCount = new AtomicLong(); AtomicInteger rowCount = new AtomicInteger(); Flux.from(connection.createStatement("DECLARE @t TABLE(i INT);INSERT INTO @t VALUES (@P1),(2),(3);SELECT * FROM @t;\n") .bind("@P1", 1) .execute()).flatMap(it -> { if (resultCounter.compareAndSet(0, 1)) { return it.getRowsUpdated().doOnNext(firstUpdateCount::set).then(); } if (resultCounter.incrementAndGet() == 2) { return it.map(((row, rowMetadata) -> { rowCount.incrementAndGet(); return new Object(); })).then(); } throw new IllegalStateException("Unexpected result"); }).as(StepVerifier::create).verifyComplete(); assertThat(resultCounter).hasValue(2); assertThat(firstUpdateCount).hasValue(3); assertThat(rowCount).hasValue(3); } @Test void shouldRunStatementWithMultipleBindingsAndResults() { AtomicBoolean firstGurard = new AtomicBoolean(); AtomicBoolean secondGurard = new AtomicBoolean(); AtomicBoolean thirdGurard = new AtomicBoolean(); AtomicBoolean fourthGurard = new AtomicBoolean(); AtomicLong firstUpdateCount = new AtomicLong(); AtomicLong secondUpdateCount = new AtomicLong(); AtomicInteger rowCount = new AtomicInteger(); Flux.from(connection.createStatement("DECLARE @t TABLE(i INT);INSERT INTO @t VALUES (@P1),(2),(3);SELECT * FROM @t;\n") .bind("@P1", 1).add() .bind("@P1", 2) .execute()).flatMap(it -> { if (firstGurard.compareAndSet(false, true)) { return it.getRowsUpdated().doOnNext(firstUpdateCount::set).then(); } if (secondGurard.compareAndSet(false, true)) { return it.map(((row, rowMetadata) -> { rowCount.incrementAndGet(); return new Object(); })).then(); } if (thirdGurard.compareAndSet(false, true)) { return it.getRowsUpdated().doOnNext(secondUpdateCount::set).then(); } if (fourthGurard.compareAndSet(false, true)) { return it.map(((row, rowMetadata) -> { rowCount.incrementAndGet(); return new Object(); })).then(); } throw new IllegalStateException("Unexpected result"); }).as(StepVerifier::create).verifyComplete(); assertThat(firstUpdateCount).hasValue(3); assertThat(secondUpdateCount).hasValue(3); assertThat(rowCount).hasValue(6); } @Test void shouldTimeoutSqlBatch() { connection.setStatementTimeout(Duration.ofMillis(100)).as(StepVerifier::create).verifyComplete(); connection.createStatement("WAITFOR DELAY @P0").fetchSize(0).bind("P0", "10:00").execute().flatMap(Result::getRowsUpdated).as(StepVerifier::create).verifyError(R2dbcTimeoutException.class); connection.createStatement("SELECT 1").execute().flatMap(it -> it.map(row -> row.get(0))).as(StepVerifier::create).expectNext(1).verifyComplete(); } @Test void shouldTimeoutCursored() { connection.setStatementTimeout(Duration.ofMillis(100)).as(StepVerifier::create).verifyComplete(); connection.createStatement("WAITFOR DELAY @P0").fetchSize(1).bind("P0", "10:00").execute().flatMap(Result::getRowsUpdated).as(StepVerifier::create).verifyError(R2dbcTimeoutException.class); connection.createStatement("SELECT 1").execute().flatMap(it -> it.map(row -> row.get(0))).as(StepVerifier::create).expectNext(1).verifyComplete(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/ParametrizedMssqlStatementStoredProcedureIntegrationTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.util.IntegrationTestSupport; import io.r2dbc.spi.Parameters; import io.r2dbc.spi.R2dbcType; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.dao.DataAccessException; import reactor.test.StepVerifier; /** * Integration tests for {@link ParametrizedMssqlStatement} to call stored procedures. * * @author Mark Paluch */ class ParametrizedMssqlStatementStoredProcedureIntegrationTests extends IntegrationTestSupport { @BeforeEach void setUp() { try { SERVER.getJdbcOperations().execute("DROP PROCEDURE test_proc"); } catch (DataAccessException ignore) { } SERVER.getJdbcOperations().execute("CREATE PROCEDURE test_proc\n" + " @TheName nvarchar(50),\n" + " @Greeting nvarchar(255) OUTPUT\n" + "AS\n" + "\n" + " SET NOCOUNT ON; \n" + " SET @Greeting = CONCAT('Hello ', @TheName)"); } @Test void shouldCallProcedure() { connection.createStatement("EXEC test_proc @P0, @Greeting OUTPUT") .bind("@P0", "Walter") .bind("@Greeting", Parameters.out(R2dbcType.VARCHAR)) .execute() .flatMap(it -> it.map((readable) -> { return readable.get(0); })) .as(StepVerifier::create) .expectNext("Hello Walter") .verifyComplete(); } @Test void shouldCallProcedureWithFetchSize() { connection.createStatement("EXEC test_proc @P0, @Greeting OUTPUT") .fetchSize(256) .bind("@P0", "Walter") .bind("@Greeting", Parameters.out(R2dbcType.VARCHAR)) .execute() .flatMap(it -> it.map((readable) -> { return readable.get(0); })) .as(StepVerifier::create) .expectNext("Hello Walter") .verifyComplete(); } @Test void shouldCallProcedureAsSegment() { connection.createStatement("EXEC test_proc @P0, @Greeting OUTPUT") .bind("@P0", "Walter") .bind("@Greeting", Parameters.out(R2dbcType.VARCHAR)) .execute() .flatMap(it -> it.filter(s -> true).map((readable) -> { return readable.get(0); })) .as(StepVerifier::create) .expectNext("Hello Walter") .verifyComplete(); } @Test void shouldCallProcedureWithFetchSizeAsSegment() { connection.createStatement("EXEC test_proc @P0, @Greeting OUTPUT") .fetchSize(256) .bind("@P0", "Walter") .bind("@Greeting", Parameters.out(R2dbcType.VARCHAR)) .execute() .flatMap(it -> it.filter(s -> true).map((readable) -> { return readable.get(0); })) .as(StepVerifier::create) .expectNext("Hello Walter") .verifyComplete(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/ParametrizedMssqlStatementUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.ParametrizedMssqlStatement.ParsedParameter; import io.r2dbc.mssql.client.TestClient; import io.r2dbc.mssql.codec.DefaultCodecs; import io.r2dbc.mssql.codec.Encoded; import io.r2dbc.mssql.codec.RpcParameterContext; import io.r2dbc.mssql.message.tds.ProtocolException; import io.r2dbc.mssql.message.token.ErrorToken; import io.r2dbc.mssql.message.token.ReturnValue; import io.r2dbc.mssql.message.token.RpcRequest; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TdsDataType; import io.r2dbc.mssql.util.TestByteBufAllocator; import io.r2dbc.mssql.util.Types; import io.r2dbc.spi.Parameters; import org.junit.jupiter.api.Test; import reactor.test.StepVerifier; import java.math.BigDecimal; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import static io.r2dbc.mssql.ParametrizedMssqlStatement.ParsedQuery; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * Unit tests for {@link ParametrizedMssqlStatement}. * * @author Mark Paluch */ class ParametrizedMssqlStatementUnitTests { PreparedStatementCache statementCache = new IndefinitePreparedStatementCache(); ConnectionOptions connectionOptions = new ConnectionOptions(sql -> true, new DefaultCodecs(), this.statementCache, true); @Test void shouldSupportSql() { assertThat(ParametrizedMssqlStatement.supports("SELECT * from FOO where firstname = @firstname")).isTrue(); assertThat(ParametrizedMssqlStatement.supports("SELECT * from FOO where firstname =@firstname")).isTrue(); assertThat(ParametrizedMssqlStatement.supports("SELECT * from FOO where firstname = @foo_bar")).isTrue(); assertThat(ParametrizedMssqlStatement.supports("SELECT * from FOO where firstname = 'foo'")).isFalse(); } @Test void shouldParseSql() { List variables = ParsedQuery.parse("SELECT * from FOO where firstname = @firstname").getParameters(); assertThat(variables).hasSize(1); assertThat(variables.get(0)).isEqualTo(new ParsedParameter("firstname", 37)); variables = ParsedQuery.parse("SELECT * from FOO where @p1 = @foo_bar").getParameters(); assertThat(variables).hasSize(2); assertThat(variables.get(0)).isEqualTo(new ParsedParameter("p1", 25)); assertThat(variables.get(1)).isEqualTo(new ParsedParameter("foo_bar", 31)); } @Test void executeWithoutBindingsShouldNotFail() { new ParametrizedMssqlStatement(TestClient.NO_OP, this.connectionOptions, "SELECT * from FOO where firstname = @firstname").execute(); new ParametrizedMssqlStatement(TestClient.NO_OP, this.connectionOptions, "SELECT * FROM users WHERE email = 'name[@]gmail.com'").execute(); } @Test void shouldBindParameterByIndex() { ParametrizedMssqlStatement statement = new ParametrizedMssqlStatement(TestClient.NO_OP, this.connectionOptions, "SELECT * from FOO where firstname = @firstname"); statement.bind(0, "name"); assertThat(statement.getBindings().first().getParameters()).containsKeys("firstname"); } @Test void shouldRejectBindIndexOutOfBounds() { ParametrizedMssqlStatement statement = new ParametrizedMssqlStatement(TestClient.NO_OP, this.connectionOptions, "SELECT * from FOO where firstname = @firstname"); assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> statement.bind(-1, "name")); assertThatExceptionOfType(IndexOutOfBoundsException.class).isThrownBy(() -> statement.bind(1, "name")); } @Test void shouldBindParameterByName() { ParametrizedMssqlStatement statement = new ParametrizedMssqlStatement(TestClient.NO_OP, this.connectionOptions, "SELECT * from FOO where firstname = @firstname"); statement.bind("firstname", "firstname"); assertThat(statement.getBindings().first().getParameters()).containsKeys("firstname"); } @Test void shouldBindParameterByNameWithPrefix() { ParametrizedMssqlStatement statement = new ParametrizedMssqlStatement(TestClient.NO_OP, this.connectionOptions, "SELECT * from FOO where firstname = @firstname"); statement.bind("@firstname", "firstname"); assertThat(statement.getBindings().first().getParameters()).containsKeys("firstname"); } @Test void shouldBindParameter() { ParametrizedMssqlStatement statement = new ParametrizedMssqlStatement(TestClient.NO_OP, this.connectionOptions, "SELECT * from FOO where amount1 = @amount1 and amount2 = @amount2"); statement.bind("amount1", Parameters.in(BigDecimal.valueOf(2.1f))); statement.bind("amount2", Parameters.in(SqlServerType.MONEY)); Map parameters = statement.getBindings().first().getParameters(); assertThat(parameters).containsKeys("amount1", "amount2"); Binding.RpcParameter amount1 = parameters.get("amount1"); assertThat(amount1.encoded.getDataType()).isEqualTo(TdsDataType.DECIMALN); Binding.RpcParameter amount2 = parameters.get("amount2"); assertThat(amount2.encoded.getDataType()).isEqualTo(TdsDataType.MONEYN); } @Test void shouldRejectBindForUnknownParameters() { ParametrizedMssqlStatement statement = new ParametrizedMssqlStatement(TestClient.NO_OP, this.connectionOptions, "SELECT * from FOO where firstname = @firstname"); assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(() -> statement.bind("foo", "name")); } @Test void shouldCachePreparedStatementHandle() { Encoded encodedPreparedStatementHandle = new DefaultCodecs().encode(TestByteBufAllocator.TEST, RpcParameterContext.in(), 1); ByteBuf value = encodedPreparedStatementHandle.getValue(); value.skipBytes(1); // skip maxlen byte TestClient testClient = TestClient.builder() .assertNextRequestWith(it -> { assertThat(it).isInstanceOf(RpcRequest.class); RpcRequest request = (RpcRequest) it; assertThat(request.getProcId()).isEqualTo(RpcRequest.Sp_CursorPrepExec); }) .thenRespond(new ReturnValue(0, null, (byte) 0, Types.integer(), value)) .build(); String sql = "SELECT * from FOO where firstname = @firstname"; ParametrizedMssqlStatement statement = new ParametrizedMssqlStatement(testClient, this.connectionOptions, sql); statement.bind("firstname", ""); Binding binding = statement.getBindings().getCurrent(); statement.execute().flatMap(MssqlResult::getRowsUpdated).subscribe(); assertThat(this.statementCache.getHandle(sql, binding)).isEqualTo(1); } @Test void shouldReusePreparedStatementHandle() { Encoded cursorId = new DefaultCodecs().encode(TestByteBufAllocator.TEST, RpcParameterContext.in(), 123); cursorId.getValue().skipBytes(1); // skip maxlen byte TestClient testClient = TestClient.builder() .assertNextRequestWith(it -> { assertThat(it).isInstanceOf(RpcRequest.class); RpcRequest request = (RpcRequest) it; assertThat(request.getProcId()).isEqualTo(RpcRequest.Sp_CursorExecute); }) .thenRespond(new ReturnValue(0, null, (byte) 0, Types.integer(), cursorId.getValue())) .build(); String sql = "SELECT * from FOO where firstname = @firstname"; ParametrizedMssqlStatement statement = new ParametrizedMssqlStatement(testClient, this.connectionOptions, sql); statement.bind("firstname", ""); Binding binding = statement.getBindings().getCurrent(); this.statementCache.putHandle(1, sql, binding); statement.execute().subscribe(); assertThat(this.statementCache.getHandle(sql, binding)).isEqualTo(1); assertThat(this.statementCache.size()).isEqualTo(1); } @Test void shouldPropagateError() { TestClient testClient = TestClient.builder() .assertNextRequestWith(it -> { assertThat(it).isInstanceOf(RpcRequest.class); RpcRequest request = (RpcRequest) it; assertThat(request.getProcId()).isEqualTo(RpcRequest.Sp_ExecuteSql); }) .thenRespond(new ErrorToken(0, 4002, (byte) 0, (byte) 16, "failure", "", "", 0)) .build(); String sql = "SELECT * from FOO where firstname = @firstname"; ParametrizedMssqlStatement statement = new ParametrizedMssqlStatement(testClient, this.connectionOptions, sql).fetchSize(0); statement.bind("firstname", ""); statement.execute().flatMap(MssqlResult::getRowsUpdated).as(StepVerifier::create).verifyError(ProtocolException.class); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/QueryMessageFlowUnitTests.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.client.Client; import io.r2dbc.mssql.client.ConnectionContext; import io.r2dbc.mssql.message.TransactionDescriptor; import io.r2dbc.mssql.message.token.DoneInProcToken; import io.r2dbc.mssql.message.token.DoneProcToken; import io.r2dbc.mssql.message.token.DoneToken; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.test.StepVerifier; import java.util.function.Predicate; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** * Unit tests for {@link QueryMessageFlow}. * * @author Mark Paluch */ @SuppressWarnings("unchecked") class QueryMessageFlowUnitTests { Client client = mock(Client.class); @BeforeEach void setUp() { when(this.client.getTransactionDescriptor()).thenReturn(TransactionDescriptor.empty()); when(this.client.getContext()).thenReturn(new ConnectionContext()); } @Test void shouldAwaitDoneProcTokenShouldNotCompleteFlow() { when(this.client.exchange(any(Publisher.class), any(Predicate.class))).thenReturn(Flux.just(DoneToken.more(20), DoneProcToken.create(0), DoneInProcToken.create(0))); QueryMessageFlow.exchange(this.client, "foo") .as(StepVerifier::create) .expectNext(DoneToken.more(20), DoneProcToken.create(0), DoneInProcToken.create(0)) .thenCancel() .verify(); } @Test void shouldAwaitDoneToken() { when(this.client.exchange(any(Publisher.class), any(Predicate.class))).thenReturn(Flux.just(DoneInProcToken.create(0), DoneToken.create(0))); QueryMessageFlow.exchange(this.client, "foo") .as(StepVerifier::create) .expectNext(DoneInProcToken.create(0), DoneToken.create(0)) .verifyComplete(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/ReturnGeneratedValuesIntegrationTests.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.util.IntegrationTestSupport; import io.r2dbc.spi.Statement; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.util.concurrent.atomic.AtomicInteger; import static org.assertj.core.api.Assertions.assertThat; /** * Integration tests for {@link Statement#returnGeneratedValues(String...)}. * * @author Mark Paluch */ class ReturnGeneratedValuesIntegrationTests extends IntegrationTestSupport { @BeforeEach void setUp() { createTable(connection); } @Test void simpleStatementShouldReturnNumberOfInsertedRows() { Flux result = connection.createStatement("INSERT INTO generated_values (Name) VALUES ('Timmy'), ('Johnny')") // .returnGeneratedValues() // .execute(); verifyRowsUpdated(result); } @Test void simpleStatementShouldReturnGeneratedValues() { Flux result = connection.createStatement("INSERT INTO generated_values (Name) VALUES ('Timmy'), ('Johnny')") // .returnGeneratedValues("id") // .execute(); verifyGeneratedValues(result); } @Test void preparedStatementShouldReturnNumberOfInsertedRows() { Flux result = connection.createStatement("INSERT INTO generated_values (Name) VALUES (@P0), (@P1)") // .bind("P0", "Timmy").bind("P1", "Johnny") .returnGeneratedValues() // .execute(); verifyRowsUpdated(result); } @Test void preparedStatementShouldReturnGeneratedValues() { Flux result = connection.createStatement("INSERT INTO generated_values (Name) VALUES (@P0), (@P1)") // .bind("P0", "Timmy").bind("P1", "Johnny") .returnGeneratedValues("id") // .execute(); verifyGeneratedValues(result); } private static void verifyRowsUpdated(Flux result) { AtomicInteger resultCounter = new AtomicInteger(); result.flatMap(it -> { resultCounter.incrementAndGet(); return it.getRowsUpdated(); }).as(StepVerifier::create) .expectNext(2L) .verifyComplete(); assertThat(resultCounter).hasValue(1); } private static void verifyGeneratedValues(Flux result) { result.flatMap(it -> it.map((row, rowMetadata) -> row.get("id"))) // .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); } private void createTable(MssqlConnection connection) { connection.createStatement("DROP TABLE generated_values").execute() .flatMap(MssqlResult::getRowsUpdated) .onErrorResume(e -> Mono.empty()) .thenMany(connection.createStatement("CREATE TABLE generated_values (" + "id int IDENTITY PRIMARY KEY, " + "name varchar(255))") .execute().flatMap(MssqlResult::getRowsUpdated).then()) .as(StepVerifier::create) .verifyComplete(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/RpcBlobUnitTests.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.util.concurrent.ImmediateEventExecutor; import io.r2dbc.mssql.client.TdsEncoder; import io.r2dbc.mssql.codec.DefaultCodecs; import io.r2dbc.mssql.codec.PlpEncoded; import io.r2dbc.mssql.codec.RpcDirection; import io.r2dbc.mssql.codec.RpcParameterContext; import io.r2dbc.mssql.message.TransactionDescriptor; import io.r2dbc.mssql.message.header.PacketIdProvider; import io.r2dbc.mssql.message.token.RpcRequest; import io.r2dbc.mssql.message.type.Collation; import io.r2dbc.spi.Blob; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; import java.nio.ByteBuffer; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** * Unit test for {@link Blob} encoding via {@link PlpEncoded} and {@link TdsEncoder}. * * @author Mark Paluch */ public class RpcBlobUnitTests { static byte[] ALL_BYTES = new byte[-(-128) + 127]; static { for (int i = -128; i < 127; i++) { ALL_BYTES[-(-128) + i] = (byte) i; } } @Test void shouldEncodeChunkedStream() { int segmentsToGenerate = 3501; Blob blob = Blob.from(Flux.range(0, segmentsToGenerate).map(it -> ByteBuffer.wrap(ALL_BYTES))); DefaultCodecs codecs = new DefaultCodecs(); Binding binding = new Binding(); binding.add("P0", RpcDirection.IN, codecs.encode(ByteBufAllocator.DEFAULT, RpcParameterContext.in(), blob)); RpcRequest request = RpcQueryMessageFlow.spExecuteSql("INSERT INTO lob_test values(@P0)", binding, Collation.RAW, TransactionDescriptor.empty()); TdsEncoder encoder = new TdsEncoder(PacketIdProvider.just(1), 8000); ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); when(ctx.alloc()).thenReturn(ByteBufAllocator.DEFAULT); when(ctx.executor()).thenReturn(ImmediateEventExecutor.INSTANCE); ChannelPromise promise = mock(ChannelPromise.class); when(ctx.newPromise()).thenReturn(promise); when(ctx.write(any(), any(ChannelPromise.class))).then(invocationOnMock -> { ByteBuf buf = invocationOnMock.getArgument(0); int toRead = buf.readableBytes(); byte[] bytes = new byte[toRead]; buf.readBytes(bytes); if (buf != Unpooled.EMPTY_BUFFER) { buf.release(); } return invocationOnMock.getArgument(1); }); Flux.from(request.encode(ByteBufAllocator.DEFAULT, 8000)) .publishOn(Schedulers.parallel()) .doOnNext(it -> encoder.write(ctx, it, promise)) .as(StepVerifier::create) .expectNextCount(32) .verifyComplete(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/RpcQueryMessageFlowUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.RpcQueryMessageFlow.CursorState; import io.r2dbc.mssql.client.Client; import io.r2dbc.mssql.codec.DefaultCodecs; import io.r2dbc.mssql.codec.RpcDirection; import io.r2dbc.mssql.codec.RpcParameterContext; import io.r2dbc.mssql.codec.RpcParameterContext.ValueContext; import io.r2dbc.mssql.message.ClientMessage; import io.r2dbc.mssql.message.Message; import io.r2dbc.mssql.message.TransactionDescriptor; import io.r2dbc.mssql.message.header.HeaderOptions; import io.r2dbc.mssql.message.header.Status; import io.r2dbc.mssql.message.header.Type; import io.r2dbc.mssql.message.token.AllHeaders; import io.r2dbc.mssql.message.token.RpcRequest; import io.r2dbc.mssql.message.type.Collation; import io.r2dbc.mssql.util.ClientMessageAssert; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.TestByteBufAllocator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import reactor.core.publisher.Sinks; import reactor.core.publisher.SynchronousSink; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; /** * Unit tests for {@link RpcQueryMessageFlow}. * * @author Mark Paluch */ @SuppressWarnings("unchecked") class RpcQueryMessageFlowUnitTests { Client client = mock(Client.class); Sinks.Many requests = mock(Sinks.Many.class); SynchronousSink sink = mock(SynchronousSink.class); Runnable completion = mock(Runnable.class); DefaultCodecs codecs = new DefaultCodecs(); // windows-1252 Collation collation = Collation.from(13632521, 52); @BeforeEach void setUp() { when(this.client.getTransactionDescriptor()).thenReturn(TransactionDescriptor.empty()); } @Test void shouldEncodeSpExecuteSql() { Binding binding = new Binding(); binding.add("P0", RpcDirection.IN, this.codecs.encode(TestByteBufAllocator.TEST, RpcParameterContext.in(ValueContext.character(this.collation, true)), "mark")); RpcRequest rpcRequest = RpcQueryMessageFlow.spExecuteSql("SELECT * FROM my_table", binding, this.collation, TransactionDescriptor.empty()); String hex = "ff ff 0a 00 00 00 00 00 e7 40" + "1f 09 04 d0 00 34 2c 00 53 00 45 00 4c 00 45 00" + "43 00 54 00 20 00 2a 00 20 00 46 00 52 00 4f 00" + "4d 00 20 00 6d 00 79 00 5f 00 74 00 61 00 62 00" + "6c 00 65 00 00 00 e7 40 1f 09 04 d0 00 34 24 00" + "40 00 50 00 30 00 20 00 6e 00 76 00 61 00 72 00" + "63 00 68 00 61 00 72 00 28 00 34 00 30 00 30 00" + "30 00 29 00 03 40 00 50 00 30 00 00 e7 40 1f 09" + "04 d0 00 34 08 00 6d 00 61 00 72 00 6b 00"; ClientMessageAssert.assertThat(rpcRequest).encoded() .hasHeader(HeaderOptions.create(Type.RPC, Status.empty())) .isEncodedAs(expected -> { AllHeaders.transactional(TransactionDescriptor.empty(), 1).encode(expected); expected.writeBytes(HexUtils.decodeToByteBuf(hex)); }); } @Test void shouldEncodeSpCursorOpen() { RpcRequest rpcRequest = RpcQueryMessageFlow.spCursorOpen("SELECT * FROM my_table", this.collation, TransactionDescriptor.empty()); String hex = "FFFF020000000001260404000000000000E7" + "401F0904D000342C00530045004C0045" + "004300540020002A002000460052004F" + "004D0020006D0079005F007400610062" + "006C0065000000260404040000000000" + "26040401200000000126040400000000"; ClientMessageAssert.assertThat(rpcRequest).encoded() .hasHeader(HeaderOptions.create(Type.RPC, Status.empty())) .isEncodedAs(expected -> { AllHeaders.transactional(TransactionDescriptor.empty(), 1).encode(expected); expected.writeBytes(HexUtils.decodeToByteBuf(hex)); }); } @Test void shouldEncodeSpCursorFetch() { RpcRequest rpcRequest = RpcQueryMessageFlow.spCursorFetch(180150003, RpcQueryMessageFlow.FETCH_NEXT, 128, TransactionDescriptor.empty()); String hex = "FFFF070002000000260404F3DEBC0A000026" + "04040200000000002604040000000000" + "0026040480000000"; ClientMessageAssert.assertThat(rpcRequest).encoded() .hasHeader(HeaderOptions.create(Type.RPC, Status.empty())) .isEncodedAs(expected -> { AllHeaders.transactional(TransactionDescriptor.empty(), 1).encode(expected); expected.writeBytes(HexUtils.decodeToByteBuf(hex)); }); } @Test void shouldEncodeSpCursorClose() { RpcRequest rpcRequest = RpcQueryMessageFlow.spCursorClose(180150003, TransactionDescriptor.empty()); String hex = "FFFF090000000000260404F3DEBC0A"; ClientMessageAssert.assertThat(rpcRequest).encoded() .hasHeader(HeaderOptions.create(Type.RPC, Status.empty())) .isEncodedAs(expected -> { AllHeaders.transactional(TransactionDescriptor.empty(), 1).encode(expected); expected.writeBytes(HexUtils.decodeToByteBuf(hex)); }); } @Test void shouldEncodeSpPrepExec() { String hex = "ff ff 05 00 00 00 00 01 26 04" + "04 00 00 00 00 00 01 26 04 04 00 00 00 00 00 00" + "e7 40 1f 09 04 d0 00 34 24 00 40 00 50 00 30 00" + "20 00 6e 00 76 00 61 00 72 00 63 00 68 00 61 00" + "72 00 28 00 34 00 30 00 30 00 30 00 29 00 00 00" + "e7 40 1f 09 04 d0 00 34 48 00 55 00 50 00 44 00" + "41 00 54 00 45 00 20 00 6d 00 79 00 5f 00 74 00" + "61 00 62 00 6c 00 65 00 20 00 73 00 65 00 74 00" + "20 00 66 00 69 00 72 00 73 00 74 00 5f 00 6e 00" + "61 00 6d 00 65 00 20 00 3d 00 20 00 40 00 50 00" + "30 00 00 00 26 04 04 04 10 00 00 00 00 26 04 04" + "01 20 00 00 00 01 26 04 04 00 00 00 00 03 40 00" + "50 00 30 00 00 e7 40 1f 09 04 d0 00 34 08 00 6d" + "00 61 00 72 00 6b 00"; String sql = "UPDATE my_table set first_name = @P0"; Binding binding = new Binding(); binding.add("P0", RpcDirection.IN, this.codecs.encode(TestByteBufAllocator.TEST, RpcParameterContext.in(ValueContext.character(this.collation, true)), "mark")); RpcRequest rpcRequest = RpcQueryMessageFlow.spCursorPrepExec(0, sql, binding, this.collation, TransactionDescriptor.empty()); ClientMessageAssert.assertThat(rpcRequest).encoded() .hasHeader(HeaderOptions.create(Type.RPC, Status.empty())) .isEncodedAs(expected -> { AllHeaders.transactional(TransactionDescriptor.empty(), 1).encode(expected); expected.writeBytes(HexUtils.decodeToByteBuf(hex)); }); } @Test void shouldEncodeSpCursorExec() { String hex = "ff ff 04 00 00 00 00 00 26 04" + "04 02 00 00 00 00 01 26 04 04 00 00 00 00 00 00" + "26 04 04 04 00 00 00 00 00 26 04 04 01 20 00 00" + "00 01 26 04 04 00 00 00 00 03 40 00 50 00 30 00" + "00 e7 40 1f 09 04 d0 00 34 08 00 6d 00 61 00 72" + "00 6b 00"; Binding binding = new Binding(); binding.add("P0", RpcDirection.IN, this.codecs.encode(TestByteBufAllocator.TEST, RpcParameterContext.in(ValueContext.character(this.collation, true)), "mark")); RpcRequest rpcRequest = RpcQueryMessageFlow.spCursorExec(2, binding, TransactionDescriptor.empty()); ClientMessageAssert.assertThat(rpcRequest).encoded() .hasHeader(HeaderOptions.create(Type.RPC, Status.empty())) .isEncodedAs(expected -> { AllHeaders.transactional(TransactionDescriptor.empty(), 1).encode(expected); expected.writeBytes(HexUtils.decodeToByteBuf(hex)); }); } @Test void shouldTransitionFromNoneToFetching() { CursorState state = new CursorState(); state.cursorId = 42; state.hasMore = true; RpcQueryMessageFlow.onDone(this.client, 128, this.requests::tryEmitNext, state, this.completion); assertThat(state.phase).isEqualTo(CursorState.Phase.FETCHING); verify(this.requests).tryEmitNext(RpcQueryMessageFlow.spCursorFetch(state.cursorId, RpcQueryMessageFlow.FETCH_NEXT, 128, TransactionDescriptor.empty())); verifyNoInteractions(this.completion); } @Test void shouldContinueFetching() { CursorState state = new CursorState(); state.cursorId = 42; state.phase = CursorState.Phase.FETCHING; state.hasSeenRows = true; RpcQueryMessageFlow.onDone(this.client, 128, this.requests::tryEmitNext, state, this.completion); assertThat(state.phase).isEqualTo(CursorState.Phase.FETCHING); verify(this.requests).tryEmitNext(RpcQueryMessageFlow.spCursorFetch(state.cursorId, RpcQueryMessageFlow.FETCH_NEXT, 128, TransactionDescriptor.empty())); verifyNoInteractions(this.completion); } @Test void shouldStopFetching() { CursorState state = new CursorState(); state.cursorId = 42; state.phase = CursorState.Phase.FETCHING; RpcQueryMessageFlow.onDone(this.client, 128, this.requests::tryEmitNext, state, this.completion); assertThat(state.phase).isEqualTo(CursorState.Phase.CLOSING); verify(this.requests).tryEmitNext(RpcQueryMessageFlow.spCursorClose(state.cursorId, TransactionDescriptor.empty())); verifyNoInteractions(this.sink); } @Test void shouldTransitionFromNoneToClosing() { CursorState state = new CursorState(); state.cursorId = 42; RpcQueryMessageFlow.onDone(this.client, 128, this.requests::tryEmitNext, state, this.completion); assertThat(state.phase).isEqualTo(CursorState.Phase.CLOSING); verify(this.requests).tryEmitNext(RpcQueryMessageFlow.spCursorClose(state.cursorId, TransactionDescriptor.empty())); verifyNoInteractions(this.completion); } @Test void shouldTransitionFromClosingToClosed() { CursorState state = new CursorState(); state.cursorId = 42; state.phase = CursorState.Phase.CLOSING; RpcQueryMessageFlow.onDone(this.client, 128, this.requests::tryEmitNext, state, this.completion); assertThat(state.phase).isEqualTo(CursorState.Phase.CLOSED); verifyNoInteractions(this.requests); verify(this.completion).run(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/SimpleMssqlStatementIntegrationTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.util.IntegrationTestSupport; import io.r2dbc.spi.R2dbcTimeoutException; import io.r2dbc.spi.Result; import org.junit.jupiter.api.Test; import reactor.test.StepVerifier; import java.time.Duration; /** * Integration tests for {@link SimpleMssqlStatement}. * * @author Mark Paluch */ class SimpleMssqlStatementIntegrationTests extends IntegrationTestSupport { @Test void shouldTimeoutSqlBatch() { connection.setStatementTimeout(Duration.ofMillis(100)).as(StepVerifier::create).verifyComplete(); connection.createStatement("WAITFOR DELAY '10:00'").fetchSize(0).execute().flatMap(Result::getRowsUpdated).as(StepVerifier::create).verifyError(R2dbcTimeoutException.class); connection.createStatement("SELECT 1").execute().flatMap(it -> it.map(row -> row.get(0))).as(StepVerifier::create).expectNext(1).verifyComplete(); } @Test void shouldTimeoutCursored() { connection.setStatementTimeout(Duration.ofMillis(100)).as(StepVerifier::create).verifyComplete(); connection.createStatement("WAITFOR DELAY '10:00'").fetchSize(100).execute().flatMap(Result::getRowsUpdated).as(StepVerifier::create).verifyError(R2dbcTimeoutException.class); connection.createStatement("SELECT 1").execute().flatMap(it -> it.map(row -> row.get(0))).as(StepVerifier::create).expectNext(1).verifyComplete(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/SimpleMssqlStatementUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.client.Client; import io.r2dbc.mssql.client.ConnectionContext; import io.r2dbc.mssql.client.TestClient; import io.r2dbc.mssql.message.ClientMessage; import io.r2dbc.mssql.message.Message; import io.r2dbc.mssql.message.TransactionDescriptor; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.message.tds.ServerCharset; import io.r2dbc.mssql.message.token.*; import io.r2dbc.mssql.message.type.Collation; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.spi.ColumnMetadata; import io.r2dbc.spi.R2dbcNonTransientResourceException; import io.r2dbc.spi.Result; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import reactor.util.annotation.Nullable; import java.math.BigDecimal; import java.nio.charset.Charset; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.function.Predicate; import static io.r2dbc.mssql.message.type.TypeInformation.Builder; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; /** * Unit tests for {@link SimpleMssqlStatement}. * * @author Mark Paluch */ class SimpleMssqlStatementUnitTests { static final Column[] COLUMNS = Arrays.asList(createColumn(0, "employee_id", SqlServerType.TINYINT, 1, LengthStrategy.FIXEDLENTYPE, null), createColumn(1, "last_name", SqlServerType.NVARCHAR, 100, LengthStrategy.USHORTLENTYPE, ServerCharset.UNICODE.charset()), createColumn(2, "first_name", SqlServerType.VARCHAR, 50, LengthStrategy.USHORTLENTYPE, ServerCharset.CP1252.charset()), createColumn(3, "salary", SqlServerType.MONEY, 8, LengthStrategy.BYTELENTYPE, null)).toArray(new Column[0]); static final ConnectionOptions OPTIONS = new TestConnectionOptions(); @Test void shouldReportNumberOfAffectedRows() { SqlBatch batch = SqlBatch.create(1, TransactionDescriptor.empty(), "SELECT * FROM foo"); ColumnMetadataToken columns = ColumnMetadataToken.create(COLUMNS); Tabular tabular = Tabular.create(columns, DoneToken.create(1)); TestClient client = TestClient.builder().expectRequest(batch).thenRespond(tabular.getTokens().toArray(new DataToken[0])).build(); SimpleMssqlStatement statement = new SimpleMssqlStatement(client, OPTIONS, "SELECT * FROM foo").fetchSize(0); statement.execute() .flatMap(Result::getRowsUpdated) .as(StepVerifier::create) .expectNext(1L) .verifyComplete(); } @Test void shouldReturnColumnData() { TestClient client = simpleResultAndCount(); SimpleMssqlStatement statement = new SimpleMssqlStatement(client, OPTIONS, "SELECT * FROM foo").fetchSize(0); statement.execute() .flatMap(result -> result.map((row, md) -> { Map rowData = new HashMap<>(); for (ColumnMetadata column : md.getColumnMetadatas()) { rowData.put(column.getName(), row.get(column.getName())); } return rowData; })) .as(StepVerifier::create) .consumeNextWith(actual -> { assertThat(actual).containsEntry("first_name", "mark").containsEntry("last_name", "paluch"); }) .verifyComplete(); } @Test void shouldFlatMapResults() { TestClient client = simpleResultAndCount(); SimpleMssqlStatement statement = new SimpleMssqlStatement(client, OPTIONS, "SELECT * FROM foo").fetchSize(0); statement.execute() .flatMap(result -> result.flatMap(segment -> Mono.just(segment.toString()))) .as(StepVerifier::create) .consumeNextWith(actual -> { assertThat(actual).contains("MssqlRow"); }) .consumeNextWith(actual -> { assertThat(actual).contains("DoneToken"); }) .verifyComplete(); } @Test void shouldFilterAndFlatMapCount() { TestClient client = simpleResultAndCount(); SimpleMssqlStatement statement = new SimpleMssqlStatement(client, OPTIONS, "SELECT * FROM foo").fetchSize(0); statement.execute() .flatMap(result -> result.filter(Result.UpdateCount.class::isInstance).flatMap(segment -> Mono.just(segment.toString()))) .as(StepVerifier::create) .consumeNextWith(actual -> { assertThat(actual).contains("DoneToken"); }) .verifyComplete(); } @Test void shouldFilterAndFlatMapData() { TestClient client = simpleResultAndCount(); SimpleMssqlStatement statement = new SimpleMssqlStatement(client, OPTIONS, "SELECT * FROM foo").fetchSize(0); statement.execute() .flatMap(result -> result.filter(Result.RowSegment.class::isInstance).flatMap(segment -> { Result.RowSegment data = (Result.RowSegment) segment; Map rowData = new HashMap<>(); for (ColumnMetadata column : data.row().getMetadata().getColumnMetadatas()) { rowData.put(column.getName(), data.row().get(column.getName())); } return Mono.just(rowData); })) .as(StepVerifier::create) .consumeNextWith(actual -> { assertThat(actual).containsEntry("first_name", "mark").containsEntry("last_name", "paluch"); }) .verifyComplete(); } @Test void shouldFilterError() { SqlBatch batch = SqlBatch.create(1, TransactionDescriptor.empty(), "SELECT * FROM foo"); TestClient client = TestClient.builder().expectRequest(batch).thenRespond(new ErrorToken(10, 10, (byte) 1, (byte) 1, "foo", null, null, 0)).build(); SimpleMssqlStatement statement = new SimpleMssqlStatement(client, OPTIONS, "SELECT * FROM foo").fetchSize(0); statement.execute() .flatMap(result -> result.filter(Result.RowSegment.class::isInstance).getRowsUpdated()) .as(StepVerifier::create) .verifyComplete(); } @Test void shouldFlatMapErrorMessage() { SqlBatch batch = SqlBatch.create(1, TransactionDescriptor.empty(), "SELECT * FROM foo"); TestClient client = TestClient.builder().expectRequest(batch).thenRespond(new ErrorToken(10, 10, (byte) 1, (byte) 2, "foo", null, null, 0)).build(); SimpleMssqlStatement statement = new SimpleMssqlStatement(client, OPTIONS, "SELECT * FROM foo").fetchSize(0); statement.execute() .flatMap(result -> result.filter(Result.Message.class::isInstance).flatMap(segment -> { return Mono.just(segment).cast(Result.Message.class); })) .as(StepVerifier::create) .consumeNextWith(actual -> { assertThat(actual.errorCode()).isEqualTo(10); assertThat(actual.message()).isEqualTo("foo"); assertThat(actual.sqlState()).isEqualTo("S0001"); assertThat(actual.exception()).isInstanceOf(R2dbcNonTransientResourceException.class); }) .verifyComplete(); } private TestClient simpleResultAndCount() { SqlBatch batch = SqlBatch.create(1, TransactionDescriptor.empty(), "SELECT * FROM foo"); ColumnMetadataToken columns = ColumnMetadataToken.create(COLUMNS); RowToken rowToken = RowTokenFactory.create(columns, buffer -> { Encode.asByte(buffer, 1); Encode.uString(buffer, "paluch", ServerCharset.UNICODE.charset()); Encode.uString(buffer, "mark", ServerCharset.CP1252.charset()); //money/salary Encode.asByte(buffer, 8); Encode.money(buffer, new BigDecimal("50.0000").unscaledValue()); }); Tabular tabular = Tabular.create(columns, rowToken, DoneToken.create(1)); return TestClient.builder().expectRequest(batch).thenRespond(tabular.getTokens().toArray(new DataToken[0])).build(); } @Test @SuppressWarnings("unchecked") void shouldPreferCursoredExecution() { Client client = mockClient(); SimpleMssqlStatement statement = new SimpleMssqlStatement(client, OPTIONS, "SELECT * FROM foo"); statement.execute().as(StepVerifier::create) .verifyComplete(); ArgumentCaptor> captor = ArgumentCaptor.forClass(Publisher.class); verify(client).exchange((Publisher) captor.capture(), any(Predicate.class)); StepVerifier.create(captor.getValue()) .consumeNextWith(it -> assertThat(it) .isInstanceOf(RpcRequest.class)) .thenCancel() .verify(); } @Test @SuppressWarnings("unchecked") void shouldForceDirectExecution() { Client client = mockClient(); SimpleMssqlStatement statement = new SimpleMssqlStatement(client, OPTIONS, "SELECT * FROM foo").fetchSize(0); statement.execute().as(StepVerifier::create) .verifyComplete(); ArgumentCaptor> captor = ArgumentCaptor.forClass(Mono.class); verify(client).exchange(captor.capture(), any(Predicate.class)); assertThat(captor.getValue().block()).isInstanceOf(SqlBatch.class); } @Test @SuppressWarnings("unchecked") void shouldPreferDirectExecution() { Client client = mockClient(); SimpleMssqlStatement statement = new SimpleMssqlStatement(client, OPTIONS, "INSERT INTO"); statement.execute().as(StepVerifier::create) .verifyComplete(); ArgumentCaptor> captor = ArgumentCaptor.forClass(Mono.class); verify(client).exchange(captor.capture(), any(Predicate.class)); assertThat(captor.getValue().block()).isInstanceOf(SqlBatch.class); } @Test @SuppressWarnings("unchecked") void shouldForceCursoredExecution() { Client client = mockClient(); SimpleMssqlStatement statement = new SimpleMssqlStatement(client, OPTIONS, "INSERT INTO").fetchSize(1); statement.execute().as(StepVerifier::create) .verifyComplete(); ArgumentCaptor> captor = ArgumentCaptor.forClass(Publisher.class); verify(client).exchange((Publisher) captor.capture(), any(Predicate.class)); StepVerifier.create(captor.getValue()) .consumeNextWith(it -> assertThat(it) .isInstanceOf(RpcRequest.class)) .thenCancel() .verify(); } @SuppressWarnings("unchecked") private static Client mockClient() { Client client = mock(Client.class); when(client.getRequiredCollation()).thenReturn(Collation.RAW); when(client.getTransactionDescriptor()).thenReturn(TransactionDescriptor.empty()); when(client.exchange(any(Publisher.class), any(Predicate.class))).thenReturn(Flux.empty()); when(client.getContext()).thenReturn(new ConnectionContext()); return client; } private static Column createColumn(int index, String name, SqlServerType serverType, int length, LengthStrategy lengthStrategy, @Nullable Charset charset) { Builder builder = TypeInformation.builder().withServerType(serverType).withMaxLength(length).withLengthStrategy(lengthStrategy); if (charset != null) { builder.withCharset(charset); } TypeInformation type = builder.build(); return new Column(index, name, type, null); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/SqlVariantIntegrationTests.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.util.IntegrationTestSupport; import org.junit.jupiter.api.Test; import reactor.test.StepVerifier; /** * Integration tests for SQL Variant showing that {@code sql_variant} is not supported. * * @author Mark Paluch */ class SqlVariantIntegrationTests extends IntegrationTestSupport { @Test void shouldExecuteBatch() { connection.createStatement(" SELECT SERVERPROPERTY('Edition')").execute() .flatMap(mssqlResult -> mssqlResult.map((row, rowMetadata) -> row.get(0))) .as(StepVerifier::create) .verifyError(UnsupportedOperationException.class); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/TestConnectionOptions.java ================================================ /* * Copyright 2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.codec.DefaultCodecs; /** * @author Mark Paluch */ class TestConnectionOptions extends ConnectionOptions { TestConnectionOptions() { super(MssqlConnectionConfiguration.DefaultCursorPreference.INSTANCE, new DefaultCodecs(), new IndefinitePreparedStatementCache(), true); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/TransactionIntegrationTests.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.api.MssqlTransactionDefinition; import io.r2dbc.mssql.util.IntegrationTestSupport; import io.r2dbc.spi.IsolationLevel; import io.r2dbc.spi.Result; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.time.Duration; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; /** * Integration tests for transactional behavior. * * @author Mark Paluch */ class TransactionIntegrationTests extends IntegrationTestSupport { @Test void savepointsSynchronized() { createTable(connection); connection.beginTransaction() .as(StepVerifier::create) .verifyComplete(); Flux.from(connection.createStatement("INSERT INTO r2dbc_example VALUES(@P1, @P2, @P3)") .bind(0, 0).bind(1, "Walter").bind(2, "White").add() .bind(0, 1).bind(1, "Jesse").bind(2, "Pinkman").execute()) .flatMap(Result::getRowsUpdated) .as(StepVerifier::create) .expectNextCount(2) .verifyComplete(); connection.createSavepoint("savepoint") .as(StepVerifier::create) .verifyComplete(); Flux.from(connection.createStatement("INSERT INTO r2dbc_example VALUES(@P1, @P2, @P3)") .bind(0, 2).bind(1, "Hank").bind(2, "Schrader").execute()) .flatMap(Result::getRowsUpdated) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); Flux.from(connection.createStatement("SELECT COUNT(*) FROM r2dbc_example") .execute()) .flatMap(result -> result.map((row, rowMetadata) -> row.get(0, Integer.class))) .as(StepVerifier::create) .expectNext(3) .verifyComplete(); connection.rollbackTransactionToSavepoint("savepoint") .as(StepVerifier::create) .verifyComplete(); Flux.from(connection.createStatement("SELECT COUNT(*) FROM r2dbc_example /* in-tx */") .execute()) .flatMap(result -> result.map((row, rowMetadata) -> row.get(0, Integer.class))) .as(StepVerifier::create) .expectNext(2) .verifyComplete(); connection.commitTransaction() .as(StepVerifier::create) .verifyComplete(); Flux.from(connection.createStatement("SELECT COUNT(*) FROM r2dbc_example /* after-tx */") .execute()) .flatMap(result -> result.map((row, rowMetadata) -> row.get(0, Integer.class))) .as(StepVerifier::create) .expectNext(2) .verifyComplete(); } @Test void savepointsConcatWith() { createTable(connection); connection.beginTransaction() .cast(Object.class) .concatWith(Flux.from(connection.createStatement("INSERT INTO r2dbc_example VALUES(@P1, @P2, @P3)") .bind(0, 0).bind(1, "Walter").bind(2, "White").execute()) .flatMap(Result::getRowsUpdated)) .concatWith(connection.createSavepoint("savepoint.1")) .concatWith(Flux.from(connection.createStatement("INSERT INTO r2dbc_example VALUES(@P1, @P2, @P3)") .bind(0, 2).bind(1, "Hank").bind(2, "Schrader").execute()) .flatMap(Result::getRowsUpdated)) .concatWith(connection.rollbackTransactionToSavepoint("savepoint.1")) .concatWith(Flux.from(connection.createStatement("SELECT COUNT(*) FROM r2dbc_example /* in-tx */") .execute()) .flatMap(result -> result.map((row, rowMetadata) -> row.get(0, Integer.class)))) .concatWith(connection.commitTransaction()) .concatWith(Flux.from(connection.createStatement("SELECT COUNT(*) FROM r2dbc_example /* after-tx */") .execute()) .flatMap(result -> result.map((row, rowMetadata) -> row.get(0, Integer.class)))) .as(StepVerifier::create) .expectNext(1L).as("Affected Rows Count from first INSERT") .expectNext(1L).as("Affected Rows Count from second INSERT") .expectNext(1).as("SELECT COUNT(*) after ROLLBACK TO SAVEPOINT") .expectNext(1).as("SELECT COUNT(*) after COMMIT") .verifyComplete(); } @Test void autoCommitDisabled() { createTable(connection); assertThat(connection.isAutoCommit()).isTrue(); connection.setAutoCommit(false) .as(StepVerifier::create) .verifyComplete(); assertThat(connection.isAutoCommit()).isFalse(); connection.createStatement("INSERT INTO r2dbc_example VALUES(0, 'Walter', 'White')").execute() .flatMap(Result::getRowsUpdated) .concatWith(connection.rollbackTransaction()) .as(StepVerifier::create) .expectNext(1L).as("Affected Rows Count from first INSERT") .verifyComplete(); connectionFactory.create().flatMapMany(c -> c.createStatement("SELECT * FROM r2dbc_example") .execute().flatMap(it -> it.map((row, metadata) -> row.get("first_name")))) .as(StepVerifier::create) .verifyComplete(); } @Test void savepointStartsTransaction() { createTable(connection); connection.createStatement("INSERT INTO r2dbc_example VALUES(1, 'Jesse', 'Pinkman')") .execute().flatMap(Result::getRowsUpdated) .as(StepVerifier::create) .expectNext(1L) .verifyComplete(); connection.createSavepoint("s1") .thenMany(connection.createStatement("INSERT INTO r2dbc_example VALUES(0, 'Walter', 'White')").execute() .flatMap(Result::getRowsUpdated)) .concatWith(Mono.fromSupplier(() -> connection.isAutoCommit())) .concatWith(connection.rollbackTransaction()) .as(StepVerifier::create) .expectNext(1L).as("Affected Rows Count from first INSERT") .expectNext(false).as("Auto-commit disabled by createSavepoint") .verifyComplete(); connection.createStatement("INSERT INTO r2dbc_example VALUES(0, 'Walter', 'White')").execute() .flatMap(Result::getRowsUpdated) .concatWith(connection.rollbackTransaction()) .as(StepVerifier::create) .expectNext(1L).as("Affected Rows Count from second INSERT") .verifyComplete(); connectionFactory.create().flatMapMany(c -> c.createStatement("SELECT * FROM r2dbc_example") .execute().flatMap(it -> it.map((row, metadata) -> row.get("first_name")))) .as(StepVerifier::create) .expectNext("Jesse") .verifyComplete(); } @Test void commitTransaction() { createTable(connection); connection.beginTransaction() .as(StepVerifier::create) .verifyComplete(); Flux.from(connection.createStatement("INSERT INTO r2dbc_example VALUES(@P1, @P2, @P3)") .bind(0, 0).bind(1, "Walter").bind(2, "White").add() .bind(0, 1).bind(1, "Jesse").bind(2, "Pinkman").execute()) .flatMap(Result::getRowsUpdated) .as(StepVerifier::create) .expectNextCount(2) .verifyComplete(); connection.commitTransaction() .as(StepVerifier::create) .verifyComplete(); Flux.from(connection.createStatement("SELECT COUNT(*) FROM r2dbc_example") .execute()) .flatMap(result -> result.map((row, rowMetadata) -> row.get(0, Integer.class))) .as(StepVerifier::create) .expectNext(2) .verifyComplete(); } @Test void rollbackTransaction() { createTable(connection); connection.beginTransaction() .as(StepVerifier::create) .verifyComplete(); Flux.from(connection.createStatement("INSERT INTO r2dbc_example VALUES(@P1, @P2, @P3)") .bind(0, 0).bind(1, "Walter").bind(2, "White").add() .bind(0, 1).bind(1, "Jesse").bind(2, "Pinkman").execute()) .flatMap(Result::getRowsUpdated) .as(StepVerifier::create) .expectNextCount(2) .verifyComplete(); connection.rollbackTransaction() .as(StepVerifier::create) .verifyComplete(); Flux.from(connection.createStatement("SELECT COUNT(*) FROM r2dbc_example") .execute()) .flatMap(result -> result.map((row, rowMetadata) -> row.get(0, Integer.class))) .as(StepVerifier::create) .expectNext(0) .verifyComplete(); } @Test void shouldBeginExtendedTransaction() { getTransactionCount() .as(StepVerifier::create) .expectNext(0) .verifyComplete(); connection.beginTransaction(MssqlTransactionDefinition.from(IsolationLevel.READ_UNCOMMITTED) .name("foo-1").mark("bar") .lockTimeout(Duration.ofMinutes(1))).as(StepVerifier::create).verifyComplete(); getTransactionCount() .as(StepVerifier::create) .expectNext(1) .verifyComplete(); connection.createStatement("SELECT @@LOCK_TIMEOUT").execute() .flatMap(it -> it.map((row, rowMetadata) -> row.get(0, Long.class))) .as(StepVerifier::create) .expectNext(TimeUnit.MINUTES.toMillis(1)) .verifyComplete(); getIsolationLevel() .as(StepVerifier::create) .expectNext(MssqlIsolationLevel.READ_UNCOMMITTED) .verifyComplete(); connection.rollbackTransaction().as(StepVerifier::create).verifyComplete(); getTransactionCount() .as(StepVerifier::create) .expectNext(0) .verifyComplete(); } private Flux getTransactionCount() { return connection.createStatement("SELECT @@TRANCOUNT").execute() .flatMap(it -> it.map((row, rowMetadata) -> row.get(0, Integer.class))); } @Test void shouldRestoreAutoCommitAfterExtendedTx() { getImplicitTransactions().as(StepVerifier::create).expectNext(false).verifyComplete(); connection.beginTransaction(IsolationLevel.READ_UNCOMMITTED).as(StepVerifier::create).verifyComplete(); assertThat(connection.isAutoCommit()).isFalse(); getImplicitTransactions().as(StepVerifier::create).expectNext(false).verifyComplete(); connection.rollbackTransaction().as(StepVerifier::create).verifyComplete(); assertThat(connection.isAutoCommit()).isTrue(); // huh? getImplicitTransactions().as(StepVerifier::create).expectNext(false).verifyComplete(); connection.setAutoCommit(true).as(StepVerifier::create).verifyComplete(); assertThat(connection.isAutoCommit()).isTrue(); getImplicitTransactions().as(StepVerifier::create).expectNext(false).verifyComplete(); assertAutoCommit(); } private void assertAutoCommit() { createTable(connection); connection.createStatement("INSERT INTO r2dbc_example VALUES(0, 'Walter', 'White')") .execute() .flatMap(Result::getRowsUpdated) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); getTransactionCount() .as(StepVerifier::create) .expectNext(0) .verifyComplete(); MssqlConnection connection = connectionFactory.create().block(); connection.createStatement("SELECT COUNT(*) FROM r2dbc_example") .execute() .flatMap(it -> it.map((row, rowMetadata) -> row.get(0, Integer.class))) .as(StepVerifier::create) .expectNext(1) .verifyComplete(); connection.close().as(StepVerifier::create).verifyComplete(); } @Test void shouldResetIsolationLevelAfterTransaction() { getIsolationLevel() .as(StepVerifier::create) .expectNext(MssqlIsolationLevel.READ_COMMITTED) .verifyComplete(); connection.beginTransaction(IsolationLevel.READ_UNCOMMITTED).as(StepVerifier::create).verifyComplete(); connection.rollbackTransaction().as(StepVerifier::create).verifyComplete(); getIsolationLevel() .as(StepVerifier::create) .expectNext(MssqlIsolationLevel.READ_COMMITTED) .verifyComplete(); assertThat(connection.getTransactionIsolationLevel()).isEqualTo(MssqlIsolationLevel.READ_COMMITTED); } @Test void shouldResetLockTimeoutAfterTransaction() { connection.createStatement("SELECT @@LOCK_TIMEOUT").execute() .flatMap(it -> it.map((row, rowMetadata) -> row.get(0, Integer.class))) .as(StepVerifier::create) .expectNext(-1) .verifyComplete(); connection.beginTransaction(MssqlTransactionDefinition.named("foo").lockTimeout(Duration.ofMinutes(60))).as(StepVerifier::create).verifyComplete(); connection.rollbackTransaction().as(StepVerifier::create).verifyComplete(); connection.createStatement("SELECT @@LOCK_TIMEOUT").execute() .flatMap(it -> it.map((row, rowMetadata) -> row.get(0, Integer.class))) .as(StepVerifier::create) .expectNext(-1) .verifyComplete(); } Mono getIsolationLevel() { return connection.createStatement("SELECT CASE transaction_isolation_level \n" + "WHEN 0 THEN 'UNSPECIFIED' \n" + "WHEN 1 THEN 'READ_UNCOMMITTED' \n" + "WHEN 2 THEN 'READ_COMMITTED' \n" + "WHEN 3 THEN 'REPEATABLE_READ' \n" + "WHEN 4 THEN 'SERIALIZABLE' \n" + "WHEN 5 THEN 'SNAPSHOT' END AS TRANSACTION_ISOLATION_LEVEL \n" + "FROM sys.dm_exec_sessions \n" + "where session_id = @@SPID").execute() .flatMap(it -> it.map((row, rowMetadata) -> row.get(0, String.class))) .map(it -> { switch (it) { case "READ_UNCOMMITTED": return MssqlIsolationLevel.READ_UNCOMMITTED; case "READ_COMMITTED": return MssqlIsolationLevel.READ_COMMITTED; case "SERIALIZABLE": return MssqlIsolationLevel.SERIALIZABLE; case "REPEATABLE_READ": return MssqlIsolationLevel.REPEATABLE_READ; case "SNAPSHOT": return MssqlIsolationLevel.SNAPSHOT; } return MssqlIsolationLevel.UNSPECIFIED; }).single(); } Mono getImplicitTransactions() { return connection.createStatement("SELECT @@OPTIONS AS IMPLICIT_TRANSACTIONS;").execute() .flatMap(it -> it.map((row, rowMetadata) -> (row.get(0, Integer.class) & 2) == 2)) .single(); } private void createTable(MssqlConnection connection) { connection.createStatement("DROP TABLE r2dbc_example").execute() .flatMap(MssqlResult::getRowsUpdated) .onErrorResume(e -> Mono.empty()) .thenMany(connection.createStatement("CREATE TABLE r2dbc_example (" + "id int PRIMARY KEY, " + "first_name varchar(255), " + "last_name varchar(255))") .execute().flatMap(MssqlResult::getRowsUpdated).then()) .as(StepVerifier::create) .verifyComplete(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/XmlIntegrationTests.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql; import io.r2dbc.mssql.util.IntegrationTestSupport; import org.junit.jupiter.api.Test; import reactor.test.StepVerifier; /** * Integration tests using XML as return type. * * @author Mark Paluch */ class XmlIntegrationTests extends IntegrationTestSupport { @Test void shouldExecuteForXmlSimple() { connection.createStatement("select 1 as a for xml path").execute() .flatMap(result -> result.map((row, rowMetadata) -> row.get(0))) .as(StepVerifier::create) .expectNext("1") .verifyComplete(); } @Test void shouldExecuteForXmlParametrized() { connection.createStatement("select 1 as a where @P0 = @P0 for xml path").bind("@P0", true).fetchSize(0).execute() .flatMap(result -> result.map((row, rowMetadata) -> row.get(0))) .as(StepVerifier::create) .expectNext("1") .verifyComplete(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/client/ConnectionStateUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client; import io.netty.channel.Channel; import io.netty.channel.ChannelPipeline; import io.r2dbc.mssql.client.ssl.SslState; import io.r2dbc.mssql.message.token.Prelogin; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import reactor.netty.Connection; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** * Unit tests for {@link ConnectionState}. * * @author Mark Paluch */ class ConnectionStateUnitTests { Connection nettyConnection = mock(Connection.class); Channel channel = mock(Channel.class); ChannelPipeline pipeline = mock(ChannelPipeline.class); @BeforeEach void setUp() { when(this.nettyConnection.channel()).thenReturn(this.channel); when(this.channel.pipeline()).thenReturn(this.pipeline); } @Test void shouldInitiateSslHandshakeForLogin() { Prelogin prelogin = Prelogin.builder().build(); ConnectionState.PRELOGIN.next(prelogin, this.nettyConnection); verify(this.pipeline).fireUserEventTriggered(SslState.LOGIN_ONLY); } @Test void shouldInitiateSslHandshakeForConnection() { Prelogin prelogin = Prelogin.builder().withEncryptionEnabled().build(); ConnectionState.PRELOGIN.next(prelogin, this.nettyConnection); verify(this.pipeline).fireUserEventTriggered(SslState.CONNECTION); } @Test void shouldAdvanceToSslHandshakeState() { Prelogin prelogin = Prelogin.builder().withEncryptionEnabled().build(); ConnectionState next = ConnectionState.PRELOGIN.next(prelogin, this.nettyConnection); assertThat(next).isEqualTo(ConnectionState.PRELOGIN_SSL_NEGOTIATION); } @Test void shouldAdvancePreloginState() { Prelogin prelogin = Prelogin.builder().withEncryptionNotSupported().build(); ConnectionState next = ConnectionState.PRELOGIN.next(prelogin, this.nettyConnection); assertThat(next).isEqualTo(ConnectionState.PRELOGIN); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/client/ReactorNettyClientIntegrationTests.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client; import io.r2dbc.mssql.MssqlConnection; import io.r2dbc.mssql.message.ClientMessage; import io.r2dbc.mssql.message.Message; import io.r2dbc.mssql.message.token.SqlBatch; import io.r2dbc.mssql.util.IntegrationTestSupport; import io.r2dbc.spi.R2dbcNonTransientResourceException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.util.ReflectionUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; import reactor.netty.Connection; import reactor.test.StepVerifier; import java.lang.reflect.Field; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; /** * Integration tests for {@link ReactorNettyClient}. */ class ReactorNettyClientIntegrationTests extends IntegrationTestSupport { static final Field CONNECTION = ReflectionUtils.findField(ReactorNettyClient.class, "connection"); static final Field CLIENT = ReflectionUtils.findField(MssqlConnection.class, "client"); static { ReflectionUtils.makeAccessible(CONNECTION); ReflectionUtils.makeAccessible(CLIENT); } private io.r2dbc.spi.Connection r2dbcConnection; private ReactorNettyClient client; private Connection connection; @BeforeEach void setUp() { this.r2dbcConnection = connectionFactory.create().block(); this.client = (ReactorNettyClient) ReflectionUtils.getField(CLIENT, this.r2dbcConnection); this.connection = (Connection) ReflectionUtils.getField(CONNECTION, this.client); } @AfterEach void tearDown() { Mono.from(r2dbcConnection.close()).subscribe(); } @Test void disconnectedShouldRejectExchange() { Connection connection = (Connection) ReflectionUtils.getField(CONNECTION, this.client); connection.channel().close().awaitUninterruptibly(); this.client.close() .thenMany(this.client.exchange(Mono.empty(), message -> true)) .as(StepVerifier::create) .verifyErrorSatisfies(t -> assertThat(t).isInstanceOf(R2dbcNonTransientResourceException.class).hasMessage("Cannot exchange messages because the connection is closed")); } @Test void shouldCancelExchangeOnCloseFirstMessage() throws Exception { Sinks.Many messages = Sinks.many().unicast().onBackpressureBuffer(); Flux query = this.client.exchange(messages.asFlux(), message -> true); CompletableFuture> future = query.collectList().toFuture(); this.connection.channel().eventLoop().execute(() -> { this.connection.channel().close(); SqlBatch batch = SqlBatch.create(0, this.client.getTransactionDescriptor(), "SELECT value FROM test"); messages.tryEmitNext(batch); }); try { future.get(9995, TimeUnit.SECONDS); fail("Expected MssqlConnectionClosedException"); } catch (ExecutionException e) { assertThat(e).hasCauseInstanceOf(ReactorNettyClient.MssqlConnectionClosedException.class).hasMessageContaining("closed"); } } } ================================================ FILE: src/test/java/io/r2dbc/mssql/client/StreamDecoderUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.r2dbc.mssql.message.Message; import io.r2dbc.mssql.message.header.Header; import io.r2dbc.mssql.message.header.HeaderOptions; import io.r2dbc.mssql.message.header.PacketIdProvider; import io.r2dbc.mssql.message.header.Status; import io.r2dbc.mssql.message.header.Type; import io.r2dbc.mssql.message.token.ColumnMetadataToken; import io.r2dbc.mssql.message.token.DoneToken; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.TestByteBufAllocator; import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link StreamDecoder}. * * @author Mark Paluch */ class StreamDecoderUnitTests { static final Client CLIENT = TestClient.NO_OP; @Test void shouldDecodeFullPacket() { StreamDecoder decoder = new StreamDecoder(); Header header = Header.create(HeaderOptions.create(Type.TABULAR_RESULT, Status.of(Status.StatusBit.EOM)), Header.LENGTH + DoneToken.LENGTH, PacketIdProvider.just(1)); DoneToken token = DoneToken.create(2); ByteBuf buffer = TestByteBufAllocator.TEST.buffer(); header.encode(buffer); token.encode(buffer); List messageStream = decoder.decode(buffer, ConnectionState.POST_LOGIN.decoder(CLIENT)); assertThat(messageStream).containsOnly(token); assertThat(decoder.getDecoderState()).isNull(); assertThat(buffer.refCnt()).isEqualTo(1); buffer.release(); } @Test void shouldDecodePartialPacket() { StreamDecoder decoder = new StreamDecoder(); DoneToken token = DoneToken.create(2); // Just the header type. ByteBuf partial = Unpooled.wrappedBuffer(new byte[]{4}); List noMessage = decoder.decode(partial, ConnectionState.POST_LOGIN.decoder(CLIENT)); assertThat(noMessage).isEmpty(); StreamDecoder.DecoderState state = decoder.getDecoderState(); assertThat(state).isNotNull(); assertThat(state.header).isNull(); assertThat(state.remainder.readableBytes()).isEqualTo(1); assertThat(state.aggregatedBody.readableBytes()).isEqualTo(0); assertThat(partial.refCnt()).isEqualTo(1); ByteBuf nextPacket = TestByteBufAllocator.TEST.buffer(); nextPacket.writeBytes(new byte[]{1, 0, 0x15, 0, 0, 0, 0}); token.encode(nextPacket); List completeMessage = decoder.decode(nextPacket, ConnectionState.POST_LOGIN.decoder(CLIENT)); assertThat(completeMessage).containsOnly(token); assertThat(decoder.getDecoderState()).isNull(); assertThat(partial.refCnt()).isEqualTo(1); assertThat(nextPacket.refCnt()).isEqualTo(1); partial.release(); nextPacket.release(); } @Test void shouldDecodePacketWithNextRemainder() { StreamDecoder decoder = new StreamDecoder(); Header header = Header.create(HeaderOptions.create(Type.TABULAR_RESULT, Status.of(Status.StatusBit.EOM)), Header.LENGTH + DoneToken.LENGTH, PacketIdProvider.just(1)); DoneToken token = DoneToken.create(2); ByteBuf buffer = TestByteBufAllocator.TEST.buffer(); header.encode(buffer); token.encode(buffer); buffer.writeByte(4); List completeMessage = decoder.decode(buffer, ConnectionState.POST_LOGIN.decoder(CLIENT)); assertThat(completeMessage).containsOnly(token); StreamDecoder.DecoderState state = decoder.getDecoderState(); assertThat(state).isNotNull(); assertThat(state.header).isNull(); assertThat(state.remainder.readableBytes()).isEqualTo(1); assertThat(state.aggregatedBody.readableBytes()).isEqualTo(0); state.release(); assertThat(buffer.refCnt()).isEqualTo(1); buffer.release(); } @Test void shouldDecodePacketWithNextRemainderAfterNextHeader() { StreamDecoder decoder = new StreamDecoder(); Header header = Header.create(HeaderOptions.create(Type.TABULAR_RESULT, Status.of(Status.StatusBit.EOM)), Header.LENGTH + DoneToken.LENGTH, PacketIdProvider.just(1)); Header header2 = Header.create(HeaderOptions.create(Type.TABULAR_RESULT, Status.of(Status.StatusBit.EOM)), Header.LENGTH + DoneToken.LENGTH, PacketIdProvider.just(2)); DoneToken token = DoneToken.create(2); ByteBuf buffer = TestByteBufAllocator.TEST.buffer(); header.encode(buffer); token.encode(buffer); header2.encode(buffer); buffer.writeBytes(new byte[]{4, 2, 1}); List completeMessage = decoder.decode(buffer, ConnectionState.POST_LOGIN.decoder(CLIENT)); assertThat(completeMessage).containsOnly(token); StreamDecoder.DecoderState state = decoder.getDecoderState(); assertThat(state).isNotNull(); assertThat(state.header).isNotNull().isEqualTo(header2); assertThat(state.remainder.readableBytes()).isEqualTo(3); assertThat(state.aggregatedBody.readableBytes()).isEqualTo(0); buffer.release(); } @Test void shouldDecodeTwoPacketsFragmented() { StreamDecoder decoder = new StreamDecoder(); Header header = Header.create(HeaderOptions.create(Type.TABULAR_RESULT, Status.of(Status.StatusBit.EOM)), Header.LENGTH + DoneToken.LENGTH, PacketIdProvider.just(1)); Header header2 = Header.create(HeaderOptions.create(Type.TABULAR_RESULT, Status.of(Status.StatusBit.EOM)), Header.LENGTH + DoneToken.LENGTH, PacketIdProvider.just(2)); DoneToken token = DoneToken.create(2); ByteBuf buffer = TestByteBufAllocator.TEST.buffer(); ByteBuf nextBuffer = TestByteBufAllocator.TEST.buffer(); token.encode(nextBuffer); header.encode(buffer); token.encode(buffer); header2.encode(buffer); buffer.writeBytes(new byte[]{nextBuffer.readByte(), nextBuffer.readByte(), nextBuffer.readByte()}); List firstMessage = decoder.decode(buffer, ConnectionState.POST_LOGIN.decoder(CLIENT)); assertThat(firstMessage).containsOnly(token); StreamDecoder.DecoderState state = decoder.getDecoderState(); assertThat(state).isNotNull(); assertThat(state.header).isNotNull().isEqualTo(header2); assertThat(state.remainder.readableBytes()).isEqualTo(3); assertThat(state.aggregatedBody.readableBytes()).isEqualTo(0); assertThat(buffer.refCnt()).isEqualTo(1); List secondMessage = decoder.decode(nextBuffer, ConnectionState.POST_LOGIN.decoder(CLIENT)); assertThat(secondMessage).containsOnly(token); assertThat(decoder.getDecoderState()).isNull(); assertThat(buffer.refCnt()).isEqualTo(1); assertThat(nextBuffer.refCnt()).isEqualTo(1); buffer.release(); nextBuffer.release(); } @Test void shouldDecodeChunkedPackets() { StreamDecoder decoder = new StreamDecoder(); Header firstHeader = Header.create(HeaderOptions.create(Type.TABULAR_RESULT, Status.empty()), Header.LENGTH + 3, PacketIdProvider.just(1)); Header lastHeader = Header.create(HeaderOptions.create(Type.TABULAR_RESULT, Status.of(Status.StatusBit.EOM)), Header.LENGTH + 10, PacketIdProvider.just(2)); DoneToken token = DoneToken.create(2); ByteBuf firstChunk = TestByteBufAllocator.TEST.buffer(); ByteBuf lastChunk = TestByteBufAllocator.TEST.buffer(); ByteBuf fullData = TestByteBufAllocator.TEST.buffer(); token.encode(fullData); firstHeader.encode(firstChunk); firstChunk.writeBytes(new byte[]{fullData.readByte(), fullData.readByte(), fullData.readByte()}); lastHeader.encode(lastChunk); lastChunk.writeBytes(fullData); List firstAttempt = decoder.decode(firstChunk, ConnectionState.POST_LOGIN.decoder(CLIENT)); assertThat(firstAttempt).isEmpty(); StreamDecoder.DecoderState state = decoder.getDecoderState(); assertThat(state).isNotNull(); assertThat(state.header).isNull(); // header completed assertThat(state.remainder.readableBytes()).isEqualTo(0); assertThat(state.aggregatedBody.readableBytes()).isEqualTo(3); assertThat(firstChunk.refCnt()).isEqualTo(1); List nextAttempt = decoder.decode(lastChunk, ConnectionState.POST_LOGIN.decoder(CLIENT)); assertThat(nextAttempt).containsOnly(token); assertThat(decoder.getDecoderState()).isNull(); assertThat(firstChunk.refCnt()).isEqualTo(1); assertThat(lastChunk.refCnt()).isEqualTo(1); firstChunk.release(); lastChunk.release(); } @Test void shouldDecodeManyChunks() { StreamDecoder decoder = new StreamDecoder(); MessageDecoder messageDecoder = ConnectionState.POST_LOGIN.decoder(CLIENT); initializeColumMetadata(decoder, messageDecoder); List chunks = createChunks(); // expect incomplete chunks to be empty for (int i = 0; i < chunks.size() - 1; i++) { assertThat(decoder.decode(chunks.get(i), messageDecoder)).isEmpty(); } // Last chunk emits the data assertThat(decoder.decode(chunks.get(chunks.size() - 1), messageDecoder)).hasSize(1); StreamDecoder.DecoderState state = decoder.getDecoderState(); assertThat(state).isNull(); assertThat(decoder.getDecoderState()).isNull(); } private List createChunks() { ByteBuf row = HexUtils.decodeToByteBuf("D1010C00700061006C007500630068" + "0004006D61726B080000000020A10700" + "10F17B0DC7C7E5C54098C7A12F7E6867" + "2408FED478E94628C6400437423146"); List chunks = new ArrayList<>(); while (row.isReadable()) { int bytesToRead = row.readableBytes(); Status status = Status.empty(); if (bytesToRead > 10) { bytesToRead = 10; } else { status = Status.of(Status.StatusBit.EOM); } Header header = Header.create(HeaderOptions.create(Type.TABULAR_RESULT, status), Header.LENGTH + bytesToRead, PacketIdProvider.just(1)); ByteBuf chunk = TestByteBufAllocator.TEST.buffer(); header.encode(chunk); chunk.writeBytes(row, bytesToRead); chunks.add(chunk); } return chunks; } private static void initializeColumMetadata(StreamDecoder decoder, MessageDecoder messageDecoder) { // Required initialization. ColMetadata does not support yet chunking. ByteBuf colmetadata = HexUtils.decodeToByteBuf("8107000000000000" + "000800300B65006D0070006C006F0079" + "00650065005F00690064000000000008" + "00E764000904D00034096C0061007300" + "74005F006E0061006D00650000000000" + "0900A732000904D000340A6600690072" + "00730074005F006E0061006D00650000" + "00000009006E0806730061006C006100" + "7200790000000000090024100366006F" + "006F000000000009006D080366006C00" + "74000000000009006D04036200610072" + "00"); Header colHeader = Header.create(HeaderOptions.create(Type.TABULAR_RESULT, Status.of(Status.StatusBit.EOM)), Header.LENGTH + colmetadata.readableBytes(), PacketIdProvider.just(1)); ByteBuf initialize = TestByteBufAllocator.TEST.heapBuffer(); colHeader.encode(initialize); initialize.writeBytes(colmetadata); assertThat(decoder.decode(initialize, messageDecoder).get(0)).isInstanceOf(ColumnMetadataToken.class); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/client/TdsEncoderUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.embedded.EmbeddedChannel; import io.r2dbc.mssql.message.header.Header; import io.r2dbc.mssql.message.header.HeaderOptions; import io.r2dbc.mssql.message.header.PacketIdProvider; import io.r2dbc.mssql.message.header.Type; import io.r2dbc.mssql.message.tds.ContextualTdsFragment; import io.r2dbc.mssql.message.tds.TdsFragment; import io.r2dbc.mssql.message.tds.TdsPacket; import io.r2dbc.mssql.message.tds.TdsPackets; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import static io.r2dbc.mssql.message.header.Status.StatusBit; import static io.r2dbc.mssql.message.header.Status.empty; import static io.r2dbc.mssql.message.header.Status.of; import static io.r2dbc.mssql.util.EmbeddedChannelAssert.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Unit tests for {@link TdsEncoder}. * * @author Mark Paluch */ class TdsEncoderUnitTests { @Test void shouldPassThruByteBuffers() { EmbeddedChannel channel = new EmbeddedChannel(); channel.pipeline().addFirst(new TdsEncoder(PacketIdProvider.just(42))); channel.writeOutbound(Unpooled.wrappedBuffer("foobar".getBytes())); assertThat(channel).outbound().hasByteBufMessage().contains("foobar"); } @Test void shouldPrependByteBuffersWithHeader() { EmbeddedChannel channel = new EmbeddedChannel(); channel.pipeline().addFirst(new TdsEncoder(PacketIdProvider.just(42))); channel.writeOutbound(HeaderOptions.create(Type.PRE_LOGIN, empty())); channel.writeOutbound(Unpooled.wrappedBuffer("foobar".getBytes())); assertThat(channel).outbound().hasByteBufMessage().isEmpty(); assertThat(channel).outbound().hasByteBufMessage().isEncodedAs(buffer -> { encodeExpectation(buffer, StatusBit.EOM, 0x0e, "foobar"); }); } @Test void shouldResetHeaderStatus() { EmbeddedChannel channel = new EmbeddedChannel(); channel.pipeline().addFirst(new TdsEncoder(PacketIdProvider.just(42))); channel.writeOutbound(HeaderOptions.create(Type.PRE_LOGIN, empty())); channel.writeOutbound(TdsEncoder.ResetHeader.INSTANCE); channel.writeOutbound(Unpooled.wrappedBuffer("foobar".getBytes())); assertThat(channel).outbound().hasByteBufMessage().isEmpty(); assertThat(channel).outbound().hasByteBufMessage().isEmpty(); assertThat(channel).outbound().hasByteBufMessage().isEncodedAs(buffer -> buffer.writeBytes("foobar".getBytes())); } @Test void shouldEncodeTdsPacket() { EmbeddedChannel channel = new EmbeddedChannel(); channel.pipeline().addFirst(new TdsEncoder(PacketIdProvider.just(42))); TdsPacket packet = TdsPackets.create(new Header(Type.PRE_LOGIN, of(StatusBit.EOM), 10, 0, 0, 0), Unpooled.wrappedBuffer("ab".getBytes())); channel.writeOutbound(packet); assertThat(channel).outbound().hasByteBufMessage().isEncodedAs(buffer -> { encodeExpectation(buffer, StatusBit.EOM, 0x0a, "ab"); }); } @Test void shouldEncodeContextualTdsFragment() { EmbeddedChannel channel = new EmbeddedChannel(); channel.pipeline().addFirst(new TdsEncoder(PacketIdProvider.just(42))); ContextualTdsFragment fragment = TdsPackets.create(HeaderOptions.create(Type.PRE_LOGIN, empty()), Unpooled.wrappedBuffer("ab".getBytes())); channel.writeOutbound(fragment); assertThat(channel).outbound().hasByteBufMessage().isEncodedAs(buffer -> { encodeExpectation(buffer, StatusBit.EOM, 0x0a, "ab"); }); } @Test void shouldEncodeAndSplitContextualTdsFragment() { EmbeddedChannel channel = new EmbeddedChannel(); channel.pipeline().addFirst(new TdsEncoder(PacketIdProvider.just(42), 12)); ContextualTdsFragment fragment = TdsPackets.create(HeaderOptions.create(Type.PRE_LOGIN, empty()), Unpooled.wrappedBuffer("foobar".getBytes())); channel.writeOutbound(fragment); // Chunk 1 assertThat(channel).outbound().hasByteBufMessage().isEncodedAs(buffer -> { encodeExpectation(buffer, StatusBit.NORMAL, 0x0c, "foob"); }); // Chunk 2 assertThat(channel).outbound().hasByteBufMessage().isEncodedAs(buffer -> { encodeExpectation(buffer, StatusBit.EOM, 0x0a, "ar"); }); } @Test void shouldEncodeTdsFragment() { EmbeddedChannel channel = new EmbeddedChannel(); TdsEncoder tdsEncoder = new TdsEncoder(PacketIdProvider.just(42)); tdsEncoder.setPacketSize(10); channel.pipeline().addFirst(tdsEncoder); TdsFragment fragment = TdsPackets.create(Unpooled.wrappedBuffer("ab".getBytes())); channel.writeOutbound(HeaderOptions.create(Type.PRE_LOGIN, empty())); channel.writeOutbound(fragment); assertThat(channel).outbound().hasByteBufMessage().isEmpty(); assertThat(channel).outbound().hasByteBufMessage().isEncodedAs(buffer -> { encodeExpectation(buffer, StatusBit.NORMAL, 0x0a, "ab"); }); } @Test void failsEncodingTdsFragmentWithoutHeader() { EmbeddedChannel channel = new EmbeddedChannel(); channel.pipeline().addFirst(new TdsEncoder(PacketIdProvider.just(42))); TdsFragment fragment = TdsPackets.create(Unpooled.wrappedBuffer("ab".getBytes())); assertThatThrownBy(() -> channel.writeOutbound(fragment)).isInstanceOf(IllegalStateException.class) .hasMessage("HeaderOptions must not be null!"); } @Test void shouldEncodeMessageSequence() { EmbeddedChannel channel = new EmbeddedChannel(); TdsEncoder encoder = new TdsEncoder(PacketIdProvider.just(42)); encoder.setPacketSize(10); channel.pipeline().addFirst(encoder); TdsFragment first = TdsPackets.first(HeaderOptions.create(Type.PRE_LOGIN, empty()), Unpooled.wrappedBuffer("ab".getBytes())); TdsFragment last = TdsPackets.last(Unpooled.wrappedBuffer("ab".getBytes())); channel.writeOutbound(first, Unpooled.wrappedBuffer("fo".getBytes()), last, Unpooled.wrappedBuffer("fo".getBytes())); assertThat(channel).outbound().hasByteBufMessage().isEncodedAs(buffer -> { encodeExpectation(buffer, StatusBit.NORMAL, 0x0a, "ab"); }); assertThat(channel).outbound().hasByteBufMessage().isEncodedAs(buffer -> { encodeExpectation(buffer, StatusBit.EOM, 0x0a, "fo"); }); assertThat(channel).outbound().hasByteBufMessage().isEncodedAs(buffer -> { encodeExpectation(buffer, StatusBit.EOM, 0x0a, "ab"); }); assertThat(channel).outbound().hasByteBufMessage().isEncodedAs(buffer -> { buffer.writeBytes("fo".getBytes()); }); } @Test void shouldChunkMessagesLargeSmall() { EmbeddedChannel channel = new EmbeddedChannel(); channel.pipeline().addFirst(new TdsEncoder(PacketIdProvider.just(42), 12)); TdsFragment first = TdsPackets.first(HeaderOptions.create(Type.PRE_LOGIN, empty()), Unpooled.wrappedBuffer("abcde".getBytes())); TdsFragment last = TdsPackets.last(Unpooled.wrappedBuffer("f".getBytes())); channel.writeOutbound(first, last); // Chunk 1 assertThat(channel).outbound().hasByteBufMessage().isEncodedAs(buffer -> { encodeExpectation(buffer, StatusBit.NORMAL, 0x0c, "abcd"); }); // Chunk 2 assertThat(channel).outbound().hasByteBufMessage().isEncodedAs(buffer -> { encodeExpectation(buffer, StatusBit.EOM, 0x0a, "ef"); }); } @Test void shouldChunkMessagesLargeLargeSmall() { EmbeddedChannel channel = new EmbeddedChannel(); channel.pipeline().addFirst(new TdsEncoder(PacketIdProvider.just(42), 12)); TdsFragment first = TdsPackets.first(HeaderOptions.create(Type.PRE_LOGIN, empty()), Unpooled.wrappedBuffer("abcde".getBytes())); TdsFragment intermediate = TdsPackets.create(Unpooled.wrappedBuffer("fghijk".getBytes())); TdsFragment last = TdsPackets.last(Unpooled.wrappedBuffer("l".getBytes())); channel.writeOutbound(first, intermediate, last); // Chunk 1 assertThat(channel).outbound().hasByteBufMessage().isEncodedAs(buffer -> { encodeExpectation(buffer, StatusBit.NORMAL, 0x0c, "abcd"); }); // Chunk 2 assertThat(channel).outbound().hasByteBufMessage().isEncodedAs(buffer -> { encodeExpectation(buffer, StatusBit.NORMAL, 0x0c, "efgh"); }); // Chunk 3 assertThat(channel).outbound().hasByteBufMessage().isEncodedAs(buffer -> { encodeExpectation(buffer, StatusBit.EOM, 0x0c, "ijkl"); }); } @Test void shouldChunkMessagesLargeLargeLarge() { EmbeddedChannel channel = new EmbeddedChannel(); channel.pipeline().addFirst(new TdsEncoder(PacketIdProvider.just(42), 12)); TdsFragment first = TdsPackets.first(HeaderOptions.create(Type.PRE_LOGIN, empty()), Unpooled.wrappedBuffer("abcde".getBytes())); TdsFragment intermediate = TdsPackets.create(Unpooled.wrappedBuffer("fghijk".getBytes())); TdsFragment last = TdsPackets.last(Unpooled.wrappedBuffer("lmnop".getBytes())); channel.writeOutbound(first, intermediate, last); // Chunk 1 assertThat(channel).outbound().hasByteBufMessage().isEncodedAs(buffer -> { encodeExpectation(buffer, StatusBit.NORMAL, 0x0c, "abcd"); }); // Chunk 2 assertThat(channel).outbound().hasByteBufMessage().isEncodedAs(buffer -> { encodeExpectation(buffer, StatusBit.NORMAL, 0x0c, "efgh"); }); // Chunk 3 assertThat(channel).outbound().hasByteBufMessage().isEncodedAs(buffer -> { encodeExpectation(buffer, StatusBit.NORMAL, 0x0c, "ijkl"); }); // Chunk 4 assertThat(channel).outbound().hasByteBufMessage().isEncodedAs(buffer -> { encodeExpectation(buffer, StatusBit.EOM, 0x0c, "mnop"); }); } @Test void shouldEstimateTdsPacketSize() { TdsEncoder encoder = new TdsEncoder(PacketIdProvider.just(42), 12); Assertions.assertThat(encoder.estimateChunkSize(1)).isEqualTo(9); Assertions.assertThat(encoder.estimateChunkSize(4)).isEqualTo(12); Assertions.assertThat(encoder.estimateChunkSize(5)).isEqualTo(12); } private static void encodeExpectation(ByteBuf buffer, StatusBit bit, int length, String content) { buffer.writeByte(18); // Type buffer.writeByte(bit.getBits()); // Status buffer.writeShort(length); // Length buffer.writeShort(0); // SPID buffer.writeByte(42); // PacketID buffer.writeByte(0); // Window buffer.writeBytes(content.getBytes()); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/client/TestClient.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client; import io.netty.buffer.ByteBufAllocator; import io.r2dbc.mssql.message.ClientMessage; import io.r2dbc.mssql.message.Message; import io.r2dbc.mssql.message.TransactionDescriptor; import io.r2dbc.mssql.message.tds.Redirect; import io.r2dbc.mssql.message.type.Collation; import io.r2dbc.mssql.util.Assert; import io.r2dbc.mssql.util.TestByteBufAllocator; import org.assertj.core.api.Assertions; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; /** * Test {@link Client} implementation. */ @SuppressWarnings({"unchecked", "rawtypes"}) public final class TestClient implements Client { public static final TestClient NO_OP = new TestClient(false, true, Flux.empty(), Optional.empty(), TransactionStatus.AUTO_COMMIT); private final boolean expectClose; private final boolean connected; private boolean closed; private final Sinks.Many requestProcessor = Sinks.many().multicast().onBackpressureBuffer(); private final Sinks.Many> responseProcessor = Sinks.many().multicast().onBackpressureBuffer(256, false); private final TransactionStatus transactionStatus; private final Optional redirect; private TestClient(boolean expectClose, boolean connected, Flux windows, Optional redirect, TransactionStatus transactionStatus) { this.expectClose = expectClose; this.connected = connected; this.redirect = redirect; this.transactionStatus = transactionStatus; Assert.requireNonNull(windows, "Windows must not be null") .map(window -> window.exchanges) .map(exchanges -> exchanges .concatMap(exchange -> this.requestProcessor.asFlux().zipWith(exchange.requests) .handle((tuple, sink) -> { Message actual = tuple.getT1(); Consumer expected = (Consumer) tuple.getT2(); try { expected.accept(actual); } catch (Throwable t) { sink.error(t); } }) .thenMany(exchange.responses))) .subscribe(this.responseProcessor::tryEmitNext, this.responseProcessor::tryEmitError, this.responseProcessor::tryEmitComplete); } public static Builder builder() { return new Builder(); } @Override public Mono attention() { return Mono.empty(); } @Override public Mono close() { return this.expectClose ? Mono.fromRunnable(() -> { this.closed = true; }) : Mono.error(new AssertionError("close called unexpectedly")); } public boolean isClosed() { return this.closed; } public Flux exchange(Publisher requests, Predicate takeUntil) { Assert.requireNonNull(requests, "requests must not be null"); return this.responseProcessor.asFlux() .doOnSubscribe(s -> Flux.from(requests) .subscribe(this.requestProcessor::tryEmitNext, this.requestProcessor::tryEmitError)) .next() .flatMapMany(Function.identity()); } @Override public ByteBufAllocator getByteBufAllocator() { return TestByteBufAllocator.TEST; } @Override public ConnectionContext getContext() { return new ConnectionContext(); } @Override public Optional getDatabaseCollation() { // windows-1252 return Optional.of(Collation.from(13632521, 52)); } @Override public Optional getDatabaseVersion() { return Optional.of("1.2.3"); } @Override public Optional getRedirect() { return this.redirect; } @Override public TransactionDescriptor getTransactionDescriptor() { return TransactionDescriptor.empty(); } @Override public TransactionStatus getTransactionStatus() { return this.transactionStatus; } @Override public boolean isColumnEncryptionSupported() { return true; } @Override public boolean isConnected() { return this.connected; } public static final class Builder { private final List> windows = new ArrayList<>(); private boolean expectClose = false; private boolean connected = true; private Optional redirect = Optional.empty(); private TransactionStatus transactionStatus = TransactionStatus.AUTO_COMMIT; private Builder() { } public TestClient build() { return new TestClient(this.expectClose, this.connected, Flux.fromIterable(this.windows).map(Window.Builder::build), this.redirect, this.transactionStatus); } public Builder expectClose() { this.expectClose = true; return this; } public Builder withConnected(boolean connected) { this.connected = connected; return this; } public Builder withRedirect(Redirect redirect) { this.redirect = Optional.of(redirect); return this; } public Builder withTransactionStatus(TransactionStatus transactionStatus) { this.transactionStatus = Assert.requireNonNull(transactionStatus, "TransactionStatus must not be nuln"); return this; } public Exchange.Builder expectRequest(ClientMessage... requests) { Assert.requireNonNull(requests, "ClientMessage requests must not be null"); Consumer[] consumers = Arrays.stream(requests).map(request -> { Consumer messageConsumer = actual -> Assertions.assertThat(actual).isEqualTo(request); return messageConsumer; }).toArray(i -> new Consumer[i]); return assertNextRequestWith(consumers); } public Exchange.Builder assertNextRequestWith(Consumer request) { Assert.requireNonNull(request, "Client Consumer must not be null"); return assertNextRequestWith(new Consumer[]{request}); } public Exchange.Builder assertNextRequestWith(Consumer... requests) { Assert.requireNonNull(requests, "Client Consumer must not be null"); Window.Builder window = new Window.Builder<>(this); this.windows.add(window); Exchange.Builder exchange = new Exchange.Builder<>(this, requests); window.exchanges.add(exchange); return exchange; } public Window.Builder window() { Window.Builder window = new Window.Builder<>(this); this.windows.add(window); return window; } } private static final class Exchange { private final Flux> requests; private final Publisher responses; private Exchange(Flux> requests, Publisher responses) { this.requests = Assert.requireNonNull(requests, "Requests must not be null"); this.responses = Assert.requireNonNull(responses, "Responses must not be null"); } public static final class Builder { private final T chain; private final Flux> requests; private Publisher responses; private Builder(T chain, Consumer... requests) { this.chain = Assert.requireNonNull(chain, "Request chain must not be null"); this.requests = Flux.just(Assert.requireNonNull(requests, "Requests must not be null")); } public T thenRespond(Message... responses) { Assert.requireNonNull(responses, "Responses must not be null"); return thenRespond(Flux.just(responses)); } T thenRespond(Publisher responses) { Assert.requireNonNull(responses, "Responses must not be null"); this.responses = responses; return this.chain; } private Exchange build() { return new Exchange(this.requests, this.responses); } } } private static final class Window { private final Flux exchanges; private Window(Flux exchanges) { this.exchanges = Assert.requireNonNull(exchanges, "Exchanges must not be null"); } public static final class Builder { private final T chain; private final List> exchanges = new ArrayList<>(); private Builder(T chain) { this.chain = Assert.requireNonNull(chain, "Chain must not be null"); } public T done() { return this.chain; } public Exchange.Builder> expectRequest(ClientMessage request) { return assertNextRequestWith(actual -> Assertions.assertThat(actual).isEqualTo(request)); } public Exchange.Builder> assertNextRequestWith(Consumer request) { Assert.requireNonNull(request, "Request must not be null"); Exchange.Builder> exchange = new Exchange.Builder<>(this, request); this.exchanges.add(exchange); return exchange; } private Window build() { return new Window(Flux.fromIterable(this.exchanges).map(Exchange.Builder::build)); } } } } ================================================ FILE: src/test/java/io/r2dbc/mssql/client/ssl/HostNamePredicateUnitTests.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client.ssl; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link HostNamePredicate}. * * @author Mark Paluch */ class HostNamePredicateUnitTests { @Test void shouldRejectAll() { assertThat(HostNamePredicate.of("")).rejects("bar"); } @Test void shouldMatchSimpleName() { assertThat(HostNamePredicate.of("foo")).accepts("foo").rejects("bar"); } @Test void shouldMatchNameWithDots() { assertThat(HostNamePredicate.of("foo.bar.baz")).accepts("foo.bar.baz").rejects("bar").rejects("baz.bar.foo"); } @Test void shouldMatchWildcard() { assertThat(HostNamePredicate.of("foo.*.baz")).accepts("foo.bar.baz").accepts("foo..baz").accepts("foo.bar.baz").rejects("foo.baz").rejects("baz.bar.foo"); } @Test void shouldMatchWildcardInWord() { assertThat(HostNamePredicate.of("foo.a*z.baz")).accepts("foo.agz.baz").accepts("foo.az.baz").rejects("foo.bar.baz"); } @Test void shouldMatchWildcardInWildcardCertificate() { assertThat(HostNamePredicate.of("*.foo.bar.net")).accepts("*.foo.bar.net").rejects("*.foo.bar.baz").rejects("*.subdomain.foo.bar.net"); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/client/ssl/TdsSslHandlerUnitTests.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client.ssl; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslHandler; import io.r2dbc.mssql.client.ConnectionContext; import io.r2dbc.mssql.message.header.Header; import io.r2dbc.mssql.message.header.PacketIdProvider; import io.r2dbc.mssql.message.header.Status; import io.r2dbc.mssql.message.header.Type; import io.r2dbc.mssql.util.TestByteBufAllocator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import java.util.stream.IntStream; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; /** * Unit tests for {@link TdsSslHandler}. * * @author Mark Paluch */ class TdsSslHandlerUnitTests { TdsSslHandler handler = new TdsSslHandler(PacketIdProvider.just(0), new SslConfiguration() { @Override public boolean isSslEnabled() { return false; } @Override public SslContext getSslContext() { return null; } }, new ConnectionContext()); SslHandler sslHandler = mock(SslHandler.class); ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); ArgumentCaptor captor = ArgumentCaptor.forClass(ByteBuf.class); @BeforeEach void setUp() { this.handler.setSslHandler(this.sslHandler); this.handler.setState(SslState.CONNECTION); } @Test void entireMessage() throws Exception { ByteBuf buffer = TestByteBufAllocator.TEST.buffer(); Header header = new Header(Type.PRE_LOGIN, Status.of(Status.StatusBit.EOM), 100, 1); header.encode(buffer); IntStream.range(0, 92).forEach(buffer::writeByte); this.handler.channelRead(this.ctx, buffer); verify(this.sslHandler).channelRead(any(), this.captor.capture()); ByteBuf value = this.captor.getValue(); assertThat(value.readableBytes()).isEqualTo(92); } @Test void singleChunkedMessage() throws Exception { ByteBuf buffer = TestByteBufAllocator.TEST.buffer(); ByteBuf expected = TestByteBufAllocator.TEST.buffer(); Header header = new Header(Type.PRE_LOGIN, Status.of(Status.StatusBit.EOM), 100, 1); header.encode(buffer); IntStream.range(0, 92).forEach(buffer::writeByte); IntStream.range(0, 92).forEach(expected::writeByte); while (buffer.isReadable()) { this.handler.channelRead(this.ctx, buffer.readRetainedSlice(Math.min(10, buffer.readableBytes()))); } buffer.release(); verify(this.sslHandler).channelRead(any(), this.captor.capture()); ByteBuf value = this.captor.getValue(); assertThat(value.readableBytes()).isEqualTo(92); assertThat(value).isEqualTo(expected); } @Test void multipleChunkedMessages() throws Exception { ByteBuf buffer = TestByteBufAllocator.TEST.buffer(); ByteBuf expected = TestByteBufAllocator.TEST.buffer(); Header chunk1 = new Header(Type.PRE_LOGIN, Status.empty(), 100, 1); chunk1.encode(buffer); IntStream.range(0, 92).forEach(buffer::writeByte); Header chunk2 = new Header(Type.PRE_LOGIN, Status.of(Status.StatusBit.EOM), 50, 1); chunk2.encode(buffer); IntStream.range(0, 42).forEach(buffer::writeByte); IntStream.range(0, 92).forEach(expected::writeByte); IntStream.range(0, 42).forEach(expected::writeByte); this.handler.channelRead(this.ctx, buffer.readRetainedSlice(Math.min(80, buffer.readableBytes()))); this.handler.channelRead(this.ctx, buffer.readRetainedSlice(Math.min(50, buffer.readableBytes()))); this.handler.channelRead(this.ctx, buffer.readRetainedSlice(Math.min(20, buffer.readableBytes()))); buffer.release(); verify(this.sslHandler).channelRead(any(), this.captor.capture()); ByteBuf value = this.captor.getValue(); assertThat(value.readableBytes()).isEqualTo(92 + 42); } @Test void channelInactiveReleasesChunk() throws Exception { ByteBuf buffer = TestByteBufAllocator.TEST.buffer(); Header header = new Header(Type.PRE_LOGIN, Status.of(Status.StatusBit.EOM), 100, 1); header.encode(buffer); IntStream.range(0, 20).forEach(buffer::writeByte); this.handler.channelRead(this.ctx, buffer.readRetainedSlice(Math.min(10, buffer.readableBytes()))); this.handler.channelRead(this.ctx, buffer.readRetainedSlice(Math.min(10, buffer.readableBytes()))); buffer.release(); assertThat(buffer.refCnt()).isNotZero(); this.handler.channelInactive(this.ctx); assertThat(buffer.refCnt()).isZero(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/client/ssl/X509CertificateUtilUnitTests.java ================================================ /* * Copyright 2020-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.client.ssl; import org.junit.jupiter.api.Test; import java.security.GeneralSecurityException; import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.Collections; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** * Unit tests for {@link X509CertificateUtil}. * * @author Mark Paluch */ class X509CertificateUtilUnitTests { @Test void shouldCorrectlyExtractSan() throws GeneralSecurityException { X509Certificate mockCert = mock(X509Certificate.class); when(mockCert.getSubjectAlternativeNames()).thenReturn(Arrays.asList(Arrays.asList(1, "invalid"), Arrays.asList(2, "valid"), Collections.singletonList(3))); List names = X509CertificateUtil.getSubjectAlternativeNames(mockCert); assertThat(names).containsOnly("valid"); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/codec/BigIntegerCodecUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.EncodedAssert; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.TestByteBufAllocator; import org.junit.jupiter.api.Test; import java.math.BigInteger; import static io.r2dbc.mssql.message.type.TypeInformation.builder; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link BigIntegerCodec}. * * @author Mark Paluch */ class BigIntegerCodecUnitTests { @Test void shouldEncodeInteger() { Encoded encoded = BigIntegerCodec.INSTANCE.encode(TestByteBufAllocator.TEST, RpcParameterContext.in(), new BigInteger("12345")); EncodedAssert.assertThat(encoded).isEqualToHex("11 26 00 03 01 39 30"); assertThat(encoded.getFormalType()).isEqualTo("decimal(38,0)"); } @Test void shouldEncodeNull() { Encoded encoded = BigIntegerCodec.INSTANCE.encodeNull(TestByteBufAllocator.TEST); EncodedAssert.assertThat(encoded).isEqualToHex("08 00"); assertThat(encoded.getFormalType()).isEqualTo("bigint"); } @Test void shouldBeAbleToDecode() { TypeInformation tinyint = builder().withServerType(SqlServerType.TINYINT).build(); TypeInformation varchar = builder().withServerType(SqlServerType.VARCHAR).build(); TypeInformation numeric = builder().withServerType(SqlServerType.NUMERIC).build(); assertThat(BigIntegerCodec.INSTANCE.canDecode(ColumnUtil.createColumn(tinyint), BigInteger.class)).isTrue(); assertThat(BigIntegerCodec.INSTANCE.canDecode(ColumnUtil.createColumn(varchar), BigInteger.class)).isFalse(); assertThat(BigIntegerCodec.INSTANCE.canDecode(ColumnUtil.createColumn(tinyint), BigInteger.class)).isTrue(); assertThat(BigIntegerCodec.INSTANCE.canDecode(ColumnUtil.createColumn(numeric), BigInteger.class)).isTrue(); } @Test void shouldDecodeFromLong() { TypeInformation type = createType(8, SqlServerType.BIGINT); ByteBuf buffer = HexUtils.decodeToByteBuf("0100000100000000"); assertThat(BigIntegerCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), BigInteger.class)).isEqualTo(BigInteger.valueOf(16777217)); } @Test void shouldDecodeFromNumeric() { TypeInformation type = TypeInformation.builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withServerType(SqlServerType.NUMERIC).withScale(0).withPrecision(5).build(); ByteBuf buffer = HexUtils.decodeToByteBuf("05 01 39 30 00 00"); assertThat(BigIntegerCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), BigInteger.class)).isEqualTo(BigInteger.valueOf(12345)); } private TypeInformation createType(int length, SqlServerType serverType) { return builder().withMaxLength(length).withLengthStrategy(LengthStrategy.FIXEDLENTYPE).withPrecision(length).withServerType(serverType).build(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/codec/BinaryCodecUnitTests.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.EncodedAssert; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.TestByteBufAllocator; import org.junit.jupiter.api.Test; import java.nio.ByteBuffer; import static io.r2dbc.mssql.message.type.TypeInformation.builder; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link BinaryCodec}. * * @author Mark Paluch */ class BinaryCodecUnitTests { @Test void shouldEncodeBinaryArray() { Encoded encoded = BinaryCodec.INSTANCE.encode(TestByteBufAllocator.TEST, RpcParameterContext.out(), "bar".getBytes()); EncodedAssert.assertThat(encoded).isEqualToHex("40 1F 03 00 62 61 72"); assertThat(encoded.getFormalType()).isEqualTo("varbinary(8000)"); } @Test void shouldEncodeBinaryByteBuffer() { Encoded encoded = BinaryCodec.INSTANCE.encode(TestByteBufAllocator.TEST, RpcParameterContext.out(), ByteBuffer.wrap("bar".getBytes())); EncodedAssert.assertThat(encoded).isEqualToHex("40 1F 03 00 62 61 72"); assertThat(encoded.getFormalType()).isEqualTo("varbinary(8000)"); } @Test void shouldEncodeNull() { Encoded encoded = BinaryCodec.INSTANCE.encodeNull(TestByteBufAllocator.TEST); EncodedAssert.assertThat(encoded).isEqualToHex("40 1F FF FF"); assertThat(encoded.getFormalType()).isEqualTo("varbinary(8000)"); } @Test void shouldBeAbleToDecodeByteArray() { TypeInformation binary = builder().withServerType(SqlServerType.BINARY).withLengthStrategy(LengthStrategy.USHORTLENTYPE).build(); assertThat(BinaryCodec.INSTANCE.canDecode(ColumnUtil.createColumn(binary), byte[].class)).isTrue(); } @Test void shouldBeAbleToDecodeByteBuffer() { TypeInformation binary = builder().withServerType(SqlServerType.BINARY).withLengthStrategy(LengthStrategy.USHORTLENTYPE).build(); assertThat(BinaryCodec.INSTANCE.canDecode(ColumnUtil.createColumn(binary), ByteBuffer.class)).isTrue(); } @Test void shouldBeAbleToDecodeVarbinaryToByteArray() { TypeInformation varbinary = builder().withServerType(SqlServerType.VARBINARY).withLengthStrategy(LengthStrategy.USHORTLENTYPE).build(); assertThat(BinaryCodec.INSTANCE.canDecode(ColumnUtil.createColumn(varbinary), byte[].class)).isTrue(); } @Test void shouldBeAbleToDecodeVarbinaryToByteBuffer() { TypeInformation varbinary = builder().withServerType(SqlServerType.VARBINARY).withLengthStrategy(LengthStrategy.USHORTLENTYPE).build(); assertThat(BinaryCodec.INSTANCE.canDecode(ColumnUtil.createColumn(varbinary), ByteBuffer.class)).isTrue(); } @Test void shouldDecodeBinaryToByteArray() { TypeInformation binary = builder().withServerType(SqlServerType.BINARY).withLengthStrategy(LengthStrategy.USHORTLENTYPE).build(); ByteBuf data = HexUtils.decodeToByteBuf("0A 00 66 6F 6F 00 00 00 00 00 00 00"); byte[] expected = new byte[10]; expected[0] = 'f'; expected[1] = 'o'; expected[2] = 'o'; assertThat(BinaryCodec.INSTANCE.decode(data, ColumnUtil.createColumn(binary), byte[].class)).isEqualTo(expected); } @Test void shouldDecodeBinaryToByteBuffer() { TypeInformation binary = builder().withServerType(SqlServerType.BINARY).withLengthStrategy(LengthStrategy.USHORTLENTYPE).build(); ByteBuf data = HexUtils.decodeToByteBuf("0A 00 66 6F 6F 00 00 00 00 00 00 00"); ByteBuffer expected = ByteBuffer.allocate(10); expected.put("foo".getBytes()).put(new byte[7]).flip(); assertThat(BinaryCodec.INSTANCE.decode(data, ColumnUtil.createColumn(binary), ByteBuffer.class)).isEqualTo(expected); } @Test void shouldDecodeBinaryNullToByteArray() { TypeInformation binary = builder().withServerType(SqlServerType.BINARY).withLengthStrategy(LengthStrategy.USHORTLENTYPE).build(); ByteBuf data = HexUtils.decodeToByteBuf("FF FF"); assertThat(BinaryCodec.INSTANCE.decode(data, ColumnUtil.createColumn(binary), byte[].class)).isNull(); } @Test void shouldDecodeBinaryNullToByteBuffer() { TypeInformation binary = builder().withServerType(SqlServerType.BINARY).withLengthStrategy(LengthStrategy.USHORTLENTYPE).build(); ByteBuf data = HexUtils.decodeToByteBuf("FF FF"); assertThat(BinaryCodec.INSTANCE.decode(data, ColumnUtil.createColumn(binary), ByteBuffer.class)).isNull(); } @Test void shouldDecodeVarBinaryToByteArray() { TypeInformation binary = builder().withServerType(SqlServerType.VARBINARY).withLengthStrategy(LengthStrategy.USHORTLENTYPE).build(); ByteBuf data = HexUtils.decodeToByteBuf("03 00 62 61 72"); byte[] expected = "bar".getBytes(); assertThat(BinaryCodec.INSTANCE.decode(data, ColumnUtil.createColumn(binary), byte[].class)).isEqualTo(expected); } @Test void shouldDecodeVarBinaryToByteBuffer() { TypeInformation binary = builder().withServerType(SqlServerType.VARBINARY).withLengthStrategy(LengthStrategy.USHORTLENTYPE).build(); ByteBuf data = HexUtils.decodeToByteBuf("03 00 62 61 72"); ByteBuffer expected = ByteBuffer.allocate(3); expected.put("bar".getBytes()).flip(); assertThat(BinaryCodec.INSTANCE.decode(data, ColumnUtil.createColumn(binary), ByteBuffer.class)).isEqualTo(expected); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/codec/BlobCodecUnitTests.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.PooledByteBufAllocator; import io.r2dbc.mssql.message.type.Length; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.PlpLength; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.EncodedAssert; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.TestByteBufAllocator; import io.r2dbc.spi.Blob; import org.junit.jupiter.api.Test; import reactor.test.StepVerifier; import java.nio.ByteBuffer; import static io.r2dbc.mssql.message.type.TypeInformation.builder; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link BlobCodec}. * * @author Mark Paluch */ class BlobCodecUnitTests { @Test void shouldEncodeNull() { Encoded encoded = BlobCodec.INSTANCE.encodeNull(TestByteBufAllocator.TEST); EncodedAssert.assertThat(encoded).isEqualToHex("40 1F FF FF"); assertThat(encoded.getFormalType()).isEqualTo("varbinary(8000)"); } @Test void shouldBeAbleToEncodeNull() { assertThat(BlobCodec.INSTANCE.canEncodeNull(Blob.class)).isTrue(); } @Test void shouldBeAbleToDecodeBinary() { TypeInformation binary = builder().withServerType(SqlServerType.BINARY).withLengthStrategy(LengthStrategy.USHORTLENTYPE).build(); assertThat(BlobCodec.INSTANCE.canDecode(ColumnUtil.createColumn(binary), Blob.class)).isTrue(); } @Test void shouldBeAbleToDecodeVarBinary() { TypeInformation varbinary = builder().withServerType(SqlServerType.VARBINARY).withLengthStrategy(LengthStrategy.USHORTLENTYPE).build(); assertThat(BlobCodec.INSTANCE.canDecode(ColumnUtil.createColumn(varbinary), Blob.class)).isTrue(); } @Test void shouldBeAbleToDecodeVarBinaryMax() { TypeInformation binary = builder().withServerType(SqlServerType.VARBINARYMAX).withLengthStrategy(LengthStrategy.PARTLENTYPE).build(); assertThat(BlobCodec.INSTANCE.canDecode(ColumnUtil.createColumn(binary), Blob.class)).isTrue(); } @Test void shouldBeAbleToDecodeImage() { TypeInformation binary = builder().withServerType(SqlServerType.IMAGE).withLengthStrategy(LengthStrategy.PARTLENTYPE).build(); assertThat(BlobCodec.INSTANCE.canDecode(ColumnUtil.createColumn(binary), Blob.class)).isTrue(); } @Test void shouldBeAbleToDecodeVarbinary() { TypeInformation varbinary = builder().withServerType(SqlServerType.VARBINARY).withLengthStrategy(LengthStrategy.USHORTLENTYPE).build(); ByteBuf data = HexUtils.decodeToByteBuf("03 00 62 61 72"); Blob blob = BlobCodec.INSTANCE.decode(data, ColumnUtil.createColumn(varbinary), Blob.class); StepVerifier.create(blob.stream()).expectNext(ByteBuffer.wrap("bar".getBytes())).verifyComplete(); } @Test void shouldBeAbleToDecodePlpStream() { TypeInformation varbinary = builder().withServerType(SqlServerType.VARBINARY).withLengthStrategy(LengthStrategy.PARTLENTYPE).build(); ByteBuf buffer = TestByteBufAllocator.TEST.buffer(12 + 24); PlpLength.of(24).encode(buffer); Length.of(8).encode(buffer, varbinary); buffer.writeBytes("C1xxxxxx".getBytes()); Length.of(8).encode(buffer, varbinary); buffer.writeBytes("C2yyyyyy".getBytes()); Length.of(8).encode(buffer, varbinary); buffer.writeBytes("C3zzzzzz".getBytes()); Blob blob = BlobCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(varbinary), Blob.class); StepVerifier.create(blob.stream()) .expectNext(ByteBuffer.wrap("C1xxxxxx".getBytes())) .expectNext(ByteBuffer.wrap("C2yyyyyy".getBytes())) .expectNext(ByteBuffer.wrap("C3zzzzzz".getBytes())) .verifyComplete(); } @Test void shouldReleaseConsumedBuffers() { TypeInformation varbinary = builder().withServerType(SqlServerType.VARBINARY).withLengthStrategy(LengthStrategy.PARTLENTYPE).build(); PooledByteBufAllocator alloc = PooledByteBufAllocator.DEFAULT; ByteBuf buffer = alloc.buffer(); PlpLength.of(8).encode(buffer); Length.of(8).encode(buffer, varbinary); buffer.writeBytes("C1xxxxxx".getBytes()); Blob blob = BlobCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(varbinary), Blob.class); buffer.release(); StepVerifier.create(blob.stream()) .expectNextCount(1) .verifyComplete(); assertThat(buffer.refCnt()).isZero(); } @Test void shouldReleaseRemainingBuffersOnCancel() { TypeInformation varbinary = builder().withServerType(SqlServerType.VARBINARY).withLengthStrategy(LengthStrategy.PARTLENTYPE).build(); ByteBuf buffer = TestByteBufAllocator.TEST.buffer(12 + 24); PlpLength.of(24).encode(buffer); Length.of(8).encode(buffer, varbinary); buffer.writeBytes("C1xxxxxx".getBytes()); Length.of(8).encode(buffer, varbinary); buffer.writeBytes("C2yyyyyy".getBytes()); Length.of(8).encode(buffer, varbinary); buffer.writeBytes("C3zzzzzz".getBytes()); Blob blob = BlobCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(varbinary), Blob.class); buffer.release(); assertThat(buffer.refCnt()).isEqualTo(3); StepVerifier.create(blob.stream(), 0) .thenRequest(1) .expectNextCount(1) .thenCancel() .verify(); assertThat(buffer.refCnt()).isZero(); } @Test void shouldReleaseOnDiscard() { TypeInformation varbinary = builder().withServerType(SqlServerType.VARBINARY).withLengthStrategy(LengthStrategy.PARTLENTYPE).build(); PooledByteBufAllocator alloc = PooledByteBufAllocator.DEFAULT; ByteBuf buffer = alloc.buffer(); PlpLength.of(8).encode(buffer); Length.of(8).encode(buffer, varbinary); buffer.writeBytes("C1xxxxxx".getBytes()); Blob blob = BlobCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(varbinary), Blob.class); buffer.release(); StepVerifier.create(blob.discard()) .verifyComplete(); assertThat(buffer.refCnt()).isZero(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/codec/BooleanCodecUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.EncodedAssert; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.TestByteBufAllocator; import org.junit.jupiter.api.Test; import static io.r2dbc.mssql.message.type.TypeInformation.builder; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link BooleanCodec}. * * @author Mark Paluch */ class BooleanCodecUnitTests { @Test void shouldEncodeBoolean() { Encoded encoded = BooleanCodec.INSTANCE.encode(TestByteBufAllocator.TEST, RpcParameterContext.out(), true); EncodedAssert.assertThat(encoded).isEqualToHex("01 01 01"); assertThat(encoded.getFormalType()).isEqualTo("tinyint"); } @Test void shouldEncodeNull() { Encoded encoded = BooleanCodec.INSTANCE.encodeNull(TestByteBufAllocator.TEST); EncodedAssert.assertThat(encoded).isEqualToHex("01 00"); assertThat(encoded.getFormalType()).isEqualTo("tinyint"); } @Test void shouldBeAbleToDecode() { TypeInformation tinyint = builder().withServerType(SqlServerType.TINYINT).build(); TypeInformation varchar = builder().withServerType(SqlServerType.VARCHAR).build(); assertThat(BooleanCodec.INSTANCE.canDecode(ColumnUtil.createColumn(tinyint), Boolean.class)).isTrue(); assertThat(BooleanCodec.INSTANCE.canDecode(ColumnUtil.createColumn(varchar), Boolean.class)).isFalse(); assertThat(BooleanCodec.INSTANCE.canDecode(ColumnUtil.createColumn(tinyint), String.class)).isFalse(); } @Test void shouldDecodeFromLong() { TypeInformation type = createType(8, SqlServerType.BIGINT); assertThat(BooleanCodec.INSTANCE.decode(HexUtils.decodeToByteBuf("0000000000000001"), ColumnUtil.createColumn(type), Boolean.class)).isTrue(); assertThat(BooleanCodec.INSTANCE.decode(HexUtils.decodeToByteBuf("0000000000000000"), ColumnUtil.createColumn(type), Boolean.class)).isFalse(); } @Test void shouldDecodeFromInteger() { TypeInformation type = createType(4, SqlServerType.INTEGER); assertThat(BooleanCodec.INSTANCE.decode(HexUtils.decodeToByteBuf("01000000"), ColumnUtil.createColumn(type), Boolean.class)).isTrue(); assertThat(BooleanCodec.INSTANCE.decode(HexUtils.decodeToByteBuf("00000000"), ColumnUtil.createColumn(type), Boolean.class)).isFalse(); } @Test void shouldDecodeFromSmallInt() { TypeInformation type = createType(2, SqlServerType.SMALLINT); assertThat(BooleanCodec.INSTANCE.decode(HexUtils.decodeToByteBuf("0100"), ColumnUtil.createColumn(type), Boolean.class)).isTrue(); } @Test void shouldDecodeFromTinyInt() { TypeInformation type = createType(1, SqlServerType.TINYINT); assertThat(BooleanCodec.INSTANCE.decode(HexUtils.decodeToByteBuf("01"), ColumnUtil.createColumn(type), Boolean.class)).isTrue(); assertThat(BooleanCodec.INSTANCE.decode(HexUtils.decodeToByteBuf("00"), ColumnUtil.createColumn(type), Boolean.class)).isFalse(); } private TypeInformation createType(int length, SqlServerType serverType) { return builder().withMaxLength(length).withLengthStrategy(LengthStrategy.FIXEDLENTYPE).withPrecision(length).withServerType(serverType).build(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/codec/ByteCodecUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.EncodedAssert; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.TestByteBufAllocator; import org.junit.jupiter.api.Test; import static io.r2dbc.mssql.message.type.TypeInformation.builder; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link ByteCodec}. * * @author Mark Paluch */ class ByteCodecUnitTests { @Test void shouldEncodeByte() { Encoded encoded = ByteCodec.INSTANCE.encode(TestByteBufAllocator.TEST, RpcParameterContext.out(), (byte) 2); EncodedAssert.assertThat(encoded).isEqualToHex("01 01 02"); assertThat(encoded.getFormalType()).isEqualTo("tinyint"); } @Test void shouldEncodeNull() { Encoded encoded = ByteCodec.INSTANCE.encodeNull(TestByteBufAllocator.TEST); EncodedAssert.assertThat(encoded).isEqualToHex("01 00"); assertThat(encoded.getFormalType()).isEqualTo("tinyint"); } @Test void shouldBeAbleToDecode() { TypeInformation tinyint = builder().withServerType(SqlServerType.TINYINT).build(); TypeInformation varchar = builder().withServerType(SqlServerType.VARCHAR).build(); assertThat(ByteCodec.INSTANCE.canDecode(ColumnUtil.createColumn(tinyint), Byte.class)).isTrue(); assertThat(ByteCodec.INSTANCE.canDecode(ColumnUtil.createColumn(varchar), Byte.class)).isFalse(); assertThat(ByteCodec.INSTANCE.canDecode(ColumnUtil.createColumn(tinyint), String.class)).isFalse(); } @Test void shouldDecodeFromLong() { TypeInformation type = createType(8, SqlServerType.BIGINT); ByteBuf buffer = HexUtils.decodeToByteBuf("FF00000000000000"); assertThat(ByteCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), Byte.class)).isEqualTo((byte) 255); } @Test void shouldDecodeFromInteger() { TypeInformation type = createType(4, SqlServerType.INTEGER); ByteBuf buffer = HexUtils.decodeToByteBuf("01000000"); assertThat(ByteCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), Byte.class)).isEqualTo((byte) 1); } @Test void shouldDecodeFromSmallInt() { TypeInformation type = createType(2, SqlServerType.SMALLINT); ByteBuf buffer = HexUtils.decodeToByteBuf("0100"); assertThat(ByteCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), Byte.class)).isEqualTo((byte) 1); } @Test void shouldDecodeTinyInt() { TypeInformation type = createType(1, SqlServerType.TINYINT); ByteBuf buffer = HexUtils.decodeToByteBuf("01"); assertThat(ByteCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), Byte.class)).isEqualTo((byte) 1); } private TypeInformation createType(int length, SqlServerType serverType) { return builder().withMaxLength(length).withLengthStrategy(LengthStrategy.FIXEDLENTYPE).withPrecision(length).withServerType(serverType).build(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/codec/ClobCodecUnitTests.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.PooledByteBufAllocator; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.message.tds.ServerCharset; import io.r2dbc.mssql.message.type.Length; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.PlpLength; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.EncodedAssert; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.TestByteBufAllocator; import io.r2dbc.spi.Clob; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.test.StepVerifier; import java.nio.charset.StandardCharsets; import static io.r2dbc.mssql.message.type.TypeInformation.builder; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link ClobCodec}. * * @author Mark Paluch */ class ClobCodecUnitTests { @Test void shouldEncodeNull() { Encoded encoded = ClobCodec.INSTANCE.encodeNull(TestByteBufAllocator.TEST); EncodedAssert.assertThat(encoded).isEqualToHex("40 1f 00 00 00 00 00 ff ff"); assertThat(encoded.getFormalType()).isEqualTo("varchar(8000)"); } @Test void shouldBeAbleToEncodeNull() { assertThat(ClobCodec.INSTANCE.canEncodeNull(Clob.class)).isTrue(); } @Test void shouldBeAbleToDecodeChar() { TypeInformation binary = builder().withServerType(SqlServerType.CHAR).withLengthStrategy(LengthStrategy.USHORTLENTYPE).build(); assertThat(ClobCodec.INSTANCE.canDecode(ColumnUtil.createColumn(binary), Clob.class)).isTrue(); } @Test void shouldBeAbleToDecodeVarVarChar() { TypeInformation varchar = builder().withServerType(SqlServerType.VARCHAR).withLengthStrategy(LengthStrategy.USHORTLENTYPE).build(); assertThat(ClobCodec.INSTANCE.canDecode(ColumnUtil.createColumn(varchar), Clob.class)).isTrue(); } @Test void shouldBeAbleToDecodeVarCharMax() { TypeInformation binary = builder().withServerType(SqlServerType.VARCHARMAX).withLengthStrategy(LengthStrategy.PARTLENTYPE).build(); assertThat(ClobCodec.INSTANCE.canDecode(ColumnUtil.createColumn(binary), Clob.class)).isTrue(); } @Test void shouldBeAbleToDecodeText() { TypeInformation binary = builder().withServerType(SqlServerType.TEXT).withLengthStrategy(LengthStrategy.PARTLENTYPE).build(); assertThat(ClobCodec.INSTANCE.canDecode(ColumnUtil.createColumn(binary), Clob.class)).isTrue(); } @Test void shouldBeAbleToDecodeNtext() { TypeInformation binary = builder().withServerType(SqlServerType.NTEXT).withLengthStrategy(LengthStrategy.PARTLENTYPE).build(); assertThat(ClobCodec.INSTANCE.canDecode(ColumnUtil.createColumn(binary), Clob.class)).isTrue(); } @Test void shouldBeAbleToDecodeVarchar() { TypeInformation varchar = builder().withServerType(SqlServerType.VARCHAR).withLengthStrategy(LengthStrategy.USHORTLENTYPE).withCharset(ServerCharset.CP1252.charset()).build(); ByteBuf data = TestByteBufAllocator.TEST.buffer(); Encode.uShort(data, 6); data.writeCharSequence("foobar", ServerCharset.CP1252.charset()); Clob clob = ClobCodec.INSTANCE.decode(data, ColumnUtil.createColumn(varchar), Clob.class); StepVerifier.create(clob.stream()).expectNext("foobar").verifyComplete(); } @Test void shouldBeAbleToDecodePlpStream() { TypeInformation varchar = builder().withServerType(SqlServerType.VARCHAR).withLengthStrategy(LengthStrategy.PARTLENTYPE).withCharset(StandardCharsets.US_ASCII).build(); ByteBuf buffer = TestByteBufAllocator.TEST.heapBuffer(12 + 24); PlpLength.of(24).encode(buffer); Length.of(8).encode(buffer, varchar); buffer.writeBytes("C1xxxxxx".getBytes()); Length.of(8).encode(buffer, varchar); buffer.writeBytes("C2yyyyyy".getBytes()); Length.of(8).encode(buffer, varchar); buffer.writeBytes("C3zzzzzz".getBytes()); Clob clob = ClobCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(varchar), Clob.class); StepVerifier.create(clob.stream()) .expectNext("C1xxxxxx") .expectNext("C2yyyyyy") .expectNext("C3zzzzzz") .verifyComplete(); } @Test void shouldReleaseConsumedBuffers() { TypeInformation varchar = builder().withServerType(SqlServerType.VARCHAR).withLengthStrategy(LengthStrategy.PARTLENTYPE).withCharset(StandardCharsets.US_ASCII).build(); PooledByteBufAllocator alloc = PooledByteBufAllocator.DEFAULT; ByteBuf buffer = alloc.buffer(); PlpLength.of(8).encode(buffer); Length.of(8).encode(buffer, varchar); buffer.writeBytes("C1xxxxxx".getBytes()); Clob clob = ClobCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(varchar), Clob.class); buffer.release(); StepVerifier.create(clob.stream()) .expectNextCount(1) .verifyComplete(); assertThat(buffer.refCnt()).isZero(); } @Test void shouldReleaseEmptyBuffer() { TypeInformation type = builder().withMaxLength(50).withLengthStrategy(LengthStrategy.PARTLENTYPE).withPrecision(50).withServerType(SqlServerType.NVARCHARMAX).withCharset(StandardCharsets.UTF_16LE).build(); ByteBuf data = HexUtils.decodeToByteBuf("00 00 00 00 00 00 00 00"); Clob clob = ClobCodec.INSTANCE.decode(data, ColumnUtil.createColumn(type), Clob.class); data.release(); Flux.from(clob.stream()) .as(StepVerifier::create) .verifyComplete(); assertThat(data.refCnt()).isZero(); } @Test void shouldDecodeVarcharMaxSplitCharacter() { TypeInformation type = builder().withMaxLength(50).withLengthStrategy(LengthStrategy.PARTLENTYPE).withPrecision(50).withServerType(SqlServerType.NVARCHARMAX).withCharset(StandardCharsets.UTF_16LE).build(); ByteBuf data = HexUtils.decodeToByteBuf("62 00 00 00 00 00 00 00 " + "2d 00 00 00 " + "6c 00 65 00 61 00 6e 00 6e 00 65 00 2e 00 61 00 73 00 68 00 74 00 6f 00 6e 00 40 00 64 00 64 00 2d 00 70 00 75 00 62 00 2e 00 63 00 6f " + "35 00 00 00 " + "00 6d 00 2c 00 64 00 61 00 76 00 69 00 64 00 2e 00 6d 00 61 00 61 00 73 00 73 00 65 00 6e 00 40 00 64 00 64 00 2d 00 70 00 75 00 62 00 2e 00 63 00 6f 00 6d 00 " + "00 00 00 00"); Clob clob = ClobCodec.INSTANCE.decode(data, ColumnUtil.createColumn(type), Clob.class); data.release(); Flux.from(clob.stream()) .as(StepVerifier::create) .expectNext("leanne.ashton@dd-pub.c") .expectNext("om,david.maassen@dd-pub.com") .verifyComplete(); assertThat(data.refCnt()).isZero(); } @Test void shouldReportRemainderInDecodeBuffer() { TypeInformation type = builder().withMaxLength(50).withLengthStrategy(LengthStrategy.PARTLENTYPE).withPrecision(50).withServerType(SqlServerType.NVARCHARMAX).withCharset(StandardCharsets.UTF_16LE).build(); ByteBuf data = HexUtils.decodeToByteBuf("2d 00 00 00 00 00 00 00 " + "2d 00 00 00 " + "6c 00 65 00 61 00 6e 00 6e 00 65 00 2e 00 61 00 73 00 68 00 74 00 6f 00 6e 00 40 00 64 00 64 00 2d 00 70 00 75 00 62 00 2e 00 63 00 6f"); Clob clob = ClobCodec.INSTANCE.decode(data, ColumnUtil.createColumn(type), Clob.class); data.release(); Flux.from(clob.stream()) .as(StepVerifier::create) .expectNext("leanne.ashton@dd-pub.c") .verifyError(ClobCodec.ClobDecodeException.class); assertThat(data.refCnt()).isZero(); } @Test void shouldReleaseRemainingBuffersOnCancel() { TypeInformation varchar = builder().withServerType(SqlServerType.VARCHAR).withLengthStrategy(LengthStrategy.PARTLENTYPE).withCharset(StandardCharsets.US_ASCII).build(); ByteBuf buffer = TestByteBufAllocator.TEST.buffer(12 + 24); PlpLength.of(24).encode(buffer); Length.of(8).encode(buffer, varchar); buffer.writeBytes("C1xxxxxx".getBytes()); Length.of(8).encode(buffer, varchar); buffer.writeBytes("C2yyyyyy".getBytes()); Length.of(8).encode(buffer, varchar); buffer.writeBytes("C3zzzzzz".getBytes()); Clob clob = ClobCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(varchar), Clob.class); buffer.release(); assertThat(buffer.refCnt()).isEqualTo(1); StepVerifier.create(clob.stream(), 0) .thenRequest(1) .expectNextCount(1) .thenCancel() .verify(); assertThat(buffer.refCnt()).isZero(); } @Test void shouldReleaseOnDiscard() { TypeInformation varchar = builder().withServerType(SqlServerType.VARCHAR).withLengthStrategy(LengthStrategy.PARTLENTYPE).withCharset(StandardCharsets.US_ASCII).build(); PooledByteBufAllocator alloc = PooledByteBufAllocator.DEFAULT; ByteBuf buffer = alloc.buffer(); PlpLength.of(8).encode(buffer); Length.of(8).encode(buffer, varchar); buffer.writeBytes("C1xxxxxx".getBytes()); Clob clob = ClobCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(varchar), Clob.class); buffer.release(); StepVerifier.create(clob.discard()) .verifyComplete(); assertThat(buffer.refCnt()).isZero(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/codec/ColumnUtil.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.r2dbc.mssql.message.token.Column; import io.r2dbc.mssql.message.type.TypeInformation; /** * Utility to create columns. * * @author Mark Paluch */ class ColumnUtil { /** * Create a mock column. * * @param typeInformation * @return */ static Column createColumn(TypeInformation typeInformation) { return new Column(0, "foo", typeInformation, null); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/codec/DecimalCodecUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.EncodedAssert; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.TestByteBufAllocator; import org.junit.jupiter.api.Test; import java.math.BigDecimal; import static io.r2dbc.mssql.message.type.TypeInformation.builder; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link DecimalCodec}. * * @author Mark Paluch */ class DecimalCodecUnitTests { @Test void shouldBeAbleToDecode() { TypeInformation tinyint = builder().withServerType(SqlServerType.TINYINT).build(); TypeInformation numeric = builder().withServerType(SqlServerType.NUMERIC).build(); assertThat(DecimalCodec.INSTANCE.canDecode(ColumnUtil.createColumn(tinyint), BigDecimal.class)).isTrue(); assertThat(DecimalCodec.INSTANCE.canDecode(ColumnUtil.createColumn(numeric), BigDecimal.class)).isTrue(); } @Test void shouldEncodeNull() { Encoded encoded = DecimalCodec.INSTANCE.encodeNull(TestByteBufAllocator.TEST); EncodedAssert.assertThat(encoded).isEqualToHex("11 26 00 00"); assertThat(encoded.getFormalType()).isEqualTo("decimal(38,0)"); } @Test void shouldEncodeNumeric5x2() { Encoded encoded = DecimalCodec.INSTANCE.encode(TestByteBufAllocator.TEST, RpcParameterContext.in(), new BigDecimal("36.89")); EncodedAssert.assertThat(encoded).isEqualToHex("11 26 02 03 01 69 0e"); assertThat(encoded.getFormalType()).isEqualTo("decimal(38,2)"); } @Test void shouldEncodeNumeric10x3() { Encoded encoded = DecimalCodec.INSTANCE.encode(TestByteBufAllocator.TEST, RpcParameterContext.in(), new BigDecimal("9.5E+2")); EncodedAssert.assertThat(encoded).isEqualToHex("11 26 00 03 01 B6 03"); assertThat(encoded.getFormalType()).isEqualTo("decimal(38,0)"); } @Test void shouldDecodeNumeric5x2() { TypeInformation type = TypeInformation.builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withServerType(SqlServerType.NUMERIC).withScale(2).withPrecision(5).build(); ByteBuf buffer = HexUtils.decodeToByteBuf("05 01 69 0E 00 00"); BigDecimal decoded = DecimalCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), BigDecimal.class); assertThat(decoded).isEqualTo("36.89"); } @Test void shouldDecodeNumeric5x0() { TypeInformation type = TypeInformation.builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withServerType(SqlServerType.NUMERIC).withScale(0).withPrecision(5).build(); ByteBuf buffer = HexUtils.decodeToByteBuf("05 01 39 30 00 00"); BigDecimal decoded = DecimalCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), BigDecimal.class); assertThat(decoded).isEqualTo("12345"); } @Test void shouldDecodeInteger() { TypeInformation type = builder().withMaxLength(4).withLengthStrategy(LengthStrategy.FIXEDLENTYPE).withPrecision(4).withServerType(SqlServerType.INTEGER).build(); ByteBuf buffer = HexUtils.decodeToByteBuf("01000000"); assertThat(DecimalCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), BigDecimal.class)).isEqualTo(new BigDecimal("1")); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/codec/DoubleCodecUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.EncodedAssert; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.TestByteBufAllocator; import org.junit.jupiter.api.Test; import static io.r2dbc.mssql.message.type.TypeInformation.builder; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.offset; /** * Unit tests for {@link DoubleCodec}. * * @author Mark Paluch */ class DoubleCodecUnitTests { @Test void shouldEncodeDouble() { Encoded encoded = DoubleCodec.INSTANCE.encode(TestByteBufAllocator.TEST, RpcParameterContext.in(), 11344.554); EncodedAssert.assertThat(encoded).isEqualToHex("08 08 FE D4 78 E9 46 28 C6 40"); assertThat(encoded.getFormalType()).isEqualTo("float"); } @Test void shouldEncodeNull() { Encoded encoded = DoubleCodec.INSTANCE.encodeNull(TestByteBufAllocator.TEST); EncodedAssert.assertThat(encoded).isEqualToHex("08 00"); assertThat(encoded.getFormalType()).isEqualTo("float"); } @Test void shouldBeAbleToDecode() { TypeInformation floatType = builder().withServerType(SqlServerType.FLOAT).build(); TypeInformation intType = builder().withServerType(SqlServerType.INTEGER).build(); assertThat(DoubleCodec.INSTANCE.canDecode(ColumnUtil.createColumn(floatType), Double.class)).isTrue(); assertThat(DoubleCodec.INSTANCE.canDecode(ColumnUtil.createColumn(intType), Double.class)).isFalse(); } @Test void shouldDecodeFloat() { TypeInformation type = builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withServerType(SqlServerType.FLOAT).withMaxLength(8).build(); ByteBuf buffer = HexUtils.decodeToByteBuf("08FED478E94628C640"); assertThat(DoubleCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), Double.class)).isCloseTo(11344.554, offset(0.01)); } @Test void shouldDecodeReal() { TypeInformation type = builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withServerType(SqlServerType.REAL).withMaxLength(4).build(); ByteBuf buffer = HexUtils.decodeToByteBuf("0437423146"); assertThat(DoubleCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), Double.class)).isCloseTo(11344.554, offset(0.01)); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/codec/EncodedUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.Unpooled; import io.r2dbc.mssql.message.type.TdsDataType; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link Encoded}. * * @author Mark Paluch */ class EncodedUnitTests { @Test void shouldReportSqlServerType() { Encoded encoded = Encoded.of(TdsDataType.INT1, Unpooled.EMPTY_BUFFER); assertThat(encoded.getFormalType()).isEqualTo("tinyint"); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/codec/FloatCodecUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.EncodedAssert; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.TestByteBufAllocator; import org.junit.jupiter.api.Test; import static io.r2dbc.mssql.message.type.TypeInformation.builder; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link FloatCodec}. * * @author Mark Paluch */ class FloatCodecUnitTests { @Test void shouldEncodeFloat() { Encoded encoded = FloatCodec.INSTANCE.encode(TestByteBufAllocator.TEST, RpcParameterContext.in(), 11344.554f); EncodedAssert.assertThat(encoded).isEqualToHex("04 04 37 42 31 46"); assertThat(encoded.getFormalType()).isEqualTo("real"); } @Test void shouldEncodeNull() { Encoded encoded = FloatCodec.INSTANCE.encodeNull(TestByteBufAllocator.TEST); EncodedAssert.assertThat(encoded).isEqualToHex("04 00"); assertThat(encoded.getFormalType()).isEqualTo("real"); } @Test void shouldBeAbleToDecode() { TypeInformation floatType = builder().withServerType(SqlServerType.FLOAT).build(); TypeInformation intType = builder().withServerType(SqlServerType.INTEGER).build(); assertThat(FloatCodec.INSTANCE.canDecode(ColumnUtil.createColumn(floatType), Float.class)).isTrue(); assertThat(FloatCodec.INSTANCE.canDecode(ColumnUtil.createColumn(intType), Float.class)).isFalse(); } @Test void shouldDecodeFloat() { TypeInformation type = builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withServerType(SqlServerType.FLOAT).withMaxLength(8).build(); ByteBuf buffer = HexUtils.decodeToByteBuf("08FED478E94628C640"); assertThat(FloatCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), Float.class)).isEqualTo((float) 11344.554); } @Test void shouldDecodeReal() { TypeInformation type = builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withServerType(SqlServerType.REAL).withMaxLength(4).build(); ByteBuf buffer = HexUtils.decodeToByteBuf("0437423146"); assertThat(FloatCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), Float.class)).isEqualTo((float) 11344.554); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/codec/IntegerCodecUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.EncodedAssert; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.TestByteBufAllocator; import org.junit.jupiter.api.Test; import static io.r2dbc.mssql.message.type.TypeInformation.builder; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link IntegerCodec}. * * @author Mark Paluch */ class IntegerCodecUnitTests { @Test void shouldEncodeInteger() { Encoded encoded = IntegerCodec.INSTANCE.encode(TestByteBufAllocator.TEST, RpcParameterContext.out(), 16777217); EncodedAssert.assertThat(encoded).isEqualToHex("04 04 01 00 00 01"); assertThat(encoded.getFormalType()).isEqualTo("int"); } @Test void shouldEncodeNull() { Encoded encoded = IntegerCodec.INSTANCE.encodeNull(TestByteBufAllocator.TEST); EncodedAssert.assertThat(encoded).isEqualToHex("04 00"); assertThat(encoded.getFormalType()).isEqualTo("int"); } @Test void shouldBeAbleToDecode() { TypeInformation tinyint = builder().withServerType(SqlServerType.TINYINT).build(); TypeInformation varchar = builder().withServerType(SqlServerType.VARCHAR).build(); TypeInformation numeric = builder().withServerType(SqlServerType.NUMERIC).build(); assertThat(IntegerCodec.INSTANCE.canDecode(ColumnUtil.createColumn(tinyint), Integer.class)).isTrue(); assertThat(IntegerCodec.INSTANCE.canDecode(ColumnUtil.createColumn(varchar), Integer.class)).isFalse(); assertThat(IntegerCodec.INSTANCE.canDecode(ColumnUtil.createColumn(tinyint), String.class)).isFalse(); assertThat(IntegerCodec.INSTANCE.canDecode(ColumnUtil.createColumn(numeric), Integer.class)).isTrue(); } @Test void shouldDecodeFromLong() { TypeInformation type = createType(8, SqlServerType.BIGINT); ByteBuf buffer = HexUtils.decodeToByteBuf("0100000100000000"); assertThat(IntegerCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), Integer.class)).isEqualTo(16777217); } @Test void shouldDecodeFromInteger() { TypeInformation type = createType(4, SqlServerType.INTEGER); ByteBuf buffer = HexUtils.decodeToByteBuf("01000000"); assertThat(IntegerCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), Integer.class)).isEqualTo(1); } @Test void shouldDecodeFromNumeric() { TypeInformation type = TypeInformation.builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withServerType(SqlServerType.NUMERIC).withScale(0).withPrecision(5).build(); ByteBuf buffer = HexUtils.decodeToByteBuf("05 01 39 30 00 00"); assertThat(IntegerCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), Integer.class)).isEqualTo(12345); } @Test void shouldDecodeFromSmallInt() { TypeInformation type = createType(2, SqlServerType.SMALLINT); ByteBuf buffer = HexUtils.decodeToByteBuf("0100"); assertThat(IntegerCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), Integer.class)).isEqualTo(1); } @Test void shouldDecodeTinyInt() { TypeInformation type = createType(1, SqlServerType.TINYINT); ByteBuf buffer = HexUtils.decodeToByteBuf("01"); assertThat(IntegerCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), Integer.class)).isEqualTo(1); } private TypeInformation createType(int length, SqlServerType serverType) { return builder().withMaxLength(length).withLengthStrategy(LengthStrategy.FIXEDLENTYPE).withPrecision(length).withServerType(serverType).build(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/codec/LocalDateCodecUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.EncodedAssert; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.TestByteBufAllocator; import org.junit.jupiter.api.Test; import java.time.LocalDate; import static io.r2dbc.mssql.message.type.TypeInformation.builder; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link LocalDateCodec}. * * @author Mark Paluch */ class LocalDateCodecUnitTests { static final TypeInformation DATE = builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withServerType(SqlServerType.DATE).build(); @Test void shouldEncodeDate() { LocalDate value = LocalDate.parse("2018-10-23"); Encoded encoded = LocalDateCodec.INSTANCE.encode(TestByteBufAllocator.TEST, RpcParameterContext.out(), value); EncodedAssert.assertThat(encoded).isEqualToHex("03 DD 3E 0B"); assertThat(encoded.getFormalType()).isEqualTo("date"); } @Test void shouldEncodeNull() { Encoded encoded = LocalDateCodec.INSTANCE.encodeNull(TestByteBufAllocator.TEST); EncodedAssert.assertThat(encoded).isEqualToHex("00"); assertThat(encoded.getFormalType()).isEqualTo("date"); } @Test void shouldDecodeNull() { ByteBuf buffer = HexUtils.decodeToByteBuf("00"); LocalDate decoded = LocalDateCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(DATE), LocalDate.class); assertThat(decoded).isNull(); } @Test void shouldDecodeDate() { ByteBuf buffer = HexUtils.decodeToByteBuf("03DD3E0B"); LocalDate decoded = LocalDateCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(DATE), LocalDate.class); assertThat(decoded).isEqualTo("2018-10-23"); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/codec/LocalDateTimeCodecUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.EncodedAssert; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.TestByteBufAllocator; import org.junit.jupiter.api.Test; import java.time.LocalDateTime; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link LocalDateTimeCodec}. * * @author Mark Paluch */ class LocalDateTimeCodecUnitTests { static final TypeInformation SMALLDATETIME = TypeInformation.builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withServerType(SqlServerType.SMALLDATETIME).build(); static final TypeInformation DATETIME = TypeInformation.builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withServerType(SqlServerType.DATETIME).build(); static final TypeInformation DATETIME2 = TypeInformation.builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withScale(7).withServerType(SqlServerType.DATETIME2).build(); @Test void shouldDecodeSmallDateTime() { ByteBuf buffer = HexUtils.decodeToByteBuf("04F5A83E00"); LocalDateTime decoded = LocalDateTimeCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(SMALLDATETIME), LocalDateTime.class); assertThat(decoded).isEqualTo("2018-06-04T01:02"); } @Test void shouldEncodeSmallDateTime() { LocalDateTime value = LocalDateTime.parse("2018-06-04T01:02"); ByteBuf encoded = TestByteBufAllocator.TEST.buffer(); LocalDateTimeCodec.encode(encoded, SqlServerType.SMALLDATETIME, 0, value); EncodedAssert.assertThat(encoded).isEqualToHex("F5A83E00"); } @Test void shouldDecodeDateTime() { ByteBuf buffer = HexUtils.decodeToByteBuf("0886A90000AA700201"); LocalDateTime decoded = LocalDateTimeCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(DATETIME), LocalDateTime.class); assertThat(decoded).isEqualTo("2018-10-27T15:40:57.1"); } @Test void shouldEncodeDateTime() { LocalDateTime value = LocalDateTime.parse("2018-10-27T15:40:57.1"); ByteBuf encoded = TestByteBufAllocator.TEST.buffer(); LocalDateTimeCodec.encode(encoded, SqlServerType.DATETIME, 0, value); EncodedAssert.assertThat(encoded).isEqualToHex("86A90000AA700201"); } @Test void shouldDecodeDateTime2() { ByteBuf buffer = HexUtils.decodeToByteBuf("082006E17483E13E0B"); LocalDateTime decoded = LocalDateTimeCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(DATETIME2), LocalDateTime.class); assertThat(decoded).isEqualTo("2018-10-27T15:41:00.162"); } @Test void shouldEncodeDateTime2() { LocalDateTime value = LocalDateTime.parse("2018-10-27T15:41:00.162"); Encoded encoded = LocalDateTimeCodec.INSTANCE.encode(TestByteBufAllocator.TEST, RpcParameterContext.in(), value); EncodedAssert.assertThat(encoded).isEqualToHex("07 08 20 06 E1 74 83 E1 3E 0B"); assertThat(encoded.getFormalType()).isEqualTo("datetime2"); } @Test void shouldEncodeNull() { Encoded encoded = LocalDateTimeCodec.INSTANCE.encodeNull(TestByteBufAllocator.TEST); EncodedAssert.assertThat(encoded).isEqualToHex("07 00"); assertThat(encoded.getFormalType()).isEqualTo("datetime2"); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/codec/LocalTimeCodecUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.EncodedAssert; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.TestByteBufAllocator; import org.junit.jupiter.api.Test; import java.time.LocalTime; import static io.r2dbc.mssql.message.type.TypeInformation.builder; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link LocalTimeCodec}. * * @author Mark Paluch */ class LocalTimeCodecUnitTests { static final TypeInformation TIME = builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withScale(7).withServerType(SqlServerType.TIME).build(); @Test void shouldEncodeTime() { LocalTime value = LocalTime.parse("18:13:14"); Encoded encoded = LocalTimeCodec.INSTANCE.encode(TestByteBufAllocator.TEST, RpcParameterContext.out(), value); EncodedAssert.assertThat(encoded).isEqualToHex("07 05 00 19 12 B9 98"); assertThat(encoded.getFormalType()).isEqualTo("time"); } @Test void shouldEncodeNull() { Encoded encoded = LocalTimeCodec.INSTANCE.encodeNull(TestByteBufAllocator.TEST); EncodedAssert.assertThat(encoded).isEqualToHex("07 00"); assertThat(encoded.getFormalType()).isEqualTo("time"); } @Test void shouldDecodeTime() { ByteBuf buffer = HexUtils.decodeToByteBuf("05 c0 c9 b1 61 5d"); LocalTime decoded = LocalTimeCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(TIME), LocalTime.class); assertThat(decoded).isEqualTo("11:08:27.100"); } @Test void shouldDecodeNull() { ByteBuf buffer = HexUtils.decodeToByteBuf("00"); LocalTime decoded = LocalTimeCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(TIME), LocalTime.class); assertThat(decoded).isNull(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/codec/LongCodecUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.EncodedAssert; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.TestByteBufAllocator; import org.junit.jupiter.api.Test; import static io.r2dbc.mssql.message.type.TypeInformation.builder; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link LongCodec}. * * @author Mark Paluch */ class LongCodecUnitTests { @Test void shouldEncodeLong() { Encoded encoded = LongCodec.INSTANCE.encode(TestByteBufAllocator.TEST, RpcParameterContext.out(), 72057594037927937L); EncodedAssert.assertThat(encoded).isEqualToHex("08 08 01 00 00 00 00 0 00 0 01"); assertThat(encoded.getFormalType()).isEqualTo("bigint"); } @Test void shouldEncodeNull() { Encoded encoded = LongCodec.INSTANCE.encodeNull(TestByteBufAllocator.TEST); EncodedAssert.assertThat(encoded).isEqualToHex("08 00"); assertThat(encoded.getFormalType()).isEqualTo("bigint"); } @Test void shouldBeAbleToDecode() { TypeInformation tinyint = builder().withServerType(SqlServerType.TINYINT).build(); TypeInformation varchar = builder().withServerType(SqlServerType.VARCHAR).build(); assertThat(LongCodec.INSTANCE.canDecode(ColumnUtil.createColumn(tinyint), Long.class)).isTrue(); assertThat(LongCodec.INSTANCE.canDecode(ColumnUtil.createColumn(varchar), Long.class)).isFalse(); assertThat(LongCodec.INSTANCE.canDecode(ColumnUtil.createColumn(tinyint), String.class)).isFalse(); } @Test void shouldDecodeFromLong() { TypeInformation type = createType(8, SqlServerType.BIGINT); ByteBuf buffer = HexUtils.decodeToByteBuf("0100000000000001"); assertThat(LongCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), Long.class)).isEqualTo(72057594037927937L); } @Test void shouldDecodeFromInteger() { TypeInformation type = createType(4, SqlServerType.INTEGER); ByteBuf buffer = HexUtils.decodeToByteBuf("01000000"); assertThat(LongCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), Long.class)).isEqualTo(1); } @Test void shouldDecodeFromSmallInt() { TypeInformation type = createType(2, SqlServerType.SMALLINT); ByteBuf buffer = HexUtils.decodeToByteBuf("0100"); assertThat(LongCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), Long.class)).isEqualTo(1); } @Test void shouldDecodeTinyInt() { TypeInformation type = createType(1, SqlServerType.TINYINT); ByteBuf buffer = HexUtils.decodeToByteBuf("01"); assertThat(LongCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), Long.class)).isEqualTo(1); } private TypeInformation createType(int length, SqlServerType serverType) { return builder().withMaxLength(length).withLengthStrategy(LengthStrategy.FIXEDLENTYPE).withPrecision(length).withServerType(serverType).build(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/codec/MoneyCodecUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.message.token.Column; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.EncodedAssert; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.TestByteBufAllocator; import org.junit.jupiter.api.Test; import java.math.BigDecimal; import static io.r2dbc.mssql.codec.ColumnUtil.createColumn; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link MoneyCodec}. * * @author Mark Paluch */ class MoneyCodecUnitTests { @Test void shouldEncodeMoney() { Encoded encoded = MoneyCodec.INSTANCE.encode(TestByteBufAllocator.TEST, RpcParameterContext.in(), new BigDecimal("7301494.4032")); EncodedAssert.assertThat(encoded).isEqualToHex("08 08 11 00 00 00 20 a1 07 00"); assertThat(encoded.getFormalType()).isEqualTo("money"); } @Test void shouldEncodeNull() { Encoded encoded = MoneyCodec.INSTANCE.encodeNull(TestByteBufAllocator.TEST); EncodedAssert.assertThat(encoded).isEqualToHex("08 00"); assertThat(encoded.getFormalType()).isEqualTo("money"); } @Test void shouldDecodeBigMoney() { Column column = createColumn(TypeInformation.builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withServerType(SqlServerType.MONEY).withMaxLength(8).build()); ByteBuf buffer = TestByteBufAllocator.TEST.buffer(9); buffer.writeByte(8); Encode.money(buffer, new BigDecimal("7301494.4032").unscaledValue()); BigDecimal decoded = MoneyCodec.INSTANCE.decode(buffer, column, BigDecimal.class); assertThat(decoded).isEqualTo("7301494.4032"); } @Test void shouldDecodeSmallMoney() { Column column = createColumn(TypeInformation.builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withServerType(SqlServerType.SMALLMONEY).withMaxLength(4).build()); ByteBuf buffer = HexUtils.decodeToByteBuf("0420A10500"); BigDecimal decoded = MoneyCodec.INSTANCE.decode(buffer, column, BigDecimal.class); assertThat(decoded).isEqualTo("36.8928"); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/codec/OffsetDateTimeCodecUnitTests.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.EncodedAssert; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.TestByteBufAllocator; import org.junit.jupiter.api.Test; import java.time.OffsetDateTime; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link OffsetDateTimeCodec}. * * @author Mark Paluch */ class OffsetDateTimeCodecUnitTests { static final TypeInformation DATETIMEOFFSET = TypeInformation.builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withScale(7).withServerType(SqlServerType.DATETIMEOFFSET).build(); @Test void shouldEncodeDatetimeoffset() { OffsetDateTime value = OffsetDateTime.parse("2018-08-27T17:41:14.890+00:45"); Encoded encoded = OffsetDateTimeCodec.INSTANCE.encode(TestByteBufAllocator.TEST, RpcParameterContext.out(), value); EncodedAssert.assertThat(encoded).isEqualToHex("07 0a a0 d8 dd f7 8d a4 3e 0b 2d 00"); assertThat(encoded.getFormalType()).isEqualTo("datetimeoffset"); } @Test void shouldEncodeDatetimeOffsetNegativeTz() { OffsetDateTime value = OffsetDateTime.parse("2018-08-27T17:41:14.890-00:45"); Encoded encoded = OffsetDateTimeCodec.INSTANCE.encode(TestByteBufAllocator.TEST, RpcParameterContext.out(), value); EncodedAssert.assertThat(encoded).isEqualToHex("07 0a a0 74 84 8a 9a a4 3e 0b d3 ff"); assertThat(encoded.getFormalType()).isEqualTo("datetimeoffset"); } @Test void shouldEncodeNull() { Encoded encoded = OffsetDateTimeCodec.INSTANCE.encodeNull(TestByteBufAllocator.TEST); EncodedAssert.assertThat(encoded).isEqualToHex("07 00"); assertThat(encoded.getFormalType()).isEqualTo("datetimeoffset"); } @Test void shouldDecodeDateTimeOffset() { ByteBuf buffer = HexUtils.decodeToByteBuf("0a a0 d8 dd f7 8d a4 3e 0b 2d 00"); OffsetDateTime decoded = OffsetDateTimeCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(DATETIMEOFFSET), OffsetDateTime.class); assertThat(decoded).isEqualTo("2018-08-27T17:41:14.890+00:45"); } @Test void shouldDecodeDateTimeOffsetNegativeTz() { ByteBuf buffer = HexUtils.decodeToByteBuf("0a a0 74 84 8a 9a a4 3e 0b d3 ff"); OffsetDateTime decoded = OffsetDateTimeCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(DATETIMEOFFSET), OffsetDateTime.class); assertThat(decoded).isEqualTo("2018-08-27T17:41:14.890-00:45"); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/codec/PlpEncodedUnitTests.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.Unpooled; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.util.TestByteBufAllocator; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; /** * Unit tests for {@link PlpEncoded}. * * @author Mark Paluch */ class PlpEncodedUnitTests { @Test void shouldSplitBigByteArray() { byte[] bytes = new byte[255]; for (int i = Byte.MIN_VALUE; i < Byte.MAX_VALUE; i++) { bytes[i - Byte.MIN_VALUE] = (byte) i; } PlpEncoded encoded = new PlpEncoded(SqlServerType.VARBINARYMAX, TestByteBufAllocator.TEST, Mono.just(Unpooled.wrappedBuffer(bytes)), () -> { }); encoded.chunked(() -> 100, false).as(StepVerifier::create) .assertNext(actual -> assertThat(actual.readableBytes()).isEqualTo(100)) .assertNext(actual -> assertThat(actual.readableBytes()).isEqualTo(100)) .assertNext(actual -> assertThat(actual.readableBytes()).isEqualTo(55)) .verifyComplete(); } @Test void shouldSplitBigByteArrayWithPlpHeaders() { byte[] bytes = new byte[255]; for (int i = Byte.MIN_VALUE; i < Byte.MAX_VALUE; i++) { bytes[i - Byte.MIN_VALUE] = (byte) i; } PlpEncoded encoded = new PlpEncoded(SqlServerType.VARBINARYMAX, TestByteBufAllocator.TEST, Mono.just(Unpooled.wrappedBuffer(bytes)), () -> { }); encoded.chunked(() -> 100, true).as(StepVerifier::create) .assertNext(actual -> assertThat(actual.readableBytes()).isEqualTo(100 + 8 + 4)) .assertNext(actual -> assertThat(actual.readableBytes()).isEqualTo(100 + 4)) .assertNext(actual -> assertThat(actual.readableBytes()).isEqualTo(55 + 4)) .verifyComplete(); } @Test void shouldRearrangeChunks() { byte[] bytes1 = new byte[255]; byte[] bytes2 = new byte[255]; for (int i = Byte.MIN_VALUE; i < Byte.MAX_VALUE; i++) { bytes1[i - Byte.MIN_VALUE] = (byte) i; bytes2[i - Byte.MIN_VALUE] = (byte) i; } PlpEncoded encoded = new PlpEncoded(SqlServerType.VARBINARYMAX, TestByteBufAllocator.TEST, Flux.just(Unpooled.wrappedBuffer(bytes1), Unpooled.wrappedBuffer(bytes2)), () -> { }); AtomicInteger size = new AtomicInteger(100); encoded.chunked(size::get).as(StepVerifier::create) .assertNext(actual -> { size.incrementAndGet(); assertThat(actual.readableBytes()).isEqualTo(100); }).assertNext(actual -> { size.incrementAndGet(); assertThat(actual.readableBytes()).isEqualTo(101); }).assertNext(actual -> { size.incrementAndGet(); assertThat(actual.readableBytes()).isEqualTo(102); }).assertNext(actual -> { size.incrementAndGet(); assertThat(actual.readableBytes()).isEqualTo(103); }).assertNext(actual -> { assertThat(actual.readableBytes()).isEqualTo(104); }).verifyComplete(); } @Test void shouldDisposeUnusedBuffers() { AtomicBoolean dispose = new AtomicBoolean(); PlpEncoded encoded = new PlpEncoded(SqlServerType.VARBINARYMAX, TestByteBufAllocator.TEST, Flux.empty(), () -> dispose.set(true)); encoded.dispose(); assertThat(dispose).isTrue(); } @Test void emptySequenceShouldNeverAllocateCompositeBuffer() { AtomicBoolean dispose = new AtomicBoolean(); ByteBufAllocator alloc = mock(ByteBufAllocator.class); CompositeByteBuf composite = spy(TestByteBufAllocator.TEST.compositeBuffer()); doReturn(composite).when(alloc).compositeBuffer(); PlpEncoded encoded = new PlpEncoded(SqlServerType.VARBINARYMAX, alloc, Flux.empty(), () -> dispose.set(true)); StepVerifier.create(encoded.chunked(() -> 1), 0).thenRequest(1).verifyComplete(); verifyNoInteractions(alloc); } @Test void shouldDisposeCompositeBufferOnComplete() { AtomicBoolean dispose = new AtomicBoolean(); ByteBufAllocator alloc = mock(ByteBufAllocator.class); CompositeByteBuf composite = spy(TestByteBufAllocator.TEST.compositeBuffer()); doReturn(composite).when(alloc).compositeBuffer(); PlpEncoded encoded = new PlpEncoded(SqlServerType.VARBINARYMAX, alloc, Flux.just(Unpooled.wrappedBuffer(new byte[]{1, 2, 3})), () -> dispose.set(true)); encoded.chunked(() -> 1) .as(StepVerifier::create) .expectNextCount(3) .verifyComplete(); verify(composite).release(); } @Test void shouldDisposeCompositeBufferOnCancel() { AtomicBoolean dispose = new AtomicBoolean(); ByteBufAllocator alloc = mock(ByteBufAllocator.class); CompositeByteBuf composite = spy(TestByteBufAllocator.TEST.compositeBuffer()); doReturn(composite).when(alloc).compositeBuffer(); PlpEncoded encoded = new PlpEncoded(SqlServerType.VARBINARYMAX, alloc, Flux.just(Unpooled.wrappedBuffer(new byte[]{1, 2, 3})), () -> dispose.set(true)); StepVerifier.create(encoded.chunked(() -> 1), 0) .thenRequest(1) .expectNextCount(1) .thenCancel() .verify(); verify(composite).release(); } @Test void shouldDisposeCompositeBufferOnError() { AtomicBoolean dispose = new AtomicBoolean(); ByteBufAllocator alloc = mock(ByteBufAllocator.class); CompositeByteBuf composite = spy(TestByteBufAllocator.TEST.compositeBuffer()); doReturn(composite).when(alloc).compositeBuffer(); PlpEncoded encoded = new PlpEncoded(SqlServerType.VARBINARYMAX, alloc, Flux.concat(Mono.just(Unpooled.wrappedBuffer(new byte[]{1, 2, 3})), Mono.error(new IllegalStateException())), () -> dispose.set(true)); encoded.chunked(() -> 1).as(StepVerifier::create) .expectNextCount(3) .verifyError(IllegalStateException.class); verify(composite).release(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/codec/RpcEncodingUnitTests.java ================================================ /* * Copyright 2020-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.r2dbc.mssql.message.tds.ServerCharset; import io.r2dbc.mssql.message.type.Collation; import io.r2dbc.mssql.util.EncodedAssert; import io.r2dbc.mssql.util.TestByteBufAllocator; import org.junit.jupiter.api.Test; /** * Unit tests for {@link RpcEncoding}. * * @author Mark Paluch */ class RpcEncodingUnitTests { @Test void shouldEncodeStringAsNVarchar() { ByteBuf buffer = TestByteBufAllocator.TEST.buffer(); RpcEncoding.encodeString(buffer, null, RpcDirection.IN, Collation.RAW, "hello world"); byte[] unicode = "hello world".getBytes(ServerCharset.UNICODE.charset()); String len = "16 00"; EncodedAssert.assertThat(buffer).isEqualToHex("00 00 e7 40 1f 00 00 00 00 00 " + len + ByteBufUtil.hexDump(unicode)); } @Test void shouldEncodeStringAsNVarcharMax() { ByteBuf buffer = TestByteBufAllocator.TEST.buffer(); RpcEncoding.encodeString(buffer, null, RpcDirection.OUT, Collation.RAW, "hello world"); byte[] unicode = "hello world".getBytes(ServerCharset.UNICODE.charset()); String uLongLen = "16 00 00 00 00 00 00 00"; String len = "16 00 00 00"; EncodedAssert.assertThat(buffer).isEqualToHex("00 01 e7 ff ff 00 00 00 00 00 " + uLongLen + len + ByteBufUtil.hexDump(unicode) + " 00 00 00 00"); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/codec/ShortCodecUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.EncodedAssert; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.TestByteBufAllocator; import org.junit.jupiter.api.Test; import static io.r2dbc.mssql.message.type.TypeInformation.builder; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link ShortCodec}. * * @author Mark Paluch */ class ShortCodecUnitTests { @Test void shouldEncodeShort() { Encoded encoded = ShortCodec.INSTANCE.encode(TestByteBufAllocator.TEST, RpcParameterContext.out(), (short) 258); EncodedAssert.assertThat(encoded).isEqualToHex("02 02 02 01"); assertThat(encoded.getFormalType()).isEqualTo("smallint"); } @Test void shouldEncodeNull() { Encoded encoded = ShortCodec.INSTANCE.encodeNull(TestByteBufAllocator.TEST); EncodedAssert.assertThat(encoded).isEqualToHex("02 00"); assertThat(encoded.getFormalType()).isEqualTo("smallint"); } @Test void shouldBeAbleToDecode() { TypeInformation tinyint = builder().withServerType(SqlServerType.TINYINT).build(); TypeInformation varchar = builder().withServerType(SqlServerType.VARCHAR).build(); assertThat(ShortCodec.INSTANCE.canDecode(ColumnUtil.createColumn(tinyint), Short.class)).isTrue(); assertThat(ShortCodec.INSTANCE.canDecode(ColumnUtil.createColumn(varchar), Short.class)).isFalse(); assertThat(ShortCodec.INSTANCE.canDecode(ColumnUtil.createColumn(tinyint), String.class)).isFalse(); } @Test void shouldDecodeFromLong() { TypeInformation type = createType(8, SqlServerType.BIGINT); ByteBuf buffer = HexUtils.decodeToByteBuf("0101000000000000"); assertThat(ShortCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), Short.class)).isEqualTo((short) 257); } @Test void shouldDecodeFromInteger() { TypeInformation type = createType(4, SqlServerType.INTEGER); ByteBuf buffer = HexUtils.decodeToByteBuf("01000000"); assertThat(ShortCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), Short.class)).isEqualTo((short) 1); } @Test void shouldDecodeFromSmallInt() { TypeInformation type = createType(2, SqlServerType.SMALLINT); ByteBuf buffer = HexUtils.decodeToByteBuf("0100"); assertThat(ShortCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), Short.class)).isEqualTo((short) 1); } @Test void shouldDecodeTinyInt() { TypeInformation type = createType(1, SqlServerType.TINYINT); ByteBuf buffer = HexUtils.decodeToByteBuf("01"); assertThat(ShortCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), Short.class)).isEqualTo((short) 1); } private TypeInformation createType(int length, SqlServerType serverType) { return builder().withMaxLength(length).withLengthStrategy(LengthStrategy.FIXEDLENTYPE).withPrecision(length).withServerType(serverType).build(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/codec/StringCodecUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.codec.RpcParameterContext.ValueContext; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.message.tds.ServerCharset; import io.r2dbc.mssql.message.type.Collation; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.EncodedAssert; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.TestByteBufAllocator; import org.junit.jupiter.api.Test; import java.nio.charset.StandardCharsets; import static io.r2dbc.mssql.message.type.TypeInformation.builder; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link StringCodec}. * * @author Mark Paluch */ class StringCodecUnitTests { @Test void shouldEncodeNvarchar() { Collation collation = Collation.from(13632521, 52); ByteBuf data = TestByteBufAllocator.TEST.buffer(); Encode.uShort(data, 12); data.writeCharSequence("foobar", ServerCharset.UNICODE.charset()); Encoded encoded = StringCodec.INSTANCE.encode(TestByteBufAllocator.TEST, RpcParameterContext.in(new RpcParameterContext.CharacterValueContext(collation, true)), "foobar"); EncodedAssert.assertThat(encoded).isEncodedAs(expected -> { expected.writeShortLE(8000); // max size // collation windows-1252 expected.writeByte(0x09); expected.writeByte(0x04); expected.writeByte(0xD0); expected.writeByte(0x00); expected.writeByte(0x34); expected.writeShortLE(12); // actual size expected.writeCharSequence("foobar", ServerCharset.UNICODE.charset()); }); assertThat(encoded.getFormalType()).isEqualTo("nvarchar(4000)"); } @Test void shouldEncodeVarchar() { Collation collation = Collation.from(13632521, 52); Encoded encoded = StringCodec.INSTANCE.encode(TestByteBufAllocator.TEST, RpcParameterContext.in(ValueContext.character(collation, false)), "foobar"); EncodedAssert.assertThat(encoded).isEncodedAs(expected -> { expected.writeShortLE(8000); // max size // collation windows-1252 expected.writeByte(0x09); expected.writeByte(0x04); expected.writeByte(0xD0); expected.writeByte(0x00); expected.writeByte(0x34); expected.writeShortLE(6); // actual size expected.writeCharSequence("foobar", ServerCharset.CP1252.charset()); }); assertThat(encoded.getFormalType()).isEqualTo("varchar(8000)"); } @Test void shouldEncodeNull() { Encoded encoded = StringCodec.INSTANCE.encodeNull(TestByteBufAllocator.TEST); EncodedAssert.assertThat(encoded).isEqualToHex("40 1f 00 00 00 00 00 ff ff"); assertThat(encoded.getFormalType()).isEqualTo("varchar(8000)"); } @Test void shouldBeAbleToDecodeUuid() { TypeInformation type = builder().withMaxLength(16).withLengthStrategy(LengthStrategy.FIXEDLENTYPE).withPrecision(16).withServerType(SqlServerType.GUID).build(); assertThat(StringCodec.INSTANCE.canDecode(ColumnUtil.createColumn(type), String.class)).isTrue(); } @Test void shouldDecodeUuid() { TypeInformation type = builder().withMaxLength(16).withLengthStrategy(LengthStrategy.FIXEDLENTYPE).withPrecision(16).withServerType(SqlServerType.GUID).build(); ByteBuf buffer = HexUtils.decodeToByteBuf("F17B0DC7C7E5C54098C7A12F7E686724FD"); String value = StringCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), String.class); assertThat(value).isEqualTo("C70D7BF1-E5C7-40C5-98C7-A12F7E686724"); } @Test void shouldDecodeVarchar() { TypeInformation type = builder().withMaxLength(50).withLengthStrategy(LengthStrategy.USHORTLENTYPE).withPrecision(50).withServerType(SqlServerType.VARCHAR).withCharset(ServerCharset.CP1252.charset()).build(); ByteBuf data = TestByteBufAllocator.TEST.buffer(); Encode.uShort(data, 6); data.writeCharSequence("foobar", ServerCharset.CP1252.charset()); String value = StringCodec.INSTANCE.decode(data, ColumnUtil.createColumn(type), String.class); assertThat(value).isEqualTo("foobar"); } @Test void shouldDecodeVarcharMax() { TypeInformation type = builder().withMaxLength(50).withLengthStrategy(LengthStrategy.PARTLENTYPE).withPrecision(50).withServerType(SqlServerType.VARCHAR).withCharset(ServerCharset.CP1252.charset()).build(); ByteBuf data = TestByteBufAllocator.TEST.buffer(); Encode.uLongLong(data, 6); Encode.asInt(data, 6); data.writeCharSequence("foobar", ServerCharset.CP1252.charset()); String value = StringCodec.INSTANCE.decode(data, ColumnUtil.createColumn(type), String.class); assertThat(value).isEqualTo("foobar"); } @Test void shouldDecodeVarcharMaxSplitCharacter() { TypeInformation type = builder().withMaxLength(50).withLengthStrategy(LengthStrategy.PARTLENTYPE).withPrecision(50).withServerType(SqlServerType.NVARCHARMAX).withCharset(StandardCharsets.UTF_16LE).build(); ByteBuf data = HexUtils.decodeToByteBuf("62 00 00 00 00 00 00 00 " + "2d 00 00 00 " + "6c 00 65 00 61 00 6e 00 6e 00 65 00 2e 00 61 00 73 00 68 00 74 00 6f 00 6e 00 40 00 64 00 64 00 2d 00 70 00 75 00 62 00 2e 00 63 00 6f " + "35 00 00 00 " + "00 6d 00 2c 00 64 00 61 00 76 00 69 00 64 00 2e 00 6d 00 61 00 61 00 73 00 73 00 65 00 6e 00 40 00 64 00 64 00 2d 00 70 00 75 00 62 00 2e 00 63 00 6f 00 6d 00 " + "00 00 00 00"); String value = StringCodec.INSTANCE.decode(data, ColumnUtil.createColumn(type), String.class); assertThat(value).isEqualTo("leanne.ashton@dd-pub.com,david.maassen@dd-pub.com"); } @Test void shouldDecodeNvarchar() { TypeInformation type = builder().withMaxLength(100).withLengthStrategy(LengthStrategy.USHORTLENTYPE).withPrecision(50).withServerType(SqlServerType.VARCHAR).withCharset(ServerCharset.UNICODE.charset()).build(); ByteBuf data = TestByteBufAllocator.TEST.buffer(); Encode.uShort(data, 12); data.writeCharSequence("foobar", ServerCharset.UNICODE.charset()); String value = StringCodec.INSTANCE.decode(data, ColumnUtil.createColumn(type), String.class); assertThat(value).isEqualTo("foobar"); } @Test void shouldDecodeChar() { TypeInformation type = builder().withMaxLength(20).withLengthStrategy(LengthStrategy.USHORTLENTYPE).withServerType(SqlServerType.CHAR).withCharset(ServerCharset.CP1252.charset()).build(); ByteBuf data = TestByteBufAllocator.TEST.buffer(); Encode.uShort(data, 20); data.writeCharSequence("foobar ", ServerCharset.CP1252.charset()); String value = StringCodec.INSTANCE.decode(data, ColumnUtil.createColumn(type), String.class); assertThat(value).isEqualTo("foobar "); } @Test void shouldDecodeText() { TypeInformation type = builder().withMaxLength(2147483647).withLengthStrategy(LengthStrategy.LONGLENTYPE).withServerType(SqlServerType.TEXT).withCharset(ServerCharset.CP1252.charset()).build(); // Text value ByteBuf data = HexUtils.decodeToByteBuf("10 64" + "75 6D 6D 79 20 74 65 78 74 70 74 72 00 00 00 64" + "75 6D 6D 79 54 53 00 0B 00 00 00 6D 79 74 65 78" + "74 76 61 6C 75 65"); String value = StringCodec.INSTANCE.decode(data, ColumnUtil.createColumn(type), String.class); assertThat(value).isEqualTo("mytextvalue"); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/codec/TimestampCodecUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.TestByteBufAllocator; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Unit tests for {@link TimestampCodec}. * * @author Mark Paluch */ class TimestampCodecUnitTests { @Test void shouldEncodeTimestamp() { byte[] value = new byte[]{0, 0, 0, 0, 0, 0, 7, -47}; assertThatThrownBy(() -> TimestampCodec.INSTANCE.encode(TestByteBufAllocator.TEST, RpcParameterContext.out(), value)).isInstanceOf(UnsupportedOperationException.class); } @Test void shouldDecodeTimestamp() { TypeInformation type = TypeInformation.builder().withLengthStrategy(LengthStrategy.USHORTLENTYPE).withServerType(SqlServerType.TIMESTAMP).build(); ByteBuf buffer = HexUtils.decodeToByteBuf("080000000000000007D1"); byte[] decoded = TimestampCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), byte[].class); assertThat(decoded).containsSequence(0, 0, 0, 0, 0, 0, 7, -47); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/codec/UuidCodecUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.EncodedAssert; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.TestByteBufAllocator; import org.junit.jupiter.api.Test; import java.util.UUID; import static io.r2dbc.mssql.message.type.TypeInformation.builder; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link UuidCodec}. * * @author Mark Paluch */ class UuidCodecUnitTests { @Test void shouldEncodeUuid() { UUID uuid = UUID.fromString("C70D7BF1-E5C7-40C5-98C7-A12F7E686724"); Encoded encoded = UuidCodec.INSTANCE.encode(TestByteBufAllocator.TEST, RpcParameterContext.out(), uuid); EncodedAssert.assertThat(encoded).isEqualToHex("10 10 F17B0DC7C7E5C54098C7A12F7E686724"); assertThat(encoded.getFormalType()).isEqualTo("uniqueidentifier"); } @Test void shouldEncodeNull() { Encoded encoded = UuidCodec.INSTANCE.encodeNull(TestByteBufAllocator.TEST); EncodedAssert.assertThat(encoded).isEqualToHex("10 00"); assertThat(encoded.getFormalType()).isEqualTo("uniqueidentifier"); } @Test void shouldDecodeUuid() { TypeInformation type = builder().withMaxLength(16).withLengthStrategy(LengthStrategy.FIXEDLENTYPE).withPrecision(16).withServerType(SqlServerType.GUID).build(); ByteBuf buffer = HexUtils.decodeToByteBuf("F17B0DC7C7E5C54098C7A12F7E686724"); UUID decoded = UuidCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(type), UUID.class); assertThat(decoded).isEqualTo(UUID.fromString("C70D7BF1-E5C7-40C5-98C7-A12F7E686724")); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/codec/ZonedDateTimeCodecUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.codec; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.EncodedAssert; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.TestByteBufAllocator; import org.junit.jupiter.api.Test; import java.time.ZonedDateTime; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link ZonedDateTimeCodec}. * * @author Mark Paluch */ class ZonedDateTimeCodecUnitTests { static final TypeInformation DATETIMEOFFSET = TypeInformation.builder().withLengthStrategy(LengthStrategy.BYTELENTYPE).withScale(7).withServerType(SqlServerType.DATETIMEOFFSET).build(); @Test void shouldEncodeDatetimeoffset() { ZonedDateTime value = ZonedDateTime.parse("2018-08-27T17:41:14.890+00:45[UT+00:45]"); Encoded encoded = ZonedDateTimeCodec.INSTANCE.encode(TestByteBufAllocator.TEST, RpcParameterContext.out(), value); EncodedAssert.assertThat(encoded).isEqualToHex("07 0a a0 d8 dd f7 8d a4 3e 0b 2d 00"); assertThat(encoded.getFormalType()).isEqualTo("datetimeoffset"); } @Test void shouldEncodeNull() { Encoded encoded = ZonedDateTimeCodec.INSTANCE.encodeNull(TestByteBufAllocator.TEST); EncodedAssert.assertThat(encoded).isEqualToHex("07 00"); assertThat(encoded.getFormalType()).isEqualTo("datetimeoffset"); } @Test void shouldDecodeDateTimeOffset() { ByteBuf buffer = HexUtils.decodeToByteBuf("0a a0 d8 dd f7 8d a4 3e 0b 2d 00"); ZonedDateTime decoded = ZonedDateTimeCodec.INSTANCE.decode(buffer, ColumnUtil.createColumn(DATETIMEOFFSET), ZonedDateTime.class); assertThat(decoded).isEqualTo("2018-08-27T17:41:14.890+00:45[UT+00:45]"); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/TDSVersionUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link TDSVersion}. * * @author Mark Paluch */ final class TDSVersionUnitTests { @Test void shouldCompareGreaterVersion() { assertThat(TDSVersion.VER_KATMAI.isGreateOrEqualsTo(TDSVersion.VER_YUKON)).isTrue(); assertThat(TDSVersion.VER_KATMAI.isGreateOrEqualsTo(TDSVersion.VER_KATMAI)).isTrue(); assertThat(TDSVersion.VER_KATMAI.isGreateOrEqualsTo(TDSVersion.VER_DENALI)).isFalse(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/header/HeaderUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.header; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * @author Mark Paluch */ final class HeaderUnitTests { @Test void shouldEncodePreLoginHeader() { Header header = new Header(Type.PRE_LOGIN, Status.of(Status.StatusBit.EOM), (short) 0x002F, (short) 0, (byte) 1, (byte) 0); ByteBuf buffer = Unpooled.buffer(8); header.encode(buffer); assertThat(buffer.writerIndex()).isEqualTo(8); assertThat(ByteBufUtil.prettyHexDump(buffer)).containsIgnoringCase("12 01 00 2F 00 00 01 00"); } @Test void shouldDecodeLoginHeader() { Header header = new Header(Type.PRE_LOGIN, Status.of(Status.StatusBit.EOM), (short) 0x002F, (short) 0, (byte) 1, (byte) 0); ByteBuf buffer = Unpooled.buffer(8); header.encode(buffer); assertThat(Header.canDecode(buffer)).isTrue(); Header decoded = Header.decode(buffer); assertThat(decoded.getType()).isEqualTo(Type.PRE_LOGIN); assertThat(decoded.getStatus().is(Status.StatusBit.EOM)).isTrue(); assertThat(decoded.getLength()).isEqualTo((short) 0x002F); assertThat(decoded.getSpid()).isEqualTo((short) 0); assertThat(decoded.getPacketId()).isEqualTo((byte) 1); assertThat(decoded.getWindow()).isEqualTo((byte) 0); } @Test void shouldNotDecodeHeader() { ByteBuf buffer = Unpooled.buffer(7); assertThat(Header.canDecode(buffer)).isFalse(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/header/StatusUnitTests.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.header; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link Status}. * * @author Mark Paluch */ class StatusUnitTests { @ParameterizedTest @EnumSource(Status.StatusBit.class) void shouldReturnCorrectStatus(Status.StatusBit bit) { if (bit == Status.StatusBit.NORMAL) { return; } Status status = Status.of(bit); assertThat(status.is(bit)).isTrue(); } @Test void shouldCreateCombinedStatus() { Status status = Status.of(Status.StatusBit.EOM, Status.StatusBit.RESET_CONNECTION); assertThat(status.is(Status.StatusBit.EOM)).isTrue(); assertThat(status.is(Status.StatusBit.RESET_CONNECTION)).isTrue(); assertThat(status.is(Status.StatusBit.RESET_CONNECTION_SKIP_TRAN)).isFalse(); assertThat(status.is(Status.StatusBit.IGNORE)).isFalse(); } @Test void shouldAddStatus() { Status status = Status.of(Status.StatusBit.EOM).and(Status.StatusBit.RESET_CONNECTION); assertThat(status.is(Status.StatusBit.EOM)).isTrue(); assertThat(status.is(Status.StatusBit.RESET_CONNECTION)).isTrue(); assertThat(status.and(Status.StatusBit.EOM)).isSameAs(status); Status ignore = status.and(Status.StatusBit.IGNORE); assertThat(ignore.is(Status.StatusBit.IGNORE)).isTrue(); assertThat(ignore).isNotSameAs(status); } @Test void shouldRemoveStatus() { Status status = Status.of(Status.StatusBit.EOM).and(Status.StatusBit.RESET_CONNECTION); assertThat(status.not(Status.StatusBit.IGNORE)).isSameAs(status); Status notEom = status.not(Status.StatusBit.EOM); assertThat(notEom.is(Status.StatusBit.EOM)).isFalse(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/header/TypeUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.header; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * @author Mark Paluch */ final class TypeUnitTests { @Test void shouldResolveType() { assertThat(Type.valueOf((byte) 0x1)).isEqualTo(Type.SQL_BATCH); } @Test void typeResolutionShouldFail() { assertThatThrownBy(() -> Type.valueOf((byte) 0xFF)).hasMessageContaining("Invalid header type: 0xFF"); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/token/AllHeadersUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.util.EncodedAssert; import io.r2dbc.mssql.util.TestByteBufAllocator; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link AllHeaders}. * * @author Mark Paluch */ class AllHeadersUnitTests { @Test void shouldEncodeProperly() { byte[] tx = new byte[]{0, 0, 0, 0, 0, 0, 0, 0}; AllHeaders header = AllHeaders.transactional(tx, 1); ByteBuf buffer = TestByteBufAllocator.TEST.buffer(); header.encode(buffer); assertThat(buffer.readableBytes()).isEqualTo(header.getLength()); EncodedAssert.assertThat(buffer).isEncodedAs(expected -> { Encode.asInt(expected, 22); // all length inclusive Encode.asInt(expected, 18); // header length inclusive Encode.uShort(expected, 2); // Tx header expected.writeBytes(tx); // Tx descriptor Encode.dword(expected, 1); // outstanding requests }); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/token/CanDecodeTestSupport.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import java.util.function.Predicate; import static org.assertj.core.api.Assertions.assertThat; /** * Support class to test {@literal canDecode} methods. * * @author Mark Paluch */ final class CanDecodeTestSupport { /** * Test {@literal canDecode} method with a message without the type byte. This test method tests positive and negative decodability scenarios by truncating the data buffer until it does not * contain any readable bytes. * * @param buffer data buffer containing the message without the type byte. * @param canDecode the {@literal canDecode} method to test. */ public static void testCanDecode(ByteBuf buffer, Predicate canDecode) { int expectedReadIndex = buffer.readerIndex(); int writerIndex = buffer.writerIndex(); assertThat(canDecode).accepts(buffer); assertThat(buffer.readerIndex()).describedAs("readIndex after first canDecode()").isEqualTo(expectedReadIndex); for (int i = 1; i < writerIndex; i++) { buffer.writerIndex(writerIndex - i); assertThat(canDecode).describedAs("canDecode() with missing " + i + " bytes").rejects(buffer); } } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/token/ColInfoTokenUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.util.HexUtils; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link ColInfoToken}. * * @author Mark Paluch */ class ColInfoTokenUnitTests { @Test void shouldDecodeToken() { ByteBuf buffer = HexUtils.decodeToByteBuf("a50900010100020100030014"); assertThat(buffer.readByte()).isEqualTo(ColInfoToken.TYPE); ColInfoToken token = ColInfoToken.decode(buffer); assertThat(token.getColumns()).hasSize(3); ColInfoToken.ColInfo column = token.getColumns().get(0); assertThat(column.getTable()).isEqualTo((byte) 1); assertThat(column.getColumn()).isEqualTo((byte) 1); assertThat(column.getStatus()).isEqualTo((byte) 0); assertThat(column.getName()).isNull(); assertThat(buffer.readableBytes()).isZero(); } @Test void shouldSkipToken() { ByteBuf buffer = HexUtils.decodeToByteBuf("a50900010100020100030014"); assertThat(buffer.readByte()).isEqualTo(ColInfoToken.TYPE); ColInfoToken token = ColInfoToken.skip(buffer); assertThat(token.getColumns()).isEmpty(); assertThat(buffer.readableBytes()).isZero(); } @Test void canDecodeShouldReportDecodability() { String data = "0900010100020100030014"; CanDecodeTestSupport.testCanDecode(HexUtils.decodeToByteBuf(data), ColInfoToken::canDecode); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/token/ColumnMetadataTokenUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.util.HexUtils; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link ColumnMetadataToken}. * * @author Mark Paluch */ class ColumnMetadataTokenUnitTests { @Test void shouldDecodeColumns() { String encoded = "04000000000000" + "000800380B65006D0070006C006F0079" + "00650065005F00690064000000000008" + "00A732000904D00034096C0061007300" + "74005F006E0061006D00650000000000" + "0900A732000904D000340A6600690072" + "00730074005F006E0061006D00650000" + "00000009006E0806730061006C006100" + "72007900"; ByteBuf buffer = HexUtils.decodeToByteBuf(encoded); ColumnMetadataToken metadata = ColumnMetadataToken.decode(buffer, true); assertThat(metadata.getColumns()).hasSize(4).extracting(Column::getName).containsSequence("employee_id", "last_name", "first_name", "salary"); } @Test void shouldDecodeIntAndVarcharMaxColumns() { // columns: id INT, content VARCHAR(MAX) String encoded = "02 00 00 00 00 00 00" + "00 09 00 26 04 02 69 00 64 00 00 00 00 00 09 00" + "A7 FF FF 09 04 D0 00 34 07 63 00 6F 00 6E 00 74" + "00 65 00 6E 00 74 00"; ByteBuf buffer = HexUtils.decodeToByteBuf(encoded); ColumnMetadataToken metadata = ColumnMetadataToken.decode(buffer, true); assertThat(metadata.getColumns()).hasSize(2); Column id = metadata.getColumns()[0]; assertThat(id.getName()).isEqualTo("id"); assertThat(id.getType().getLengthStrategy()).isEqualTo(LengthStrategy.BYTELENTYPE); Column content = metadata.getColumns()[1]; assertThat(content.getName()).isEqualTo("content"); assertThat(content.getType().getLengthStrategy()).isEqualTo(LengthStrategy.PARTLENTYPE); } @Test void shouldDecodeIntBinaryAndVarbinaryColumns() { // columns: id INT, binfix BINARY(10), binvar VARBINARY(255) String encoded = "03 00 00 00 00 00 00" + "00 08 00 38 02 69 00 64 00 00 00 00 00 09 00 AD" + "0A 00 06 62 00 69 00 6E 00 66 00 69 00 78 00 00" + "00 00 00 09 00 A5 FF 00 06 62 00 69 00 6E 00 76" + "00 61 00 72 00"; ByteBuf buffer = HexUtils.decodeToByteBuf(encoded); ColumnMetadataToken metadata = ColumnMetadataToken.decode(buffer, true); assertThat(metadata.getColumns()).hasSize(3); Column id = metadata.getColumns()[0]; assertThat(id.getName()).isEqualTo("id"); assertThat(id.getType().getLengthStrategy()).isEqualTo(LengthStrategy.FIXEDLENTYPE); Column binfix = metadata.getColumns()[1]; assertThat(binfix.getName()).isEqualTo("binfix"); assertThat(binfix.getType().getLengthStrategy()).isEqualTo(LengthStrategy.USHORTLENTYPE); assertThat(binfix.getType().getServerType()).isEqualTo(SqlServerType.BINARY); Column binvar = metadata.getColumns()[2]; assertThat(binvar.getName()).isEqualTo("binvar"); assertThat(binvar.getType().getLengthStrategy()).isEqualTo(LengthStrategy.USHORTLENTYPE); assertThat(binvar.getType().getServerType()).isEqualTo(SqlServerType.VARBINARY); } @Test void canDecodeShouldReportDecodability() { String data = "04000000000000" + "000800380B65006D0070006C006F0079" + "00650065005F00690064000000000008" + "00A732000904D00034096C0061007300" + "74005F006E0061006D00650000000000" + "0900A732000904D000340A6600690072" + "00730074005F006E0061006D00650000" + "00000009006E0806730061006C006100" + "72007900"; ColumnMetadataToken metadata = ColumnMetadataToken.decode(HexUtils.decodeToByteBuf(data), true); CanDecodeTestSupport.testCanDecode(HexUtils.decodeToByteBuf(data), buffer -> ColumnMetadataToken.canDecode(buffer, true)); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/token/DoneInProcUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.util.HexUtils; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link DoneInProcToken}. * * @author Mark Paluch */ class DoneInProcUnitTests { @Test void shouldDecodeToken() { ByteBuf buffer = HexUtils.decodeToByteBuf("FF1000C1000100000000000000"); assertThat(buffer.readByte()).isEqualTo(DoneInProcToken.TYPE); DoneInProcToken token = DoneInProcToken.decode(buffer); assertThat(token.isDone()).isTrue(); assertThat(token.hasMore()).isFalse(); assertThat(token.hasCount()).isTrue(); assertThat(token.getRowCount()).isEqualTo(1); } @Test void canDecodeShouldReportDecodability() { String data = "1000C1000100000000000000"; CanDecodeTestSupport.testCanDecode(HexUtils.decodeToByteBuf(data), DoneInProcToken::canDecode); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/token/DoneProcUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.util.HexUtils; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link DoneProcToken}. * * @author Mark Paluch */ class DoneProcUnitTests { @Test void shouldDecodeToken() { ByteBuf buffer = HexUtils.decodeToByteBuf("FE1000C1000100000000000000"); assertThat(buffer.readByte()).isEqualTo(DoneProcToken.TYPE); DoneProcToken token = DoneProcToken.decode(buffer); assertThat(token.isDone()).isTrue(); assertThat(token.hasMore()).isFalse(); assertThat(token.hasCount()).isTrue(); assertThat(token.getRowCount()).isEqualTo(1); } @Test void canDecodeShouldReportDecodability() { String data = "1000C1000100000000000000"; CanDecodeTestSupport.testCanDecode(HexUtils.decodeToByteBuf(data), DoneProcToken::canDecode); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/token/DoneTokenUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.util.HexUtils; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link DoneToken}. * * @author Mark Paluch */ class DoneTokenUnitTests { @Test void shouldDecodeToken() { ByteBuf buffer = HexUtils.decodeToByteBuf("FD1000C1000100000000000000"); assertThat(buffer.readByte()).isEqualTo(DoneToken.TYPE); DoneToken token = DoneToken.decode(buffer); assertThat(token.isDone()).isTrue(); assertThat(token.isError()).isFalse(); assertThat(token.hasMore()).isFalse(); assertThat(token.hasCount()).isTrue(); assertThat(token.getRowCount()).isEqualTo(1); } @Test void shouldErrorDecodeToken() { ByteBuf buffer = HexUtils.decodeToByteBuf("FD1200C1000100000000000000"); assertThat(buffer.readByte()).isEqualTo(DoneToken.TYPE); DoneToken token = DoneToken.decode(buffer); assertThat(token.isDone()).isTrue(); assertThat(token.isError()).isTrue(); assertThat(token.hasMore()).isFalse(); assertThat(token.hasCount()).isTrue(); assertThat(token.getRowCount()).isEqualTo(1); } @Test void canDecodeShouldReportDecodability() { String data = "1000C1000100000000000000"; CanDecodeTestSupport.testCanDecode(HexUtils.decodeToByteBuf(data), DoneToken::canDecode); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/token/EnvChangeTokenUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.r2dbc.mssql.message.tds.Redirect; import io.r2dbc.mssql.util.HexUtils; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link EnvChangeToken}. * * @author Mark Paluch */ final class EnvChangeTokenUnitTests { @Test void shouldDecodeDatabaseChange() { ByteBuf buffer = HexUtils.decodeToByteBuf("E31B0001066D0061007300740065007200066D0061007300740065007200"); assertThat(buffer.readByte()).isEqualTo(EnvChangeToken.TYPE); EnvChangeToken token = EnvChangeToken.decode(buffer); assertThat(token.getChangeType()).isEqualTo(EnvChangeToken.EnvChangeType.Database); assertThat(token.getNewValueString()).isEqualTo("master"); assertThat(token.getOldValueString()).isEqualTo("master"); } @Test void shouldDecodeLanguageChange() { ByteBuf buffer = HexUtils.decodeToByteBuf("e31700020a750073005f0065" + "006e0067006c0069007300680000"); assertThat(buffer.readByte()).isEqualTo(EnvChangeToken.TYPE); EnvChangeToken token = EnvChangeToken.decode(buffer); assertThat(token.getChangeType()).isEqualTo(EnvChangeToken.EnvChangeType.Language); assertThat(token.getNewValueString()).isEqualTo("us_english"); assertThat(token.getOldValueString()).isEqualTo(""); } @Test void canDecodeShouldReportDecodability() { String data = "1B0001066D0061007300740065007200066D0061007300740065007200"; CanDecodeTestSupport.testCanDecode(HexUtils.decodeToByteBuf(data), EnvChangeToken::canDecode); } @Test void shouldDecodeRoute() { ByteBuf buffer = HexUtils.decodeToByteBuf("e316001413000039300700740065007300740069006e006700"); assertThat(buffer.readByte()).isEqualTo(EnvChangeToken.TYPE); EnvChangeToken token = EnvChangeToken.decode(buffer); Redirect redirect = Redirect.decode(Unpooled.wrappedBuffer(token.getNewValue())); assertThat(redirect.getPort()).isEqualTo(12345); assertThat(redirect.getServerName()).isEqualTo("testing"); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/token/ErrorTokenUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.util.HexUtils; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link ErrorToken}. * * @author Mark Paluch */ class ErrorTokenUnitTests { @Test void shouldDecodeErrorToken() { ByteBuf buffer = HexUtils.decodeToByteBuf("AA5C00CF00000001" + "101B0049006E00760061006C00690064" + "00200063006F006C0075006D006E0020" + "006E0061006D00650020002700660073" + "006400660027002E000C610036003800" + "38003000390032006100370039006600" + "35000001000000"); assertThat(buffer.readByte()).isEqualTo(ErrorToken.TYPE); ErrorToken errorToken = ErrorToken.decode(buffer); assertThat(errorToken.getNumber()).isEqualTo(207); assertThat(errorToken.getState()).isEqualTo((byte) 1); assertThat(errorToken.getMessage()).isEqualTo("Invalid column name 'fsdf'."); assertThat(errorToken.getServerName()).isEqualTo("a688092a79f5"); assertThat(errorToken.getClassification()).isEqualTo(AbstractInfoToken.Classification.GENERAL_ERROR); assertThat(errorToken.getProcName()).isEqualTo(""); assertThat(errorToken.getLineNumber()).isEqualTo(16777216); } @Test void canDecodeShouldReportDecodability() { String data = "5C00CF00000001" + "101B0049006E00760061006C00690064" + "00200063006F006C0075006D006E0020" + "006E0061006D00650020002700660073" + "006400660027002E000C610036003800" + "38003000390032006100370039006600" + "35000001000000"; CanDecodeTestSupport.testCanDecode(HexUtils.decodeToByteBuf(data), ErrorToken::canDecode); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/token/FeatureExtAckTokenUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.token.FeatureExtAckToken.ColumnEncryption; import io.r2dbc.mssql.util.HexUtils; import org.junit.jupiter.api.Test; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link FeatureExtAckToken}. * * @author Mark Paluch */ class FeatureExtAckTokenUnitTests { @Test void shouldDecode() { ByteBuf buffer = HexUtils.decodeToByteBuf("ae040100000001ff"); assertThat(buffer.readByte()).isEqualTo(FeatureExtAckToken.TYPE); FeatureExtAckToken token = FeatureExtAckToken.decode(buffer); List tokens = token.getFeatureTokens(); assertThat(tokens).hasSize(1); ColumnEncryption encryption = (ColumnEncryption) tokens.get(0); assertThat(encryption.getTceVersion()).isEqualTo((byte) 1); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/token/IdentifierUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Unit tests for {@link Identifier}. * * @author Mark Paluch */ class IdentifierUnitTests { @Test void shouldCreateObjectIdentifier() { Identifier identifier = Identifier.objectName("foo"); assertThat(identifier.asEscapedString()).isEqualTo("[foo]"); } @Test void shouldCreateFullQualifier() { Identifier identifier = Identifier.builder().objectName("obj").schemaName("schema").databaseName("db").serverName("server").build(); assertThat(identifier.asEscapedString()).isEqualTo("[server].[db].[schema].[obj]"); } @Test void shouldRejectServernameWithoutDatabase() { assertThatThrownBy(() -> Identifier.builder().objectName("obj").serverName("server").build()).isInstanceOf(IllegalStateException.class); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/token/InfoTokenUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.util.HexUtils; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link InfoToken}. * * @author Mark Paluch */ final class InfoTokenUnitTests { @Test void shouldDecode() { ByteBuf buffer = HexUtils.decodeToByteBuf("AB700045160000020025" + "004300680061006E0067006500640020" + "00640061007400610062006100730065" + "00200063006F006E0074006500780074" + "00200074006F00200027006D00610073" + "0074006500720027002E000C61003600" + "38003800300039003200610037003900" + "660035000001000000"); assertThat(buffer.readByte()).isEqualTo(InfoToken.TYPE); InfoToken infoToken = InfoToken.decode(buffer); assertThat(infoToken.getNumber()).isEqualTo(5701); assertThat(infoToken.getState()).isEqualTo((byte) 2); assertThat(infoToken.getMessage()).isEqualTo("Changed database context to 'master'."); assertThat(infoToken.getServerName()).isEqualTo("a688092a79f5"); assertThat(infoToken.getProcName()).isEqualTo(""); assertThat(infoToken.getLineNumber()).isEqualTo(16777216); } @Test void shouldDecodeLanguageChange() { ByteBuf buffer = HexUtils.decodeToByteBuf("ab74" + "0047160000010027004300680061006e" + "0067006500640020006c0061006e0067" + "00750061006700650020007300650074" + "00740069006e006700200074006f0020" + "00750073005f0065006e0067006c0069" + "00730068002e000c6100360038003800" + "30003900320061003700390066003500" + "0001000000"); assertThat(buffer.readByte()).isEqualTo(InfoToken.TYPE); InfoToken infoToken = InfoToken.decode(buffer); assertThat(infoToken.getNumber()).isEqualTo(5703); assertThat(infoToken.getState()).isEqualTo((byte) 1); assertThat(infoToken.getMessage()).isEqualTo("Changed language setting to us_english."); assertThat(infoToken.getServerName()).isEqualTo("a688092a79f5"); assertThat(infoToken.getProcName()).isEqualTo(""); assertThat(infoToken.getLineNumber()).isEqualTo(16777216); } @Test void canDecodeShouldReportDecodability() { String data = "74" + "0047160000010027004300680061006e" + "0067006500640020006c0061006e0067" + "00750061006700650020007300650074" + "00740069006e006700200074006f0020" + "00750073005f0065006e0067006c0069" + "00730068002e000c6100360038003800" + "30003900320061003700390066003500" + "0001000000"; CanDecodeTestSupport.testCanDecode(HexUtils.decodeToByteBuf(data), InfoToken::canDecode); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/token/Login7UnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import io.r2dbc.mssql.message.TDSVersion; import io.r2dbc.mssql.message.header.Type; import io.r2dbc.mssql.message.tds.ContextualTdsFragment; import io.r2dbc.mssql.util.TestByteBufAllocator; import io.r2dbc.mssql.util.Version; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link Login7}. * * @author Mark Paluch */ final class Login7UnitTests { @Test void shouldRenderSimpleLoginPacket() { Login7 login7 = Login7.builder() .serverName("localhost") .hostName("some-fancy-hostname") .username("sa") .password("super-secret") .database("master") .clientLibraryName("MyDriver") .applicationName("MyApp") .clientLibraryVersion(Version.parse("6.4")) .tdsVersion(TDSVersion.VER_DENALI).build(); ByteBuf buffer = Unpooled.buffer(400); login7.encode(buffer); // Header: 100100eb00000100 byte[] expected = ByteBufUtil.decodeHexDump("e300000004000074" + "401f0000000004060000000000000000" + "e003001800000000000000005e001300" + "8400020088000c00a0000500aa000900" + "bc000400c000080000000000d0000600" + "00000000000000000000000000000000" + "00000000000073006f006d0065002d00" + "660061006e00630079002d0068006f00" + "730074006e0061006d00650073006100" + "92a5f2a5a2a5f3a582a577a592a5f3a5" + "93a582a5f3a5e2a54d00790041007000" + "70006c006f00630061006c0068006f00" + "73007400dc0000004d00790044007200" + "69007600650072006d00610073007400" + "65007200040100000001ff"); assertThat(ByteBufUtil.prettyHexDump(buffer)) .isEqualTo(ByteBufUtil.prettyHexDump(Unpooled.wrappedBuffer(expected))); } @Test void shouldEncodePacket() { Login7 login7 = Login7.builder() .serverName("localhost") .hostName("some-fancy-hostname") .username("sa") .password("super-secret") .database("master") .clientLibraryName("MyDriver") .applicationName("MyApp") .clientLibraryVersion(Version.parse("6.4")) .tdsVersion(TDSVersion.VER_DENALI).build(); Mono.just(login7.encode(TestByteBufAllocator.TEST, 0)) .cast(ContextualTdsFragment.class) .as(StepVerifier::create) .consumeNextWith(actual -> { assertThat(actual.getHeaderOptions().getType()).isEqualTo(Type.TDS7_LOGIN); assertThat(actual.getHeaderOptions().getStatus().getValue()).isEqualTo((byte) 0); }).verifyComplete(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/token/LoginAckTokenUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.TDSVersion; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.Version; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * @author Mark Paluch */ final class LoginAckTokenUnitTests { @Test void shouldDecode() { ByteBuf buffer = HexUtils.decodeToByteBuf("ad36000174000004164d00" + "6900630072006f0073006f0066007400" + "2000530051004c002000530065007200" + "760065007200000000000e000bde"); assertThat(buffer.readByte()).isEqualTo(LoginAckToken.TYPE); LoginAckToken token = LoginAckToken.decode(buffer); assertThat(token.getTdsVersion()).isEqualTo(TDSVersion.VER_DENALI.getVersion()); assertThat(token.getClientInterface()).isEqualTo(LoginAckToken.CLIENT_INTEFACE_TSQL); assertThat(token.getProgrName()).startsWith("Microsoft SQL Server"); assertThat(token.getVersion()).isEqualTo(Version.parse("14.0.3038")); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/token/NbcRowTokenUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.tds.ServerCharset; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.Types; import org.junit.jupiter.api.Test; import java.util.Arrays; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link NbcRowToken}. * * @author Mark Paluch */ class NbcRowTokenUnitTests { TypeInformation integerType = Types.integer(); TypeInformation stringType = Types.varchar(255); Column[] columns = Arrays.asList(new Column(0, "id", this.integerType), new Column(1, "first_name", this.stringType), new Column(2, "last_name", this.stringType), new Column(3, "other", this.stringType), new Column(4, "other2", this.stringType), new Column(5, "other3", this.stringType), new Column(6, "rowstat", this.integerType)).toArray(new Column[0]); @Test void shouldDecodeNbcRow() { ByteBuf data = HexUtils.decodeToByteBuf("D2 1C 04 01 00 00 00 01 00 61 02 00 78 61 04 01 00 00 00"); assertThat(data.readByte()).isEqualTo(NbcRowToken.TYPE); NbcRowToken rowToken = NbcRowToken.decode(data, this.columns); assertThat(rowToken.getColumnData(0)).isNotNull(); assertThat(rowToken.getColumnData(1)).isNotNull(); assertThat(rowToken.getColumnData(2)).isNull(); assertThat(rowToken.getColumnData(3)).isNull(); assertThat(rowToken.getColumnData(4)).isNull(); assertThat(rowToken.getColumnData(5)).isNotNull(); assertThat(rowToken.getColumnData(6)).isNotNull(); } @Test void canDecodeShouldReportDecodability() { String data = "1C 04 01 00 00 00 01 00 61 02 00 78 61 04 01 00 00 00"; CanDecodeTestSupport.testCanDecode(HexUtils.decodeToByteBuf(data), buffer -> NbcRowToken.canDecode(buffer, this.columns)); } @Test void shouldDecodePlpNull() { TypeInformation integerType = TypeInformation.builder().withServerType(SqlServerType.INTEGER).withLengthStrategy(LengthStrategy.BYTELENTYPE).build(); TypeInformation plpType = TypeInformation.builder().withServerType(SqlServerType.VARCHARMAX).withLengthStrategy(LengthStrategy.PARTLENTYPE).withCharset(ServerCharset.CP1252.charset()).build(); Column id = new Column(0, "id", integerType); Column content = new Column(1, "content", plpType); ColumnMetadataToken columns = ColumnMetadataToken.create(new Column[]{id, content}); ByteBuf rowData = HexUtils.decodeToByteBuf("02 04 01 00 00 00"); NbcRowToken row = NbcRowToken.decode(rowData, columns.getColumns()); assertThat(row.getColumnData(0).readableBytes()).isEqualTo(5); assertThat(row.getColumnData(1)).isNull(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/token/OrderTokenUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.util.HexUtils; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link OrderToken}. * * @author Mark Paluch */ class OrderTokenUnitTests { @Test void shouldDecodeOrderToken() { ByteBuf buffer = HexUtils.decodeToByteBuf("a9 02 00 02 00"); assertThat(buffer.readByte()).isEqualTo(OrderToken.TYPE); OrderToken orderToken = OrderToken.decode(buffer); assertThat(orderToken.getOrderByColumns()).containsOnly(2); } @Test void canDecodeShouldReportDecodability() { String data = "02 00 02 00"; CanDecodeTestSupport.testCanDecode(HexUtils.decodeToByteBuf(data), OrderToken::canDecode); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/token/PreloginUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import io.r2dbc.mssql.message.header.Type; import io.r2dbc.mssql.message.tds.ContextualTdsFragment; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.TestByteBufAllocator; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link Prelogin}. * * @author Mark Paluch */ final class PreloginUnitTests { @Test void shouldUsePreloginHeader() { Prelogin prelogin = Prelogin.builder().build(); Mono.just(prelogin.encode(TestByteBufAllocator.TEST, 0)) .cast(ContextualTdsFragment.class) .as(StepVerifier::create) .consumeNextWith(actual -> { assertThat(actual.getHeaderOptions().getType()).isEqualTo(Type.PRE_LOGIN); }).verifyComplete(); } @Test void shouldEncodePrelogin() { Prelogin prelogin = Prelogin.builder().build(); ByteBuf buffer = Unpooled.buffer(32); prelogin.encode(buffer); String result = ByteBufUtil.prettyHexDump(buffer); // Version header at offset 10, length 6 assertThat(result).containsSequence("00 00 10 00 06"); } @Test void shouldEncodePreloginVersion() { Prelogin.Version token = new Prelogin.Version(0, 0); assertThat(token.getTokenHeaderLength()).isEqualTo(5); assertThat(token.getDataLength()).isEqualTo(6); ByteBuf buffer = Unpooled.buffer(11); token.encodeToken(buffer, 0); token.encodeStream(buffer); // Token header sequence assertThat(buffer.array()).startsWith(0, 0, 0, 0, 6); // Token data sequence assertThat(buffer.array()).endsWith(0, 0, 0, 0, 0, 0); } @Test void shouldEncodeEncryption() { Prelogin.Encryption token = new Prelogin.Encryption(Prelogin.Encryption.ENCRYPT_NOT_SUP); assertThat(token.getTokenHeaderLength()).isEqualTo(5); assertThat(token.getDataLength()).isEqualTo(1); ByteBuf buffer = Unpooled.buffer(6); token.encodeToken(buffer, 0); token.encodeStream(buffer); // Token header sequence assertThat(buffer.array()).startsWith(1, 0, 0, 0, 1); // Token data sequence assertThat(buffer.array()).endsWith(Prelogin.Encryption.ENCRYPT_NOT_SUP); } @Test void shouldDecodePreloginResponse() { // Server version 14.0.3038.14 // Encryption disabled // Instance validation 0x00 String response = "00 00 10 00 06 01 00 16 00 01 02 00 17 00 01 ff 0e 00 0b de 00 00 02 00"; ByteBuf buffer = HexUtils.decodeToByteBuf(response); Prelogin prelogin = Prelogin.decode(buffer); assertThat(prelogin.getTokens()).hasSize(4); assertThat(prelogin.getToken(Prelogin.Version.class)).isNotEmpty().hasValueSatisfying(actual -> { assertThat(actual.getVersion()).isEqualTo(14); assertThat(actual.getSubbuild()).isEqualTo((short) 3038); }); assertThat(prelogin.getToken(Prelogin.Encryption.class)).isNotEmpty().hasValueSatisfying(actual -> { assertThat(actual.getEncryption()).isEqualTo(Prelogin.Encryption.ENCRYPT_NOT_SUP); }); assertThat(prelogin.getToken(Prelogin.Terminator.class)).hasValue(Prelogin.Terminator.INSTANCE); } @Test void decodeShouldConsumeRemainingBytes() { String response = "00 00 10 00 06 01 00 16 00 01 02 00 17 00 01 ff 0e 00 0b de 00 00 02 00 01 02 03 04"; ByteBuf buffer = HexUtils.decodeToByteBuf(response); Prelogin.decode(buffer); assertThat(buffer.readerIndex()).isEqualTo(buffer.writerIndex()); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/token/ReturnValueUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.util.EncodedAssert; import io.r2dbc.mssql.util.HexUtils; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link ReturnValue}. * * @author Mark Paluch */ class ReturnValueUnitTests { @Test void shouldDecode() { ByteBuf buffer = HexUtils.decodeToByteBuf("AC0000000100000000000026" + "0404F3DEBC0A"); assertThat(buffer.readByte()).isEqualTo(ReturnValue.TYPE); ReturnValue returnValue = ReturnValue.decode(buffer, false); assertThat(returnValue.getOrdinal()).isEqualTo(0); assertThat(returnValue.getParameterName()).isEqualTo(""); assertThat(returnValue.getStatus()).isEqualTo((byte) 1); assertThat(returnValue.getValueType().getServerType()).isEqualTo(SqlServerType.INTEGER); assertThat(returnValue.getValueType().getLengthStrategy()).isEqualTo(LengthStrategy.BYTELENTYPE); assertThat(returnValue.getValueType().getPrecision()).isEqualTo(10); assertThat(buffer.readerIndex()).isEqualTo(buffer.writerIndex()); EncodedAssert.assertThat(returnValue.getValue()).isEncodedAs(expected -> { expected.writeByte(4); // length expected.writeIntLE(180150003); }); } @Test void canDecodeShouldReportDecodability() { String data = "00000001000000000000260404F3DEBC0A"; CanDecodeTestSupport.testCanDecode(HexUtils.decodeToByteBuf(data), buffer -> ReturnValue.canDecode(buffer, true)); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/token/RowTokenFactory.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.util.TestByteBufAllocator; import java.util.List; import java.util.function.Consumer; /** * Utility to create a {@link RowToken}. * * @author Mark Paluch */ public final class RowTokenFactory { /** * Creates a {@link RowToken} using {@link ColumnMetadataToken} and an encoded data buffer. * * @param columnMetadata * @param bufferWriter * @return */ public static RowToken create(ColumnMetadataToken columnMetadata, Consumer bufferWriter) { return create(columnMetadata.getColumns(), bufferWriter); } /** * Creates a {@link RowToken} using {@link Column}s and an encoded data buffer. * * @param columns * @param bufferWriter * @return */ public static RowToken create(List columns, Consumer bufferWriter) { ByteBuf buffer = TestByteBufAllocator.TEST.buffer(); bufferWriter.accept(buffer); return RowToken.decode(buffer, columns.toArray(new Column[0])); } /** * Creates a {@link RowToken} using {@link Column}s and an encoded data buffer. * * @param columns * @param bufferWriter * @return */ public static RowToken create(Column[] columns, Consumer bufferWriter) { ByteBuf buffer = TestByteBufAllocator.TEST.buffer(); bufferWriter.accept(buffer); return RowToken.decode(buffer, columns); } private RowTokenFactory() { } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/token/RowTokenUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.message.tds.ServerCharset; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; import io.r2dbc.mssql.util.HexUtils; import org.junit.jupiter.api.Test; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link RowToken}. * * @author Mark Paluch */ class RowTokenUnitTests { @Test void shouldDecodeRow() { ByteBuf buffer = HexUtils.decodeToByteBuf("8107000000000000" + "000800300B65006D0070006C006F0079" + "00650065005F00690064000000000008" + "00E764000904D00034096C0061007300" + "74005F006E0061006D00650000000000" + "0900A732000904D000340A6600690072" + "00730074005F006E0061006D00650000" + "00000009006E0806730061006C006100" + "7200790000000000090024100366006F" + "006F000000000009006D080366006C00" + "74000000000009006D04036200610072" + "00D1010C00700061006C007500630068" + "0004006D61726B080000000020A10700" + "10F17B0DC7C7E5C54098C7A12F7E6867" + "2408FED478E94628C6400437423146"); Tabular tabular = Tabular.decode(buffer, true); assertThat(tabular.getTokens()).hasSize(2); RowToken rowToken = tabular.getRequiredToken(RowToken.class); assertThat(rowToken.getColumnData(0)).isNotNull(); assertThat(rowToken.getColumnData(1)).isNotNull(); assertThat(rowToken.getColumnData(2)).isNotNull(); assertThat(rowToken.getColumnData(3)).isNotNull(); buffer.release(); } @Test void canDecodeShouldReportDecodability() { ByteBuf rowMetadata = HexUtils.decodeToByteBuf("8107000000000000" + "000800300B65006D0070006C006F0079" + "00650065005F00690064000000000008" + "00E764000904D00034096C0061007300" + "74005F006E0061006D00650000000000" + "0900A732000904D000340A6600690072" + "00730074005F006E0061006D00650000" + "00000009006E0806730061006C006100" + "7200790000000000090024100366006F" + "006F000000000009006D080366006C00" + "74000000000009006D04036200610072" + "00"); ColumnMetadataToken columns = ColumnMetadataToken.decode(rowMetadata.skipBytes(1), true); String row = "010C00700061006C007500630068" + "0004006D61726B080000000020A10700" + "10F17B0DC7C7E5C54098C7A12F7E6867" + "2408FED478E94628C6400437423146"; CanDecodeTestSupport.testCanDecode(HexUtils.decodeToByteBuf(row), buffer -> RowToken.canDecode(buffer, columns.getColumns())); } @Test void shouldDecodeIntAndVarcharMax() throws IOException { TypeInformation integerType = TypeInformation.builder().withServerType(SqlServerType.INTEGER).withLengthStrategy(LengthStrategy.BYTELENTYPE).build(); TypeInformation plpType = TypeInformation.builder().withServerType(SqlServerType.VARCHARMAX).withLengthStrategy(LengthStrategy.PARTLENTYPE).withCharset(ServerCharset.CP1252.charset()).build(); Column id = new Column(0, "id", integerType); Column content = new Column(1, "content", plpType); ColumnMetadataToken columns = ColumnMetadataToken.create(new Column[]{id, content}); ByteBuf rowData = loadRowData("int-varcharmax-data.txt"); RowToken row = RowToken.decode(rowData, columns.getColumns()); assertThat(row.getColumnData(0).readableBytes()).isEqualTo(5); assertThat(row.getColumnData(1).readableBytes()).isEqualTo(10016); rowData.release(); row.release(); } @Test void shouldDecodeIntAndVarcharMaxNull() { TypeInformation integerType = TypeInformation.builder().withServerType(SqlServerType.INTEGER).withLengthStrategy(LengthStrategy.BYTELENTYPE).build(); TypeInformation plpType = TypeInformation.builder().withServerType(SqlServerType.VARCHARMAX).withLengthStrategy(LengthStrategy.PARTLENTYPE).withCharset(ServerCharset.CP1252.charset()).build(); Column id = new Column(0, "id", integerType); Column content = new Column(1, "content", plpType); ColumnMetadataToken columns = ColumnMetadataToken.create(new Column[]{id, content}); ByteBuf rowData = HexUtils.decodeToByteBuf("04 01 00 00 00 FF FF FF FF FF FF FF FF"); RowToken row = RowToken.decode(rowData, columns.getColumns()); assertThat(row.getColumnData(0).readableBytes()).isEqualTo(5); assertThat(row.getColumnData(1)).isNull(); rowData.release(); row.release(); } @Test void canDecodeShouldReportPlpDecodability() throws IOException { TypeInformation integerType = TypeInformation.builder().withServerType(SqlServerType.INTEGER).withLengthStrategy(LengthStrategy.BYTELENTYPE).build(); TypeInformation plpType = TypeInformation.builder().withServerType(SqlServerType.VARCHARMAX).withLengthStrategy(LengthStrategy.PARTLENTYPE).withCharset(ServerCharset.CP1252.charset()).build(); Column id = new Column(0, "id", integerType); Column content = new Column(1, "content", plpType); ColumnMetadataToken columns = ColumnMetadataToken.create(new Column[]{id, content}); ByteBuf rowData = loadRowData("int-varcharmax-data.txt"); CanDecodeTestSupport.testCanDecode(rowData, buffer -> RowToken.canDecode(buffer, columns.getColumns())); rowData.release(); } @Test void shouldReleaseBuffersProperly() throws IOException { TypeInformation integerType = TypeInformation.builder().withServerType(SqlServerType.INTEGER).withLengthStrategy(LengthStrategy.BYTELENTYPE).build(); TypeInformation plpType = TypeInformation.builder().withServerType(SqlServerType.VARCHARMAX).withLengthStrategy(LengthStrategy.PARTLENTYPE).withCharset(ServerCharset.CP1252.charset()).build(); Column id = new Column(0, "id", integerType); Column content = new Column(1, "content", plpType); ColumnMetadataToken columns = ColumnMetadataToken.create(new Column[]{id, content}); ByteBuf rowData = loadRowData("int-varcharmax-data.txt"); RowToken row = RowToken.decode(rowData, columns.getColumns()); ByteBuf idData = row.getColumnData(0); ByteBuf contentData = row.getColumnData(1); assertThat(idData.refCnt()).isNotZero(); assertThat(contentData.refCnt()).isNotZero(); rowData.release(); row.release(); assertThat(idData.refCnt()).isZero(); assertThat(contentData.refCnt()).isZero(); } private static ByteBuf loadRowData(String resource) throws IOException { StringBuffer buffer = new StringBuffer(); try (InputStream in = RowTokenUnitTests.class.getClassLoader().getResourceAsStream(resource)) { if (in == null) { throw new FileNotFoundException(resource); } BufferedReader reader = new BufferedReader(new InputStreamReader(in)); String line; while ((line = reader.readLine()) != null) { buffer.append(line); } } return HexUtils.decodeToByteBuf(buffer.toString()); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/token/RpcRequestUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.r2dbc.mssql.codec.BlobCodec; import io.r2dbc.mssql.codec.Encoded; import io.r2dbc.mssql.codec.RpcDirection; import io.r2dbc.mssql.codec.RpcParameterContext; import io.r2dbc.mssql.message.TransactionDescriptor; import io.r2dbc.mssql.message.header.HeaderOptions; import io.r2dbc.mssql.message.header.Status; import io.r2dbc.mssql.message.header.Type; import io.r2dbc.mssql.message.tds.Encode; import io.r2dbc.mssql.message.type.Collation; import io.r2dbc.mssql.util.ClientMessageAssert; import io.r2dbc.mssql.util.HexUtils; import io.r2dbc.mssql.util.TestByteBufAllocator; import io.r2dbc.spi.Blob; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.test.StepVerifier; import java.nio.ByteBuffer; /** * Unit tests for {@link RpcRequest}. * * @author Mark Paluch */ class RpcRequestUnitTests { @Test void shouldEncodeSpCursorOpen() { int SCROLLOPT_FAST_FORWARD = 16; int CCOPT_READ_ONLY = 1; int CCOPT_ALLOW_DIRECT = 8192; int resultSetScrollOpt = SCROLLOPT_FAST_FORWARD; int resultSetCCOpt = CCOPT_READ_ONLY | CCOPT_ALLOW_DIRECT; Collation collation = Collation.from(13632521, 52); RpcRequest rpcRequest = RpcRequest.builder() // .withProcId(RpcRequest.Sp_CursorOpen) // .withTransactionDescriptor(TransactionDescriptor.empty()) .withParameter(RpcDirection.OUT, 0) // cursor .withParameter(RpcDirection.IN, collation, "SELECT * FROM my_table") .withParameter(RpcDirection.IN, resultSetScrollOpt) // scrollopt .withParameter(RpcDirection.IN, resultSetCCOpt) // ccopt .withParameter(RpcDirection.OUT, 0) // rowcount .build(); String hex = "00 01 26 04 04 00 00 00 00 00 00 E7" + "40 1f 09 04 D0 00 34 2C 00 53 00 45 00 4C 00 45" + "00 43 00 54 00 20 00 2A 00 20 00 46 00 52 00 4F" + "00 4D 00 20 00 6D 00 79 00 5F 00 74 00 61 00 62" + "00 6C 00 65 00 00 00 26 04 04 10 00 00 00 00 00" + "26 04 04 01 20 00 00 00 01 26 04 04 00 00 00 00"; ClientMessageAssert.assertThat(rpcRequest).encoded() .hasHeader(HeaderOptions.create(Type.RPC, Status.empty())) .isEncodedAs(expected -> { AllHeaders.transactional(TransactionDescriptor.empty(), 1).encode(expected); Encode.uShort(expected, 0xFFFF); // proc Id switch Encode.uShort(expected, 0x02); // proc Id Encode.asByte(expected, 0); // option flag Encode.asByte(expected, 0); // status flag expected.writeBytes(HexUtils.decodeToByteBuf(hex)); // encoded parameters }); } @Test void shouldEncodeStream() { int SCROLLOPT_FAST_FORWARD = 16; int CCOPT_READ_ONLY = 1; int CCOPT_ALLOW_DIRECT = 8192; int resultSetScrollOpt = SCROLLOPT_FAST_FORWARD; int resultSetCCOpt = CCOPT_READ_ONLY | CCOPT_ALLOW_DIRECT; Collation collation = Collation.from(13632521, 52); Blob blob = Blob.from(Flux.range(0, 10) .map(it -> String.format("X%05d 0123456789", it)) .map(String::getBytes) .map(ByteBuffer::wrap)); Encoded encoded = BlobCodec.INSTANCE.encode(TestByteBufAllocator.TEST, RpcParameterContext.in(), blob); RpcRequest rpcRequest = RpcRequest.builder() // .withProcId(RpcRequest.Sp_CursorOpen) // .withTransactionDescriptor(TransactionDescriptor.empty()) .withParameter(RpcDirection.OUT, 0) // cursor .withParameter(RpcDirection.IN, collation, "SELECT * FROM my_table") .withParameter(RpcDirection.IN, resultSetScrollOpt) // scrollopt .withParameter(RpcDirection.IN, resultSetCCOpt) // ccopt .withParameter(RpcDirection.OUT, 0) // rowcount .withParameter(RpcDirection.IN, encoded) .build(); Flux.from(rpcRequest.encode(TestByteBufAllocator.TEST, 10)) .doOnNext(it -> it.getByteBuf().release()) .as(StepVerifier::create) .expectNextCount(12) .verifyComplete(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/token/SqlBatchUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.r2dbc.mssql.message.TransactionDescriptor; import io.r2dbc.mssql.message.header.HeaderOptions; import io.r2dbc.mssql.message.header.Status; import io.r2dbc.mssql.message.header.Type; import io.r2dbc.mssql.message.tds.Encode; import org.junit.jupiter.api.Test; import static io.r2dbc.mssql.util.ClientMessageAssert.assertThat; /** * Unit tests for {@link SqlBatch}. * * @author Mark Paluch */ class SqlBatchUnitTests { @Test void shouldEncodeProperly() { SqlBatch batch = SqlBatch.create(1, TransactionDescriptor.empty(), "SELECT * FROM employees;"); assertThat(batch).encoded() // .hasHeader(HeaderOptions.create(Type.SQL_BATCH, Status.empty())) // .isEncodedAs(it -> { Encode.dword(it, 22); // Total header length Encode.dword(it, 18); // MARS header length Encode.uShort(it, 2); // Tx Descriptor header it.writeBytes(new byte[8]); // Tx descriptor Encode.dword(it, 1); // outstanding requests Encode.unicodeStream(it, batch.getSql()); }); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/token/TabnameTokenUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.util.HexUtils; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link TabnameToken}. * * @author Mark Paluch */ class TabnameTokenUnitTests { @Test void shouldDecodeToken() { ByteBuf buffer = HexUtils.decodeToByteBuf("a413000108006d0079005f007400" + "610062006c006500"); assertThat(buffer.readByte()).isEqualTo(TabnameToken.TYPE); TabnameToken token = TabnameToken.decode(buffer); assertThat(token.getTableNames()).hasSize(1); Identifier name = token.getTableNames().get(0); assertThat(name.getObjectName()).isEqualTo("my_table"); } @Test void canDecodeShouldReportDecodability() { String data = "13000108006d0079005f007400" + "610062006c006500"; CanDecodeTestSupport.testCanDecode(HexUtils.decodeToByteBuf(data), TabnameToken::canDecode); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/token/TabularUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.token; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import io.r2dbc.mssql.util.HexUtils; import org.junit.jupiter.api.Test; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link LoginAckToken}. * * @author Mark Paluch */ final class TabularUnitTests { @Test void shouldDecodeLoginAck() { String hex = "e31b0001066d0061" + "007300740065007200066d0061007300" + "740065007200ab700045160000020025" + "004300680061006e0067006500640020" + "00640061007400610062006100730065" + "00200063006f006e0074006500780074" + "00200074006f00200027006d00610073" + "0074006500720027002e000c61003600" + "38003800300039003200610037003900" + "660035000001000000e3080007050904" + "d0003400e31700020a750073005f0065" + "006e0067006c0069007300680000ab74" + "0047160000010027004300680061006e" + "0067006500640020006c0061006e0067" + "00750061006700650020007300650074" + "00740069006e006700200074006f0020" + "00750073005f0065006e0067006c0069" + "00730068002e000c6100360038003800" + "30003900320061003700390066003500" + "0001000000ad36000174000004164d00" + "6900630072006f0073006f0066007400" + "2000530051004c002000530065007200" + "760065007200000000000e000bdee313" + "00040438003000300030000434003000" + "39003600ae040100000001fffd000000" + "000000000000000000"; ByteBuf byteBuf = Unpooled.wrappedBuffer(ByteBufUtil.decodeHexDump(hex)); Tabular tabular = Tabular.decode(byteBuf, false); assertThat(tabular.getTokens()).hasSize(9); assertThat(tabular.getRequiredToken(LoginAckToken.class)).isNotNull(); assertThat(tabular .getRequiredToken(EnvChangeToken.class, it -> it.getChangeType() == EnvChangeToken.EnvChangeType.Database) .getNewValueString()).isEqualTo("master"); } @Test void colMetadataShouldReplacePreviousMetadata() { ByteBuf firstMetadata = HexUtils.decodeToByteBuf("8107000000000000" + "000800300B65006D0070006C006F0079" + "00650065005F00690064000000000008" + "00E764000904D00034096C0061007300" + "74005F006E0061006D00650000000000" + "0900A732000904D000340A6600690072" + "00730074005F006E0061006D00650000" + "00000009006E0806730061006C006100" + "7200790000000000090024100366006F" + "006F000000000009006D080366006C00" + "74000000000009006D04036200610072" + "00"); ByteBuf nextMetadata = HexUtils.decodeToByteBuf("8102000000000000" + "00090026010c6e0075006c006c006100" + "62006c0065005f0063006f006c000000" + "00000000380752004f00570053005400" + "41005400"); ByteBuf rowData = HexUtils.decodeToByteBuf("d1014201000000"); Tabular.TabularDecoder decoder = Tabular.createDecoder(true); // initialize decoder.decode(firstMetadata); decoder.decode(nextMetadata); List rows = decoder.decode(rowData); assertThat(rows).hasSize(1); assertThat(rowData.readableBytes()).isEqualTo(0); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/type/CollationUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.type; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.util.HexUtils; import org.junit.jupiter.api.Test; import java.nio.charset.Charset; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link Collation}. * * @author Mark Paluch */ class CollationUnitTests { @Test void shouldDecodeCp1252CollationFromSortId() { ByteBuf buffer = HexUtils.decodeToByteBuf("0904D00034"); Collation collation = Collation.decode(buffer); assertThat(collation.getSortId()).isEqualTo(52); assertThat(collation.getCharset()).isEqualTo(Charset.forName("windows-1252")); } @Test void shouldDecodeEnglishCollationFromSortId() { ByteBuf buffer = HexUtils.decodeToByteBuf("0904D00000"); Collation collation = Collation.decode(buffer); assertThat(collation.getSortId()).isEqualTo(0); assertThat(collation.getCharset()).isEqualTo(Charset.forName("windows-1252")); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/message/type/TypeBuilderUnitTests.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.message.type; import io.netty.buffer.ByteBuf; import io.r2dbc.mssql.util.HexUtils; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; /** * @author Mark Paluch */ class TypeBuilderUnitTests { @Test void shouldDecodeInt() { ByteBuf buffer = HexUtils.decodeToByteBuf("000000000800380B"); TypeInformation typeInformation = TypeBuilder.decode(buffer, true); assertThat(typeInformation.getMaxLength()).isEqualTo(4); assertThat(typeInformation.getServerType()).isEqualTo(SqlServerType.INTEGER); assertThat(typeInformation.getLengthStrategy()).isEqualTo(LengthStrategy.FIXEDLENTYPE); assertThat(typeInformation.getDisplaySize()).isEqualTo(11); } @Test void canDecodeShouldCheckDecodingAbility() { ByteBuf buffer = HexUtils.decodeToByteBuf("000000000800380B"); assertThat(TypeBuilder.canDecode(buffer, true)).isTrue(); assertThat(buffer.readerIndex()).isEqualTo(0); assertThat(TypeBuilder.canDecode(HexUtils.decodeToByteBuf("000000000800"), true)).isFalse(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/util/ClientMessageAssert.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.util; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.r2dbc.mssql.message.ClientMessage; import io.r2dbc.mssql.message.header.HeaderOptions; import io.r2dbc.mssql.message.tds.ContextualTdsFragment; import io.r2dbc.mssql.message.tds.TdsFragment; import io.r2dbc.mssql.message.tds.TdsPacket; import org.assertj.core.api.AbstractObjectAssert; import org.assertj.core.api.Assertions; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import java.nio.charset.Charset; import java.util.function.Consumer; /** * Assertion utility for {@link ClientMessage}. * * @author Mark Paluch */ public final class ClientMessageAssert extends AbstractObjectAssert { private ClientMessageAssert(ClientMessage actual) { super(actual, ClientMessageAssert.class); } /** * Create an assertion for a {@link ClientMessage}. * * @param actual */ public static ClientMessageAssert assertThat(ClientMessage actual) { return new ClientMessageAssert(actual); } /** * Assert the encoded form of {@link ClientMessage}. * * @see TdsFragment */ @SuppressWarnings("unchecked") public TdsFragmentAssert encoded() { Object encoded = this.actual.encode(TestByteBufAllocator.TEST, 0); if (encoded instanceof TdsFragment) { return new TdsFragmentAssert((TdsFragment) encoded); } return new TdsFragmentAssert(Mono.from((Publisher) encoded).block()); } /** * Assertions for {@link TdsFragment}. */ public static class TdsFragmentAssert extends AbstractObjectAssert { private TdsFragmentAssert(TdsFragment actual) { super(actual, TdsFragmentAssert.class); } /** * Assert that the actual {@link TdsFragment message} contains the expected string by converting the message using the default {@link Charset}. * * @param expected the expected string */ public TdsFragmentAssert contains(String expected) { return contains(expected, Charset.defaultCharset()); } /** * Assert that the actual {@link TdsFragment message} contains the expected string by converting the message using the given {@link Charset}. * * @param expected the expected string */ public TdsFragmentAssert contains(String expected, Charset charset) { isNotNull(); new EncodedAssert(this.actual.getByteBuf()).contains(expected, charset); return this; } /** * Assert that the actual {@link TdsFragment message} is empty. */ public TdsFragmentAssert isEmpty() { Assertions.assertThat(this.actual.getByteBuf().readableBytes()).isEqualTo(0); return this; } /** * Assert that the actual {@link TdsFragment message} declares the expected {@link HeaderOptions} * * @param expected the expected {@link HeaderOptions}. */ public TdsFragmentAssert hasHeader(HeaderOptions expected) { isNotNull(); Assertions.assertThat(this.actual).isInstanceOfAny(ContextualTdsFragment.class, TdsPacket.class); if (this.actual instanceof ContextualTdsFragment) { ContextualTdsFragment contextual = (ContextualTdsFragment) this.actual; Assertions.assertThat(contextual.getHeaderOptions().getType()).isEqualTo(expected.getType()); Assertions.assertThat(contextual.getHeaderOptions().getStatus()).isEqualTo(expected.getStatus()); } return this; } /** * Assert that the actual {@link TdsFragment message} is encoded as described by the expected encoded buffer. The expected value is passed to a {@link Consumer} that writes the expectation * to the {@link ByteBuf data * buffer}. * * @param encoded expectation writer. */ public TdsFragmentAssert isEncodedAs(Consumer encoded) { isNotNull(); ByteBuf expected = TestByteBufAllocator.TEST.buffer(); encoded.accept(expected); Assertions.assertThat(ByteBufUtil.prettyHexDump(this.actual.getByteBuf())).describedAs("ByteBuf") .isEqualTo(ByteBufUtil.prettyHexDump(expected)); return this; } } } ================================================ FILE: src/test/java/io/r2dbc/mssql/util/EmbeddedChannelAssert.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.util; import io.netty.buffer.ByteBuf; import io.netty.channel.embedded.EmbeddedChannel; import org.assertj.core.api.AbstractObjectAssert; import org.assertj.core.api.Assertions; import java.util.Queue; /** * Assertion utility for {@link EmbeddedChannel} to verify channel interaction. * * @author Mark Paluch */ public final class EmbeddedChannelAssert extends AbstractObjectAssert { private EmbeddedChannelAssert(EmbeddedChannel actual) { super(actual, EmbeddedChannelAssert.class); } /** * Create an assertion for an {@link EmbeddedChannel}. * * @param actual the channel. */ public static EmbeddedChannelAssert assertThat(EmbeddedChannel actual) { return new EmbeddedChannelAssert(actual); } /** * Assert inbound (received) messages. */ public MessagesAssert inbound() { return new MessagesAssert("inbound", this.actual.inboundMessages()); } /** * Assert outbound (sent) messages. */ public MessagesAssert outbound() { return new MessagesAssert("outbound", this.actual.outboundMessages()); } /** * Assertions for messages of the embedded channel. */ public static final class MessagesAssert extends AbstractObjectAssert> { private final String direction; private MessagesAssert(String direction, Queue actual) { super(actual, MessagesAssert.class); this.direction = direction; } /** * Assert that the message contains {@link ByteBuf} messages. */ public EncodedAssert hasByteBufMessage() { isNotNull(); Object poll = this.actual.poll(); Assertions.assertThat(poll).describedAs(this.direction + " message").isNotNull().isInstanceOf(ByteBuf.class); return new EncodedAssert((ByteBuf) poll); } } } ================================================ FILE: src/test/java/io/r2dbc/mssql/util/EncodedAssert.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.util; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.r2dbc.mssql.codec.Encoded; import org.assertj.core.api.AbstractObjectAssert; import org.assertj.core.api.Assertions; import java.nio.charset.Charset; import java.util.function.Consumer; /** * Assertions for {@link ByteBuf}. * * @author Mark Paluch */ public final class EncodedAssert extends AbstractObjectAssert { EncodedAssert(ByteBuf actual) { super(actual, EncodedAssert.class); } /** * Create an assertion for a {@link ByteBuf}. * * @param actual */ public static EncodedAssert assertThat(ByteBuf actual) { return new EncodedAssert(actual); } /** * Create an assertion for a {@link ByteBuf}. * * @param actual */ public static EncodedAssert assertThat(Encoded actual) { return new EncodedAssert(actual.getValue()); } /** * Assert that the actual {@link ByteBuf data buffer} contains the expected string by converting the buffer using the default {@link Charset}. * * @param expected the expected string. */ public EncodedAssert contains(String expected) { return contains(expected, Charset.defaultCharset()); } /** * Assert that the actual {@link ByteBuf data buffer} contains the expected string by converting the buffer using the given {@link Charset}. * * @param expected the expected string. * @param charset the charset to use. */ public EncodedAssert contains(String expected, Charset charset) { isNotNull(); String actual = this.actual.toString(charset); Assertions.assertThat(actual).contains(expected); return this; } /** * Assert that the actual {@link ByteBuf data buffer} is equal to the expected hexadecimal representation. * * @param expected the expected value represented as hexadecimal characters. */ public EncodedAssert isEqualToHex(String expected) { isNotNull(); Assertions.assertThat(ByteBufUtil.prettyHexDump(this.actual)).describedAs("ByteBuf") .isEqualTo(ByteBufUtil.prettyHexDump(HexUtils.decodeToByteBuf(expected))); return this; } /** * Assert that the actual {@link ByteBuf data buffer} is empty (i.e. contains no readable bytes). */ public void isEmpty() { Assertions.assertThat(this.actual.readableBytes()).isEqualTo(0); } /** * Assert that the actual {@link ByteBuf data buffer} is encoded as described by the expected encoded buffer. The expected value is passed to a {@link Consumer} that writes the expectation to * the {@link ByteBuf data * buffer}. * * @param encoded expectation writer. */ public EncodedAssert isEncodedAs(Consumer encoded) { isNotNull(); ByteBuf expected = TestByteBufAllocator.TEST.buffer(); encoded.accept(expected); Assertions.assertThat(ByteBufUtil.prettyHexDump(this.actual)).describedAs("ByteBuf") .isEqualTo(ByteBufUtil.prettyHexDump(expected)); ReferenceCountUtil.maybeSafeRelease(this.actual); return this; } } ================================================ FILE: src/test/java/io/r2dbc/mssql/util/FluxDiscardOnCancelUnitTests.java ================================================ /* * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.util; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Hooks; import reactor.test.StepVerifier; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.IntStream; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link FluxDiscardOnCancel}. * * @author Mark Paluch */ class FluxDiscardOnCancelUnitTests { @Test void shouldEmitAllItemsOnSubscription() { Iterator items = createItems(4); Flux.fromIterable(() -> items) .as(Operators::discardOnCancel) .as(StepVerifier::create) .expectNext(0, 1, 2, 3) .verifyComplete(); } @Test @SuppressWarnings("unchecked") void considersAssemblyHook() { List publishers = new ArrayList<>(); Hooks.onEachOperator(objectPublisher -> { publishers.add(objectPublisher); return objectPublisher; }); Iterator items = createItems(4); Flux.fromIterable(() -> items) .transform(Operators::discardOnCancel) .as(StepVerifier::create) .expectNextCount(4) .verifyComplete(); assertThat(publishers).hasSize(2).extracting(Object::getClass).contains((Class) FluxDiscardOnCancel.class); } @Test void considersOnDropHook() { List discard = new ArrayList<>(); Iterator items = createItems(4); Flux.fromIterable(() -> items) .as(Operators::discardOnCancel) .doOnDiscard(Object.class, discard::add) .as(it -> StepVerifier.create(it, 0)) .thenRequest(2) .expectNext(0, 1) .thenCancel() .verify(); assertThat(discard).containsOnly(2, 3); } @Test void considersCancelSignalPropagation() { AtomicBoolean cancelled = new AtomicBoolean(); Iterator items = createItems(4); Flux.fromIterable(() -> items) .as(it -> Operators.discardOnCancel(it, () -> cancelled.set(true))) .as(it -> StepVerifier.create(it, 0)) .thenRequest(2) .expectNext(0, 1) .thenCancel() .verify(); assertThat(cancelled).isTrue(); } @Test void shouldNotConsumeItemsOnCancel() { Iterator items = createItems(4); Flux.fromIterable(() -> items) .as(it -> StepVerifier.create(it, 0)) .thenRequest(2) .expectNext(0, 1) .thenCancel() .verify(); assertThat(items).toIterable().containsOnly(3); } @Test void shouldConsumeAndDiscardItemsOnCancel() { Iterator items = createItems(4); Flux.fromIterable(() -> items) .as(Operators::discardOnCancel) .as(it -> StepVerifier.create(it, 0)) .thenRequest(2) .expectNext(0, 1) .thenCancel() .verify(); assertThat(items).toIterable().isEmpty(); } static Iterator createItems(int count) { return IntStream.range(0, count).boxed().iterator(); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/util/HexUtils.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.util; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; /** * Utilities for working with {@link ByteBuf}s. */ public final class HexUtils { private HexUtils() { } /** * Decode a {@link String} containing Hex-encoded bytes into a {@link ByteBuf}. * * @param chars the {@link String} to decode * @return the {@link ByteBuf} decoded from the {@link String} */ public static ByteBuf decodeToByteBuf(String chars) { Assert.requireNonNull(chars, "String must not be null"); return Unpooled.wrappedBuffer(ByteBufUtil.decodeHexDump(chars.replaceAll(" ", ""))); } } ================================================ FILE: src/test/java/io/r2dbc/mssql/util/IntegrationTestSupport.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.util; import io.r2dbc.mssql.MssqlConnection; import io.r2dbc.mssql.MssqlConnectionFactory; import io.r2dbc.mssql.MssqlConnectionFactoryProvider; import io.r2dbc.spi.ConnectionFactories; import io.r2dbc.spi.ConnectionFactoryOptions; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.RegisterExtension; import reactor.test.StepVerifier; import java.util.function.Predicate; import static io.r2dbc.spi.ConnectionFactoryOptions.*; /** * Support class for integration tests. * * @author Mark Paluch */ public abstract class IntegrationTestSupport { @RegisterExtension protected static final MsSqlServerExtension SERVER = new MsSqlServerExtension(); protected static MssqlConnectionFactory connectionFactory; protected static MssqlConnection connection; @BeforeAll static void beforeAll() { ConnectionFactoryOptions options = builder().build(); connectionFactory = (MssqlConnectionFactory) ConnectionFactories.get(options); connection = connectionFactory.create().block(); } public static ConnectionFactoryOptions.Builder builder() { Predicate preferCursoredExecution = sql -> sql.contains("cursored"); return ConnectionFactoryOptions.builder() .option(DRIVER, MssqlConnectionFactoryProvider.MSSQL_DRIVER) .option(HOST, SERVER.getHost()) .option(PORT, SERVER.getPort()) .option(PASSWORD, SERVER.getPassword()) .option(USER, SERVER.getUsername()) .option(MssqlConnectionFactoryProvider.PREFER_CURSORED_EXECUTION, preferCursoredExecution); } @BeforeEach void setUp() { connection.setAutoCommit(true).as(StepVerifier::create).verifyComplete(); } @AfterAll static void afterAll() { System.out.println("close"); if (connection != null) { connection.close().subscribe(); } } } ================================================ FILE: src/test/java/io/r2dbc/mssql/util/MsSqlServerExtension.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.util; import com.zaxxer.hikari.HikariDataSource; import io.r2dbc.mssql.MssqlConnectionConfiguration; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.JdbcTemplate; import org.testcontainers.containers.JdbcDatabaseContainer; import org.testcontainers.containers.MSSQLServerContainer; import reactor.util.annotation.Nullable; import java.io.IOException; import java.net.Socket; import java.util.function.Supplier; /** * Test container extension for Microsoft SQL Server. */ @SuppressWarnings({"rawtypes"}) public final class MsSqlServerExtension implements BeforeAllCallback, AfterAllCallback { private volatile MSSQLServerContainer containerInstance = null; private final Supplier> container = () -> { if (this.containerInstance != null) { return this.containerInstance; } return this.containerInstance = new MSSQLServerContainer("mcr.microsoft.com/mssql/server:2022-latest") { protected void configure() { this.addExposedPort(MS_SQL_SERVER_PORT); this.addEnv("ACCEPT_EULA", "Y"); this.addEnv("SA_PASSWORD", getPassword()); this.withReuse(true); } }; }; private HikariDataSource dataSource; private JdbcOperations jdbcOperations; private final DatabaseContainer sqlServer = External.INSTANCE.isAvailable() ? External.INSTANCE : new TestContainer(this.container.get()); private final boolean useTestContainer = this.sqlServer instanceof TestContainer; @Override public void beforeAll(ExtensionContext context) { initialize(); } public void initialize() { if (this.useTestContainer) { this.container.get().start(); } HikariDataSource hikariDataSource = new HikariDataSource(); hikariDataSource.setJdbcUrl("jdbc:sqlserver://" + getHost() + ":" + getPort() + ";database=master;sendStringParametersAsUnicode=true;encrypt=false"); hikariDataSource.setUsername(getUsername()); hikariDataSource.setPassword(getPassword()); this.dataSource = hikariDataSource; this.dataSource.setMaximumPoolSize(1); this.jdbcOperations = new JdbcTemplate(this.dataSource); } @Override public void afterAll(ExtensionContext context) { } public MssqlConnectionConfiguration.Builder configBuilder() { return MssqlConnectionConfiguration.builder().host(getHost()).port(getPort()).username(getUsername()).password(getPassword()); } public MssqlConnectionConfiguration getConnectionConfiguration() { return configBuilder().build(); } public HikariDataSource getDataSource() { return this.dataSource; } @Nullable public JdbcOperations getJdbcOperations() { return this.jdbcOperations; } public String getHost() { return this.sqlServer.getHost(); } public int getPort() { return this.sqlServer.getPort(); } public String getUsername() { return this.sqlServer.getUsername(); } public String getPassword() { return this.sqlServer.getPassword(); } /** * Interface to be implemented by database providers (provided database, test container). */ interface DatabaseContainer { String getHost(); int getPort(); String getUsername(); String getPassword(); } /** * Externally provided SQL Server instance. */ static class External implements DatabaseContainer { public static final External INSTANCE = new External(); @Override public String getHost() { return "localhost"; } @Override public int getPort() { return 1433; } @Override public String getUsername() { return "sa"; } @Override public String getPassword() { return "A_Str0ng_Required_Password"; } /** * Returns whether this container is available. */ @SuppressWarnings("try") boolean isAvailable() { try (Socket ignored = new Socket(getHost(), getPort())) { return true; } catch (IOException e) { return false; } } } /** * {@link DatabaseContainer} provided by {@link JdbcDatabaseContainer}. */ static class TestContainer implements DatabaseContainer { private final JdbcDatabaseContainer container; TestContainer(JdbcDatabaseContainer container) { this.container = container; } @Override public String getHost() { return this.container.getContainerIpAddress(); } @Override public int getPort() { return this.container.getMappedPort(MSSQLServerContainer.MS_SQL_SERVER_PORT); } @Override public String getUsername() { return this.container.getUsername(); } @Override public String getPassword() { return this.container.getPassword(); } } } ================================================ FILE: src/test/java/io/r2dbc/mssql/util/TestByteBufAllocator.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.util; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.UnpooledByteBufAllocator; import io.netty.util.internal.PlatformDependent; /** * {@link ByteBufAllocator} used for tests. */ public final class TestByteBufAllocator { public static final ByteBufAllocator TEST = new UnpooledByteBufAllocator(PlatformDependent.directBufferPreferred(), true); private TestByteBufAllocator() { } } ================================================ FILE: src/test/java/io/r2dbc/mssql/util/Types.java ================================================ /* * Copyright 2018-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.r2dbc.mssql.util; import io.r2dbc.mssql.message.type.LengthStrategy; import io.r2dbc.mssql.message.type.SqlServerType; import io.r2dbc.mssql.message.type.TypeInformation; /** * Type collection for testing. * * @author Mark Paluch */ public class Types { private static final TypeInformation integer = TypeInformation.builder().withScale(4).withMaxLength(4).withLengthStrategy(LengthStrategy.BYTELENTYPE).withServerType(SqlServerType.INTEGER).build(); public static TypeInformation integer() { return integer; } public static TypeInformation varchar(int length) { return TypeInformation.builder().withServerType(SqlServerType.VARCHAR).withLengthStrategy(LengthStrategy.USHORTLENTYPE).withMaxLength(length).build(); } private Types() { } } ================================================ FILE: src/test/resources/int-varcharmax-data.txt ================================================ 04 01 00 00 00 10 27 00 00 00 00 00 00 F7 1E 00 00 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 19 08 00 00 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 00 00 00 00 ================================================ FILE: src/test/resources/logback-test.xml ================================================ %d{yyyy-MM-dd} | %d{HH:mm:ss.SSS} | %-20.20thread | %5p | %logger{25} | %m%n