diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ColumnNameSet.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ColumnNameSet.java deleted file mode 100644 index b15de4bc9..000000000 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ColumnNameSet.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql; - -import io.asyncer.r2dbc.mysql.internal.util.InternalArrays; - -import java.util.AbstractSet; -import java.util.Arrays; -import java.util.Collection; -import java.util.Comparator; -import java.util.Iterator; -import java.util.Objects; -import java.util.Set; -import java.util.Spliterator; -import java.util.Spliterators; -import java.util.function.Consumer; -import java.util.function.Predicate; - -import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.require; -import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; - -/** - * An implementation of {@link Set}{@code <}{@link String}{@code >} for {@code RowMetadata.getColumnNames} - * results. - * - * @see MySqlNames column name searching rules. - */ -final class ColumnNameSet extends AbstractSet implements Set { - - static final Comparator NAME_COMPARATOR = (left, right) -> - MySqlNames.compare(left.getName(), right.getName()); - - private final String[] originNames; - - private final String[] sortedNames; - - /** - * Construct a {@link ColumnNameSet} by sorted {@code names} without array copy. - * - * @param originNames must be the original order. - * @param sortedNames must be sorted by {@link MySqlNames#compare(String, String)}. - */ - private ColumnNameSet(String[] originNames, String[] sortedNames) { - this.originNames = originNames; - this.sortedNames = sortedNames; - } - - @Override - public boolean contains(Object o) { - if (o instanceof String) { - return findIndex((String) o) >= 0; - } - - return false; - } - - @Override - public Iterator iterator() { - return InternalArrays.asIterator(originNames); - } - - @Override - public int size() { - return originNames.length; - } - - @Override - public boolean isEmpty() { - return originNames.length == 0; - } - - @Override - public Spliterator spliterator() { - return Spliterators.spliterator(this.originNames, - Spliterator.NONNULL | Spliterator.ORDERED | Spliterator.IMMUTABLE); - } - - @Override - public void forEach(Consumer action) { - Objects.requireNonNull(action); - - for (String name : this.originNames) { - action.accept(name); - } - } - - @Override - public String[] toArray() { - return Arrays.copyOf(originNames, originNames.length); - } - - @SuppressWarnings({ "unchecked", "SuspiciousSystemArraycopy" }) - @Override - public T[] toArray(T[] a) { - Objects.requireNonNull(a); - - int size = originNames.length; - - if (a.length < size) { - return (T[]) Arrays.copyOf(originNames, size, a.getClass()); - } else { - System.arraycopy(originNames, 0, a, 0, size); - - if (a.length > size) { - a[size] = null; - } - - return a; - } - } - - @Override - public boolean add(String s) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean addAll(Collection c) { - Objects.requireNonNull(c); - - if (!c.isEmpty()) { - throw new UnsupportedOperationException(); - } - - return false; - } - - @Override - public boolean remove(Object o) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean removeIf(Predicate filter) { - Objects.requireNonNull(filter); - - for (String name : this.originNames) { - if (filter.test(name)) { - throw new UnsupportedOperationException(); - } - } - - return false; - } - - @Override - public boolean removeAll(Collection c) { - Objects.requireNonNull(c); - - if (!c.isEmpty()) { - throw new UnsupportedOperationException(); - } - - return false; - } - - @SuppressWarnings("SuspiciousMethodCalls") - @Override - public boolean retainAll(Collection c) { - Objects.requireNonNull(c); - - if (!c.containsAll(this)) { - throw new UnsupportedOperationException(); - } - - return false; - } - - @Override - public void clear() { - throw new UnsupportedOperationException(); - } - - @Override - public String toString() { - return Arrays.toString(originNames); - } - - int findIndex(String name) { - return MySqlNames.nameSearch(this.sortedNames, name); - } - - String[] getSortedNames() { - return sortedNames; - } - - static ColumnNameSet of(String name) { - requireNonNull(name, "name must not be null"); - - String[] names = new String[] { name }; - return new ColumnNameSet(names, names); - } - - static ColumnNameSet of(String[] originNames, String[] sortedNames) { - requireNonNull(originNames, "originNames must not be null"); - requireNonNull(sortedNames, "sortedNames must not be null"); - require(originNames.length == sortedNames.length, - "The length of origin names the same as sorted names one"); - - return new ColumnNameSet(originNames, sortedNames); - } -} diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InsertSyntheticRow.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InsertSyntheticRow.java index a9865419c..bba48841c 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InsertSyntheticRow.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InsertSyntheticRow.java @@ -52,15 +52,11 @@ final class InsertSyntheticRow implements MySqlRow, MySqlRowMetadata, MySqlColum private final long lastInsertId; - private final ColumnNameSet nameSet; - InsertSyntheticRow(Codecs codecs, String keyName, long lastInsertId) { this.codecs = requireNonNull(codecs, "codecs must not be null"); this.keyName = requireNonNull(keyName, "keyName must not be null"); // lastInsertId may be negative if key is BIGINT UNSIGNED and value overflow than signed int64. this.lastInsertId = lastInsertId; - // Singleton name must be sorted. - this.nameSet = ColumnNameSet.of(keyName); } @Override @@ -164,14 +160,14 @@ public T get(String name, ParameterizedType type) { lastInsertId < 0 ? Long.toUnsignedString(lastInsertId) : lastInsertId)); } - private void assertValidName(String name) { - if (!contains0(name)) { - throw new NoSuchElementException("Column name '" + name + "' does not exist in " + this.nameSet); - } + private boolean contains0(final String name) { + return keyName.equalsIgnoreCase(name); } - private boolean contains0(String name) { - return nameSet.contains(name); + private void assertValidName(final String name) { + if (!contains0(name)) { + throw new NoSuchElementException("Column name '" + name + "' does not exist in {" + name + '}'); + } } private T get0(Class type) { diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnDescriptor.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnDescriptor.java index 5f3720c08..8f8d2e9e0 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnDescriptor.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnDescriptor.java @@ -23,6 +23,7 @@ import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.asyncer.r2dbc.mysql.message.server.DefinitionMetadataMessage; import io.r2dbc.spi.Nullability; +import org.jetbrains.annotations.VisibleForTesting; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.require; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; @@ -50,7 +51,8 @@ final class MySqlColumnDescriptor implements MySqlColumnMetadata { private final int collationId; - private MySqlColumnDescriptor(int index, short typeId, String name, int definitions, + @VisibleForTesting + MySqlColumnDescriptor(int index, short typeId, String name, int definitions, long size, int decimals, int collationId) { require(index >= 0, "index must not be a negative integer"); require(size >= 0, "size must not be a negative integer"); diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlNames.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlNames.java deleted file mode 100644 index 29d85dfe3..000000000 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlNames.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql; - -/** - * A utility considers column names searching logic which use a special compare rule for sort and special - * binary search. - * - *
  • Sort: compare with case insensitive first, then compare with case sensitive when they equals by - * case insensitive.
  • - *
  • Search: find with case sensitive first, then find with case insensitive when not found in case - * sensitive.
- *

- * For example: - * Sort first: abc AB a Abc Ab ABC A ab b B -> A a B b AB Ab ab ABC Abc abc - * Then find "aB" use the same compare rule, - * - * @see #compare(String, String) - */ -final class MySqlNames { - - /** - * Find the best match of target string. This means that if it cannot find case-sensitive content, it will - * try to find with case-insensitive. If the target string is enclosed by {@literal `} and contains at - * least 1 character in quotes, it will find with case-sensitive only. - * - * @param names column names ordered by {@link #compare}. - * @param name the target string. - * @return found index, or a negative integer means not found. - */ - static int nameSearch(String[] names, String name) { - int size = name.length(); - return binarySearch(names, name, size <= 2 || name.charAt(0) != '`' || name.charAt(size - 1) != '`'); - } - - private static int binarySearch(String[] names, String name, boolean ignoreCase) { - int left = 0, right = names.length - 1, middle, compared; - int nameStart = ignoreCase ? 0 : 1, nameEnd = ignoreCase ? name.length() : name.length() - 1; - int ciResult = -1; - String value; - - while (left <= right) { - // `left + (right - left) / 2` for ensure no overflow, - // `left + (right - left) / 2` = `(left + right) >>> 1` - // when `left` and `right` is not negative integer. - // And `left` must greater or equals than 0, - // `right` greater then or equals to `left`. - middle = (left + right) >>> 1; - value = names[middle]; - compared = compare0(value, name, nameStart, nameEnd); - - if (compared < 0) { - left = middle + 1; - - if (compared == -2) { - // Match succeed if case-insensitive, always use last - // matched result that will be closer to `name`. - ciResult = middle; - } - } else if (compared > 0) { - right = middle - 1; - - if (compared == 2) { - // Match succeed if case-insensitive, always use last - // matched result that will be closer to `name`. - ciResult = middle; - } - } else { - // Match succeed when case-sensitive. - return middle; - } - } - - return ignoreCase ? ciResult : -1; - } - - /** - * Compares double strings and return an integer of both difference. If the integer is {@code 0} means - * both strings equals even case-sensitive, absolute value is {@code 2} means it is equals by - * case-insensitive but not equals when case-sensitive, absolute value is {@code 4} means it is not equals - * even case-insensitive. - *

- * Note: visible for unit tests. - * - * @param left the {@link String} of left. - * @param right the {@link String} of right. - * @return an integer of both difference. - */ - static int compare(String left, String right) { - return compare0(left, right, 0, right.length()); - } - - private static int compare0(String left, String right, int start, int end) { - int leftSize = left.length(), rightSize = end - start; - int minSize = Math.min(leftSize, rightSize); - // Case sensitive comparator result. - int csCompared = 0; - char leftCh, rightCh; - - for (int i = 0; i < minSize; i++) { - leftCh = left.charAt(i); - rightCh = right.charAt(i + start); - - if (leftCh != rightCh) { - if (csCompared == 0) { - // Compare end if is case-sensitive comparator. - csCompared = leftCh - rightCh; - } - - // Use `Character.toLowerCase` for all latin alphabets, not just ASCII. - leftCh = Character.toLowerCase(leftCh); - rightCh = Character.toLowerCase(rightCh); - - if (leftCh != rightCh) { - // Not equals even case-insensitive. - return leftCh < rightCh ? -4 : 4; - } - } - } - - // Length not equals means both strings not equals even case-insensitive. - if (leftSize != rightSize) { - return leftSize < rightSize ? -4 : 4; - } - - // Equals when case-insensitive, use case-sensitive. - return csCompared < 0 ? -2 : (csCompared > 0 ? 2 : 0); - } - - private MySqlNames() { } -} diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlRowDescriptor.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlRowDescriptor.java index 1b5311da6..6d568ca60 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlRowDescriptor.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlRowDescriptor.java @@ -19,54 +19,34 @@ import io.asyncer.r2dbc.mysql.api.MySqlRowMetadata; import io.asyncer.r2dbc.mysql.internal.util.InternalArrays; import io.asyncer.r2dbc.mysql.message.server.DefinitionMetadataMessage; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.VisibleForTesting; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Locale; +import java.util.Map; import java.util.NoSuchElementException; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; /** * An implementation of {@link MySqlRowMetadata} for MySQL database text/binary results. - * - * @see MySqlNames column name searching rules. */ final class MySqlRowDescriptor implements MySqlRowMetadata { private final MySqlColumnDescriptor[] originMetadata; - private final MySqlColumnDescriptor[] sortedMetadata; - - private final ColumnNameSet nameSet; - - private MySqlRowDescriptor(MySqlColumnDescriptor[] metadata) { - int size = metadata.length; - - switch (size) { - case 0: - throw new IllegalArgumentException("Least 1 column metadata"); - case 1: - String name = metadata[0].getName(); - - this.originMetadata = metadata; - this.sortedMetadata = metadata; - this.nameSet = ColumnNameSet.of(name); - - break; - default: - MySqlColumnDescriptor[] sortedMetadata = new MySqlColumnDescriptor[size]; - System.arraycopy(metadata, 0, sortedMetadata, 0, size); - Arrays.sort(sortedMetadata, ColumnNameSet.NAME_COMPARATOR); - - String[] originNames = getNames(metadata); - String[] sortedNames = getNames(sortedMetadata); + @Nullable + private Map nameIndexMap; - this.originMetadata = metadata; - this.sortedMetadata = sortedMetadata; - this.nameSet = ColumnNameSet.of(originNames, sortedNames); - - break; - } + /** + * Visible for testing + */ + @VisibleForTesting + MySqlRowDescriptor(MySqlColumnDescriptor[] metadata) { + originMetadata = metadata; } @Override @@ -78,24 +58,43 @@ public MySqlColumnDescriptor getColumnMetadata(int index) { return originMetadata[index]; } + private static Map createIndexMap(MySqlColumnDescriptor[] metadata) { + final int size = metadata.length; + final Map map = new HashMap<>(size); + + for (int i = 0; i < size; ++i) { + map.putIfAbsent(metadata[i].getName().toLowerCase(Locale.ROOT), i); + } + + return map; + } + + private int find(final String name) { + Map nameIndexMap = this.nameIndexMap; + if (null == nameIndexMap) { + nameIndexMap = this.nameIndexMap = createIndexMap(originMetadata); + } + return nameIndexMap.getOrDefault(name.toLowerCase(Locale.ROOT), -1); + } + @Override public MySqlColumnDescriptor getColumnMetadata(String name) { requireNonNull(name, "name must not be null"); - int index = nameSet.findIndex(name); + final int index = find(name); if (index < 0) { throw new NoSuchElementException("Column name '" + name + "' does not exist"); } - return sortedMetadata[index]; + return originMetadata[index]; } @Override public boolean contains(String name) { requireNonNull(name, "name must not be null"); - return nameSet.contains(name); + return find(name) >= 0; } @Override @@ -105,8 +104,7 @@ public List getColumnMetadatas() { @Override public String toString() { - return "MySqlRowDescriptor{metadata=" + Arrays.toString(originMetadata) + ", sortedNames=" + - Arrays.toString(nameSet.getSortedNames()) + '}'; + return "MySqlRowDescriptor{metadata=" + Arrays.toString(originMetadata) + '}'; } MySqlColumnDescriptor[] unwrap() { @@ -123,15 +121,4 @@ static MySqlRowDescriptor create(DefinitionMetadataMessage[] columns) { return new MySqlRowDescriptor(metadata); } - - private static String[] getNames(MySqlColumnDescriptor[] metadata) { - int size = metadata.length; - String[] names = new String[size]; - - for (int i = 0; i < size; ++i) { - names[i] = metadata[i].getName(); - } - - return names; - } } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlNamesTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlNamesTest.java deleted file mode 100644 index ce4f27387..000000000 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlNamesTest.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql; - -import org.jetbrains.annotations.Nullable; -import org.junit.jupiter.api.Test; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; -import java.util.TreeSet; -import java.util.function.Consumer; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * Unit tests for {@link MySqlNames}. - */ -class MySqlNamesTest { - - private static final String[] NAMES = { "c", "dD", "cBc", "Dca", "ADC", "DcA", "abc", "b", "B", "dA", - "AB", "a", "Abc", "ABC", "A", "ab", "cc", "Da", "CbC" }; - - private static final String[] SINGLETON = { "name" }; - - private static final Set CS_NAME_SET = new HashSet<>(); - - private static final Set CI_NAME_SET = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); - - static { - CS_NAME_SET.addAll(Arrays.asList(NAMES)); - CI_NAME_SET.addAll(Arrays.asList(NAMES)); - Arrays.sort(NAMES, MySqlNames::compare); - } - - @Test - void nameSearch() { - Consumer judge = name -> { - if (CS_NAME_SET.contains(name)) { - assertEquals(NAMES[MySqlNames.nameSearch(NAMES, name)], name); - assertEquals(NAMES[MySqlNames.nameSearch(NAMES, String.format("`%s`", name))], name); - } else if (CI_NAME_SET.contains(name)) { - assertTrue(NAMES[MySqlNames.nameSearch(NAMES, name)].equalsIgnoreCase(name)); - assertEquals(MySqlNames.nameSearch(NAMES, String.format("`%s`", name)), -1); - } else { - assertEquals(MySqlNames.nameSearch(NAMES, name), -1); - assertEquals(MySqlNames.nameSearch(NAMES, String.format("`%s`", name)), -1); - } - }; - nameGenerate(1, judge); - nameGenerate(2, judge); - nameGenerate(3, judge); - - assertEquals(SINGLETON[MySqlNames.nameSearch(SINGLETON, "name")], "name"); - assertEquals(SINGLETON[MySqlNames.nameSearch(SINGLETON, "Name")], "name"); - assertEquals(SINGLETON[MySqlNames.nameSearch(SINGLETON, "nAMe")], "name"); - assertEquals(SINGLETON[MySqlNames.nameSearch(SINGLETON, "`name`")], "name"); - assertEquals(MySqlNames.nameSearch(SINGLETON, "`Name`"), -1); - assertEquals(MySqlNames.nameSearch(SINGLETON, "`nAMe`"), -1); - } - - /** - * A full-arrangement of repeatable selections is generated in 'a' - 'd' and 'A' - 'D' of fixed length - * String. - *

- * e.g. input: 2, publish: aa ab ac ad aA aB aC ... DB DC DD - */ - private static void nameGenerate(int length, Consumer nameConsumer) { - nameGen0(length, null, nameConsumer); - } - - private static void nameGen0(int length, @Nullable String prefix, Consumer nameConsumer) { - if (length <= 1) { - for (char c = 'a'; c < 'e'; ++c) { - if (prefix == null) { - nameConsumer.accept(Character.toString(c)); - } else { - nameConsumer.accept(prefix + c); - } - } - for (char c = 'A'; c < 'E'; ++c) { - if (prefix == null) { - nameConsumer.accept(Character.toString(c)); - } else { - nameConsumer.accept(prefix + c); - } - } - } else { - for (char c = 'a'; c < 'e'; ++c) { - if (prefix == null) { - nameGen0(length - 1, Character.toString(c), nameConsumer); - } else { - nameGen0(length - 1, prefix + c, nameConsumer); - } - } - for (char c = 'A'; c < 'E'; ++c) { - if (prefix == null) { - nameGen0(length - 1, Character.toString(c), nameConsumer); - } else { - nameGen0(length - 1, prefix + c, nameConsumer); - } - } - } - } -} diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlRowDescriptorTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlRowDescriptorTest.java new file mode 100644 index 000000000..d1514648e --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlRowDescriptorTest.java @@ -0,0 +1,65 @@ +package io.asyncer.r2dbc.mysql; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.NoSuchElementException; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + + +class MySqlRowDescriptorTest { + @ParameterizedTest + @MethodSource("arguments") + public void findColumnByNameTest(final String q, final int index, final String... names) { + + // given + final MySqlRowDescriptor metadata = create(names); + + // when + if (index == -1) { + assertThrows(NoSuchElementException.class, () -> metadata.getColumnMetadata(q)); + return; + } + final MySqlColumnDescriptor actual = metadata.getColumnMetadata(q); + + // then + assertEquals(index, actual.getIndex()); + } + + private static Stream arguments() { + return Stream.of( + // not found + Arguments.of("`alpha`", -1, + new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }), + Arguments.of("omega", -1, + new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }), + + // found + Arguments.of("alpha", 0, new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }), + Arguments.of("Alpha", 0, new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }), + + Arguments.of("beta", 1, new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }), + Arguments.of("Beta", 1, new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }), + + Arguments.of("delta", 3, new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }), + Arguments.of("Delta", 3, new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }), + + Arguments.of("gamma", 4, new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }), + Arguments.of("Gamma", 4, new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }) + ); + } + + private static MySqlRowDescriptor create(final String... names) { + MySqlColumnDescriptor[] metadata = new MySqlColumnDescriptor[names.length]; + for (int i = 0; i < names.length; ++i) { + metadata[i] = + new MySqlColumnDescriptor(i, (short) 0, names[i], 0, 0, 0, 1); + } + return new MySqlRowDescriptor(metadata); + } + + +} \ No newline at end of file