Skip to content

Commit

Permalink
Add support for image, binary, varbinary, and varbinary(max)
Browse files Browse the repository at this point in the history
We now support decoding binary data with fixed, variable, and PLP
length strategies.

[resolves #3]
  • Loading branch information
mp911de committed Apr 3, 2019
1 parent 8efb1a8 commit 3b554df
Show file tree
Hide file tree
Showing 7 changed files with 459 additions and 18 deletions.
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@ This driver provides the following features:
* 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)`, and national variants, see below for exceptions)
* Extensive type support (including `TEXT`, `VARCHAR(MAX)`, `IMAGE`, `VARBINARY(MAX)` and national variants, see below for exceptions)

Next steps:

* Add encoding for remaining codecs (VARBINARY, XML, UDT)
* Execution of stored procedures
* Add support for TVP and UDTs

Expand Down Expand Up @@ -158,7 +157,7 @@ This reference table shows the type mapping between [Microsoft SQL Server][m] an
| [`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[]`
| [`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]
Expand All @@ -169,10 +168,10 @@ This reference table shows the type mapping between [Microsoft SQL Server][m] an
| [`nvarcharmax`][sql-n(var)char-ref] | [`String`][java-string-ref]
| [`text`][sql-(n)text-ref] | [`String`][java-string-ref]
| [`ntext`][sql-(n)text-ref] | [`String`][java-string-ref]
| [`image`][sql-(n)text-ref] | Not yet supported.
| [`binary`][sql-binary-ref] | Not yet supported.
| [`varbinary`][sql-binary-ref] | Not yet supported.
| [`varbinarymax`][sql-binary-ref] | Not yet supported.
| [`image`][sql-(n)text-ref] | [**`byte[]`**][java-byte-ref], [`ByteBuffer`][java-ByteBuffer-ref]
| [`binary`][sql-binary-ref] | [**`byte[]`**][java-byte-ref], [`ByteBuffer`][java-ByteBuffer-ref]
| [`varbinary`][sql-binary-ref] | [**`byte[]`**][java-byte-ref], [`ByteBuffer`][java-ByteBuffer-ref]
| [`varbinarymax`][sql-binary-ref] | [**`byte[]`**][java-byte-ref], [`ByteBuffer`][java-ByteBuffer-ref]
| [`sql_variant`][sql-sql-variant-ref] | Not yet supported.
| [`xml`][sql-xml-ref] | Not yet supported.
| [`udt`][sql-udt-ref] | Not yet supported.
Expand All @@ -181,8 +180,8 @@ This reference table shows the type mapping between [Microsoft SQL Server][m] an

Types in **bold** indicate the native (default) Java type.

**Note:** `text`, `ntext`, `varchar(max)` and `nvarchar(max)` values are fully materialized in the client before decoding.
Make sure to account for proper memory sizing.
**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
Expand Down Expand Up @@ -212,6 +211,7 @@ Make sure to account for proper memory sizing.
[java-bigdecimal-ref]: https://docs.oracle.com/javase/8/docs/api/java/math/BigDecimal.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
Expand Down
183 changes: 183 additions & 0 deletions src/main/java/io/r2dbc/mssql/codec/BinaryCodec.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF 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.LengthStrategy;
import io.r2dbc.mssql.message.type.PlpLength;
import io.r2dbc.mssql.message.type.SqlServerType;
import io.r2dbc.mssql.message.type.TdsDataType;
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.ByteBuffer;
import java.util.EnumSet;
import java.util.Set;

/**
* Codec for binary values that are represented as {@code byte[]} or {@link ByteBuffer}.
*
* <ul>
* <li>Server types: {@link SqlServerType#BINARY}, {@link SqlServerType#VARBINARY}, {@link SqlServerType#VARBINARYMAX}, and {@link SqlServerType#IMAGE}.</li>
* <li>Java type: {@code byte[]}, {@link ByteBuffer}</li>
* <li>Downcast: none</li>
* </ul>
*
* @author Mark Paluch
*/
public class BinaryCodec implements Codec<Object> {

/**
* Singleton instance.
*/
public static final BinaryCodec INSTANCE = new BinaryCodec();

private static final Set<SqlServerType> 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");

ByteBuf buffer;

if (value instanceof byte[]) {

byte[] bytes = (byte[]) value;
buffer = RpcEncoding.prepareBuffer(allocator, TdsDataType.BIGVARBINARY.getLengthStrategy(), SqlServerType.VARBINARY.getMaxLength(), bytes.length);
buffer.writeBytes(bytes);
} else {

ByteBuffer bytes = (ByteBuffer) value;
buffer = RpcEncoding.prepareBuffer(allocator, TdsDataType.BIGVARBINARY.getLengthStrategy(), SqlServerType.VARBINARY.getMaxLength(), bytes.remaining());
buffer.writeBytes(bytes);
}

return new VarbinaryEncoded(TdsDataType.BIGVARBINARY, buffer);
}

@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<Object> getType() {
return (Class) byte[].class;
}

@Override
public Encoded encodeNull(ByteBufAllocator allocator) {

Assert.requireNonNull(allocator, "ByteBufAllocator must not be null");

ByteBuf buffer = allocator.buffer(4);
Encode.uShort(buffer, SqlServerType.VARBINARY.getMaxLength());
Encode.uShort(buffer, Length.USHORT_NULL);

return new VarbinaryEncoded(TdsDataType.BIGVARBINARY, buffer);
}

@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<? extends Object> 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<? extends Object> valueType) {

if (valueType.isAssignableFrom(byte[].class)) {

byte[] bytes = new byte[length.getLength()];
buffer.readBytes(bytes);
return bytes;
}

ByteBuffer bytes = ByteBuffer.allocate(length.getLength());
buffer.readBytes(bytes);
bytes.flip();
return bytes;
}

static class VarbinaryEncoded extends RpcEncoding.HintedEncoded {

private static final String FORMAL_TYPE = SqlServerType.VARBINARY + "(" + TypeUtils.SHORT_VARTYPE_MAX_BYTES + ")";

VarbinaryEncoded(TdsDataType dataType, ByteBuf value) {
super(dataType, SqlServerType.VARBINARY, value);
}

@Override
public String getFormalType() {
return FORMAL_TYPE;
}
}
}
1 change: 1 addition & 0 deletions src/main/java/io/r2dbc/mssql/codec/DefaultCodecs.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public DefaultCodecs() {

// Prioritized Codecs
StringCodec.INSTANCE,
BinaryCodec.INSTANCE,

BooleanCodec.INSTANCE,
ByteCodec.INSTANCE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public enum SqlServerType {
NVARCHARMAX(Category.LONG_NCHARACTER, "nvarchar"),
NTEXT(Category.LONG_NCHARACTER, "ntext", TdsDataType.NTEXT),
BINARY(Category.BINARY, "binary"),
VARBINARY(Category.BINARY, "varbinary"),
VARBINARY(Category.BINARY, "varbinary", 8000, TdsDataType.BIGVARBINARY),
VARBINARYMAX(Category.LONG_BINARY, "varbinary"),
IMAGE(Category.LONG_BINARY, "image", TdsDataType.IMAGE),
DECIMAL(Category.NUMERIC, "decimal", 38, TdsDataType.DECIMALN),
Expand Down
64 changes: 56 additions & 8 deletions src/test/java/io/r2dbc/mssql/CodecIntegrationTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import reactor.test.StepVerifier;

import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
Expand All @@ -34,6 +35,8 @@
import java.util.Optional;
import java.util.UUID;

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.
*
Expand All @@ -52,7 +55,7 @@ void shouldEncodeBooleanAsBit() {

@Test
void shouldEncodeBooleanAsTinyint() {
testType(connection, "TINYINT", true, (byte) 1);
testType(connection, "TINYINT", true, Boolean.class, (byte) 1);
}

@Test
Expand Down Expand Up @@ -117,7 +120,7 @@ void shouldEncodeDateTime2() {

@Test
void shouldEncodeZonedDateTimeAsDatetimeoffset() {
testType(connection, "DATETIMEOFFSET", ZonedDateTime.parse("2018-08-27T17:41:14.890+00:45"), OffsetDateTime.parse("2018-08-27T17:41:14.890+00:45"));
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"));
}

@Test
Expand Down Expand Up @@ -160,11 +163,52 @@ void shouldEncodeStringAsNText() {
testType(connection, "NTEXT", "Hello, World! äöü");
}


@Test
void shouldEncodeByteArrayAsBinary() {
testType(connection, "BINARY(9)", "foobarbaz".getBytes());
}

@Test
void shouldEncodeByteArrayAsVarBinary() {
testType(connection, "VARBINARY(9)", "foobarbaz".getBytes());
}

@Test
void shouldEncodeByteArrayAsVarBinaryMax() {
testType(connection, "VARBINARY(MAX)", "foobarbaz".getBytes());
}

@Test
void shouldEncodeByteArrayAsImage() {
testType(connection, "IMAGE", "foobarbaz".getBytes());
}

@Test
void shouldEncodeByteBufferAsBinary() {
testType(connection, "BINARY(9)", ByteBuffer.wrap("foobarbaz".getBytes()), ByteBuffer.class, "foobarbaz".getBytes());
}

@Test
void shouldEncodeByteBufferAsVarBinary() {
testType(connection, "VARBINARY(9)", ByteBuffer.wrap("foobarbaz".getBytes()), ByteBuffer.class, "foobarbaz".getBytes());
}

@Test
void shouldEncodeByteBufferAsVarBinaryMax() {
testType(connection, "VARBINARY(MAX)", ByteBuffer.wrap("foobarbaz".getBytes()), ByteBuffer.class, "foobarbaz".getBytes());
}

@Test
void shouldEncodeByteBufferAsImage() {
testType(connection, "IMAGE", ByteBuffer.wrap("foobarbaz".getBytes()), ByteBuffer.class, "foobarbaz".getBytes());
}

private void testType(MssqlConnection connection, String columnType, Object value) {
testType(connection, columnType, value, value);
testType(connection, columnType, value, value.getClass(), value);
}

private void testType(MssqlConnection connection, String columnType, Object value, Object expectedGetObjectValue) {
private void testType(MssqlConnection connection, String columnType, Object value, Class<?> valueClass, Object expectedGetObjectValue) {

createTable(connection, columnType);

Expand All @@ -176,18 +220,22 @@ private void testType(MssqlConnection connection, String columnType, Object valu
.expectNext(1)
.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", value.getClass())))
.flatMap(it -> it.map((row, rowMetadata) -> (Object) row.get("my_col", valueClass)))
.as(StepVerifier::create)
.expectNext(value)
.consumeNextWith(actual -> assertThat(actual).isEqualTo(value))
.verifyComplete();

connection.createStatement("SELECT my_col FROM codec_test")
.execute()
.flatMap(it -> it.map((row, rowMetadata) -> row.get("my_col")))
.as(StepVerifier::create)
.expectNext(expectedGetObjectValue)
.consumeNextWith(actual -> assertThat(actual).isEqualTo(expectedGetObjectValue))
.verifyComplete();

Flux.from(connection.createStatement("UPDATE codec_test SET my_col = @P0")
Expand All @@ -200,7 +248,7 @@ private void testType(MssqlConnection connection, String columnType, Object valu

connection.createStatement("SELECT my_col FROM codec_test")
.execute()
.flatMap(it -> it.map((row, rowMetadata) -> Optional.ofNullable((Object) row.get("my_col", value.getClass()))))
.flatMap(it -> it.map((row, rowMetadata) -> Optional.ofNullable((Object) row.get("my_col", valueClass))))
.as(StepVerifier::create)
.expectNext(Optional.empty())
.verifyComplete();
Expand Down
Loading

0 comments on commit 3b554df

Please sign in to comment.