|
16 | 16 |
|
17 | 17 | package io.cdap.cdap.storage.spanner; |
18 | 18 |
|
| 19 | +import com.google.cloud.spanner.Value; |
19 | 20 | import io.cdap.cdap.api.metrics.MetricsCollector; |
20 | 21 | import io.cdap.cdap.spi.data.StorageProviderContext; |
21 | 22 | import io.cdap.cdap.spi.data.StructuredTable; |
22 | 23 | import io.cdap.cdap.spi.data.StructuredTableAdmin; |
23 | 24 | import io.cdap.cdap.spi.data.StructuredTableTest; |
| 25 | +import io.cdap.cdap.spi.data.table.field.Field; |
| 26 | +import io.cdap.cdap.spi.data.table.field.Fields; |
| 27 | +import io.cdap.cdap.spi.data.table.field.Range; |
24 | 28 | import io.cdap.cdap.spi.data.transaction.TransactionRunner; |
| 29 | +import java.util.Arrays; |
25 | 30 | import java.util.Collections; |
26 | 31 | import java.util.HashMap; |
| 32 | +import java.util.List; |
27 | 33 | import java.util.Map; |
28 | 34 | import java.util.Optional; |
| 35 | +import junitparams.JUnitParamsRunner; |
| 36 | +import junitparams.Parameters; |
| 37 | +import junitparams.naming.TestCaseName; |
29 | 38 | import org.junit.AfterClass; |
| 39 | +import org.junit.Assert; |
30 | 40 | import org.junit.Assume; |
31 | 41 | import org.junit.BeforeClass; |
32 | 42 | import org.junit.Ignore; |
| 43 | +import org.junit.Test; |
| 44 | +import org.junit.runner.RunWith; |
33 | 45 |
|
34 | 46 | /** |
35 | 47 | * Unit tests for GCP spanner implementation of the {@link StructuredTable}. This test needs the following |
|
43 | 55 | * json that has the "Cloud Spanner Database User" role</li> |
44 | 56 | * </ul> |
45 | 57 | */ |
| 58 | +@RunWith(JUnitParamsRunner.class) |
46 | 59 | public class SpannerStructuredTableTest extends StructuredTableTest { |
47 | 60 |
|
48 | 61 | private static SpannerStorageProvider storageProvider; |
@@ -93,6 +106,113 @@ protected TransactionRunner getTransactionRunner() { |
93 | 106 | return storageProvider.getTransactionRunner(); |
94 | 107 | } |
95 | 108 |
|
| 109 | + @Test |
| 110 | + @Parameters(method = "getCompositeKeyConditionScenarios") |
| 111 | + @TestCaseName("{0}") |
| 112 | + public void testGetCompositeKeyCondition(String description, List<Field<?>> fields, |
| 113 | + Range.Bound bound, boolean isLowerBound, int startParamIndex, String expectedSql, |
| 114 | + Map<String, Value> expectedParams) { |
| 115 | + SpannerStructuredTableSchema schema = new SpannerStructuredTableSchema(SIMPLE_SPEC.getTableId(), |
| 116 | + SIMPLE_SPEC.getFieldTypes(), SIMPLE_SPEC.getPrimaryKeys(), SIMPLE_SPEC.getIndexes(), null); |
| 117 | + try (SpannerStructuredTable table = new SpannerStructuredTable(null, schema, null)) { |
| 118 | + Map<String, Value> actualParams = new HashMap<>(); |
| 119 | + String actualSql = table.getCompositeKeyCondition(fields, bound, isLowerBound, actualParams, |
| 120 | + startParamIndex); |
| 121 | + Assert.assertEquals("SQL mismatch in: " + description, expectedSql, actualSql); |
| 122 | + Assert.assertEquals("Params mismatch in: " + description, expectedParams, actualParams); |
| 123 | + } |
| 124 | + } |
| 125 | + |
| 126 | + private Object[] getCompositeKeyConditionScenarios() { |
| 127 | + return new Object[]{ |
| 128 | + // Format: Name | Fields | Bound Type | isLower | startParamIndex | Expected SQL | Expected Params |
| 129 | + new Object[]{ |
| 130 | + "COMPOSITE: (col1, col2) >= ('A', 5) [Lower Inclusive]", |
| 131 | + Arrays.asList(Fields.stringField("col1", "A"), Fields.intField("col2", 5)), |
| 132 | + Range.Bound.INCLUSIVE, true, 0, |
| 133 | + "((`col1` > @p_0) OR (`col1` = @p_0 AND `col2` >= @p_1))", |
| 134 | + params("p_0", Value.string("A"), "p_1", Value.int64(5)) |
| 135 | + }, |
| 136 | + new Object[]{ |
| 137 | + "COMPOSITE: (col1, col2) > ('A', 5) [Lower Exclusive]", |
| 138 | + Arrays.asList(Fields.stringField("col1", "A"), Fields.intField("col2", 5)), |
| 139 | + Range.Bound.EXCLUSIVE, true, 0, |
| 140 | + "((`col1` > @p_0) OR (`col1` = @p_0 AND `col2` > @p_1))", |
| 141 | + params("p_0", Value.string("A"), "p_1", Value.int64(5)) |
| 142 | + }, |
| 143 | + new Object[]{ |
| 144 | + "COMPOSITE: (col1, col2) <= ('B', 10) [Upper Inclusive]", |
| 145 | + Arrays.asList(Fields.stringField("col1", "B"), Fields.intField("col2", 10)), |
| 146 | + Range.Bound.INCLUSIVE, false, 0, |
| 147 | + "((`col1` < @p_0) OR (`col1` = @p_0 AND `col2` <= @p_1))", |
| 148 | + params("p_0", Value.string("B"), "p_1", Value.int64(10)) |
| 149 | + }, |
| 150 | + |
| 151 | + new Object[]{ |
| 152 | + "SINGLE: col1 >= 'A' [Lower Inclusive]", |
| 153 | + Collections.singletonList(Fields.stringField("col1", "A")), |
| 154 | + Range.Bound.INCLUSIVE, true, 0, |
| 155 | + "((`col1` >= @p_0))", |
| 156 | + params("p_0", Value.string("A")) |
| 157 | + }, |
| 158 | + new Object[]{ |
| 159 | + "SINGLE: col1 < 'Z' [Upper Exclusive]", |
| 160 | + Collections.singletonList(Fields.stringField("col1", "Z")), |
| 161 | + Range.Bound.EXCLUSIVE, false, 0, |
| 162 | + "((`col1` < @p_0))", |
| 163 | + params("p_0", Value.string("Z")) |
| 164 | + }, |
| 165 | + |
| 166 | + new Object[]{ |
| 167 | + "DEEP: (k1, k2, k3, k4) <= ('x', 1, true, 99) [Upper Inclusive]", |
| 168 | + Arrays.asList( |
| 169 | + Fields.stringField("k1", "x"), |
| 170 | + Fields.intField("k2", 1), |
| 171 | + Fields.booleanField("k3", true), |
| 172 | + Fields.longField("k4", 99L) |
| 173 | + ), |
| 174 | + Range.Bound.INCLUSIVE, false, 0, |
| 175 | + "((`k1` < @p_0) OR " + |
| 176 | + "(`k1` = @p_0 AND `k2` < @p_1) OR " + |
| 177 | + "(`k1` = @p_0 AND `k2` = @p_1 AND `k3` < @p_2) OR " + |
| 178 | + "(`k1` = @p_0 AND `k2` = @p_1 AND `k3` = @p_2 AND `k4` <= @p_3))", |
| 179 | + params("p_0", Value.string("x"), "p_1", Value.int64(1), "p_2", Value.bool(true), "p_3", |
| 180 | + Value.int64(99L)) |
| 181 | + }, |
| 182 | + |
| 183 | + new Object[]{ |
| 184 | + "EDGE: Empty field list returns empty string", |
| 185 | + Collections.emptyList(), |
| 186 | + Range.Bound.INCLUSIVE, true, 0, |
| 187 | + "", |
| 188 | + Collections.emptyMap() |
| 189 | + }, |
| 190 | + |
| 191 | + new Object[]{ |
| 192 | + "OFFSET: 1 Field, Lower Inclusive, startParamIndex = 5", |
| 193 | + Collections.singletonList(Fields.stringField("id", "123")), |
| 194 | + Range.Bound.INCLUSIVE, true, 5, |
| 195 | + "((`id` >= @p_5))", |
| 196 | + params("p_5", Value.string("123")) |
| 197 | + }, |
| 198 | + new Object[]{ |
| 199 | + "OFFSET: 2 Fields, Upper Exclusive, startParamIndex = 10", |
| 200 | + Arrays.asList(Fields.stringField("col1", "X"), Fields.intField("col2", 99)), |
| 201 | + Range.Bound.EXCLUSIVE, false, 10, |
| 202 | + "((`col1` < @p_10) OR (`col1` = @p_10 AND `col2` < @p_11))", |
| 203 | + params("p_10", Value.string("X"), "p_11", Value.int64(99)) |
| 204 | + } |
| 205 | + }; |
| 206 | + } |
| 207 | + |
| 208 | + private static Map<String, Value> params(Object... entries) { |
| 209 | + Map<String, Value> map = new HashMap<>(); |
| 210 | + for (int i = 0; i < entries.length; i += 2) { |
| 211 | + map.put((String) entries[i], (Value) entries[i + 1]); |
| 212 | + } |
| 213 | + return map; |
| 214 | + } |
| 215 | + |
96 | 216 | private static final class MockStorageProviderContext implements StorageProviderContext { |
97 | 217 |
|
98 | 218 | private final Map<String, String> config; |
|
0 commit comments