diff --git a/.classpath b/.classpath index c3b4b716..ebfacabc 100644 --- a/.classpath +++ b/.classpath @@ -1,15 +1,34 @@ - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/design.md b/doc/design.md index aa444da3..1b8837ef 100644 --- a/doc/design.md +++ b/doc/design.md @@ -11,7 +11,7 @@ The Data Query Language (DQL) building block is responsible for managing `SELECT ### Fluent Programming -###### Statement Construction With Fluent Programming +#### Statement Construction With Fluent Programming `dsn~statement-construction-with-fluent-programming~1` All statement builders use the "fluent programming" model, where the return type of each builder step determines the possible next structural elements that can be added. @@ -112,4 +112,70 @@ Covers: * `req~rendering.sql.confiugrable-identifier-quoting~1` +Needs: impl, utest + +### Exasol Dialect Specific + +#### Converting from 64 bit Integers to INTERVAL DAY TO SECOND +`dsn~exasol.converting-int-to-interval-day-to-second~1` + +The data converter converts integers to `INTERVAL DAY TO SECOND`. + +Covers: + +* `req~integer-interval-conversion~1` + +Needs: impl, utest + +#### Parsing INTERVAL DAY TO SECOND From Strings +`dsn~exasol.parsing-interval-day-to-second-from-strings~1` + +The data converter can parse `INTERVAL DAY TO SECOND` from strings in the following format: + + interval-d2s = [ days SP ] hours ":" minutes [ ":" seconds [ "." milliseconds ] ] + + hours = ( "2" "0" - "3" ) / ( [ "0" / "1" ] DIGIT ) + + minutes = ( "5" DIGIT ) / ( [ "0" - "4" ] DIGIT ) + + seconds = ( "5" DIGIT ) / ( [ "0" - "4" ] DIGIT ) + + milliseconds = 1*3DIGIT + +Examples are `12:30`, `12:30.081` or `100 12:30:00.081`. + +Covers: + +* `req~integer-interval-conversion~1` + +Needs: impl, utest + +#### Converting from 64 bit Integers to INTERVAL YEAR TO MONTH +`dsn~exasol.converting-int-to-interval-year-to-month~1` + +The data converter converts integers to `INTERVAL YEAR TO MONTH`. + +Covers: + +* `req~integer-interval-conversion~1` + +Needs: impl, utest + +#### Parsing INTERVAL YEAR TO MONTH From Strings +`dsn~exasol.parsing-interval-year-to-month-from-strings~1` + +The data converter can parse `INTERVAL YEAR TO MONTH` from strings in the following format: + + interval-y2m = days "-" months + + days = 1*9DIGIT + + months = ( "1" "0" - "2" ) / DIGIT + +Examples are `0-1` and `100-11`. + +Covers: + +* `req~integer-interval-conversion~1` + Needs: impl, utest \ No newline at end of file diff --git a/doc/system_requirements.md b/doc/system_requirements.md index 109c6efd..f6287272 100644 --- a/doc/system_requirements.md +++ b/doc/system_requirements.md @@ -84,6 +84,17 @@ Making sure at compile time that illegal constructs do not compile make the resu Needs: req +### Data Conversion +`feat~data-conversion~1` + +ESB converts between values of compatible data types. + +Rationale: + +Different databases and related tools use different ways to store and process similar data types. A collection of well-tested converters saves the API users time and trouble. + +Needs: req + ## Functional Requirements ### Statement Structure @@ -202,7 +213,7 @@ Covers: Needs: dsn -###### Configurable Identifier Quoting +#### Configurable Identifier Quoting `req~rendering.sql.confiugrable-identifier-quoting~1` ESB allows users to choose whether the following identifiers should be quoted in the rendered query: @@ -221,10 +232,6 @@ Covers: Needs: dsn -#### TODO - -* One line / pretty - #### SELECT Statement Rendering `req~rendering.sql.select~1` @@ -247,19 +254,19 @@ Covers: Needs: dsn -### TODO +### Exasol Dialect Specific Requirements + +###### Integer - Interval Conversion +`req~integer-interval-conversion~1` ---- +ESB converts values of type `INTERVAL` to integer and vice-versa. -SELECT -* Fields -* Asterisk ("*") +Rationale: + +Neighboring systems of an Exasol database often do not have equivalent data types, so conversion to a primitive data type is required. -FROM +Covers: -( INNER / ( LEFT / RIGHT / FULL ) OUTER ) JOIN -* ON +* [feat~data-conversion~1](#data-conversion) -LIMIT -* offset -* count +Needs: dsn \ No newline at end of file diff --git a/pom.xml b/pom.xml index efa0541c..d891ef7e 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,12 @@ ${junit.platform.version} test + + org.junit.jupiter + junit-jupiter-params + ${junit.version} + test + org.hamcrest hamcrest-all diff --git a/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java new file mode 100644 index 00000000..bb55524b --- /dev/null +++ b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java @@ -0,0 +1,141 @@ +package com.exasol.datatype.interval; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * This class implements the Exasol-proprietary data type INTERVAL DAY(x) TO SECONDS(y). It supports + * conversions to and from strings and from milliseconds. + * + *

+ * In Exasol this data type represents a time difference consisting of the following components: + *

+ *
    + *
  • days
  • + *
  • hours
  • + *
  • minutes
  • + *
  • seconds
  • + *
  • milliseconds (or fraction of seconds)
  • + *
+ * + * Since milliseconds are the highest resolution, each interval can also be expressed as a total number of milliseconds. + * This is also the recommended way to represent the interval values in other systems which do not natively support this + * data type. + */ +public class IntervalDayToSecond { + private static final long MILLIS_PER_SECOND = 1000L; + private static final long SECONDS_PER_MINUTE = 60L; + private static final long MINUTES_PER_HOUR = 60L; + private static final long HOURS_PER_DAY = 24L; + private static final long MILLIS_PER_MINUTE = SECONDS_PER_MINUTE * MILLIS_PER_SECOND; + private static final long MILLIS_PER_HOUR = MINUTES_PER_HOUR * MILLIS_PER_MINUTE; + private static final long MILLIS_PER_DAY = HOURS_PER_DAY * MILLIS_PER_HOUR; + private static final int DAYS_MATCHING_GROUP = 1; + private static final int HOURS_MATCHING_GROUP = 2; + private static final int MINUTES_MATCHING_GROUP = 3; + private static final int SECONDS_MATCHING_GROUP = 4; + private static final int MILLIS_MATCHING_GROUP = 5; + private static final Pattern INTERVAL_PATTERN = Pattern.compile("(?:(\\d{1,9})\\s+)?" // days + + "(\\d{1,2})" // hours + + ":(\\d{1,2})" // minutes + + "(?::(\\d{1,2})" // seconds + + "(?:\\.(\\d{1,3}))?)?" // milliseconds + ); + private final long value; + + private IntervalDayToSecond(final long value) { + this.value = value; + } + + private IntervalDayToSecond(final String text) { + final Matcher matcher = INTERVAL_PATTERN.matcher(text); + if (matcher.matches()) { + this.value = MILLIS_PER_DAY * parseMatchingGroupToLong(matcher, DAYS_MATCHING_GROUP) // + + MILLIS_PER_HOUR * parseMatchingGroupToLong(matcher, HOURS_MATCHING_GROUP) // + + MILLIS_PER_MINUTE * parseMatchingGroupToLong(matcher, MINUTES_MATCHING_GROUP) // + + MILLIS_PER_SECOND * parseMatchingGroupToLong(matcher, SECONDS_MATCHING_GROUP) // + + parseMatchingGroupToLong(matcher, MILLIS_MATCHING_GROUP); + } else { + throw new IllegalArgumentException( + "Text \"" + text + "\" cannot be parsed to an INTERVAL. Must match \"" + INTERVAL_PATTERN + "\""); + } + } + + private long parseMatchingGroupToLong(final Matcher matcher, final int groupNumber) { + return (matcher.group(groupNumber) == null) ? 0 : Long.parseLong(matcher.group(groupNumber)); + } + + @Override + public String toString() { + return hasDays() // + ? String.format("%d %d:%02d:%02d.%03d", getDays(), getHours(), getMinutes(), getSeconds(), getMillis()) // + : String.format("%d:%02d:%02d.%03d", getHours(), getMinutes(), getSeconds(), getMillis()); + } + + private boolean hasDays() { + return this.value >= MILLIS_PER_DAY; + } + + private long getDays() { + return this.value / MILLIS_PER_DAY; + } + + private long getHours() { + return this.value / MILLIS_PER_HOUR % HOURS_PER_DAY; + } + + private long getMinutes() { + return this.value / MILLIS_PER_MINUTE % MINUTES_PER_HOUR; + } + + private long getSeconds() { + return this.value / MILLIS_PER_SECOND % SECONDS_PER_MINUTE; + } + + private long getMillis() { + return this.value % MILLIS_PER_SECOND; + } + + /** + * Create an {@link IntervalDayToSecond} from a number of milliseconds + * + * @param value total length of the interval in milliseconds + * @return interval with milliseconds resolution + */ + // [impl->dsn~exasol.converting-int-to-interval-day-to-second~1] + public static IntervalDayToSecond ofMillis(final long value) { + return new IntervalDayToSecond(value); + } + + /** + * Parse an {@link IntervalDayToSecond} from a string + * + *

+ * The accepted format is: + *

+ *

+ * [dddddddd ]hh:mm[:ss[.SSS]] + *

+ * Where + *

+ *
+ *
d
+ *
day, 1-9 digits, optional
+ *
h
+ *
hours, 1-2 digits, mandatory
+ *
m
+ *
minutes, 1-2 digits, mandatory
+ *
s
+ *
seconds, 1-2 digits, optional
+ *
S
+ *
milliseconds, 1-3 digits, optional
+ *
+ * + * @param text string representing an interval + * @return interval with milliseconds resolution + */ + // [impl->dsn~exasol.parsing-interval-day-to-second-from-strings~1] + public static IntervalDayToSecond parse(final String text) { + return new IntervalDayToSecond(text); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/datatype/interval/IntervalYearToMonth.java b/src/main/java/com/exasol/datatype/interval/IntervalYearToMonth.java new file mode 100644 index 00000000..613a615a --- /dev/null +++ b/src/main/java/com/exasol/datatype/interval/IntervalYearToMonth.java @@ -0,0 +1,97 @@ +package com.exasol.datatype.interval; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * This class implements the Exasol-proprietary data type INTERVAL YEAR(x) TO MONTH(y). It supports + * conversions to and from strings and from a number of months. + * + *

+ * In Exasol this data type represents a time difference consisting of the following components: + *

+ *
    + *
  • years
  • + *
  • months
  • + *
+ * + * Since months are the highest resolution, each interval can also be expressed as a total number of months. This is + * also the recommended way to represent the interval values in other systems which do not natively support this data + * type. + */ +public class IntervalYearToMonth { + private static final long MONTHS_PER_YEAR = 12L; + private static final int YEARS_MATCHING_GROUP = 1; + private static final int MONTHS_MATCHING_GROUP = 2; + private static final Pattern INTERVAL_PATTERN = Pattern.compile("(\\d{1,9})-(\\d{1,2})"); + private final long value; + + private IntervalYearToMonth(final long value) { + this.value = value; + } + + private IntervalYearToMonth(final String text) { + final Matcher matcher = INTERVAL_PATTERN.matcher(text); + if (matcher.matches()) { + this.value = MONTHS_PER_YEAR * parseMatchingGroupToLong(matcher, YEARS_MATCHING_GROUP) // + + parseMatchingGroupToLong(matcher, MONTHS_MATCHING_GROUP); + } else { + throw new IllegalArgumentException( + "Text \"" + text + "\" cannot be parsed to an INTERVAL. Must match \"" + INTERVAL_PATTERN + "\""); + } + } + + private long parseMatchingGroupToLong(final Matcher matcher, final int groupNumber) { + return Long.parseLong(matcher.group(groupNumber)); + } + + @Override + public String toString() { + return String.format("%d-%02d", getYears(), getMonths()); + } + + private long getYears() { + return this.value / MONTHS_PER_YEAR; + } + + private long getMonths() { + return this.value % MONTHS_PER_YEAR; + } + + /** + * Create an {@link IntervalDayToSecond} from a number of months + * + * @param value total length of the interval in months + * @return interval with months resolution + */ + // [impl->dsn~exasol.converting-int-to-interval-year-to-month~1] + public static IntervalYearToMonth ofMonths(final long value) { + return new IntervalYearToMonth(value); + } + + /** + * Parse an {@link IntervalDayToSecond} from a string + * + *

+ * The accepted format is: + *

+ *

+ * YYYYYYYYY:MM + *

+ * Where + *

+ *
+ *
Y
+ *
years, 1-9 digits, mandatory
+ *
M
+ *
months, 1-2 digits, mandatory
+ *
+ * + * @param text string representing an interval + * @return interval with months resolution + */ + // [impl->dsn~exasol.parsing-interval-year-to-month-from-strings~1] + public static IntervalYearToMonth parse(final String text) { + return new IntervalYearToMonth(text); + } +} \ No newline at end of file diff --git a/src/test/java/com/exasol/datatype/interval/TestIntervalDayToSecond.java b/src/test/java/com/exasol/datatype/interval/TestIntervalDayToSecond.java new file mode 100644 index 00000000..285b5461 --- /dev/null +++ b/src/test/java/com/exasol/datatype/interval/TestIntervalDayToSecond.java @@ -0,0 +1,47 @@ +package com.exasol.datatype.interval; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class TestIntervalDayToSecond { + // [utest->dsn~exasol.converting-int-to-interval-day-to-second~1] + @ParameterizedTest + @CsvSource({ // + 0L + ", '0:00:00.000'", // + 999L + ", '0:00:00.999'", // + 59L * 1000 + ", '0:00:59.000'", // + 59L * 60 * 1000 + ", '0:59:00.000'", // + 23L * 60 * 60 * 1000 + ", '23:00:00.000'", // + 999999999L * 24 * 60 * 60 * 1000 + ", '999999999 0:00:00.000'", // + 1L * 24 * 60 * 60 * 1000 + 1 * 60 * 60 * 1000 + 1 * 60 * 1000 + 1 * 1000 + 1 + ", '1 1:01:01.001'" // + }) + void testofMillis(final long value, final String expected) { + assertThat(IntervalDayToSecond.ofMillis(value).toString(), equalTo(expected)); + } + + // [utest->dsn~exasol.parsing-interval-day-to-second-from-strings~1] + @ParameterizedTest + @CsvSource({ "'0:0', '0:00:00.000'", // + "'1:2:3', '1:02:03.000'", // + "'11:22:33.444', '11:22:33.444'", // + "'1 22:33:44.555', '1 22:33:44.555'", // + "'999999999 22:33:44', '999999999 22:33:44.000'" // + }) + void testParse(final String text, final String expected) { + assertThat(IntervalDayToSecond.parse(text).toString(), equalTo(expected)); + } + + // [utest->dsn~exasol.parsing-interval-day-to-second-from-strings~1] + @ParameterizedTest + @ValueSource(strings = { "0", ":0", "1.0", "123:45", "12:234", "12:34:567", "12:34:56:7890", // + "1000000000 0:0" // + }) + void testParseIllegalInputThrowsException(final String text) { + assertThrows(IllegalArgumentException.class, () -> IntervalDayToSecond.parse(text)); + } +} \ No newline at end of file diff --git a/src/test/java/com/exasol/datatype/interval/TestIntervalYearToMonth.java b/src/test/java/com/exasol/datatype/interval/TestIntervalYearToMonth.java new file mode 100644 index 00000000..8f25d81e --- /dev/null +++ b/src/test/java/com/exasol/datatype/interval/TestIntervalYearToMonth.java @@ -0,0 +1,42 @@ +package com.exasol.datatype.interval; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class TestIntervalYearToMonth { + // [utest->dsn~exasol.converting-int-to-interval-year-to-month~1] + @ParameterizedTest + @CsvSource({ // + 0L + ", '0-00'", // + 11L + ", '0-11'", // + 999999999L * 12 + ", '999999999-00'", // + 999999999L * 12 + 11 + ", '999999999-11'", // + 1L * 12 + 1 + ", '1-01'" // + }) + void testOfMonths(final long value, final String expected) { + assertThat(IntervalYearToMonth.ofMonths(value).toString(), equalTo(expected)); + } + + // [utest->dsn~exasol.parsing-interval-year-to-month-from-strings~1] + @ParameterizedTest + @CsvSource({ "'0-0', '0-00'", // + "'1-2', '1-02'", // + "'22-11', '22-11'", // + "'999999999-11', '999999999-11'" // + }) + void testParse(final String text, final String expected) { + assertThat(IntervalYearToMonth.parse(text).toString(), equalTo(expected)); + } + + // [utest->dsn~exasol.parsing-interval-year-to-month-from-strings~1] + @ParameterizedTest + @ValueSource(strings = { "0", "-0", "0-", "0-123", "1000000000-0" }) + void testParseIllegalInputThrowsException(final String text) { + assertThrows(IllegalArgumentException.class, () -> IntervalYearToMonth.parse(text)); + } +} \ No newline at end of file