Skip to content

Commit 7316400

Browse files
feature: Add Float4Vector support - Add FloatVectorAccessor, tests, and wire up in factory (#46)
This change adds support for the Float4Vector data type in the JDBC driver, allowing proper handling of 32-bit floating point numbers.
1 parent 01ede8d commit 7316400

File tree

6 files changed

+356
-0
lines changed

6 files changed

+356
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,7 @@ pom.xml.bak
3131
bin
3232
out
3333
.worktree
34+
gradle
35+
!gradle/libs.versions.toml
36+
!gradle/wrapper/gradle-wrapper.jar
37+
!gradle/wrapper/gradle-wrapper.properties

jdbc-core/src/main/java/com/salesforce/datacloud/jdbc/core/accessor/QueryJDBCAccessorFactory.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.salesforce.datacloud.jdbc.core.accessor.impl.DateVectorAccessor;
2222
import com.salesforce.datacloud.jdbc.core.accessor.impl.DecimalVectorAccessor;
2323
import com.salesforce.datacloud.jdbc.core.accessor.impl.DoubleVectorAccessor;
24+
import com.salesforce.datacloud.jdbc.core.accessor.impl.FloatVectorAccessor;
2425
import com.salesforce.datacloud.jdbc.core.accessor.impl.LargeListVectorAccessor;
2526
import com.salesforce.datacloud.jdbc.core.accessor.impl.ListVectorAccessor;
2627
import com.salesforce.datacloud.jdbc.core.accessor.impl.TimeStampVectorAccessor;
@@ -34,6 +35,7 @@
3435
import org.apache.arrow.vector.DateMilliVector;
3536
import org.apache.arrow.vector.DecimalVector;
3637
import org.apache.arrow.vector.FixedSizeBinaryVector;
38+
import org.apache.arrow.vector.Float4Vector;
3739
import org.apache.arrow.vector.Float8Vector;
3840
import org.apache.arrow.vector.IntVector;
3941
import org.apache.arrow.vector.LargeVarBinaryVector;
@@ -79,6 +81,8 @@ public static QueryJDBCAccessor createAccessor(
7981
return new DecimalVectorAccessor((DecimalVector) vector, getCurrentRow, wasNullConsumer);
8082
} else if (arrowType.equals(Types.MinorType.BIT)) {
8183
return new BooleanVectorAccessor((BitVector) vector, getCurrentRow, wasNullConsumer);
84+
} else if (arrowType.equals(Types.MinorType.FLOAT4)) {
85+
return new FloatVectorAccessor((Float4Vector) vector, getCurrentRow, wasNullConsumer);
8286
} else if (arrowType.equals(Types.MinorType.FLOAT8)) {
8387
return new DoubleVectorAccessor((Float8Vector) vector, getCurrentRow, wasNullConsumer);
8488
} else if (arrowType.equals(Types.MinorType.TINYINT)) {
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Copyright (c) 2024, Salesforce, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.salesforce.datacloud.jdbc.core.accessor.impl;
17+
18+
import com.salesforce.datacloud.jdbc.core.accessor.QueryJDBCAccessor;
19+
import com.salesforce.datacloud.jdbc.core.accessor.QueryJDBCAccessorFactory;
20+
import com.salesforce.datacloud.jdbc.exception.DataCloudJDBCException;
21+
import java.math.BigDecimal;
22+
import java.math.RoundingMode;
23+
import java.sql.SQLException;
24+
import java.util.function.IntSupplier;
25+
import lombok.val;
26+
import org.apache.arrow.vector.Float4Vector;
27+
import org.apache.arrow.vector.holders.NullableFloat4Holder;
28+
29+
public class FloatVectorAccessor extends QueryJDBCAccessor {
30+
31+
private final Float4Vector vector;
32+
private final NullableFloat4Holder holder;
33+
34+
private static final String INVALID_VALUE_ERROR_RESPONSE = "BigDecimal doesn't support Infinite/NaN";
35+
36+
public FloatVectorAccessor(
37+
Float4Vector vector,
38+
IntSupplier currentRowSupplier,
39+
QueryJDBCAccessorFactory.WasNullConsumer setCursorWasNull) {
40+
super(currentRowSupplier, setCursorWasNull);
41+
this.holder = new NullableFloat4Holder();
42+
this.vector = vector;
43+
}
44+
45+
@Override
46+
public Class<?> getObjectClass() {
47+
return Float.class;
48+
}
49+
50+
@Override
51+
public float getFloat() {
52+
vector.get(getCurrentRow(), holder);
53+
54+
this.wasNull = holder.isSet == 0;
55+
this.wasNullConsumer.setWasNull(this.wasNull);
56+
if (this.wasNull) {
57+
return 0;
58+
}
59+
60+
return holder.value;
61+
}
62+
63+
@Override
64+
public Object getObject() {
65+
final float value = this.getFloat();
66+
67+
return this.wasNull ? null : value;
68+
}
69+
70+
@Override
71+
public String getString() {
72+
final float value = this.getFloat();
73+
return this.wasNull ? null : Float.toString(value);
74+
}
75+
76+
@Override
77+
public boolean getBoolean() {
78+
return this.getFloat() != 0.0f;
79+
}
80+
81+
@Override
82+
public byte getByte() {
83+
return (byte) this.getFloat();
84+
}
85+
86+
@Override
87+
public short getShort() {
88+
return (short) this.getFloat();
89+
}
90+
91+
@Override
92+
public int getInt() {
93+
return (int) this.getFloat();
94+
}
95+
96+
@Override
97+
public long getLong() {
98+
return (long) this.getFloat();
99+
}
100+
101+
@Override
102+
public double getDouble() {
103+
return this.getFloat();
104+
}
105+
106+
@Override
107+
public BigDecimal getBigDecimal() throws SQLException {
108+
final float value = this.getFloat();
109+
if (Float.isInfinite(value) || Float.isNaN(value)) {
110+
val rootCauseException = new UnsupportedOperationException(INVALID_VALUE_ERROR_RESPONSE);
111+
throw new DataCloudJDBCException(INVALID_VALUE_ERROR_RESPONSE, "2200G", rootCauseException);
112+
}
113+
return this.wasNull ? null : BigDecimal.valueOf(value);
114+
}
115+
116+
@Override
117+
public BigDecimal getBigDecimal(int scale) throws SQLException {
118+
final float value = this.getFloat();
119+
if (Float.isInfinite(value) || Float.isNaN(value)) {
120+
val rootCauseException = new UnsupportedOperationException(INVALID_VALUE_ERROR_RESPONSE);
121+
throw new DataCloudJDBCException(INVALID_VALUE_ERROR_RESPONSE, "2200G", rootCauseException);
122+
}
123+
return this.wasNull ? null : BigDecimal.valueOf(value).setScale(scale, RoundingMode.HALF_UP);
124+
}
125+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright (c) 2024, Salesforce, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.salesforce.datacloud.jdbc.core;
17+
18+
import static com.salesforce.datacloud.jdbc.hyper.HyperTestBase.assertWithStatement;
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
21+
import com.salesforce.datacloud.jdbc.hyper.HyperTestBase;
22+
import java.sql.ResultSet;
23+
import java.util.ArrayList;
24+
import java.util.List;
25+
import java.util.stream.Collectors;
26+
import java.util.stream.IntStream;
27+
import lombok.val;
28+
import org.junit.jupiter.api.Test;
29+
import org.junit.jupiter.api.extension.ExtendWith;
30+
31+
@ExtendWith(HyperTestBase.class)
32+
public class Float4VectorTest {
33+
34+
@Test
35+
public void testFloat4VectorQuery() {
36+
// Expected values from generate_series(1, 10, 0.5)
37+
val expectedValues =
38+
IntStream.rangeClosed(0, 18).mapToObj(i -> 1.0f + (i * 0.5f)).collect(Collectors.toList());
39+
40+
assertWithStatement(statement -> {
41+
ResultSet rs = statement.executeQuery("select i::real from generate_series(1, 10, 0.5) g(i)");
42+
43+
List<Float> actualValues = new ArrayList<>();
44+
while (rs.next()) {
45+
actualValues.add(rs.getFloat(1));
46+
}
47+
48+
assertThat(actualValues).containsExactlyElementsOf(expectedValues);
49+
});
50+
}
51+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* Copyright (c) 2024, Salesforce, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.salesforce.datacloud.jdbc.core.accessor.impl;
17+
18+
import static org.junit.jupiter.api.Assertions.assertThrows;
19+
20+
import com.google.common.collect.ImmutableList;
21+
import com.salesforce.datacloud.jdbc.core.accessor.SoftAssertions;
22+
import com.salesforce.datacloud.jdbc.exception.DataCloudJDBCException;
23+
import com.salesforce.datacloud.jdbc.util.RootAllocatorTestExtension;
24+
import com.salesforce.datacloud.jdbc.util.TestWasNullConsumer;
25+
import java.math.BigDecimal;
26+
import java.math.RoundingMode;
27+
import java.util.Collections;
28+
import java.util.List;
29+
import java.util.Random;
30+
import java.util.concurrent.atomic.AtomicInteger;
31+
import java.util.stream.Collectors;
32+
import java.util.stream.IntStream;
33+
import lombok.val;
34+
import org.assertj.core.api.ThrowingConsumer;
35+
import org.assertj.core.api.junit.jupiter.InjectSoftAssertions;
36+
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension;
37+
import org.junit.jupiter.api.BeforeAll;
38+
import org.junit.jupiter.api.Test;
39+
import org.junit.jupiter.api.extension.ExtendWith;
40+
import org.junit.jupiter.api.extension.RegisterExtension;
41+
42+
@ExtendWith(SoftAssertionsExtension.class)
43+
public class FloatVectorAccessorTest {
44+
@InjectSoftAssertions
45+
private SoftAssertions collector;
46+
47+
@RegisterExtension
48+
public static RootAllocatorTestExtension extension = new RootAllocatorTestExtension();
49+
50+
private static final int total = 10;
51+
private static final Random random = new Random(10);
52+
private static final List<Float> values = IntStream.range(0, total - 2)
53+
.mapToDouble(x -> random.nextFloat())
54+
.filter(Double::isFinite)
55+
.mapToObj(x -> (float) x)
56+
.collect(Collectors.toList());
57+
58+
@BeforeAll
59+
static void setup() {
60+
values.add(null);
61+
values.add(null);
62+
Collections.shuffle(values);
63+
}
64+
65+
private TestWasNullConsumer iterate(List<Float> values, BuildThrowingConsumer builder) {
66+
val consumer = new TestWasNullConsumer(collector);
67+
68+
try (val vector = extension.createFloat4Vector(values)) {
69+
val i = new AtomicInteger(0);
70+
val sut = new FloatVectorAccessor(vector, i::get, consumer);
71+
72+
for (; i.get() < vector.getValueCount(); i.incrementAndGet()) {
73+
val expected = values.get(i.get());
74+
val s = builder.buildSatisfies(expected);
75+
collector.assertThat(sut).satisfies(b -> s.accept((FloatVectorAccessor) b));
76+
}
77+
}
78+
79+
return consumer;
80+
}
81+
82+
private TestWasNullConsumer iterate(BuildThrowingConsumer builder) {
83+
val consumer = iterate(values, builder);
84+
consumer.assertThat().hasNullSeen(2).hasNotNullSeen(values.size() - 2);
85+
return consumer;
86+
}
87+
88+
@FunctionalInterface
89+
private interface BuildThrowingConsumer {
90+
ThrowingConsumer<FloatVectorAccessor> buildSatisfies(Float expected);
91+
}
92+
93+
@Test
94+
void testShouldGetFloatMethodFromFloat4Vector() {
95+
iterate(expected -> sut -> collector.assertThat(sut).hasFloat(expected == null ? 0.0f : expected));
96+
}
97+
98+
@Test
99+
void testShouldGetObjectMethodFromFloat4Vector() {
100+
iterate(expected -> sut -> collector.assertThat(sut).hasObject(expected));
101+
}
102+
103+
@Test
104+
void testShouldGetStringMethodFromFloat4Vector() {
105+
iterate(expected ->
106+
sut -> collector.assertThat(sut).hasString(expected == null ? null : Float.toString(expected)));
107+
}
108+
109+
@Test
110+
void testShouldGetBooleanMethodFromFloat4Vector() {
111+
iterate(expected -> sut -> collector.assertThat(sut).hasBoolean(expected != null && (expected != 0.0f)));
112+
}
113+
114+
@Test
115+
void testShouldGetByteMethodFromFloat4Vector() {
116+
iterate(expected -> sut -> collector.assertThat(sut).hasByte((byte) (expected == null ? 0.0f : expected)));
117+
}
118+
119+
@Test
120+
void testShouldGetShortMethodFromFloat4Vector() {
121+
iterate(expected -> sut -> collector.assertThat(sut).hasShort((short) (expected == null ? 0.0f : expected)));
122+
}
123+
124+
@Test
125+
void testShouldGetIntMethodFromFloat4Vector() {
126+
iterate(expected -> sut -> collector.assertThat(sut).hasInt((int) (expected == null ? 0.0f : expected)));
127+
}
128+
129+
@Test
130+
void testShouldGetLongMethodFromFloat4Vector() {
131+
iterate(expected -> sut -> collector.assertThat(sut).hasLong((long) (expected == null ? 0.0f : expected)));
132+
}
133+
134+
@Test
135+
void testShouldGetDoubleMethodFromFloat4Vector() {
136+
iterate(expected -> sut -> collector.assertThat(sut).hasDouble(expected == null ? 0.0f : expected));
137+
}
138+
139+
@Test
140+
void testGetBigDecimalIllegalFloatsMethodFromFloat4Vector() {
141+
val consumer = iterate(
142+
ImmutableList.of(Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY, Float.NaN),
143+
expected -> sut -> assertThrows(DataCloudJDBCException.class, sut::getBigDecimal));
144+
consumer.assertThat().hasNullSeen(0).hasNotNullSeen(3);
145+
}
146+
147+
@Test
148+
void testShouldGetBigDecimalWithScaleMethodFromFloat4Vector() {
149+
val scale = 9;
150+
val big = Float.MAX_VALUE;
151+
val expected = BigDecimal.valueOf(big).setScale(scale, RoundingMode.HALF_UP);
152+
iterate(
153+
ImmutableList.of(Float.MAX_VALUE),
154+
e -> sut -> collector.assertThat(sut.getBigDecimal(scale)).isEqualTo(expected));
155+
}
156+
}

jdbc-core/src/test/java/com/salesforce/datacloud/jdbc/util/RootAllocatorTestExtension.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.apache.arrow.vector.DateMilliVector;
3636
import org.apache.arrow.vector.DecimalVector;
3737
import org.apache.arrow.vector.FixedSizeBinaryVector;
38+
import org.apache.arrow.vector.Float4Vector;
3839
import org.apache.arrow.vector.Float8Vector;
3940
import org.apache.arrow.vector.IntVector;
4041
import org.apache.arrow.vector.LargeVarBinaryVector;
@@ -100,6 +101,21 @@ public Float8Vector createFloat8Vector(List<Double> values) {
100101
return vector;
101102
}
102103

104+
public Float4Vector createFloat4Vector(List<Float> values) {
105+
val vector = new Float4Vector("test-float-vector", getRootAllocator());
106+
vector.allocateNew(values.size());
107+
for (int i = 0; i < values.size(); i++) {
108+
Float f = values.get(i);
109+
if (f == null) {
110+
vector.setNull(i);
111+
} else {
112+
vector.setSafe(i, f);
113+
}
114+
}
115+
vector.setValueCount(values.size());
116+
return vector;
117+
}
118+
103119
public VarCharVector createVarCharVectorFrom(List<String> values) {
104120
val vector = new VarCharVector("test-varchar-vector", getRootAllocator());
105121
vector.setValueCount(values.size());

0 commit comments

Comments
 (0)