diff --git a/partiql-cli/src/main/kotlin/org/partiql/cli/Main.kt b/partiql-cli/src/main/kotlin/org/partiql/cli/Main.kt index 514f46028e..d524965836 100644 --- a/partiql-cli/src/main/kotlin/org/partiql/cli/Main.kt +++ b/partiql-cli/src/main/kotlin/org/partiql/cli/Main.kt @@ -16,11 +16,11 @@ package org.partiql.cli import com.amazon.ion.system.IonReaderBuilder -import com.amazon.ion.system.IonTextWriterBuilder import com.amazon.ionelement.api.ionListOf import com.amazon.ionelement.api.ionNull import com.amazon.ionelement.api.loadAllElements import org.partiql.cli.io.Format +import org.partiql.cli.io.PartiQLCursorWriter import org.partiql.cli.pipeline.Pipeline import org.partiql.cli.shell.Shell import org.partiql.eval.PartiQLEngine @@ -32,7 +32,6 @@ import org.partiql.spi.connector.Connector import org.partiql.spi.connector.sql.info.InfoSchema import org.partiql.types.StaticType import org.partiql.value.PartiQLValueExperimental -import org.partiql.value.toIon import picocli.CommandLine import java.io.File import java.io.InputStream @@ -77,7 +76,8 @@ internal class Version : CommandLine.IVersionProvider { ], showDefaultValues = true ) -internal class MainCommand() : Runnable { +internal class MainCommand : Runnable { + // TODO: Need to add tests to CLI. All tests were removed in the same commit as this TODO. See Git blame. internal companion object { private const val SHEBANG_PREFIX = "#!" @@ -95,6 +95,13 @@ internal class MainCommand() : Runnable { ) var strict: Boolean = false + @CommandLine.Option( + names = ["--debug"], + description = ["THIS IS FOR INTERNAL DEVELOPMENT USE ONLY. Shows typing information in the output."], + hidden = true + ) + var debug: Boolean = false + @CommandLine.Option( names = ["-f", "--format"], description = ["The data format, using the form [:]."], @@ -150,7 +157,7 @@ internal class MainCommand() : Runnable { true -> Pipeline.strict() else -> Pipeline.default() } - Shell(pipeline, session()).start() + Shell(pipeline, session(), debug).start() } @OptIn(PartiQLValueExperimental::class) @@ -167,10 +174,8 @@ internal class MainCommand() : Runnable { error(result.cause.stackTrace) } is PartiQLResult.Value -> { - // TODO handle output format - val ion = result.value.toIon() - val writer = IonTextWriterBuilder.pretty().build(System.out as Appendable) - ion.writeTo(writer) + val writer = PartiQLCursorWriter(System.out, debug) + writer.append(result.value) println() } } diff --git a/partiql-cli/src/main/kotlin/org/partiql/cli/format/ExplainFormatter.kt b/partiql-cli/src/main/kotlin/org/partiql/cli/format/ExplainFormatter.kt index 1d9404eb8b..e7488a39b4 100644 --- a/partiql-cli/src/main/kotlin/org/partiql/cli/format/ExplainFormatter.kt +++ b/partiql-cli/src/main/kotlin/org/partiql/cli/format/ExplainFormatter.kt @@ -14,7 +14,6 @@ package org.partiql.cli.format - // internal object ExplainFormatter { // // internal fun format(result: PartiQLResult.Explain.Domain): String { diff --git a/partiql-cli/src/main/kotlin/org/partiql/cli/io/PartiQLCursorWriter.kt b/partiql-cli/src/main/kotlin/org/partiql/cli/io/PartiQLCursorWriter.kt new file mode 100644 index 0000000000..0474de91c9 --- /dev/null +++ b/partiql-cli/src/main/kotlin/org/partiql/cli/io/PartiQLCursorWriter.kt @@ -0,0 +1,213 @@ +package org.partiql.cli.io + +import org.partiql.value.PartiQLCursor +import org.partiql.value.PartiQLValueExperimental +import org.partiql.value.PartiQLValueType +import org.partiql.value.datetime.Date +import org.partiql.value.datetime.Time +import org.partiql.value.datetime.TimeZone +import org.partiql.value.datetime.Timestamp +import kotlin.math.abs + +/** + * Aids in appending [PartiQLCursor] to [out]. + * + * Prints in a human-readable fashion. Indents appropriately. Example output: + * ``` + * partiql ▶ SELECT VALUE { 'a': { 'b': t } } FROM <<1, 2>> AS t + * | + * << + * { + * 'a': { + * 'b': 1 + * } + * }, + * { + * 'a': { + * 'b': 2 + * } + * } + * >> + * ``` + * + * @param debug specifies whether to also output typing information; if set to true, values will have their types prefixed + * to the output; for example: `int32::512`, `string::'hello, world!'`, and `null.int64`; if set to false, values will + * be printed as-is; for example: `512`, `'hello, world!'`, and `null`. + */ +class PartiQLCursorWriter( + private val out: Appendable, + private val debug: Boolean = false +) { + + /** + * Determines how much to indent + */ + private var indent = 0 + + @OptIn(PartiQLValueExperimental::class) + fun append(data: PartiQLCursor) { + for (element in data) { + writeValue(data, element) + } + } + + @OptIn(PartiQLValueExperimental::class) + private fun writeValue(data: PartiQLCursor, element: PartiQLValueType) { + when (element) { + PartiQLValueType.ANY -> error("This shouldn't have happened.") + PartiQLValueType.BOOL -> writeScalar(data, "bool", PartiQLCursor::getBoolValue) + PartiQLValueType.INT8 -> writeScalar(data, "int8", PartiQLCursor::getInt8Value) + PartiQLValueType.INT16 -> writeScalar(data, "int16", PartiQLCursor::getInt16Value) + PartiQLValueType.INT32 -> writeScalar(data, "int32", PartiQLCursor::getInt32Value) + PartiQLValueType.INT64 -> writeScalar(data, "int64", PartiQLCursor::getInt64Value) + PartiQLValueType.INT -> writeScalar(data, "int", PartiQLCursor::getIntValue) + PartiQLValueType.DECIMAL -> writeScalar(data, "decimal", PartiQLCursor::getDecimalValue) + PartiQLValueType.DECIMAL_ARBITRARY -> writeScalar(data, "decimal_arbitrary", PartiQLCursor::getDecimalArbitraryValue) + PartiQLValueType.FLOAT32 -> writeScalar(data, "float32", PartiQLCursor::getFloat32Value) + PartiQLValueType.FLOAT64 -> writeScalar(data, "float64", PartiQLCursor::getFloat64Value) + PartiQLValueType.CHAR -> writeScalar(data, "char", PartiQLCursor::getCharValue) + PartiQLValueType.STRING -> writeScalar(data, "string", PartiQLCursor::getStringValue) + PartiQLValueType.SYMBOL -> writeScalar(data, "symbol", PartiQLCursor::getSymbolValue) + PartiQLValueType.BINARY -> writeScalar(data, "binary", PartiQLCursor::getBinaryValue) + PartiQLValueType.BYTE -> writeScalar(data, "byte", PartiQLCursor::getByteValue) + PartiQLValueType.BLOB -> writeScalar(data, "blob", PartiQLCursor::getBlobValue) + PartiQLValueType.CLOB -> writeScalar(data, "clob", PartiQLCursor::getClobValue) + PartiQLValueType.DATE -> writeScalar(data, "date") { it.dateValue.getLiteralString() } + PartiQLValueType.TIME -> writeScalar(data, "time") { it.timeValue.getLiteralString() } + PartiQLValueType.TIMESTAMP -> writeScalar(data, "timestamp") { it.timestampValue.getLiteralString() } + PartiQLValueType.INTERVAL -> writeScalar(data, "interval", PartiQLCursor::getIntervalValue) + PartiQLValueType.BAG -> writeCollection(data, "bag", "<<", ">>", named = false) + PartiQLValueType.LIST -> writeCollection(data, "list", "[", "]", named = false) + PartiQLValueType.SEXP -> writeCollection(data, "sexp", "(", ")", named = false) + PartiQLValueType.STRUCT -> writeCollection(data, "struct", "{", "}", named = true) + PartiQLValueType.NULL -> writeScalar(data, "null") { d -> "null".also { assert(d.isNullValue) } } + PartiQLValueType.MISSING -> writeScalar(data, "missing") { d -> "missing".also { assert(d.isMissingValue) } } + } + } + + @OptIn(PartiQLValueExperimental::class) + private fun writeCollection(data: PartiQLCursor, type: String, prefix: String, postfix: String, named: Boolean) { + if (appendPotentialNullValue(data, type)) { + return + } + // Print value prefix (AKA: << for bag, [ for list, or ( for s-exp) + appendTypePrefixIfDebugEnabled(type) + out.appendLine(prefix) + + // Print children + stepIn(data) + for (child in data) { + out.append(buildIndent()) + if (named) { + val fieldName = data.fieldName + out.append('\'') + out.append(fieldName) + out.append('\'') + out.append(": ") + } + writeValue(data, child) + when (data.hasNext()) { + true -> out.appendLine(",") + false -> out.appendLine() + } + } + stepOut(data) + + // Print value postfix + out.append(buildIndent()) + out.append(postfix) + } + + private fun writeScalar(data: PartiQLCursor, type: String, transform: (PartiQLCursor) -> Any) { + if (appendPotentialNullValue(data, type)) { + return + } + appendTypePrefixIfDebugEnabled(type) + out.append(transform(data).toString()) + } + + private fun appendTypePrefixIfDebugEnabled(type: String) { + if (debug) { + out.append(type) + out.append("::") + } + } + + /** + * @return true if the value was null and [out] was appended to; false if the value was not null and [out] was + * not appended to. + */ + private fun appendPotentialNullValue(data: PartiQLCursor, type: String): Boolean { + if (data.isNullValue) { + out.append("null") + // Print out the type of the null. AKA: null.bag + if (debug) { + out.append('.') + out.append(type) + } + return true + } + return false + } + + private fun stepIn(data: PartiQLCursor) { + data.stepIn() + indent++ + } + + private fun stepOut(data: PartiQLCursor) { + data.stepOut() + indent-- + } + + private fun buildIndent(): String { + var prefix = "" + for (i in 1..indent) { + prefix += " " + } + return prefix + } + + private fun Time.getLiteralString(): String { + val tz = this.timeZone.getTypeString() + return "TIME $tz '${this.hour}:${this.minute}:${this.decimalSecond}'" + } + + private fun Date.getLiteralString(): String { + val dateString = "${this.year.pad(4)}-${this.month.pad()}-${this.day.pad()}" + return "DATE '$dateString'" + } + + private fun Timestamp.getLiteralString(): String { + val tz = this.timeZone.getTypeString() + val dateString = "${this.year.pad(4)}-${this.month.pad()}-${this.day.pad()}" + val timeString = "${this.hour.pad()}:${this.minute.pad()}:${this.decimalSecond}" + val tzLiteral = this.timeZone.getLiteralTimeZoneString() + return "TIMESTAMP $tz '$dateString $timeString$tzLiteral'" + } + + private fun TimeZone?.getTypeString() = when (this) { + null -> "WITHOUT TIME ZONE" + is TimeZone.UnknownTimeZone -> "WITH UNKNOWN TIME ZONE" + is TimeZone.UtcOffset -> "WITH TIME ZONE" + } + + private fun TimeZone?.getLiteralTimeZoneString() = when (this) { + null -> "" + is TimeZone.UnknownTimeZone -> "-00:00" + is TimeZone.UtcOffset -> { + val sign = when (this.totalOffsetMinutes >= 0) { + true -> "+" + false -> "-" + } + val offset = abs(this.totalOffsetMinutes) + val hours = offset.div(60) + val minutes = offset - (hours * 60) + "$sign${hours.pad()}:${minutes.pad()}" + } + } + + private fun Int.pad(length: Int = 2): String { + return this.toString().padStart(length, '0') + } +} diff --git a/partiql-cli/src/main/kotlin/org/partiql/cli/shell/Shell.kt b/partiql-cli/src/main/kotlin/org/partiql/cli/shell/Shell.kt index 8051481f66..3c1ba77474 100644 --- a/partiql-cli/src/main/kotlin/org/partiql/cli/shell/Shell.kt +++ b/partiql-cli/src/main/kotlin/org/partiql/cli/shell/Shell.kt @@ -15,7 +15,6 @@ package org.partiql.cli.shell import com.amazon.ion.system.IonSystemBuilder -import com.amazon.ion.system.IonTextWriterBuilder import com.amazon.ionelement.api.toIonValue import com.google.common.util.concurrent.Uninterruptibles import org.fusesource.jansi.AnsiConsole @@ -32,6 +31,7 @@ import org.jline.utils.AttributedStyle import org.jline.utils.AttributedStyle.BOLD import org.jline.utils.InfoCmp import org.joda.time.Duration +import org.partiql.cli.io.PartiQLCursorWriter import org.partiql.cli.pipeline.Pipeline import org.partiql.eval.PartiQLResult import org.partiql.plugins.fs.toIon @@ -40,8 +40,6 @@ import org.partiql.spi.BindingName import org.partiql.spi.BindingPath import org.partiql.spi.connector.ConnectorHandle import org.partiql.value.PartiQLValueExperimental -import org.partiql.value.io.PartiQLValueTextWriter -import software.amazon.ion.IonSystem import java.io.Closeable import java.io.PrintStream import java.nio.file.Path @@ -119,9 +117,13 @@ val exiting = AtomicBoolean(false) val doneCompiling = AtomicBoolean(true) val donePrinting = AtomicBoolean(true) +/** + * @param debug specifies whether to print typing information or not. + */ internal class Shell( private val pipeline: Pipeline, private val session: Pipeline.Session, + private val debug: Boolean ) { private var state: State = State(false) @@ -308,7 +310,7 @@ internal class Shell( when (result) { is PartiQLResult.Error -> throw result.cause is PartiQLResult.Value -> { - val writer = PartiQLValueTextWriter(out) + val writer = PartiQLCursorWriter(out, debug) writer.append(result.value) out.appendLine() out.appendLine() diff --git a/partiql-cli/src/test/kotlin/org/partiql/cli/CliTest.kt b/partiql-cli/src/test/kotlin/org/partiql/cli/CliTest.kt deleted file mode 100644 index ccc2be8d2a..0000000000 --- a/partiql-cli/src/test/kotlin/org/partiql/cli/CliTest.kt +++ /dev/null @@ -1,304 +0,0 @@ -/* - * Copyright 2019 Amazon.com, Inc. or its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at: - * - * http://aws.amazon.com/apache2.0/ - * - * or in the "license" file accompanying this file. This file 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 org.partiql.cli - -import com.amazon.ion.IonException -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.partiql.cli.pipeline.AbstractPipeline -import org.partiql.lang.eval.BAG_ANNOTATION -import org.partiql.lang.eval.EvaluationException -import org.partiql.lang.eval.MISSING_ANNOTATION -import org.partiql.lang.eval.ProjectionIterationBehavior -import org.partiql.lang.eval.TypedOpBehavior -import org.partiql.lang.eval.TypingMode -import org.partiql.lang.eval.UndefinedVariableBehavior -import java.io.ByteArrayOutputStream -import java.io.File -import java.io.FileOutputStream -import java.nio.file.Files - -class CliTest { - private val output = ByteArrayOutputStream() - private var testFile: File? = null - - @BeforeEach - fun setUp() { - output.reset() - testFile = Files.createTempFile("test", "ion").toFile() - } - - @AfterEach - fun cleanTestFile() { - Files.deleteIfExists(testFile!!.toPath()) - } - - @Test - fun runQueryOnSingleValue() { - val query = "SELECT * FROM input_data" - val input = "[{'a': 1}]" - val expected = "$BAG_ANNOTATION::[{a: 1}]" - - val ionInputResult = makeCliAndGetResult(query, input, inputFormat = MainCommand.InputFormat.ION) - val partiqlInputResult = makeCliAndGetResult(query, input, inputFormat = MainCommand.InputFormat.PARTIQL) - - assertAsIon(expected, ionInputResult) - assertAsIon(expected, partiqlInputResult) - } - - @Test - fun runQueryOnMultipleIonValuesFailure() { - val query = "SELECT * FROM input_data" - val input = "1 2" - assertThrows { - makeCliAndGetResult(query, input) - } - } - - @Test - fun runQueryOnMultipleIonValuesSuccess() { - val query = "SELECT * FROM input_data" - val input = "{a:1} {a:2}" - val expected = "$BAG_ANNOTATION::[{a:1}, {a:2}]" - - val result = makeCliAndGetResult(query, input, wrapIon = true) - - assertAsIon(expected, result) - } - - @Test - fun specifyingWrapIonWithPartiQLInput() { - val query = "SELECT * FROM input_data" - val input = "{a:1} {a:2}" - assertThrows { - makeCliAndGetResult(query, input, wrapIon = true, inputFormat = MainCommand.InputFormat.PARTIQL) - } - } - - @Test - fun runQueryOnMultipleValues() { - val query = "SELECT * FROM input_data" - val input = "[{'a': 1},{'a': 2},{'a': 3}]" - val expected = "$BAG_ANNOTATION::[{a: 1},{a: 2},{a: 3}]" - - val ionInputResult = makeCliAndGetResult(query, input, inputFormat = MainCommand.InputFormat.ION) - val partiqlInputResult = makeCliAndGetResult(query, input, inputFormat = MainCommand.InputFormat.PARTIQL) - - assertAsIon(expected, ionInputResult) - assertAsIon(expected, partiqlInputResult) - } - - @Test - fun caseInsensitiveBindingName() { - val query = "SELECT * FROM input_dAta" - val input = "[{'a': 1}]" - val expected = "$BAG_ANNOTATION::[{a: 1}]" - - val ionInputResult = makeCliAndGetResult(query, input, inputFormat = MainCommand.InputFormat.ION) - val partiqlInputResult = makeCliAndGetResult(query, input, inputFormat = MainCommand.InputFormat.PARTIQL) - - assertAsIon(expected, ionInputResult) - assertAsIon(expected, partiqlInputResult) - } - - @Test - fun withBinding() { - val query = "SELECT v, d FROM bound_value v, input_data d" - val input = "[{'a': 1}]" - val wrappedInput = "{'a': 1}" - val bindings = mapOf("bound_value" to "{b: 1}").asBinding() - val expected = "$BAG_ANNOTATION::[{v: {b: 1}, d: {a: 1}}]" - - val wrappedInputResult = makeCliAndGetResult(query, wrappedInput, bindings = bindings, wrapIon = true) - val ionInputResult = makeCliAndGetResult(query, input, bindings = bindings) - val partiqlInputResult = makeCliAndGetResult(query, input, bindings = bindings, inputFormat = MainCommand.InputFormat.PARTIQL) - - assertAsIon(expected, wrappedInputResult) - assertAsIon(expected, ionInputResult) - assertAsIon(expected, partiqlInputResult) - } - - @Test - fun withShadowingBinding() { - val query = "SELECT * FROM input_data" - val input = "[{'a': 1}]" - val wrappedInput = "{'a': 1}" - val bindings = mapOf("input_data" to "{b: 1}").asBinding() - val expected = "$BAG_ANNOTATION::[{a: 1}]" - - val wrappedInputResult = makeCliAndGetResult(query, wrappedInput, bindings = bindings, wrapIon = true) - val ionInputResult = makeCliAndGetResult(query, input, bindings = bindings) - val partiqlInputResult = makeCliAndGetResult(query, input, bindings = bindings, inputFormat = MainCommand.InputFormat.PARTIQL) - - assertAsIon(expected, wrappedInputResult) - assertAsIon(expected, ionInputResult) - assertAsIon(expected, partiqlInputResult) - } - - @Test - fun withPartiQLOutput() { - val query = "SELECT * FROM input_data" - val input = "[{a: 1}]" - val wrappedInput = "{a: 1}" - val expected = "<<{'a': 1}>>" - - val wrappedInputResult = makeCliAndGetResult(query, wrappedInput, wrapIon = true, outputFormat = MainCommand.OutputFormat.PARTIQL) - val ionInputResult = makeCliAndGetResult(query, input, inputFormat = MainCommand.InputFormat.ION, outputFormat = MainCommand.OutputFormat.PARTIQL) - - assertEquals(expected, wrappedInputResult) - assertEquals(expected, ionInputResult) - } - - @Test - fun withPartiQLPrettyOutput() { - val query = "SELECT * FROM input_data" - val input = "[{a: 1, b: 2}]" - val expected = "<<\n {\n 'a': 1,\n 'b': 2\n }\n>>" - - val actual = makeCliAndGetResult(query, input, inputFormat = MainCommand.InputFormat.ION, outputFormat = MainCommand.OutputFormat.PARTIQL_PRETTY) - - assertEquals(expected, actual) - } - - @Test - fun withIonTextOutput() { - val query = "SELECT * FROM input_data" - val input = "[{a: 1}, {b: 1}]" - val expected = "$BAG_ANNOTATION::[{a:1}\n,{b:1}\n]" - - val actual = makeCliAndGetResult(query, input, inputFormat = MainCommand.InputFormat.ION, outputFormat = MainCommand.OutputFormat.ION_TEXT) - - assertAsIon(expected, actual) - } - - @Test - fun withIonTextOutputToFile() { - val query = "SELECT * FROM input_data" - val input = "[{'a': 1}, {'b': 1}]" - val expected = "$BAG_ANNOTATION::[{a:1}\n,{b:1}\n]" - makeCliAndGetResult(query, input, inputFormat = MainCommand.InputFormat.ION, outputFormat = MainCommand.OutputFormat.ION_TEXT, output = FileOutputStream(testFile)) - val ionInputResult = testFile!!.bufferedReader().use { it.readText() } - assertAsIon(expected, ionInputResult) - - makeCliAndGetResult(query, input, inputFormat = MainCommand.InputFormat.PARTIQL, outputFormat = MainCommand.OutputFormat.ION_TEXT, output = FileOutputStream(testFile)) - val partiqlInputResult = testFile!!.bufferedReader().use { it.readText() } - assertAsIon(expected, partiqlInputResult) - } - - @Test - fun withoutInput() { - val query = "1" - val expected = "1" - - val actual = makeCliAndGetResult(query) - - assertAsIon(expected, actual) - } - - @Test - fun withoutInputWithInputDataBindingThrowsException() { - val query = "SELECT * FROM input_data" - assertThrows { - makeCliAndGetResult(query) - } - } - - @Test - fun runQueryInPermissiveMode() { - val pipeline = AbstractPipeline.create(AbstractPipeline.PipelineOptions(typingMode = TypingMode.PERMISSIVE)) - val query = "1 + 'foo'" - val actual = makeCliAndGetResult(query, pipeline = pipeline) - - assertAsIon("$MISSING_ANNOTATION::null", actual) - } - - @Test - fun runWithTypedOpBehaviorHonorParameters() { - val pipeline = AbstractPipeline.create(AbstractPipeline.PipelineOptions(typedOpBehavior = TypedOpBehavior.HONOR_PARAMETERS)) - val query = "CAST('abcde' as VARCHAR(3));" - val actual = makeCliAndGetResult(query, pipeline = pipeline) - - assertAsIon("\"abc\"", actual) - } - - @Test - fun runWithProjectionIterationFilterMissingFailure() { - val pipeline = AbstractPipeline.create(AbstractPipeline.PipelineOptions(projectionIterationBehavior = ProjectionIterationBehavior.FILTER_MISSING)) - val input = "<<{'a': null, 'b': missing, 'c': 1}>>" - val query = "SELECT a, b, c FROM input_data" - assertThrows { - makeCliAndGetResult(query, input, pipeline = pipeline, inputFormat = MainCommand.InputFormat.PARTIQL) - } - } - - @Test() - fun runWithProjectionIterationFilterMissingSuccess() { - val pipeline = AbstractPipeline.create(AbstractPipeline.PipelineOptions(projectionIterationBehavior = ProjectionIterationBehavior.FILTER_MISSING)) - val input = "<<{'a': null, 'b': missing, 'c': 1}>>" - val query = "SELECT * FROM input_data" - val actual = makeCliAndGetResult(query, input, pipeline = pipeline, inputFormat = MainCommand.InputFormat.PARTIQL) - assertAsIon("$BAG_ANNOTATION::[{a:null,c:1}]", actual) - } - - @Test - fun runWithProjectionIterationUnfiltered() { - val pipeline = AbstractPipeline.create(AbstractPipeline.PipelineOptions(projectionIterationBehavior = ProjectionIterationBehavior.UNFILTERED)) - val input = "<<{'a': null, 'b': missing, 'c': 1}>>" - val query = "SELECT a, b, c FROM input_data" - val actual = makeCliAndGetResult(query, input, pipeline = pipeline, inputFormat = MainCommand.InputFormat.PARTIQL) - assertAsIon("$BAG_ANNOTATION::[{a:null,c:1}]", actual) - } - - @Test - fun runWithUndefinedVariableError() { - val pipeline = AbstractPipeline.create(AbstractPipeline.PipelineOptions(undefinedVariableBehavior = UndefinedVariableBehavior.ERROR)) - val input = "<<{'a': 1}>>" - val query = "SELECT * FROM undefined_variable" - assertThrows { - makeCliAndGetResult(query, input, pipeline = pipeline, inputFormat = MainCommand.InputFormat.PARTIQL) - } - } - - @Test - fun runWithUndefinedVariableMissing() { - val pipeline = AbstractPipeline.create(AbstractPipeline.PipelineOptions(undefinedVariableBehavior = UndefinedVariableBehavior.MISSING)) - val input = "<<{'a': 1}>>" - val query = "SELECT * FROM undefined_variable" - val actual = makeCliAndGetResult(query, input, pipeline = pipeline, inputFormat = MainCommand.InputFormat.PARTIQL) - assertAsIon("$BAG_ANNOTATION::[{}]", actual) - } - - @Test - fun partiqlInputSuccess() { - val query = "SELECT * FROM input_data" - val input = "<<{'a': 1}, {'b': 1}>>" - val expected = "$BAG_ANNOTATION::[{a:1}\n,{b:1}\n]" - - val partiqlInputResult = makeCliAndGetResult(query, input, inputFormat = MainCommand.InputFormat.PARTIQL) - assertAsIon(expected, partiqlInputResult) - } - - @Test - fun partiqlInputFailure() { - val query = "SELECT * FROM input_data" - val input = "<<{'a': 1}, {'b': 1}>>" - assertThrows { - makeCliAndGetResult(query, input, inputFormat = MainCommand.InputFormat.ION) - } - } -} diff --git a/partiql-cli/src/test/kotlin/org/partiql/cli/CliTestUtility.kt b/partiql-cli/src/test/kotlin/org/partiql/cli/CliTestUtility.kt deleted file mode 100644 index 0163067f8a..0000000000 --- a/partiql-cli/src/test/kotlin/org/partiql/cli/CliTestUtility.kt +++ /dev/null @@ -1,59 +0,0 @@ -package org.partiql.cli - -import com.amazon.ion.IonSystem -import com.amazon.ion.system.IonSystemBuilder -import org.junit.jupiter.api.Assertions.assertEquals -import org.partiql.cli.pipeline.AbstractPipeline -import org.partiql.cli.query.Cli -import org.partiql.cli.utils.EmptyInputStream -import org.partiql.lang.eval.Bindings -import org.partiql.lang.eval.ExprValue -import java.io.ByteArrayOutputStream -import java.io.OutputStream - -/** - * Initializes a CLI and runs the passed-in query - */ -internal fun makeCliAndGetResult( - query: String, - input: String? = null, - inputFormat: MainCommand.InputFormat = MainCommand.InputFormat.ION, - bindings: Bindings = Bindings.empty(), - outputFormat: MainCommand.OutputFormat = MainCommand.OutputFormat.ION_TEXT, - output: OutputStream = ByteArrayOutputStream(), - ion: IonSystem = IonSystemBuilder.standard().build(), - pipeline: AbstractPipeline = AbstractPipeline.standard(), - wrapIon: Boolean = false -): String { - val cli = Cli( - ion, - input?.byteInputStream(Charsets.UTF_8) ?: EmptyInputStream(), - inputFormat, - output, - outputFormat, - pipeline, - bindings, - query, - wrapIon - ) - cli.run() - return output.toString() -} - -/** - * An assertion helper - */ -fun assertAsIon(expected: String, actual: String) { - val ion = IonSystemBuilder.standard().build() - assertAsIon(ion, expected, actual) -} - -/** - * An assertion helper - */ -fun assertAsIon(ion: IonSystem, expected: String, actual: String) = assertEquals(ion.loader.load(expected), ion.loader.load(actual)) - -fun String.singleIonExprValue(ion: IonSystem = IonSystemBuilder.standard().build()) = ExprValue.of(ion.singleValue(this)) - -fun Map.asBinding() = - Bindings.ofMap(this.mapValues { it.value.singleIonExprValue() }) diff --git a/partiql-cli/src/test/kotlin/org/partiql/cli/functions/PowTest.kt b/partiql-cli/src/test/kotlin/org/partiql/cli/functions/PowTest.kt deleted file mode 100644 index 14f784dcc5..0000000000 --- a/partiql-cli/src/test/kotlin/org/partiql/cli/functions/PowTest.kt +++ /dev/null @@ -1,35 +0,0 @@ -// -// package org.partiql.cli.functions -// -// import org.junit.jupiter.api.Assertions.assertEquals -// import org.junit.jupiter.api.Disabled -// import org.junit.jupiter.api.Test -// import org.partiql.cli.makeCliAndGetResult -// import org.partiql.cli.pipeline.AbstractPipeline -// import java.nio.file.Paths -// -// /** -// * Class `PowTest` is used to test the 'test_power' function, which calculates the base to the power of exponent. -// * It is a plugin mockdb functions loaded by Java Service Loader. -// * -// * @property pipeline Creates a pipeline using service loaded functions. It allows to process a stream of records. -// * -// * @constructor Creates an instance of `PowTest`. -// */ -// @Disabled -// class PowTest { -// -// val pluginPath = Paths.get(System.getProperty("testingPluginDirectory")) -// -// private val pipeline = AbstractPipeline.create( -// AbstractPipeline.PipelineOptions( -// functions = ServiceLoaderUtil.loadFunctions(pluginPath) -// ) -// ) -// -// @Test -// fun PowTest() { -// val result = makeCliAndGetResult(query = "test_power(2,3)", pipeline = pipeline) -// assertEquals(8.0, result.toDouble()) -// } -// } diff --git a/partiql-cli/src/test/kotlin/org/partiql/cli/functions/QueryDDBTest.kt b/partiql-cli/src/test/kotlin/org/partiql/cli/functions/QueryDDBTest.kt deleted file mode 100644 index 79215b7527..0000000000 --- a/partiql-cli/src/test/kotlin/org/partiql/cli/functions/QueryDDBTest.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2019 Amazon.com, Inc. or its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at: - * - * http://aws.amazon.com/apache2.0/ - * - * or in the "license" file accompanying this file. This file 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 org.partiql.cli.functions - -import com.amazon.ion.system.IonSystemBuilder -import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient -import com.amazonaws.services.dynamodbv2.model.AttributeValue -import com.amazonaws.services.dynamodbv2.model.ExecuteStatementResult -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.mockito.ArgumentMatchers -import org.mockito.Mockito -import org.mockito.MockitoAnnotations -import org.mockito.junit.jupiter.MockitoExtension -import org.partiql.lang.eval.EvaluationSession -import org.partiql.lang.eval.ExprValue -import org.partiql.lang.eval.toIonValue - -@ExtendWith(MockitoExtension::class) -class QueryDDBTest { - - private val ion = IonSystemBuilder.standard().build() - private val session = EvaluationSession.standard() - private val client: AmazonDynamoDBClient = Mockito.mock(AmazonDynamoDBClient::class.java) - private lateinit var function: QueryDDB - - @BeforeEach - fun setUp() { - MockitoAnnotations.openMocks(this) - this.function = QueryDDB(ion, client) - } - - @Test - fun basicQuery() { - // Arrange - val arguments = listOf(ExprValue.newString("SELECT * FROM test;")) - val mockAttrValue = AttributeValue() - mockAttrValue.s = "value" - val mockResults = listOf(mapOf("key" to mockAttrValue)) - val mockResult = ExecuteStatementResult().withItems(mockResults) - Mockito.doReturn(mockResult).`when`(client).executeStatement(ArgumentMatchers.any()) - val expected = "[{key: \"value\"}]" - - // Act - val result = function.callWithRequired(session, arguments) - - // Assert - assertAsIon(expected, result.toIonValue(ion).toString()) - } - - @Test - fun basicQueryWithNextToken() { - // Arrange - val arguments = listOf(ExprValue.newString("SELECT * FROM test;")) - val mockAttrValue = AttributeValue() - mockAttrValue.s = "value" - val mockResults = listOf(mapOf("key" to mockAttrValue)) - val mockResult1 = ExecuteStatementResult().withItems(mockResults).withNextToken("1") - val mockResult2 = ExecuteStatementResult().withItems(mockResults) - Mockito.`when`(client.executeStatement(ArgumentMatchers.any())).thenReturn(mockResult1).thenReturn(mockResult2) - val expected = "[{key: \"value\"}, {key: \"value\"}]" - - // Act - val result = function.callWithRequired(session, arguments) - - // Assert - assertAsIon(expected, result.toIonValue(ion).toString()) - } - - private fun assertAsIon(expected: String, actual: String) { - Assertions.assertEquals(ion.loader.load(expected), ion.loader.load(actual)) - } -} diff --git a/partiql-cli/src/test/kotlin/org/partiql/cli/functions/ReadFileTest.kt b/partiql-cli/src/test/kotlin/org/partiql/cli/functions/ReadFileTest.kt deleted file mode 100644 index 493a18cfab..0000000000 --- a/partiql-cli/src/test/kotlin/org/partiql/cli/functions/ReadFileTest.kt +++ /dev/null @@ -1,221 +0,0 @@ -/* - * Copyright 2019 Amazon.com, Inc. or its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at: - * - * http://aws.amazon.com/apache2.0/ - * - * or in the "license" file accompanying this file. This file 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 org.partiql.cli.functions - -import com.amazon.ion.IonType -import com.amazon.ion.IonValue -import com.amazon.ion.system.IonSystemBuilder -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.junit.jupiter.api.extension.ExtensionContext -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.Arguments -import org.junit.jupiter.params.provider.ArgumentsProvider -import org.junit.jupiter.params.provider.ArgumentsSource -import org.partiql.lang.eval.BAG_ANNOTATION -import org.partiql.lang.eval.EvaluationSession -import org.partiql.lang.eval.ExprValue -import org.partiql.lang.eval.MISSING_ANNOTATION -import org.partiql.lang.eval.toIonValue -import org.partiql.lang.util.asSequence -import java.util.stream.Stream - -class ReadFileTest { - private val ion = IonSystemBuilder.standard().build() - private val function = ReadFile_1(ion) - private val function2 = ReadFile_2(ion) - private val session = EvaluationSession.standard() - - private fun String.exprValue() = ExprValue.of(ion.singleValue(this)) - - companion object { - private fun getResourcePath(name: String): String { - val url = ReadFileTest::class.java.classLoader.getResource("read_file_tests/$name") - return url!!.path - } - } - - private fun IonValue.removeAnnotations() { - when (this.type) { - // Remove $missing annotation from NULL for assertions - IonType.NULL -> this.removeTypeAnnotation(MISSING_ANNOTATION) - IonType.DATAGRAM, - IonType.SEXP, - IonType.STRUCT, - IonType.LIST -> { - // Remove $bag annotation from LIST for assertions - if (this.type == IonType.LIST) { - this.removeTypeAnnotation(BAG_ANNOTATION) - } - // Recursively remove annotations - this.asSequence().forEach { - it.removeAnnotations() - } - } - else -> { /* ok to do nothing. */ } - } - } - - private fun IonValue.cloneAndRemoveAnnotations() = this.clone().apply { - removeAnnotations() - makeReadOnly() - } - - private fun assertValues(expectedIon: String, value: ExprValue) { - val expectedValues = ion.singleValue(expectedIon) - - assertEquals(expectedValues, value.toIonValue(ion).cloneAndRemoveAnnotations()) - } - - @Test - fun testError() { - val path = getResourcePath("data.ion") - val args = listOf("\"$path\"").map { it.exprValue() } - assertThrows { - function.callWithRequired(session, args) - } - } - - @Test - fun testError2() { - val path = getResourcePath("data.ion") - val args = listOf("\"$path\"").map { it.exprValue() } - assertThrows { - function2.callWithRequired(session, args + listOf("{type:\"ion\"}".exprValue())) - } - } - - @ParameterizedTest - @ArgumentsSource(SuccessTestProvider::class) - fun test(tc: SuccessTestProvider.TestCase) { - val path = getResourcePath(tc.filename) - val args = listOf("\"$path\"").map { it.exprValue() } - val actual = when (tc.additionalOptions) { - null -> function.callWithRequired(session, args) - else -> function2.callWithRequired(session, args + listOf(tc.additionalOptions.exprValue())) - } - assertValues(tc.expected, actual) - } - - class SuccessTestProvider : ArgumentsProvider { - data class TestCase( - val filename: String, - val expected: String, - val additionalOptions: String? = null - ) - - override fun provideArguments(context: ExtensionContext?): Stream { - return tests.map { Arguments.of(it) }.stream() - } - - private val tests = listOf( - TestCase( - filename = "data_list.ion", - expected = "[1, 2]" - ), - TestCase( - filename = "data_list.ion", - expected = "[1, 2]", - additionalOptions = "{type:\"ion\"}" - ), - TestCase( - filename = "data.csv", - expected = "[{_1:\"1\",_2:\"2\"}]", - additionalOptions = "{type:\"csv\"}" - ), - TestCase( - filename = "data_with_ion_symbol_as_input.csv", - expected = "[{_1:\"1\",_2:\"2\"}]", - additionalOptions = "{type:csv}" - ), - TestCase( - filename = "data_with_double_quotes_escape.csv", - expected = "[{_1:\"1,2\",_2:\"2\"}]", - additionalOptions = "{type:\"csv\"}" - ), - TestCase( - filename = "csv_with_empty_lines.csv", - expected = "[{_1:\"1\",_2:\"2\"},{_1:\"3\"}]", - additionalOptions = "{type:\"csv\"}" - ), - TestCase( - filename = "data_with_header_line.csv", - expected = "[{col1:\"1\",col2:\"2\"}]", - additionalOptions = "{type:\"csv\", header:true}" - ), - TestCase( - filename = "data.tsv", - expected = "[{_1:\"1\",_2:\"2\"}]", - additionalOptions = "{type:\"tsv\"}" - ), - TestCase( - filename = "data_with_header_line.tsv", - expected = "[{col1:\"1\",col2:\"2\"}]", - additionalOptions = "{type:\"tsv\", header:true}" - ), - TestCase( - filename = "simple_excel.csv", - expected = "[{title:\"harry potter\",category:\"book\",price:\"7.99\"}]", - additionalOptions = "{type:\"excel_csv\", header:true}" - ), - TestCase( - filename = "simple_postgresql.csv", - expected = "[{id:\"1\",name:\"B\\\"ob\",balance:\"10000.00\"}]", - additionalOptions = "{type:\"postgresql_csv\", header:true}" - ), - TestCase( - filename = "simple_postgresql.txt", - expected = "[{id:\"1\",name:\"Bob\",balance:\"10000.00\"}]", - additionalOptions = "{type:\"postgresql_text\", header:true}" - ), - TestCase( - filename = "simple_mysql.csv", - expected = "[{id:\"1\",name:\"B\\\"ob\",balance:\"10000.00\"}]", - additionalOptions = "{type:\"mysql_csv\", header:true}" - ), - TestCase( - filename = "customized.csv", - expected = "[{id:\"1\",name:\"Bob\",balance:\"10000.00\"}]", - additionalOptions = "{type:\"customized\", header:true, delimiter:' '}" - ), - TestCase( - filename = "customized_ignore_empty.csv", - expected = "[{id:\"\"},{id:\"1\",name:\"Bob\",balance:\"10000.00\"}]", - additionalOptions = "{type:\"customized\", header:true, ignore_empty_line: false}" - ), - TestCase( - filename = "customized_ignore_surrounding.csv", - expected = "[{id:\" 1 \",name:\" Bob \",balance:\" 10000.00 \"}]", - additionalOptions = "{type:\"customized\", header:true, ignore_surrounding_space:false, trim:false}" - ), - TestCase( - filename = "customized_line_breaker.csv", - expected = "[{id:\"1\",name:\"Bob\",balance:\"10000.00\"}]", - additionalOptions = "{type:\"customized\", header:true, line_breaker:'\\\r\\\n'}" - ), - TestCase( - filename = "customized_escape.csv", - expected = "[{id:\"\\\"1\",name:\"Bob\",balance:\"10000.00\"}]", - additionalOptions = "{type:\"customized\", header:true, escape:'/'}" - ), - TestCase( - filename = "customized_quote.csv", - expected = "[{id:\"1,\",name:\"Bob\",balance:\"10000.00\"}]", - additionalOptions = "{type:\"customized\", header:true, quote:\"'\"}" - ) - ) - } -} diff --git a/partiql-cli/src/test/kotlin/org/partiql/cli/functions/TrimLeadTest.kt b/partiql-cli/src/test/kotlin/org/partiql/cli/functions/TrimLeadTest.kt deleted file mode 100644 index 88be2377f0..0000000000 --- a/partiql-cli/src/test/kotlin/org/partiql/cli/functions/TrimLeadTest.kt +++ /dev/null @@ -1,40 +0,0 @@ -// -// package org.partiql.cli.functions -// -// import org.junit.jupiter.api.Assertions.assertEquals -// import org.junit.jupiter.api.Disabled -// import org.junit.jupiter.api.Test -// import org.partiql.cli.makeCliAndGetResult -// import org.partiql.cli.pipeline.AbstractPipeline -// import org.partiql.cli.utils.ServiceLoaderUtil -// import java.nio.file.Paths -// -// /** -// * Class `TrimLeadTest` is used to test the 'trim_lead' function, which is used to trim the leading whitespace characters -// * from the string it processes. It is a plugin mockdb functions loaded by Java Service Loader. -// * -// * @property pipeline Creates a pipeline using service loaded functions. It allows to process a stream of records. -// * -// * @constructor Creates an instance of `TrimLeadTest`. -// */ -// @Disabled -// class TrimLeadTest { -// -// val pluginPath = Paths.get(System.getProperty("testingPluginDirectory")) -// -// private val pipeline = AbstractPipeline.create( -// AbstractPipeline.PipelineOptions( -// functions = ServiceLoaderUtil.loadFunctions(pluginPath) -// ) -// ) -// -// @Test -// fun TrimTest() { -// val input = "' hello'" -// val expected = "\"hello\"" -// -// val result = makeCliAndGetResult(query = "trim_lead($input)", pipeline = pipeline) -// -// assertEquals(expected, result.trim()) -// } -// } diff --git a/partiql-cli/src/test/kotlin/org/partiql/cli/functions/WriteFileTest.kt b/partiql-cli/src/test/kotlin/org/partiql/cli/functions/WriteFileTest.kt deleted file mode 100644 index da943767f8..0000000000 --- a/partiql-cli/src/test/kotlin/org/partiql/cli/functions/WriteFileTest.kt +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright 2019 Amazon.com, Inc. or its affiliates. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at: - * - * http://aws.amazon.com/apache2.0/ - * - * or in the "license" file accompanying this file. This file 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 org.partiql.cli.functions - -import com.amazon.ion.system.IonSystemBuilder -import org.junit.jupiter.api.AfterAll -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.Test -import org.partiql.cli.assertAsIon -import org.partiql.cli.makeCliAndGetResult -import org.partiql.cli.pipeline.AbstractPipeline -import org.partiql.lang.eval.BAG_ANNOTATION -import org.partiql.lang.eval.EvaluationSession -import org.partiql.lang.eval.ExprValue -import org.partiql.lang.eval.toIonValue -import java.io.ByteArrayOutputStream -import java.io.OutputStream -import java.nio.file.Files -import java.nio.file.Path -import java.util.UUID -class WriteFileTest { - - private val ion = IonSystemBuilder.standard().build() - private val function = WriteFile_1(ion) - private val function2 = WriteFile_2(ion) - private val session = EvaluationSession.standard() - private val pipeline = AbstractPipeline.create( - AbstractPipeline.PipelineOptions( - functions = listOf(WriteFile_1(ion)) - ) - ) - - private fun String.exprValue() = ExprValue.of(ion.singleValue(this)) - - private val outputStream: OutputStream = ByteArrayOutputStream() - - companion object { - - private var tmp: Path? = null - - const val TRUE_STRING = "true" - - @JvmStatic - @BeforeAll - fun beforeAll() { - try { - tmp = Files.createTempDirectory("partiqltest") - } catch (_: IllegalArgumentException) { - // already existed - } - } - - @JvmStatic - @AfterAll - fun afterAll() { - tmp!!.toFile().deleteRecursively() - } - - fun createRandomTmpFilePath(): Path = Files.createTempFile(tmp!!, UUID.randomUUID().toString(), ".ion") - } - - @Test - fun unit_success_writeIonAsDefault() { - val filePath = createRandomTmpFilePath() - val args = listOf("\"$filePath\"", "[1, 2]").map { it.exprValue() } - function.callWithRequired(session, args).toIonValue(ion) - - val expected = "[1, 2]" - - assertEquals(ion.loader.load(expected), ion.loader.load(filePath.toFile().readText())) - } - - @Test - fun unit_success_readIon() { - val filePath = createRandomTmpFilePath() - val args = listOf("\"$filePath\"", "[1, 2]").map { it.exprValue() } - val additionalOptions = """{type: "ion"}""".exprValue() - function2.callWithRequired(session, args + listOf(additionalOptions)).toIonValue(ion) - - val expected = "[1, 2]" - - assertEquals(ion.loader.load(expected), ion.loader.load(filePath.toFile().readText())) - } - - @Test - fun integration_success_singleValueStruct() { - // Arrange - val filePath = createRandomTmpFilePath() - val query = "write_file('$filePath', SELECT * FROM input_data)" - val input = "{a: 1}" - val expected = "$BAG_ANNOTATION::[{a: 1}]" - - // Act - val cliResponse = - makeCliAndGetResult(query = query, input = input, output = outputStream, pipeline = pipeline) - - // Assert - assertAsIon(TRUE_STRING, cliResponse) - assertEquals(ion.loader.load(expected), ion.loader.load(filePath.toFile().readText())) - } - - @Test - fun integration_success_nestedValueStruct() { - // Arrange - val filePath = createRandomTmpFilePath() - val query = "write_file('$filePath', SELECT a.b FROM input_data)" - val input = "{a: {b: 1}}" - val expected = "$BAG_ANNOTATION::[{b: 1}]" - - // Act - val cliResponse = - makeCliAndGetResult(query = query, input = input, output = outputStream, pipeline = pipeline) - - // Assert - assertAsIon(TRUE_STRING, cliResponse) - assertEquals(ion.loader.load(expected), ion.loader.load(filePath.toFile().readText())) - } - - @Test - fun integration_success_nestedValue() { - // Arrange - val filePath = createRandomTmpFilePath() - val query = "write_file('$filePath', SELECT VALUE a FROM input_data)" - val input = "{a: {b: 1}}" - val expected = "$BAG_ANNOTATION::[{b: 1}]" - - // Act - val cliResponse = - makeCliAndGetResult(query = query, input = input, output = outputStream, pipeline = pipeline) - - // Assert - assertAsIon(TRUE_STRING, cliResponse) - assertEquals(ion.loader.load(expected), ion.loader.load(filePath.toFile().readText())) - } - - @Test - fun integration_success_nestedValueInt() { - // Arrange - val filePath = createRandomTmpFilePath() - val query = "write_file('$filePath', SELECT VALUE a.b FROM input_data)" - val input = "{a: {b: 1}}" - val expected = "$BAG_ANNOTATION::[1]" - - // Act - val cliResponse = - makeCliAndGetResult(query = query, input = input, output = outputStream, pipeline = pipeline) - - // Assert - assertAsIon(TRUE_STRING, cliResponse) - assertEquals(ion.loader.load(expected), ion.loader.load(filePath.toFile().readText())) - } - - @Test - fun integration_success_nestedValueList() { - // Arrange - val filePath = createRandomTmpFilePath() - val query = "write_file('$filePath', SELECT VALUE a.b FROM input_data)" - val input = "{a: {b: [ 1, 2 ]}}" - val expected = "$BAG_ANNOTATION::[[ 1, 2 ]]" - - // Act - val cliResponse = - makeCliAndGetResult(query = query, input = input, output = outputStream, pipeline = pipeline) - - // Assert - assertAsIon(TRUE_STRING, cliResponse) - assertEquals(ion.loader.load(expected), ion.loader.load(filePath.toFile().readText())) - } - - @Test - fun integration_success_int() { - // Arrange - val filePath = Files.createTempFile("integration_success_int", ".ion") - val query = "write_file('$filePath', SELECT VALUE a FROM input_data)" - val input = "{a : 5}" - val expected = "$BAG_ANNOTATION::[5]" - - // Act - val cliResponse = - makeCliAndGetResult(query = query, input = input, output = outputStream, pipeline = pipeline) - - // Assert - assertAsIon(TRUE_STRING, cliResponse) - assertEquals(ion.loader.load(expected), ion.loader.load(filePath.toFile().readText())) - } -} diff --git a/partiql-cli/src/test/kotlin/org/partiql/cli/utils/ServiceLoaderUtilTest.kt b/partiql-cli/src/test/kotlin/org/partiql/cli/utils/ServiceLoaderUtilTest.kt deleted file mode 100644 index 801b260c5d..0000000000 --- a/partiql-cli/src/test/kotlin/org/partiql/cli/utils/ServiceLoaderUtilTest.kt +++ /dev/null @@ -1,20 +0,0 @@ -// -// import org.junit.jupiter.api.Assertions.assertTrue -// import org.junit.jupiter.api.Disabled -// import org.junit.jupiter.api.Test -// import org.partiql.cli.utils.ServiceLoaderUtil -// import org.partiql.lang.eval.ExprFunction -// import java.nio.file.Paths -// -// @Disabled -// class ServiceLoaderUtilTest { -// @Test -// fun `loadPlugins loads the correct plugins`() { -// -// val pluginPath = Paths.get(System.getProperty("testingPluginDirectory")) -// val functions: List = ServiceLoaderUtil.loadFunctions(pluginPath) -// -// assertTrue(functions.map { it.signature.name }.contains("trim_lead")) -// assertTrue(functions.map { it.signature.name }.contains("test_power")) -// } -// } diff --git a/partiql-cli/src/test/resources/junit-platform.properties b/partiql-cli/src/test/resources/junit-platform.properties deleted file mode 100644 index ad19ea833b..0000000000 --- a/partiql-cli/src/test/resources/junit-platform.properties +++ /dev/null @@ -1,3 +0,0 @@ -junit.jupiter.execution.parallel.enabled = true -junit.jupiter.execution.parallel.mode.default = concurrent -junit.jupiter.execution.parallel.mode.classes.default = concurrent \ No newline at end of file diff --git a/partiql-cli/src/test/resources/read_file_tests/csv_with_empty_lines.csv b/partiql-cli/src/test/resources/read_file_tests/csv_with_empty_lines.csv deleted file mode 100644 index 27b5cbb9c6..0000000000 --- a/partiql-cli/src/test/resources/read_file_tests/csv_with_empty_lines.csv +++ /dev/null @@ -1,4 +0,0 @@ -1,2 - -3 - diff --git a/partiql-cli/src/test/resources/read_file_tests/customized.csv b/partiql-cli/src/test/resources/read_file_tests/customized.csv deleted file mode 100644 index 0831b9082f..0000000000 --- a/partiql-cli/src/test/resources/read_file_tests/customized.csv +++ /dev/null @@ -1,2 +0,0 @@ -id name balance -1 Bob 10000.00 diff --git a/partiql-cli/src/test/resources/read_file_tests/customized_escape.csv b/partiql-cli/src/test/resources/read_file_tests/customized_escape.csv deleted file mode 100644 index 6bc3d70f05..0000000000 --- a/partiql-cli/src/test/resources/read_file_tests/customized_escape.csv +++ /dev/null @@ -1,2 +0,0 @@ -id,name,balance -"/"1",Bob,10000.00 diff --git a/partiql-cli/src/test/resources/read_file_tests/customized_ignore_empty.csv b/partiql-cli/src/test/resources/read_file_tests/customized_ignore_empty.csv deleted file mode 100644 index 1b95c774d4..0000000000 --- a/partiql-cli/src/test/resources/read_file_tests/customized_ignore_empty.csv +++ /dev/null @@ -1,3 +0,0 @@ -id,name,balance - -1,Bob,10000.00 diff --git a/partiql-cli/src/test/resources/read_file_tests/customized_ignore_surrounding.csv b/partiql-cli/src/test/resources/read_file_tests/customized_ignore_surrounding.csv deleted file mode 100644 index ed7735f624..0000000000 --- a/partiql-cli/src/test/resources/read_file_tests/customized_ignore_surrounding.csv +++ /dev/null @@ -1,2 +0,0 @@ -id,name,balance - 1 , Bob , 10000.00 \ No newline at end of file diff --git a/partiql-cli/src/test/resources/read_file_tests/customized_line_breaker.csv b/partiql-cli/src/test/resources/read_file_tests/customized_line_breaker.csv deleted file mode 100644 index 176d27599c..0000000000 --- a/partiql-cli/src/test/resources/read_file_tests/customized_line_breaker.csv +++ /dev/null @@ -1,2 +0,0 @@ -id,name,balance -1,Bob,10000.00 diff --git a/partiql-cli/src/test/resources/read_file_tests/customized_quote.csv b/partiql-cli/src/test/resources/read_file_tests/customized_quote.csv deleted file mode 100644 index df1ba59e10..0000000000 --- a/partiql-cli/src/test/resources/read_file_tests/customized_quote.csv +++ /dev/null @@ -1,2 +0,0 @@ -id,name,balance -'1,',Bob,10000.00 diff --git a/partiql-cli/src/test/resources/read_file_tests/data.csv b/partiql-cli/src/test/resources/read_file_tests/data.csv deleted file mode 100644 index d72f2010a8..0000000000 --- a/partiql-cli/src/test/resources/read_file_tests/data.csv +++ /dev/null @@ -1 +0,0 @@ -1,2 diff --git a/partiql-cli/src/test/resources/read_file_tests/data.ion b/partiql-cli/src/test/resources/read_file_tests/data.ion deleted file mode 100644 index 8d04f961a0..0000000000 --- a/partiql-cli/src/test/resources/read_file_tests/data.ion +++ /dev/null @@ -1 +0,0 @@ -1 2 diff --git a/partiql-cli/src/test/resources/read_file_tests/data.tsv b/partiql-cli/src/test/resources/read_file_tests/data.tsv deleted file mode 100644 index 9874d6464a..0000000000 --- a/partiql-cli/src/test/resources/read_file_tests/data.tsv +++ /dev/null @@ -1 +0,0 @@ -1 2 diff --git a/partiql-cli/src/test/resources/read_file_tests/data_list.ion b/partiql-cli/src/test/resources/read_file_tests/data_list.ion deleted file mode 100644 index 44e2ace7e5..0000000000 --- a/partiql-cli/src/test/resources/read_file_tests/data_list.ion +++ /dev/null @@ -1 +0,0 @@ -[1, 2] diff --git a/partiql-cli/src/test/resources/read_file_tests/data_with_double_quotes_escape.csv b/partiql-cli/src/test/resources/read_file_tests/data_with_double_quotes_escape.csv deleted file mode 100644 index b368444829..0000000000 --- a/partiql-cli/src/test/resources/read_file_tests/data_with_double_quotes_escape.csv +++ /dev/null @@ -1 +0,0 @@ -"1,2",2 diff --git a/partiql-cli/src/test/resources/read_file_tests/data_with_header_line.csv b/partiql-cli/src/test/resources/read_file_tests/data_with_header_line.csv deleted file mode 100644 index aa667e7cd2..0000000000 --- a/partiql-cli/src/test/resources/read_file_tests/data_with_header_line.csv +++ /dev/null @@ -1,2 +0,0 @@ -col1,col2 -1,2 diff --git a/partiql-cli/src/test/resources/read_file_tests/data_with_header_line.tsv b/partiql-cli/src/test/resources/read_file_tests/data_with_header_line.tsv deleted file mode 100644 index bcd73173eb..0000000000 --- a/partiql-cli/src/test/resources/read_file_tests/data_with_header_line.tsv +++ /dev/null @@ -1,2 +0,0 @@ -col1 col2 -1 2 diff --git a/partiql-cli/src/test/resources/read_file_tests/data_with_ion_symbol_as_input.csv b/partiql-cli/src/test/resources/read_file_tests/data_with_ion_symbol_as_input.csv deleted file mode 100644 index d72f2010a8..0000000000 --- a/partiql-cli/src/test/resources/read_file_tests/data_with_ion_symbol_as_input.csv +++ /dev/null @@ -1 +0,0 @@ -1,2 diff --git a/partiql-cli/src/test/resources/read_file_tests/simple_excel.csv b/partiql-cli/src/test/resources/read_file_tests/simple_excel.csv deleted file mode 100644 index 290e649882..0000000000 --- a/partiql-cli/src/test/resources/read_file_tests/simple_excel.csv +++ /dev/null @@ -1,2 +0,0 @@ -title,category,price -harry potter,book,7.99 diff --git a/partiql-cli/src/test/resources/read_file_tests/simple_mysql.csv b/partiql-cli/src/test/resources/read_file_tests/simple_mysql.csv deleted file mode 100644 index 135b78e779..0000000000 --- a/partiql-cli/src/test/resources/read_file_tests/simple_mysql.csv +++ /dev/null @@ -1,2 +0,0 @@ -id name balance -1 B"ob 10000.00 diff --git a/partiql-cli/src/test/resources/read_file_tests/simple_postgresql.csv b/partiql-cli/src/test/resources/read_file_tests/simple_postgresql.csv deleted file mode 100644 index a3abcb70a8..0000000000 --- a/partiql-cli/src/test/resources/read_file_tests/simple_postgresql.csv +++ /dev/null @@ -1,2 +0,0 @@ -id,name,balance -1,B""ob,10000.00 diff --git a/partiql-cli/src/test/resources/read_file_tests/simple_postgresql.txt b/partiql-cli/src/test/resources/read_file_tests/simple_postgresql.txt deleted file mode 100644 index c2018d83fb..0000000000 --- a/partiql-cli/src/test/resources/read_file_tests/simple_postgresql.txt +++ /dev/null @@ -1,2 +0,0 @@ -id name balance -1 Bob 10000.00 diff --git a/partiql-eval/src/main/kotlin/org/partiql/eval/PartiQLEngineDefault.kt b/partiql-eval/src/main/kotlin/org/partiql/eval/PartiQLEngineDefault.kt index 9cf78860d8..1f386d0cd4 100644 --- a/partiql-eval/src/main/kotlin/org/partiql/eval/PartiQLEngineDefault.kt +++ b/partiql-eval/src/main/kotlin/org/partiql/eval/PartiQLEngineDefault.kt @@ -4,6 +4,7 @@ import org.partiql.eval.internal.Compiler import org.partiql.eval.internal.Environment import org.partiql.eval.internal.Symbols import org.partiql.plan.PartiQLPlan +import org.partiql.value.PartiQLCursor import org.partiql.value.PartiQLValue import org.partiql.value.PartiQLValueExperimental @@ -33,7 +34,8 @@ internal class PartiQLEngineDefault : PartiQLEngine { return when (statement) { is PartiQLStatement.Query -> try { val value = statement.execute() - PartiQLResult.Value(value) + val data = PartiQLCursor.of(value) + PartiQLResult.Value(data) } catch (ex: Exception) { PartiQLResult.Error(ex) } diff --git a/partiql-eval/src/main/kotlin/org/partiql/eval/PartiQLResult.kt b/partiql-eval/src/main/kotlin/org/partiql/eval/PartiQLResult.kt index 3a1f6c1a30..8e87850b4a 100644 --- a/partiql-eval/src/main/kotlin/org/partiql/eval/PartiQLResult.kt +++ b/partiql-eval/src/main/kotlin/org/partiql/eval/PartiQLResult.kt @@ -1,12 +1,12 @@ package org.partiql.eval -import org.partiql.value.PartiQLValue +import org.partiql.value.PartiQLCursor import org.partiql.value.PartiQLValueExperimental public sealed interface PartiQLResult { @OptIn(PartiQLValueExperimental::class) - public data class Value(public val value: PartiQLValue) : PartiQLResult + public data class Value(public val value: PartiQLCursor) : PartiQLResult public data class Error(public val cause: Throwable) : PartiQLResult } diff --git a/partiql-eval/src/test/kotlin/org/partiql/eval/internal/PartiQLEngineDefaultTest.kt b/partiql-eval/src/test/kotlin/org/partiql/eval/internal/PartiQLEngineDefaultTest.kt index d8dc6ecb9f..e34ba36249 100644 --- a/partiql-eval/src/test/kotlin/org/partiql/eval/internal/PartiQLEngineDefaultTest.kt +++ b/partiql-eval/src/test/kotlin/org/partiql/eval/internal/PartiQLEngineDefaultTest.kt @@ -21,6 +21,7 @@ import org.partiql.types.StaticType import org.partiql.value.CollectionValue import org.partiql.value.PartiQLValue import org.partiql.value.PartiQLValueExperimental +import org.partiql.value.PartiQLValueLoader import org.partiql.value.bagValue import org.partiql.value.boolValue import org.partiql.value.decimalValue @@ -1215,7 +1216,7 @@ class PartiQLEngineDefaultTest { throw returned.cause } } - val output = result.value + val output = PartiQLValueLoader.standard().load(result.value) assert(expected == output) { comparisonString(expected, output, plan.plan) } @@ -1284,7 +1285,7 @@ class PartiQLEngineDefaultTest { val plan = planner.plan(statement, session) val prepared = engine.prepare(plan.plan, PartiQLEngine.Session(mapOf("memory" to connector), mode = mode)) when (val result = engine.execute(prepared)) { - is PartiQLResult.Value -> return result.value to plan.plan + is PartiQLResult.Value -> return PartiQLValueLoader.standard().load(result.value) to plan.plan is PartiQLResult.Error -> throw result.cause } } diff --git a/partiql-spi/src/main/kotlin/org/partiql/spi/connector/ConnectorMetadata.kt b/partiql-spi/src/main/kotlin/org/partiql/spi/connector/ConnectorMetadata.kt index 3ae991daee..b71867d90d 100644 --- a/partiql-spi/src/main/kotlin/org/partiql/spi/connector/ConnectorMetadata.kt +++ b/partiql-spi/src/main/kotlin/org/partiql/spi/connector/ConnectorMetadata.kt @@ -60,5 +60,4 @@ public interface ConnectorMetadata { */ @FnExperimental public fun getAggregation(path: BindingPath): ConnectorHandle.Agg? - } diff --git a/partiql-types/build.gradle.kts b/partiql-types/build.gradle.kts index b7559baacf..f7f12f4a4e 100644 --- a/partiql-types/build.gradle.kts +++ b/partiql-types/build.gradle.kts @@ -23,6 +23,15 @@ dependencies { implementation(Deps.kotlinxCollections) } +// Need to add this as we have both Java and Kotlin sources. Dokka already handles multi-language projects. If +// Javadoc is enabled, we end up overwriting index.html (causing compilation errors). +tasks.withType() { + enabled = false +} +tasks.withType() { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + publish { artifactId = "partiql-types" name = "PartiQL Types" diff --git a/partiql-types/src/main/java/org/partiql/value/PartiQLCursor.java b/partiql-types/src/main/java/org/partiql/value/PartiQLCursor.java new file mode 100644 index 0000000000..eb3bd6dcbd --- /dev/null +++ b/partiql-types/src/main/java/org/partiql/value/PartiQLCursor.java @@ -0,0 +1,191 @@ +package org.partiql.value; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Iterator; +import org.jetbrains.annotations.NotNull; +import org.partiql.value.datetime.Date; +import org.partiql.value.datetime.Time; +import org.partiql.value.datetime.Timestamp; + +/** + * Data representing a database result set, which is usually generated by executing a statement that queries the database. + *

+ * A {@link PartiQLCursor} object maintains a cursor pointing to its current position in the underlying data. Initially the + * cursor is positioned before the first value. The {@link #next()} method moves the cursor to the next value. Please use + * {@link #hasNext()} before calling {@link #next()}. + * + * @see PartiQLValueLoader#load(PartiQLCursor) + * @see PartiQLValue + */ +public interface PartiQLCursor extends AutoCloseable, Iterator { + + @Override + @NotNull + PartiQLValueType next(); + + /** + * Positions the reader just before the contents of the current value, which must be a container (list, bag, + * sexp, or struct). There's no current value immediately after stepping in, so the next thing you'll want to do is call + * {@link #hasNext()} and {@link #next()} to move onto the first child value. + *

+ * If the container itself is the null value, stepIn() shall fail. Please use {@link #isNullValue()} before + * invoking this. + *

+ * At any time {@link #stepOut()} may be called to move the cursor back to (just after) the parent value, even if + * there are more children remaining. + */ + public void stepIn(); + + /** + * Positions the iterator after the current parent's value, moving up one level in the data hierarchy. There's no + * current value immediately after stepping out, so the next thing you'll want to do is call {@link #hasNext()} and + * {@link #next()} to move onto the following value. + */ + public void stepOut(); + + /** + * Determines whether the current value is a null value of any type (for example, null or null.int). It should be + * called before calling getters that return value types (int, long, boolean, double). + */ + public boolean isNullValue(); + + /** + * Determines whether the current value is the missing value. Similarly, one can invoke {@link #getType()}. + */ + public boolean isMissingValue(); + + /** + * @return the type of the data at the cursor. + */ + @NotNull + public PartiQLValueType getType(); + + /** + * @return the field name of the current value; or null if there is no valid current value or if the current value + * is not a field of a struct. + */ + public String getFieldName(); + + /** + * This is only applicable when the current value's type is {@link PartiQLValueType#STRING}. + */ + @NotNull + String getStringValue(); + + /** + * This is only applicable when the current value's type is {@link PartiQLValueType#CHAR}. + */ + @NotNull + String getCharValue(); + + /** + * This is only applicable when the current value's type is {@link PartiQLValueType#SYMBOL}. + */ + @NotNull + String getSymbolValue(); + + /** + * This is only applicable when the current value's type is {@link PartiQLValueType#BOOL}. + */ + public boolean getBoolValue(); + + /** + * This is only applicable when the current value's type is {@link PartiQLValueType#BINARY}. + */ + public byte[] getBinaryValue(); + + /** + * This is only applicable when the current value's type is {@link PartiQLValueType#BLOB}. + */ + public byte[] getBlobValue(); + + /** + * This is only applicable when the current value's type is {@link PartiQLValueType#CLOB}. + */ + public byte[] getClobValue(); + + /** + * This is only applicable when the current value's type is {@link PartiQLValueType#BYTE}. + */ + public byte getByteValue(); + + /** + * This is only applicable when the current value's type is {@link PartiQLValueType#DATE}. + */ + @NotNull + public Date getDateValue(); + + /** + * This is only applicable when the current value's type is {@link PartiQLValueType#TIME}. + */ + @NotNull + public Time getTimeValue(); + + /** + * This is only applicable when the current value's type is {@link PartiQLValueType#TIMESTAMP}. + */ + @NotNull + public Timestamp getTimestampValue(); + + /** + * This is only applicable when the current value's type is {@link PartiQLValueType#INTERVAL}. + */ + @Deprecated + public long getIntervalValue(); + + /** + * This is only applicable when the current value's type is {@link PartiQLValueType#INT8}. + */ + public byte getInt8Value(); + + /** + * This is only applicable when the current value's type is {@link PartiQLValueType#INT16}. + */ + public short getInt16Value(); + + /** + * This is only applicable when the current value's type is {@link PartiQLValueType#INT32}. + */ + public int getInt32Value(); + + /** + * This is only applicable when the current value's type is {@link PartiQLValueType#INT64}. + */ + public long getInt64Value(); + + /** + * This is only applicable when the current value's type is {@link PartiQLValueType#INT}. + */ + @NotNull + public BigInteger getIntValue(); + + /** + * This is only applicable when the current value's type is {@link PartiQLValueType#FLOAT32}. + */ + public float getFloat32Value(); + + /** + * This is only applicable when the current value's type is {@link PartiQLValueType#FLOAT64}. + */ + public double getFloat64Value(); + + /** + * This is only applicable when the current value's type is {@link PartiQLValueType#DECIMAL}. + */ + @NotNull + public BigDecimal getDecimalValue(); + + /** + * This is only applicable when the current value's type is {@link PartiQLValueType#DECIMAL_ARBITRARY}. + */ + @NotNull + public BigDecimal getDecimalArbitraryValue(); + + /** + * Converts a {@link PartiQLValue} into {@link PartiQLCursor}. + */ + static PartiQLCursor of(PartiQLValue value) { + return new PartiQLCursorDefault(value); + } +} diff --git a/partiql-types/src/main/java/org/partiql/value/PartiQLCursorDefault.java b/partiql-types/src/main/java/org/partiql/value/PartiQLCursorDefault.java new file mode 100644 index 0000000000..bc4483f921 --- /dev/null +++ b/partiql-types/src/main/java/org/partiql/value/PartiQLCursorDefault.java @@ -0,0 +1,398 @@ +package org.partiql.value; + +import kotlin.Pair; +import org.jetbrains.annotations.NotNull; +import org.partiql.value.datetime.Date; +import org.partiql.value.datetime.Time; +import org.partiql.value.datetime.Timestamp; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Stack; + +class PartiQLCursorDefault implements PartiQLCursor { + + @NotNull + private final Stack> iteratorStack; + + @NotNull + private Iterator currentIter; + + private NamedValue currentValue; + + PartiQLCursorDefault(PartiQLValue delegate) { + List wrappedList = new ArrayList<>(); + wrappedList.add(delegate); + Iterator topLevelIterator = unnamed(wrappedList.iterator()); + this.iteratorStack = new Stack<>(); + this.iteratorStack.push(topLevelIterator); + this.currentIter = topLevelIterator; + this.currentValue = null; + } + + @Override + public void close() { + currentIter = Collections.emptyIterator(); + currentValue = null; + iteratorStack.empty(); + } + + @Override + public boolean hasNext() { + return currentIter.hasNext(); + } + + @NotNull + @Override + public PartiQLValueType next() { + currentValue = currentIter.next(); + return currentValue.value.getType(); + } + + @Override + public void stepIn() { + org.partiql.value.PartiQLValue value = currentValue.value; + PartiQLValueType type = currentValue.value.getType(); + Iterator children; + switch (type) { + case LIST: + @SuppressWarnings("unchecked") + ListValue list = (ListValue) value; + children = unnamed(list.iterator()); + break; + case BAG: + @SuppressWarnings("unchecked") + BagValue bag = (BagValue) value; + children = unnamed(bag.iterator()); + break; + case SEXP: + @SuppressWarnings("unchecked") + SexpValue sexp = (SexpValue) value; + children = unnamed(sexp.iterator()); + break; + case STRUCT: + @SuppressWarnings("unchecked") + StructValue struct = (StructValue) value; + children = named(struct.getEntries()); + break; + default: + throw new IllegalStateException(); + } + iteratorStack.push(children); + currentValue = null; + currentIter = iteratorStack.peek(); + } + + @Override + public void stepOut() { + iteratorStack.pop(); + currentValue = null; + currentIter = iteratorStack.peek(); + } + + @Override + public boolean isNullValue() { + return currentValue.value.isNull(); + } + + @Override + public boolean isMissingValue() { + return currentValue.value.getType() == PartiQLValueType.MISSING; + } + + @NotNull + @Override + public PartiQLValueType getType() { + return currentValue.value.getType(); + } + + @Override + public String getFieldName() { + return currentValue.name; + } + + @NotNull + @Override + public String getStringValue() { + if (currentValue.value.getType() == PartiQLValueType.STRING) { + return Objects.requireNonNull(((StringValue) currentValue.value).getValue()); + } + throw new IllegalStateException(); + } + + @NotNull + @Override + public String getCharValue() { + if (currentValue.value.getType() == PartiQLValueType.CHAR) { + return Objects.requireNonNull((Objects.requireNonNull(((CharValue) currentValue.value).getValue()).toString())); + } + throw new IllegalStateException(); + } + + @NotNull + @Override + public String getSymbolValue() { + if (currentValue.value.getType() == PartiQLValueType.SYMBOL) { + return Objects.requireNonNull(((SymbolValue) currentValue.value).getValue()); + } + throw new IllegalStateException(); + } + + @Override + public boolean getBoolValue() { + PartiQLValueType type = currentValue.value.getType(); + if (type == PartiQLValueType.BOOL) { + BoolValue value = (BoolValue) (currentValue.value); + return Boolean.TRUE.equals(value.getValue()); + } else { + throw new IllegalStateException(); + } + } + + @Override + public byte[] getBinaryValue() { + PartiQLValueType type = currentValue.value.getType(); + if (type == PartiQLValueType.BINARY) { + BinaryValue binaryValue = (BinaryValue) (currentValue.value); + return Objects.requireNonNull(binaryValue.getValue()).toByteArray(); + } + throw new IllegalStateException(); + } + + @Override + public byte[] getBlobValue() { + PartiQLValueType type = currentValue.value.getType(); + if (type == PartiQLValueType.BLOB) { + BlobValue blobValue = (BlobValue) (currentValue.value); + return Objects.requireNonNull(blobValue.getValue()); + } + throw new IllegalStateException(); + } + + @Override + public byte[] getClobValue() { + PartiQLValueType type = currentValue.value.getType(); + if (type == PartiQLValueType.CLOB) { + ClobValue clobValue = (ClobValue) (currentValue.value); + return Objects.requireNonNull(clobValue.getValue()); + } + throw new IllegalStateException(); + } + + @Override + public byte getByteValue() { + PartiQLValueType type = currentValue.value.getType(); + if (type == PartiQLValueType.BYTE) { + ByteValue byteValue = (ByteValue) (currentValue.value); + return Objects.requireNonNull(byteValue.getValue()); + } + throw new IllegalStateException(); + } + + @Override + @NotNull + public Date getDateValue() { + PartiQLValueType type = currentValue.value.getType(); + if (type == PartiQLValueType.DATE) { + DateValue value = (DateValue) (currentValue.value); + return Objects.requireNonNull(value.getValue()); + } + throw new IllegalStateException(); + } + + @Override + @NotNull + public Time getTimeValue() { + PartiQLValueType type = currentValue.value.getType(); + if (type == PartiQLValueType.TIME) { + TimeValue value = (TimeValue) (currentValue.value); + return Objects.requireNonNull(value.getValue()); + } + throw new IllegalStateException(); + } + + @Override + @NotNull + public Timestamp getTimestampValue() { + PartiQLValueType type = currentValue.value.getType(); + if (type == PartiQLValueType.TIMESTAMP) { + TimestampValue value = (TimestampValue) (currentValue.value); + return Objects.requireNonNull(value.getValue()); + } + throw new IllegalStateException(); + } + + @Deprecated + @Override + public long getIntervalValue() { + PartiQLValueType type = currentValue.value.getType(); + if (type == PartiQLValueType.INTERVAL) { + IntervalValue value = (IntervalValue) (currentValue.value); + return Objects.requireNonNull(value.getValue()); + } + throw new IllegalStateException(); + } + + @Override + public byte getInt8Value() { + PartiQLValueType type = currentValue.value.getType(); + if (type == PartiQLValueType.INT8) { + Int8Value value = (Int8Value) (currentValue.value); + return Objects.requireNonNull(value.getValue()); + } + throw new IllegalStateException(); + } + + @Override + public short getInt16Value() { + PartiQLValueType type = currentValue.value.getType(); + if (type == PartiQLValueType.INT16) { + Int16Value value = (Int16Value) (currentValue.value); + return Objects.requireNonNull(value.getValue()); + } + throw new IllegalStateException(); + } + + @Override + public int getInt32Value() { + PartiQLValueType type = currentValue.value.getType(); + if (type == PartiQLValueType.INT32) { + Int32Value value = (Int32Value) (currentValue.value); + return Objects.requireNonNull(value.getValue()); + } + throw new IllegalStateException(); + } + + @Override + public long getInt64Value() { + PartiQLValueType type = currentValue.value.getType(); + if (type == PartiQLValueType.INT64) { + Int64Value value = (Int64Value) (currentValue.value); + return Objects.requireNonNull(value.getValue()); + } + throw new IllegalStateException(); + } + + @Override + @NotNull + public BigInteger getIntValue() { + PartiQLValueType type = currentValue.value.getType(); + if (type == PartiQLValueType.INT) { + IntValue value = (IntValue) (currentValue.value); + return Objects.requireNonNull(value.getValue()); + } + throw new IllegalStateException(); + } + + @Override + public float getFloat32Value() { + PartiQLValueType type = currentValue.value.getType(); + if (type == PartiQLValueType.FLOAT32) { + Float32Value value = (Float32Value) (currentValue.value); + return Objects.requireNonNull(value.getValue()); + } + throw new IllegalStateException(); + } + + @Override + public double getFloat64Value() { + PartiQLValueType type = currentValue.value.getType(); + if (type == PartiQLValueType.FLOAT64) { + Float64Value value = (Float64Value) (currentValue.value); + return Objects.requireNonNull(value.getValue()); + } + throw new IllegalStateException(); + } + + @Override + @NotNull + public BigDecimal getDecimalValue() { + PartiQLValueType type = currentValue.value.getType(); + if (type == PartiQLValueType.DECIMAL) { + DecimalValue value = (DecimalValue) (currentValue.value); + return Objects.requireNonNull(value.getValue()); + } + throw new IllegalStateException(); + } + + @NotNull + @Override + public BigDecimal getDecimalArbitraryValue() { + PartiQLValueType type = currentValue.value.getType(); + if (type == PartiQLValueType.DECIMAL_ARBITRARY) { + DecimalValue value = (DecimalValue) (currentValue.value); + return Objects.requireNonNull(value.getValue()); + } + throw new IllegalStateException(); + } + + private NamedIterator named(Iterable> values) { + return new NamedIterator(values); + } + + private UnnamedIterator unnamed(Iterator values) { + return new UnnamedIterator(values); + } + + private static class UnnamedIterator implements Iterator { + + @NotNull + Iterator values; + + UnnamedIterator(@NotNull Iterator values) { + this.values = values; + } + + @Override + public boolean hasNext() { + return values.hasNext(); + } + + @Override + public NamedValue next() { + return new NamedValue(values.next()); + } + } + + private static class NamedIterator implements Iterator { + + @NotNull + Iterator> values; + + NamedIterator(@NotNull Iterable> values) { + this.values = values.iterator(); + } + + @Override + public boolean hasNext() { + return values.hasNext(); + } + + @Override + public NamedValue next() { + Pair next = values.next(); + return new NamedValue(next.getFirst(), next.getSecond()); + } + } + + private static class NamedValue { + public String name; + + @NotNull + public PartiQLValue value; + + private NamedValue(String name, @NotNull PartiQLValue value) { + this.name = name; + this.value = value; + } + + private NamedValue(@NotNull PartiQLValue value) { + this.name = null; + this.value = value; + } + } +} diff --git a/partiql-types/src/main/java/org/partiql/value/PartiQLValueLoader.java b/partiql-types/src/main/java/org/partiql/value/PartiQLValueLoader.java new file mode 100644 index 0000000000..ae73c4c72f --- /dev/null +++ b/partiql-types/src/main/java/org/partiql/value/PartiQLValueLoader.java @@ -0,0 +1,30 @@ +package org.partiql.value; + +import org.jetbrains.annotations.NotNull; + +/** + * Provides functions for loading {@link PartiQLCursor} into {@link PartiQLValue} instances. + * + * @see PartiQLCursor + */ +public interface PartiQLValueLoader { + /** + * Loads un-materialized {@link PartiQLCursor} into a materialized {@link PartiQLValue}. The {@link PartiQLCursor} cursor + * must be set before the value that you'd like to load. + *

+ * This method will invoke {@link PartiQLCursor#next()}. This method will not throw an error is there is + * more data to be processed after the value immediately following the cursor. + * + * @param data the PartiQL data to load. + * @return a materialized, in-memory instance of a {@link PartiQLValue} containing the contents of the {@code data}. + */ + @NotNull + PartiQLValue load(@NotNull PartiQLCursor data); + + /** + * @return a basic implementation of {@link PartiQLValueLoader}. + */ + static PartiQLValueLoader standard() { + return new PartiQLValueLoaderDefault(); + } +} diff --git a/partiql-types/src/main/java/org/partiql/value/PartiQLValueLoaderDefault.java b/partiql-types/src/main/java/org/partiql/value/PartiQLValueLoaderDefault.java new file mode 100644 index 0000000000..a31c5b4050 --- /dev/null +++ b/partiql-types/src/main/java/org/partiql/value/PartiQLValueLoaderDefault.java @@ -0,0 +1,134 @@ +package org.partiql.value; + +import kotlin.Pair; +import kotlin.jvm.functions.Function1; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.List; + +import static org.partiql.value.PartiQL.*; + +class PartiQLValueLoaderDefault implements PartiQLValueLoader { + @NotNull + @Override + public PartiQLValue load(@NotNull PartiQLCursor data) { + data.next(); + return _loadSingleValue(data); + } + + /** + * MUST place the cursor on the data before executing. + * + * @param data the input PartiQL Data. + * @return a materialized PartiQL Value + */ + @NotNull + private PartiQLValue _loadSingleValue(@NotNull PartiQLCursor data) { + PartiQLValueType type = data.getType(); + switch (type) { + case BOOL: + return boolValue(orNull(data, PartiQLCursor::getBoolValue)); + case INT8: + return int8Value(orNull(data, PartiQLCursor::getInt8Value)); + case INT16: + return int16Value(orNull(data, PartiQLCursor::getInt16Value)); + case INT32: + return int32Value(orNull(data, PartiQLCursor::getInt32Value)); + case INT64: + return int64Value(orNull(data, PartiQLCursor::getInt64Value)); + case INT: + return intValue(orNull(data, PartiQLCursor::getIntValue)); + case LIST: + return collectionValue(data, PartiQL::listValue); + case BAG: + return collectionValue(data, PartiQL::bagValue); + case SEXP: + return collectionValue(data, PartiQL::sexpValue); + case STRUCT: + return createStructValue(data); + case NULL: + return nullValue(); + case STRING: + return stringValue(orNull(data, PartiQLCursor::getStringValue)); + case SYMBOL: + return symbolValue(orNull(data, PartiQLCursor::getSymbolValue)); + case CHAR: + // TODO: The implementation of CHAR VALUE is wrong. + String val = orNull(data, PartiQLCursor::getCharValue); + if (val == null) { + return charValue(null); + } + return charValue(val.charAt(0)); + case MISSING: + return missingValue(); + case DECIMAL_ARBITRARY: + return decimalValue(orNull(data, PartiQLCursor::getDecimalArbitraryValue)); + case DECIMAL: + return decimalValue(orNull(data, PartiQLCursor::getDecimalValue)); + case INTERVAL: + return intervalValue(orNull(data, PartiQLCursor::getIntervalValue)); + case TIMESTAMP: + return timestampValue(orNull(data, PartiQLCursor::getTimestampValue)); + case DATE: + return dateValue(orNull(data, PartiQLCursor::getDateValue)); + case CLOB: + return clobValue(orNull(data, PartiQLCursor::getClobValue)); + case BLOB: + return blobValue(orNull(data, PartiQLCursor::getBlobValue)); + case BINARY: + byte[] bytes = orNull(data, PartiQLCursor::getBinaryValue); + if (bytes == null) { + return binaryValue(null); + } + return binaryValue(BitSet.valueOf(bytes)); + case BYTE: + return byteValue(orNull(data, PartiQLCursor::getByteValue)); + case TIME: + return timeValue(orNull(data, PartiQLCursor::getTimeValue)); + case FLOAT32: + return float32Value(orNull(data, PartiQLCursor::getFloat32Value)); + case FLOAT64: + return float64Value(orNull(data, PartiQLCursor::getFloat64Value)); + case ANY: + default: + throw new IllegalStateException("Cannot load data of type: " + type); + } + } + + private R orNull(PartiQLCursor data, Function1 result) { + return data.isNullValue() ? null : result.invoke(data); + } + + private PartiQLValue collectionValue(PartiQLCursor data, Function1, PartiQLValue> collectionConstructor) { + if (data.isNullValue()) { + return collectionConstructor.invoke(null); + } + data.stepIn(); + List values = new ArrayList<>(); + while (data.hasNext()) { + data.next(); + PartiQLValue value = this._loadSingleValue(data); + values.add(value); + } + data.stepOut(); + return collectionConstructor.invoke(values); + } + + private PartiQLValue createStructValue(PartiQLCursor data) { + if (data.isNullValue()) { + return structValue((Iterable>) null); + } + data.stepIn(); + List> values = new ArrayList<>(); + while (data.hasNext()) { + data.next(); + String name = data.getFieldName(); + PartiQLValue value = this._loadSingleValue(data); + values.add(new Pair<>(name, value)); + } + data.stepOut(); + return structValue(values); + } +} diff --git a/plugins/partiql-fs/src/main/kotlin/org/partiql/plugins/fs/connector/FsMetadata.kt b/plugins/partiql-fs/src/main/kotlin/org/partiql/plugins/fs/connector/FsMetadata.kt index 740444cfa3..b30fddb657 100644 --- a/plugins/partiql-fs/src/main/kotlin/org/partiql/plugins/fs/connector/FsMetadata.kt +++ b/plugins/partiql-fs/src/main/kotlin/org/partiql/plugins/fs/connector/FsMetadata.kt @@ -4,7 +4,6 @@ import org.partiql.plugins.fs.index.FsIndex import org.partiql.plugins.fs.index.FsNode import org.partiql.spi.BindingPath import org.partiql.spi.connector.ConnectorHandle -import org.partiql.spi.connector.ConnectorObject import org.partiql.spi.connector.ConnectorPath import org.partiql.spi.connector.ConnectorSession import org.partiql.spi.connector.sql.SqlMetadata diff --git a/plugins/partiql-fs/src/main/kotlin/org/partiql/plugins/fs/connector/FsObject.kt b/plugins/partiql-fs/src/main/kotlin/org/partiql/plugins/fs/connector/FsObject.kt index 66e9c288ae..e9473f6d87 100644 --- a/plugins/partiql-fs/src/main/kotlin/org/partiql/plugins/fs/connector/FsObject.kt +++ b/plugins/partiql-fs/src/main/kotlin/org/partiql/plugins/fs/connector/FsObject.kt @@ -11,5 +11,4 @@ import org.partiql.types.StaticType internal class FsObject(private val type: StaticType) : ConnectorObject { override fun getType(): StaticType = type - } diff --git a/plugins/partiql-fs/src/main/kotlin/org/partiql/plugins/fs/index/FsIndex.kt b/plugins/partiql-fs/src/main/kotlin/org/partiql/plugins/fs/index/FsIndex.kt index b980114442..922b7a7661 100644 --- a/plugins/partiql-fs/src/main/kotlin/org/partiql/plugins/fs/index/FsIndex.kt +++ b/plugins/partiql-fs/src/main/kotlin/org/partiql/plugins/fs/index/FsIndex.kt @@ -15,7 +15,7 @@ internal class FsIndex(private val root: FsNode) { /** * Search the FsNode for the type. */ - fun search(path: BindingPath): Pair? { + fun search(path: BindingPath): Pair? { val match = mutableListOf() var curr: FsNode? = root for (step in path.steps) { @@ -38,7 +38,7 @@ internal class FsIndex(private val root: FsNode) { /** * List all FsNodes in the path. */ - fun list(path: BindingPath): List { + fun list(path: BindingPath): List { var curr: FsNode? = root for (step in path.steps) { if (curr == null) return emptyList() diff --git a/plugins/partiql-fs/src/main/kotlin/org/partiql/plugins/fs/index/FsMatch.kt b/plugins/partiql-fs/src/main/kotlin/org/partiql/plugins/fs/index/FsMatch.kt index bb2d004e8d..40401c766e 100644 --- a/plugins/partiql-fs/src/main/kotlin/org/partiql/plugins/fs/index/FsMatch.kt +++ b/plugins/partiql-fs/src/main/kotlin/org/partiql/plugins/fs/index/FsMatch.kt @@ -1,3 +1,3 @@ package org.partiql.plugins.fs.index -internal typealias KMatch = List +internal typealias FsMatch = List diff --git a/test/partiql-tests-runner/src/test/kotlin/org/partiql/runner/executor/EvalExecutor.kt b/test/partiql-tests-runner/src/test/kotlin/org/partiql/runner/executor/EvalExecutor.kt index fd45707425..86a5aaf05e 100644 --- a/test/partiql-tests-runner/src/test/kotlin/org/partiql/runner/executor/EvalExecutor.kt +++ b/test/partiql-tests-runner/src/test/kotlin/org/partiql/runner/executor/EvalExecutor.kt @@ -26,8 +26,10 @@ import org.partiql.spi.BindingPath import org.partiql.spi.connector.Connector import org.partiql.spi.connector.ConnectorSession import org.partiql.types.StaticType +import org.partiql.value.PartiQLCursor import org.partiql.value.PartiQLValue import org.partiql.value.PartiQLValueExperimental +import org.partiql.value.PartiQLValueLoader import org.partiql.value.io.PartiQLValueIonReaderBuilder import org.partiql.value.toIon @@ -49,20 +51,23 @@ class EvalExecutor( override fun fromIon(value: IonValue): PartiQLResult { val partiql = PartiQLValueIonReaderBuilder.standard().build(value.toIonElement()).read() - - return PartiQLResult.Value(partiql) + val data = PartiQLCursor.of(partiql) + return PartiQLResult.Value(data) } override fun toIon(value: PartiQLResult): IonValue { if (value is PartiQLResult.Value) { - return value.value.toIon().toIonValue(ION) + val actualValue = PartiQLValueLoader.standard().load(value.value) + return actualValue.toIon().toIonValue(ION) } error("PartiQLResult cannot be converted to Ion") } override fun compare(actual: PartiQLResult, expect: PartiQLResult): Boolean { if (actual is PartiQLResult.Value && expect is PartiQLResult.Value) { - return valueComparison(actual.value, expect.value) + val value = PartiQLValueLoader.standard().load(actual.value) + val expectedValue = PartiQLValueLoader.standard().load(expect.value) + return valueComparison(value, expectedValue) } error("Cannot compare different types of PartiQLResult") }