diff --git a/core/trino-main/src/main/java/io/trino/connector/system/GlobalSystemConnector.java b/core/trino-main/src/main/java/io/trino/connector/system/GlobalSystemConnector.java index 047dcff8c139..a08b3a97e147 100644 --- a/core/trino-main/src/main/java/io/trino/connector/system/GlobalSystemConnector.java +++ b/core/trino-main/src/main/java/io/trino/connector/system/GlobalSystemConnector.java @@ -14,14 +14,19 @@ package io.trino.connector.system; import com.google.common.collect.ImmutableSet; +import io.trino.operator.table.Sequence.SequenceFunctionHandle; import io.trino.spi.connector.CatalogHandle; import io.trino.spi.connector.CatalogHandle.CatalogVersion; import io.trino.spi.connector.ConnectorMetadata; import io.trino.spi.connector.ConnectorSession; +import io.trino.spi.connector.ConnectorSplitManager; +import io.trino.spi.connector.ConnectorSplitSource; import io.trino.spi.connector.ConnectorTransactionHandle; import io.trino.spi.connector.SystemTable; +import io.trino.spi.function.SchemaFunctionName; import io.trino.spi.procedure.Procedure; import io.trino.spi.ptf.ConnectorTableFunction; +import io.trino.spi.ptf.ConnectorTableFunctionHandle; import io.trino.spi.transaction.IsolationLevel; import io.trino.transaction.InternalConnector; import io.trino.transaction.TransactionId; @@ -30,6 +35,7 @@ import java.util.Set; +import static io.trino.operator.table.Sequence.getSequenceFunctionSplitSource; import static io.trino.spi.connector.CatalogHandle.createRootCatalogHandle; import static java.util.Objects.requireNonNull; @@ -80,4 +86,21 @@ public Set getTableFunctions() { return tableFunctions; } + + @Override + public ConnectorSplitManager getSplitManager() + { + return new ConnectorSplitManager() + { + @Override + public ConnectorSplitSource getSplits(ConnectorTransactionHandle transaction, ConnectorSession session, SchemaFunctionName name, ConnectorTableFunctionHandle functionHandle) + { + if (functionHandle instanceof SequenceFunctionHandle sequenceFunctionHandle) { + return getSequenceFunctionSplitSource(sequenceFunctionHandle); + } + + throw new UnsupportedOperationException(); + } + }; + } } diff --git a/core/trino-main/src/main/java/io/trino/connector/system/SystemConnectorModule.java b/core/trino-main/src/main/java/io/trino/connector/system/SystemConnectorModule.java index 8693413f279b..1561fcaa3be6 100644 --- a/core/trino-main/src/main/java/io/trino/connector/system/SystemConnectorModule.java +++ b/core/trino-main/src/main/java/io/trino/connector/system/SystemConnectorModule.java @@ -32,12 +32,11 @@ import io.trino.connector.system.jdbc.TypesJdbcTable; import io.trino.connector.system.jdbc.UdtJdbcTable; import io.trino.operator.table.ExcludeColumns; +import io.trino.operator.table.Sequence; import io.trino.spi.connector.SystemTable; import io.trino.spi.procedure.Procedure; import io.trino.spi.ptf.ConnectorTableFunction; -import static com.google.inject.multibindings.Multibinder.newSetBinder; - public class SystemConnectorModule implements Module { @@ -79,7 +78,9 @@ public void configure(Binder binder) binder.bind(GlobalSystemConnector.class).in(Scopes.SINGLETON); - newSetBinder(binder, ConnectorTableFunction.class).addBinding().toProvider(ExcludeColumns.class).in(Scopes.SINGLETON); + Multibinder tableFunctions = Multibinder.newSetBinder(binder, ConnectorTableFunction.class); + tableFunctions.addBinding().toProvider(ExcludeColumns.class).in(Scopes.SINGLETON); + tableFunctions.addBinding().toProvider(Sequence.class).in(Scopes.SINGLETON); } @ProvidesIntoSet diff --git a/core/trino-main/src/main/java/io/trino/metadata/GlobalFunctionCatalog.java b/core/trino-main/src/main/java/io/trino/metadata/GlobalFunctionCatalog.java index ce2ca868363f..08cb59e97a92 100644 --- a/core/trino-main/src/main/java/io/trino/metadata/GlobalFunctionCatalog.java +++ b/core/trino-main/src/main/java/io/trino/metadata/GlobalFunctionCatalog.java @@ -18,6 +18,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Multimap; import io.trino.operator.table.ExcludeColumns; +import io.trino.operator.table.Sequence; import io.trino.spi.function.AggregationFunctionMetadata; import io.trino.spi.function.AggregationImplementation; import io.trino.spi.function.BoundSignature; @@ -49,6 +50,7 @@ import static io.trino.metadata.OperatorNameUtil.isOperatorName; import static io.trino.metadata.OperatorNameUtil.unmangleOperator; import static io.trino.operator.table.ExcludeColumns.getExcludeColumnsFunctionProcessorProvider; +import static io.trino.operator.table.Sequence.getSequenceFunctionProcessorProvider; import static io.trino.spi.function.FunctionKind.AGGREGATE; import static io.trino.spi.type.BigintType.BIGINT; import static io.trino.spi.type.BooleanType.BOOLEAN; @@ -181,6 +183,9 @@ public TableFunctionProcessorProvider getTableFunctionProcessorProvider(SchemaFu if (name.equals(new SchemaFunctionName(BUILTIN_SCHEMA, ExcludeColumns.NAME))) { return getExcludeColumnsFunctionProcessorProvider(); } + if (name.equals(new SchemaFunctionName(BUILTIN_SCHEMA, Sequence.NAME))) { + return getSequenceFunctionProcessorProvider(); + } return null; } diff --git a/core/trino-main/src/main/java/io/trino/operator/table/Sequence.java b/core/trino-main/src/main/java/io/trino/operator/table/Sequence.java new file mode 100644 index 000000000000..c14ee6737543 --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/operator/table/Sequence.java @@ -0,0 +1,302 @@ +/* + * Licensed under the Apache License, Version 2.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.trino.operator.table; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.trino.plugin.base.classloader.ClassLoaderSafeConnectorTableFunction; +import io.trino.spi.HostAddress; +import io.trino.spi.PageBuilder; +import io.trino.spi.TrinoException; +import io.trino.spi.block.BlockBuilder; +import io.trino.spi.connector.ConnectorSession; +import io.trino.spi.connector.ConnectorSplit; +import io.trino.spi.connector.ConnectorSplitSource; +import io.trino.spi.connector.ConnectorTransactionHandle; +import io.trino.spi.connector.FixedSplitSource; +import io.trino.spi.ptf.AbstractConnectorTableFunction; +import io.trino.spi.ptf.Argument; +import io.trino.spi.ptf.ConnectorTableFunction; +import io.trino.spi.ptf.ConnectorTableFunctionHandle; +import io.trino.spi.ptf.ReturnTypeSpecification.DescribedTable; +import io.trino.spi.ptf.ScalarArgument; +import io.trino.spi.ptf.ScalarArgumentSpecification; +import io.trino.spi.ptf.TableFunctionAnalysis; +import io.trino.spi.ptf.TableFunctionProcessorProvider; +import io.trino.spi.ptf.TableFunctionProcessorState; +import io.trino.spi.ptf.TableFunctionSplitProcessor; + +import javax.inject.Provider; + +import java.math.BigInteger; +import java.util.List; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkArgument; +import static io.airlift.slice.SizeOf.instanceSize; +import static io.trino.metadata.GlobalFunctionCatalog.BUILTIN_SCHEMA; +import static io.trino.operator.table.Sequence.SequenceFunctionSplit.MAX_SPLIT_SIZE; +import static io.trino.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static io.trino.spi.ptf.Descriptor.descriptor; +import static io.trino.spi.ptf.TableFunctionProcessorState.Finished.FINISHED; +import static io.trino.spi.ptf.TableFunctionProcessorState.Processed.produced; +import static io.trino.spi.ptf.TableFunctionProcessorState.Processed.usedInputAndProduced; +import static io.trino.spi.type.BigintType.BIGINT; +import static java.lang.String.format; + +public class Sequence + implements Provider +{ + public static final String NAME = "sequence"; + + @Override + public ConnectorTableFunction get() + { + return new ClassLoaderSafeConnectorTableFunction(new SequenceFunction(), getClass().getClassLoader()); + } + + public static class SequenceFunction + extends AbstractConnectorTableFunction + { + private static final String START_ARGUMENT_NAME = "START"; + private static final String STOP_ARGUMENT_NAME = "STOP"; + private static final String STEP_ARGUMENT_NAME = "STEP"; + + public SequenceFunction() + { + super( + BUILTIN_SCHEMA, + NAME, + ImmutableList.of( + ScalarArgumentSpecification.builder() + .name(START_ARGUMENT_NAME) + .type(BIGINT) + .defaultValue(0L) + .build(), + ScalarArgumentSpecification.builder() + .name(STOP_ARGUMENT_NAME) + .type(BIGINT) + .build(), + ScalarArgumentSpecification.builder() + .name(STEP_ARGUMENT_NAME) + .type(BIGINT) + .defaultValue(1L) + .build()), + new DescribedTable(descriptor(ImmutableList.of("sequential_number"), ImmutableList.of(BIGINT)))); + } + + @Override + public TableFunctionAnalysis analyze(ConnectorSession session, ConnectorTransactionHandle transaction, Map arguments) + { + Object startValue = ((ScalarArgument) arguments.get(START_ARGUMENT_NAME)).getValue(); + if (startValue == null) { + throw new TrinoException(INVALID_FUNCTION_ARGUMENT, "Start is null"); + } + + Object stopValue = ((ScalarArgument) arguments.get(STOP_ARGUMENT_NAME)).getValue(); + if (stopValue == null) { + throw new TrinoException(INVALID_FUNCTION_ARGUMENT, "Stop is null"); + } + + Object stepValue = ((ScalarArgument) arguments.get(STEP_ARGUMENT_NAME)).getValue(); + if (stepValue == null) { + throw new TrinoException(INVALID_FUNCTION_ARGUMENT, "Step is null"); + } + + long start = (long) startValue; + long stop = (long) stopValue; + long step = (long) stepValue; + + if (start < stop && step <= 0) { + throw new TrinoException(INVALID_FUNCTION_ARGUMENT, format("Step must be positive for sequence [%s, %s]", start, stop)); + } + + if (start > stop && step >= 0) { + throw new TrinoException(INVALID_FUNCTION_ARGUMENT, format("Step must be negative for sequence [%s, %s]", start, stop)); + } + + return TableFunctionAnalysis.builder() + .handle(new SequenceFunctionHandle(start, stop, start == stop ? 0 : step)) + .build(); + } + } + + public record SequenceFunctionHandle(long start, long stop, long step) + implements ConnectorTableFunctionHandle + {} + + public static ConnectorSplitSource getSequenceFunctionSplitSource(SequenceFunctionHandle handle) + { + // using BigInteger to avoid long overflow since it's not in the main data processing loop + BigInteger start = BigInteger.valueOf(handle.start()); + BigInteger stop = BigInteger.valueOf(handle.stop()); + BigInteger step = BigInteger.valueOf(handle.step()); + + if (step.equals(BigInteger.ZERO)) { + checkArgument(start.equals(stop), "start is not equal to stop for step = 0"); + return new FixedSplitSource(ImmutableList.of(new SequenceFunctionSplit(start.longValueExact(), stop.longValueExact()))); + } + + ImmutableList.Builder splits = ImmutableList.builder(); + + BigInteger totalSteps = stop.subtract(start).divide(step).add(BigInteger.ONE); + BigInteger totalSplits = totalSteps.divide(BigInteger.valueOf(MAX_SPLIT_SIZE)).add(BigInteger.ONE); + BigInteger[] stepsPerSplit = totalSteps.divideAndRemainder(totalSplits); + BigInteger splitJump = stepsPerSplit[0].subtract(BigInteger.ONE).multiply(step); + + BigInteger splitStart = start; + for (BigInteger i = BigInteger.ZERO; i.compareTo(totalSplits) < 0; i = i.add(BigInteger.ONE)) { + BigInteger splitStop = splitStart.add(splitJump); + // distribute the remaining steps between the initial splits, one step per split + if (i.compareTo(stepsPerSplit[1]) < 0) { + splitStop = splitStop.add(step); + } + splits.add(new SequenceFunctionSplit(splitStart.longValueExact(), splitStop.longValueExact())); + splitStart = splitStop.add(step); + } + + return new FixedSplitSource(splits.build()); + } + + public static class SequenceFunctionSplit + implements ConnectorSplit + { + private static final int INSTANCE_SIZE = instanceSize(SequenceFunctionSplit.class); + public static final int DEFAULT_SPLIT_SIZE = 1000000; + public static final int MAX_SPLIT_SIZE = 1000000; + + // the first value of sub-sequence + private final long start; + + // the last value of sub-sequence. this value is aligned so that it belongs to the sequence. + private final long stop; + + @JsonCreator + public SequenceFunctionSplit(@JsonProperty("start") long start, @JsonProperty("stop") long stop) + { + this.start = start; + this.stop = stop; + } + + @JsonProperty + public long getStart() + { + return start; + } + + @JsonProperty + public long getStop() + { + return stop; + } + + @Override + public boolean isRemotelyAccessible() + { + return true; + } + + @Override + public List getAddresses() + { + return ImmutableList.of(); + } + + @Override + public Object getInfo() + { + return ImmutableMap.builder() + .put("start", start) + .put("stop", stop) + .buildOrThrow(); + } + + @Override + public long getRetainedSizeInBytes() + { + return INSTANCE_SIZE; + } + } + + public static TableFunctionProcessorProvider getSequenceFunctionProcessorProvider() + { + return new TableFunctionProcessorProvider() + { + @Override + public TableFunctionSplitProcessor getSplitProcessor(ConnectorSession session, ConnectorTableFunctionHandle handle) + { + return new SequenceFunctionProcessor(((SequenceFunctionHandle) handle).step()); + } + }; + } + + public static class SequenceFunctionProcessor + implements TableFunctionSplitProcessor + { + private final PageBuilder page = new PageBuilder(ImmutableList.of(BIGINT)); + private final long step; + private long start; + private long stop; + private boolean finished; + + public SequenceFunctionProcessor(long step) + { + this.step = step; + } + + @Override + public TableFunctionProcessorState process(ConnectorSplit split) + { + if (split != null) { + SequenceFunctionSplit sequenceSplit = (SequenceFunctionSplit) split; + start = sequenceSplit.getStart(); + stop = sequenceSplit.getStop(); + BlockBuilder block = page.getBlockBuilder(0); + while (start != stop && !page.isFull()) { + page.declarePosition(); + BIGINT.writeLong(block, start); + start += step; + } + if (!page.isFull()) { + page.declarePosition(); + BIGINT.writeLong(block, start); + finished = true; + return usedInputAndProduced(page.build()); + } + return usedInputAndProduced(page.build()); + } + + if (finished) { + return FINISHED; + } + + page.reset(); + BlockBuilder block = page.getBlockBuilder(0); + while (start != stop && !page.isFull()) { + page.declarePosition(); + BIGINT.writeLong(block, start); + start += step; + } + if (!page.isFull()) { + page.declarePosition(); + BIGINT.writeLong(block, start); + finished = true; + return produced(page.build()); + } + return produced(page.build()); + } + } +} diff --git a/docs/src/main/sphinx/functions/table.rst b/docs/src/main/sphinx/functions/table.rst index b53068728174..c06186210aa8 100644 --- a/docs/src/main/sphinx/functions/table.rst +++ b/docs/src/main/sphinx/functions/table.rst @@ -43,6 +43,31 @@ Built-in table functions The argument ``input`` is a table or a query. The argument ``columns`` is a descriptor without types. +.. function:: sequence(start => bigint, stop => bigint, step => bigint) -> table(sequential_number bigint) + :noindex: + + Returns a single column ``sequential_number`` containing a sequence of + bigint:: + + SELECT * + FROM TABLE(sequence( + start => 1000000, + stop => -2000000, + step => -3)) + + ``start`` is the first element in te sequence. The default value is ``0``. + + ``stop`` is the end of the range, inclusive. The last element in the + sequence is equal to ``stop``, or it is the last value within range, + reachable by steps. + + ``step`` is the difference between subsequent values. The default value is + ``1``. + +.. note:: + + The result of the ``sequence`` table function might not be ordered. + Table function invocation ------------------------- diff --git a/testing/trino-tests/src/test/java/io/trino/tests/TestSequenceFunction.java b/testing/trino-tests/src/test/java/io/trino/tests/TestSequenceFunction.java new file mode 100644 index 000000000000..344ea7c7089c --- /dev/null +++ b/testing/trino-tests/src/test/java/io/trino/tests/TestSequenceFunction.java @@ -0,0 +1,325 @@ +/* + * Licensed under the Apache License, Version 2.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.trino.tests; + +import io.trino.testing.AbstractTestQueryFramework; +import io.trino.testing.DistributedQueryRunner; +import io.trino.testing.QueryRunner; +import org.testng.annotations.Test; + +import static io.trino.operator.table.Sequence.SequenceFunctionSplit.DEFAULT_SPLIT_SIZE; +import static io.trino.testing.TestingSession.testSessionBuilder; +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class TestSequenceFunction + extends AbstractTestQueryFramework +{ + @Override + protected QueryRunner createQueryRunner() + throws Exception + { + return DistributedQueryRunner.builder(testSessionBuilder().build()).build(); + } + + @Test + public void testSequence() + { + assertThat(query(""" + SELECT * + FROM TABLE(sequence(0, 8000, 3)) + """)) + .matches("SELECT * FROM UNNEST(sequence(0, 8000, 3))"); + + assertThat(query("SELECT * FROM TABLE(sequence(1, 10, 3))")) + .matches("VALUES BIGINT '1', 4, 7, 10"); + + assertThat(query("SELECT * FROM TABLE(sequence(1, 10, 6))")) + .matches("VALUES BIGINT '1', 7"); + + assertThat(query("SELECT * FROM TABLE(sequence(-1, -10, -3))")) + .matches("VALUES BIGINT '-1', -4, -7, -10"); + + assertThat(query("SELECT * FROM TABLE(sequence(-1, -10, -6))")) + .matches("VALUES BIGINT '-1', -7"); + + assertThat(query("SELECT * FROM TABLE(sequence(-5, 5, 3))")) + .matches("VALUES BIGINT '-5', -2, 1, 4"); + + assertThat(query("SELECT * FROM TABLE(sequence(5, -5, -3))")) + .matches("VALUES BIGINT '5', 2, -1, -4"); + + assertThat(query("SELECT * FROM TABLE(sequence(0, 10, 3))")) + .matches("VALUES BIGINT '0', 3, 6, 9"); + + assertThat(query("SELECT * FROM TABLE(sequence(0, -10, -3))")) + .matches("VALUES BIGINT '0', -3, -6, -9"); + } + + @Test + public void testDefaultArguments() + { + assertThat(query(""" + SELECT * + FROM TABLE(sequence(stop => 10)) + """)) + .matches("SELECT * FROM UNNEST(sequence(0, 10, 1))"); + } + + @Test + public void testInvalidArgument() + { + assertThatThrownBy(() -> query(""" + SELECT * + FROM TABLE(sequence( + start => -5, + stop => 10, + step => -2)) + """)) + .hasMessage("Step must be positive for sequence [-5, 10]"); + + assertThatThrownBy(() -> query(""" + SELECT * + FROM TABLE(sequence( + start => 10, + stop => -5, + step => 2)) + """)) + .hasMessage("Step must be negative for sequence [10, -5]"); + + assertThatThrownBy(() -> query(""" + SELECT * + FROM TABLE(sequence( + start => null, + stop => -5, + step => 2)) + """)) + .hasMessage("Start is null"); + + assertThatThrownBy(() -> query(""" + SELECT * + FROM TABLE(sequence( + start => 10, + stop => null, + step => 2)) + """)) + .hasMessage("Stop is null"); + + assertThatThrownBy(() -> query(""" + SELECT * + FROM TABLE(sequence( + start => 10, + stop => -5, + step => null)) + """)) + .hasMessage("Step is null"); + } + + @Test + public void testSingletonSequence() + { + assertThat(query(""" + SELECT * + FROM TABLE(sequence( + start => 10, + stop => 10, + step => 2)) + """)) + .matches("VALUES BIGINT '10'"); + + assertThat(query(""" + SELECT * + FROM TABLE(sequence( + start => 10, + stop => 10, + step => -2)) + """)) + .matches("VALUES BIGINT '10'"); + + assertThat(query(""" + SELECT * + FROM TABLE(sequence( + start => 10, + stop => 10, + step => 0)) + """)) + .matches("VALUES BIGINT '10'"); + } + + @Test + public void testBigStep() + { + assertThat(query(""" + SELECT * + FROM TABLE(sequence( + start => 10, + stop => -5, + step => %s)) + """.formatted(Long.MIN_VALUE / (DEFAULT_SPLIT_SIZE - 1)))) + .matches("VALUES BIGINT '10'"); + + assertThat(query(""" + SELECT * + FROM TABLE(sequence( + start => 10, + stop => -5, + step => %s)) + """.formatted(Long.MIN_VALUE / (DEFAULT_SPLIT_SIZE - 1) - 1))) + .matches("VALUES BIGINT '10'"); + + assertThat(query(""" + SELECT DISTINCT x - lag(x, 1) OVER(ORDER BY x DESC) + FROM TABLE(sequence( + start => %s, + stop => %s, + step => %s)) t(x) + """.formatted(Long.MAX_VALUE, Long.MIN_VALUE, Long.MIN_VALUE / (DEFAULT_SPLIT_SIZE - 1) - 1))) + .matches(format("VALUES (null), (%s)", Long.MIN_VALUE / (DEFAULT_SPLIT_SIZE - 1) - 1)); + + assertThat(query(""" + SELECT * + FROM TABLE(sequence( + start => 10, + stop => -5, + step => %s)) + """.formatted(Long.MIN_VALUE))) + .matches("VALUES BIGINT '10'"); + + assertThat(query(""" + SELECT * + FROM TABLE(sequence( + start => -5, + stop => 10, + step => %s)) + """.formatted(Long.MAX_VALUE / (DEFAULT_SPLIT_SIZE - 1)))) + .matches("VALUES BIGINT '-5'"); + + assertThat(query(""" + SELECT * + FROM TABLE(sequence( + start => -5, + stop => 10, + step => %s)) + """.formatted(Long.MAX_VALUE / (DEFAULT_SPLIT_SIZE - 1) + 1))) + .matches("VALUES BIGINT '-5'"); + + assertThat(query(""" + SELECT DISTINCT x - lag(x, 1) OVER(ORDER BY x) + FROM TABLE(sequence( + start => %s, + stop => %s, + step => %s)) t(x) + """.formatted(Long.MIN_VALUE, Long.MAX_VALUE, Long.MAX_VALUE / (DEFAULT_SPLIT_SIZE - 1) + 1))) + .matches(format("VALUES (null), (%s)", Long.MAX_VALUE / (DEFAULT_SPLIT_SIZE - 1) + 1)); + + assertThat(query(""" + SELECT * + FROM TABLE(sequence( + start => -5, + stop => 10, + step => %s)) + """.formatted(Long.MAX_VALUE))) + .matches("VALUES BIGINT '-5'"); + } + + @Test + public void testMultipleSplits() + { + long sequenceLength = DEFAULT_SPLIT_SIZE * 10 + DEFAULT_SPLIT_SIZE / 2; + long start = 10; + long step = 5; + long stop = start + (sequenceLength - 1) * step; + assertThat(query(""" + SELECT count(x), count(DISTINCT x), min(x), max(x) + FROM TABLE(sequence( + start => %s, + stop => %s, + step => %s)) t(x) + """.formatted(start, stop, step))) + .matches(format("SELECT BIGINT '%s', BIGINT '%s', BIGINT '%s', BIGINT '%s'", sequenceLength, sequenceLength, start, stop)); + + sequenceLength = DEFAULT_SPLIT_SIZE * 4 + DEFAULT_SPLIT_SIZE / 2; + stop = start + (sequenceLength - 1) * step; + assertThat(query(""" + SELECT min(x), max(x) + FROM TABLE(sequence( + start => %s, + stop => %s, + step => %s)) t(x) + """.formatted(start, stop, step))) + .matches(format("SELECT BIGINT '%s', BIGINT '%s'", start, stop)); + + step = -5; + stop = start + (sequenceLength - 1) * step; + assertThat(query(""" + SELECT max(x), min(x) + FROM TABLE(sequence( + start => %s, + stop => %s, + step => %s)) t(x) + """.formatted(start, stop, step))) + .matches(format("SELECT BIGINT '%s', BIGINT '%s'", start, stop)); + } + + @Test + public void testEdgeValues() + { + long start = Long.MIN_VALUE + 15; + long stop = Long.MIN_VALUE + 3; + long step = -10; + assertThat(query(""" + SELECT * + FROM TABLE(sequence( + start => %s, + stop => %s, + step => %s)) + """.formatted(start, stop, step))) + .matches(format("VALUES (%s), (%s)", start, start + step)); + + start = Long.MIN_VALUE + 1 - (DEFAULT_SPLIT_SIZE - 1) * step; + stop = Long.MIN_VALUE + 1; + assertThat(query(""" + SELECT max(x), min(x) + FROM TABLE(sequence( + start => %s, + stop => %s, + step => %s)) t(x) + """.formatted(start, stop, step))) + .matches(format("SELECT %s, %s", start, Long.MIN_VALUE + 1)); + + start = Long.MAX_VALUE - 15; + stop = Long.MAX_VALUE - 3; + step = 10; + assertThat(query(""" + SELECT * + FROM TABLE(sequence( + start => %s, + stop => %s, + step => %s)) + """.formatted(start, stop, step))) + .matches(format("VALUES (%s), (%s)", start, start + step)); + + start = Long.MAX_VALUE - 1 - (DEFAULT_SPLIT_SIZE - 1) * step; + stop = Long.MAX_VALUE - 1; + assertThat(query(""" + SELECT min(x), max(x) + FROM TABLE(sequence( + start => %s, + stop => %s, + step => %s)) t(x) + """.formatted(start, stop, step))) + .matches(format("SELECT %s, %s", start, Long.MAX_VALUE - 1)); + } +}