diff --git a/.idea/icon.png b/.idea/icon.png deleted file mode 100644 index e45ccd0e9752..000000000000 Binary files a/.idea/icon.png and /dev/null differ diff --git a/core/trino-parser/src/main/java/io/trino/sql/parser/AstBuilder.java b/core/trino-parser/src/main/java/io/trino/sql/parser/AstBuilder.java index 6cdc3086b816..150fc462e54a 100644 --- a/core/trino-parser/src/main/java/io/trino/sql/parser/AstBuilder.java +++ b/core/trino-parser/src/main/java/io/trino/sql/parser/AstBuilder.java @@ -223,7 +223,6 @@ import io.trino.sql.tree.QuerySpecification; import io.trino.sql.tree.RangeQuantifier; import io.trino.sql.tree.RefreshMaterializedView; -import io.trino.sql.tree.RefreshView; import io.trino.sql.tree.Relation; import io.trino.sql.tree.RenameColumn; import io.trino.sql.tree.RenameMaterializedView; @@ -315,7 +314,6 @@ import io.trino.sql.tree.ZeroOrMoreQuantifier; import io.trino.sql.tree.ZeroOrOneQuantifier; import org.antlr.v4.runtime.ParserRuleContext; -import org.antlr.v4.runtime.RuleContext; import org.antlr.v4.runtime.Token; import org.antlr.v4.runtime.tree.ParseTree; import org.antlr.v4.runtime.tree.TerminalNode; @@ -383,14 +381,380 @@ class AstBuilder extends SqlBaseBaseVisitor { - private int parameterPosition; private final Optional baseLocation; + private int parameterPosition; AstBuilder(Optional baseLocation) { this.baseLocation = requireNonNull(baseLocation, "location is null"); } + private static SaveMode toSaveMode(TerminalNode replace, TerminalNode exists) + { + boolean isReplace = replace != null; + boolean isNotExists = exists != null; + checkArgument(!(isReplace && isNotExists), "'OR REPLACE' and 'IF NOT EXISTS' clauses can not be used together"); + + if (isReplace) { + return REPLACE; + } + + if (isNotExists) { + return IGNORE; + } + + return FAIL; + } + + private static List extractPrivileges(List privilegesOrRoles) + { + return privilegesOrRoles.stream().map(SqlBaseParser.PrivilegeOrRoleContext::getText).collect(toImmutableList()); + } + + private static boolean isPrivilegeKeywordContext(SqlBaseParser.PrivilegeOrRoleContext context) + { + return context.CREATE() != null || context.SELECT() != null || context.INSERT() != null || context.UPDATE() != null || context.DELETE() != null; + } + + private static List flatten(ParserRuleContext root, Function>> extractChildren) + { + List result = new ArrayList<>(); + Deque pending = new ArrayDeque<>(); + pending.push(root); + + while (!pending.isEmpty()) { + ParserRuleContext next = pending.pop(); + + Optional> children = extractChildren.apply(next); + if (children.isEmpty()) { + result.add(next); + } + else { + for (int i = children.get().size() - 1; i >= 0; i--) { + pending.push(children.get().get(i)); + } + } + } + + return result; + } + + private static Optional getRowsPerMatch(SqlBaseParser.RowsPerMatchContext context) + { + if (context == null) { + return Optional.empty(); + } + + if (context.ONE() != null) { + return Optional.of(ONE); + } + + if (context.emptyMatchHandling() == null) { + return Optional.of(ALL_SHOW_EMPTY); + } + + if (context.emptyMatchHandling().SHOW() != null) { + return Optional.of(ALL_SHOW_EMPTY); + } + + if (context.emptyMatchHandling().OMIT() != null) { + return Optional.of(ALL_OMIT_EMPTY); + } + + return Optional.of(ALL_WITH_UNMATCHED); + } + + private static Optional parsePrecision(Token precision) + { + if (precision == null) { + return Optional.empty(); + } + + return Optional.of(Integer.parseInt(precision.getText())); + } + + // ******************* statements ********************** + + private static Trim.Specification toTrimSpecification(Token token) + { + return switch (token.getType()) { + case SqlBaseLexer.BOTH -> Trim.Specification.BOTH; + case SqlBaseLexer.LEADING -> Trim.Specification.LEADING; + case SqlBaseLexer.TRAILING -> Trim.Specification.TRAILING; + default -> throw new IllegalArgumentException("Unsupported trim specification: " + token.getText()); + }; + } + + private static JsonFormat getJsonFormat(SqlBaseParser.JsonRepresentationContext context) + { + if (context.UTF8() != null) { + return UTF8; + } + if (context.UTF16() != null) { + return UTF16; + } + if (context.UTF32() != null) { + return UTF32; + } + return JSON; + } + + private static String decodeUnicodeLiteral(SqlBaseParser.UnicodeStringLiteralContext context) + { + char escape; + if (context.UESCAPE() != null) { + String escapeString = unquote(context.STRING().getText()); + check(!escapeString.isEmpty(), "Empty Unicode escape character", context); + check(escapeString.length() == 1, "Invalid Unicode escape character: " + escapeString, context); + escape = escapeString.charAt(0); + check(isValidUnicodeEscape(escape), "Invalid Unicode escape character: " + escapeString, context); + } + else { + escape = '\\'; + } + + String rawContent = unquote(context.UNICODE_STRING().getText().substring(2)); + StringBuilder unicodeStringBuilder = new StringBuilder(); + StringBuilder escapedCharacterBuilder = new StringBuilder(); + int charactersNeeded = 0; + UnicodeDecodeState state = UnicodeDecodeState.EMPTY; + for (int i = 0; i < rawContent.length(); i++) { + char ch = rawContent.charAt(i); + switch (state) { + case EMPTY -> { + if (ch == escape) { + state = UnicodeDecodeState.ESCAPED; + } + else { + unicodeStringBuilder.append(ch); + } + } + case ESCAPED -> { + if (ch == escape) { + unicodeStringBuilder.append(escape); + state = UnicodeDecodeState.EMPTY; + } + else if (ch == '+') { + state = UnicodeDecodeState.UNICODE_SEQUENCE; + charactersNeeded = 6; + } + else if (isHexDigit(ch)) { + state = UnicodeDecodeState.UNICODE_SEQUENCE; + charactersNeeded = 4; + escapedCharacterBuilder.append(ch); + } + else { + throw parseError("Invalid hexadecimal digit: " + ch, context); + } + } + case UNICODE_SEQUENCE -> { + check(isHexDigit(ch), "Incomplete escape sequence: " + escapedCharacterBuilder, context); + escapedCharacterBuilder.append(ch); + if (charactersNeeded == escapedCharacterBuilder.length()) { + String currentEscapedCode = escapedCharacterBuilder.toString(); + escapedCharacterBuilder.setLength(0); + int codePoint = Integer.parseInt(currentEscapedCode, 16); + check(Character.isValidCodePoint(codePoint), "Invalid escaped character: " + currentEscapedCode, context); + if (Character.isSupplementaryCodePoint(codePoint)) { + unicodeStringBuilder.appendCodePoint(codePoint); + } + else { + char currentCodePoint = (char) codePoint; + if (Character.isSurrogate(currentCodePoint)) { + throw parseError("Invalid escaped character: %s. Escaped character is a surrogate. Use '\\+123456' instead.".formatted(currentEscapedCode), context); + } + unicodeStringBuilder.append(currentCodePoint); + } + state = UnicodeDecodeState.EMPTY; + charactersNeeded = -1; + } + else { + check(charactersNeeded > escapedCharacterBuilder.length(), "Unexpected escape sequence length: " + escapedCharacterBuilder.length(), context); + } + } + default -> throw new UnsupportedOperationException(); + } + } + + check(state == UnicodeDecodeState.EMPTY, "Incomplete escape sequence: " + escapedCharacterBuilder, context); + return unicodeStringBuilder.toString(); + } + + private static String unquote(String value) + { + return value.substring(1, value.length() - 1) + .replace("''", "'"); + } + + private static LikeClause.PropertiesOption getPropertiesOption(Token token) + { + return switch (token.getType()) { + case SqlBaseLexer.INCLUDING -> LikeClause.PropertiesOption.INCLUDING; + case SqlBaseLexer.EXCLUDING -> LikeClause.PropertiesOption.EXCLUDING; + default -> throw new IllegalArgumentException("Unsupported LIKE option type: " + token.getText()); + }; + } + + private static boolean isDistinct(SqlBaseParser.SetQuantifierContext setQuantifier) + { + return setQuantifier != null && setQuantifier.DISTINCT() != null; + } + + private static boolean isHexDigit(char c) + { + return ((c >= '0') && (c <= '9')) || + ((c >= 'A') && (c <= 'F')) || + ((c >= 'a') && (c <= 'f')); + } + + private static boolean isValidUnicodeEscape(char c) + { + return c < 0x7F && c > 0x20 && !isHexDigit(c) && c != '"' && c != '+' && c != '\''; + } + + private static ArithmeticBinaryExpression.Operator getArithmeticBinaryOperator(Token operator) + { + return switch (operator.getType()) { + case SqlBaseLexer.PLUS -> ArithmeticBinaryExpression.Operator.ADD; + case SqlBaseLexer.MINUS -> ArithmeticBinaryExpression.Operator.SUBTRACT; + case SqlBaseLexer.ASTERISK -> ArithmeticBinaryExpression.Operator.MULTIPLY; + case SqlBaseLexer.SLASH -> ArithmeticBinaryExpression.Operator.DIVIDE; + case SqlBaseLexer.PERCENT -> ArithmeticBinaryExpression.Operator.MODULUS; + default -> throw new UnsupportedOperationException("Unsupported operator: " + operator.getText()); + }; + } + + private static ComparisonExpression.Operator getComparisonOperator(Token symbol) + { + return switch (symbol.getType()) { + case SqlBaseLexer.EQ -> ComparisonExpression.Operator.EQUAL; + case SqlBaseLexer.NEQ -> ComparisonExpression.Operator.NOT_EQUAL; + case SqlBaseLexer.LT -> ComparisonExpression.Operator.LESS_THAN; + case SqlBaseLexer.LTE -> ComparisonExpression.Operator.LESS_THAN_OR_EQUAL; + case SqlBaseLexer.GT -> ComparisonExpression.Operator.GREATER_THAN; + case SqlBaseLexer.GTE -> ComparisonExpression.Operator.GREATER_THAN_OR_EQUAL; + default -> throw new IllegalArgumentException("Unsupported operator: " + symbol.getText()); + }; + } + + private static IntervalLiteral.IntervalField getIntervalFieldType(Token token) + { + return switch (token.getType()) { + case SqlBaseLexer.YEAR -> IntervalLiteral.IntervalField.YEAR; + case SqlBaseLexer.MONTH -> IntervalLiteral.IntervalField.MONTH; + case SqlBaseLexer.DAY -> IntervalLiteral.IntervalField.DAY; + case SqlBaseLexer.HOUR -> IntervalLiteral.IntervalField.HOUR; + case SqlBaseLexer.MINUTE -> IntervalLiteral.IntervalField.MINUTE; + case SqlBaseLexer.SECOND -> IntervalLiteral.IntervalField.SECOND; + default -> throw new IllegalArgumentException("Unsupported interval field: " + token.getText()); + }; + } + + private static IntervalLiteral.Sign getIntervalSign(Token token) + { + return switch (token.getType()) { + case SqlBaseLexer.MINUS -> IntervalLiteral.Sign.NEGATIVE; + case SqlBaseLexer.PLUS -> IntervalLiteral.Sign.POSITIVE; + default -> throw new IllegalArgumentException("Unsupported sign: " + token.getText()); + }; + } + + private static WindowFrame.Type getFrameType(Token type) + { + return switch (type.getType()) { + case SqlBaseLexer.RANGE -> WindowFrame.Type.RANGE; + case SqlBaseLexer.ROWS -> WindowFrame.Type.ROWS; + case SqlBaseLexer.GROUPS -> WindowFrame.Type.GROUPS; + default -> throw new IllegalArgumentException("Unsupported frame type: " + type.getText()); + }; + } + + private static FrameBound.Type getBoundedFrameBoundType(Token token) + { + return switch (token.getType()) { + case SqlBaseLexer.PRECEDING -> FrameBound.Type.PRECEDING; + case SqlBaseLexer.FOLLOWING -> FrameBound.Type.FOLLOWING; + default -> throw new IllegalArgumentException("Unsupported bound type: " + token.getText()); + }; + } + + private static FrameBound.Type getUnboundedFrameBoundType(Token token) + { + return switch (token.getType()) { + case SqlBaseLexer.PRECEDING -> FrameBound.Type.UNBOUNDED_PRECEDING; + case SqlBaseLexer.FOLLOWING -> FrameBound.Type.UNBOUNDED_FOLLOWING; + default -> throw new IllegalArgumentException("Unsupported bound type: " + token.getText()); + }; + } + + private static SampledRelation.Type getSamplingMethod(Token token) + { + return switch (token.getType()) { + case SqlBaseLexer.BERNOULLI -> SampledRelation.Type.BERNOULLI; + case SqlBaseLexer.SYSTEM -> SampledRelation.Type.SYSTEM; + default -> throw new IllegalArgumentException("Unsupported sampling method: " + token.getText()); + }; + } + + private static SortItem.NullOrdering getNullOrderingType(Token token) + { + return switch (token.getType()) { + case SqlBaseLexer.FIRST -> SortItem.NullOrdering.FIRST; + case SqlBaseLexer.LAST -> SortItem.NullOrdering.LAST; + default -> throw new IllegalArgumentException("Unsupported ordering: " + token.getText()); + }; + } + + private static SortItem.Ordering getOrderingType(Token token) + { + return switch (token.getType()) { + case SqlBaseLexer.ASC -> SortItem.Ordering.ASCENDING; + case SqlBaseLexer.DESC -> SortItem.Ordering.DESCENDING; + default -> throw new IllegalArgumentException("Unsupported ordering: " + token.getText()); + }; + } + + private static QuantifiedComparisonExpression.Quantifier getComparisonQuantifier(Token symbol) + { + return switch (symbol.getType()) { + case SqlBaseLexer.ALL -> QuantifiedComparisonExpression.Quantifier.ALL; + case SqlBaseLexer.ANY -> QuantifiedComparisonExpression.Quantifier.ANY; + case SqlBaseLexer.SOME -> QuantifiedComparisonExpression.Quantifier.SOME; + default -> throw new IllegalArgumentException("Unsupported quantifier: " + symbol.getText()); + }; + } + + private static void check(boolean condition, String message, ParserRuleContext context) + { + if (!condition) { + throw parseError(message, context); + } + } + + private static ParsingException parseError(String message, ParserRuleContext context) + { + return new ParsingException(message, null, context.getStart().getLine(), context.getStart().getCharPositionInLine() + 1); + } + + private static QueryPeriod.RangeType getRangeType(Token token) + { + return switch (token.getType()) { + case SqlBaseLexer.TIMESTAMP -> QueryPeriod.RangeType.TIMESTAMP; + case SqlBaseLexer.VERSION -> QueryPeriod.RangeType.VERSION; + default -> throw new IllegalArgumentException("Unsupported query period range type: " + token.getText()); + }; + } + + private static void validateArgumentAlias(Identifier alias, ParserRuleContext context) + { + check( + alias.isDelimited() || !alias.getValue().equalsIgnoreCase("COPARTITION"), + "The word \"COPARTITION\" is ambiguous in this context. " + + "To alias an argument, precede the alias with \"AS\". " + + "To specify co-partitioning, change the argument order so that the last argument cannot be aliased.", + context); + } + @Override public Node visitSingleStatement(SqlBaseParser.SingleStatementContext context) { @@ -427,8 +791,6 @@ public Node visitStandaloneFunctionSpecification(SqlBaseParser.StandaloneFunctio return visit(context.functionSpecification()); } - // ******************* statements ********************** - @Override public Node visitUse(SqlBaseParser.UseContext context) { @@ -530,23 +892,6 @@ public Node visitSetAuthorization(SqlBaseParser.SetAuthorizationContext context) getPrincipalSpecification(context.principal())); } - private static SaveMode toSaveMode(TerminalNode replace, TerminalNode exists) - { - boolean isReplace = replace != null; - boolean isNotExists = exists != null; - checkArgument(!(isReplace && isNotExists), "'OR REPLACE' and 'IF NOT EXISTS' clauses can not be used together"); - - if (isReplace) { - return REPLACE; - } - - if (isNotExists) { - return IGNORE; - } - - return FAIL; - } - @Override public Node visitCreateTableAsSelect(SqlBaseParser.CreateTableAsSelectContext context) { @@ -640,14 +985,6 @@ public Node visitRefreshMaterializedView(SqlBaseParser.RefreshMaterializedViewCo new Table(getQualifiedName(context.qualifiedName()))); } - @Override - public Node visitRefreshView(SqlBaseParser.RefreshViewContext context) - { - return new RefreshView( - getLocation(context), - getQualifiedName(context.qualifiedName())); - } - @Override public Node visitDropMaterializedView(SqlBaseParser.DropMaterializedViewContext context) { @@ -683,7 +1020,7 @@ public Node visitInsertInto(SqlBaseParser.InsertIntoContext context) return new Insert( getLocation(context), - new Table(getLocation(context), getQualifiedName(context.qualifiedName()), visitIfPresent(context.branch, Identifier.class)), + new Table(getQualifiedName(context.qualifiedName())), columnAliases, (Query) visit(context.rootQuery())); } @@ -693,7 +1030,7 @@ public Node visitDelete(SqlBaseParser.DeleteContext context) { return new Delete( getLocation(context), - new Table(getLocation(context), getQualifiedName(context.qualifiedName()), visitIfPresent(context.branch, Identifier.class)), + new Table(getLocation(context), getQualifiedName(context.qualifiedName())), visitIfPresent(context.booleanExpression(), Expression.class)); } @@ -702,7 +1039,7 @@ public Node visitUpdate(SqlBaseParser.UpdateContext context) { return new Update( getLocation(context), - new Table(getLocation(context), getQualifiedName(context.qualifiedName()), visitIfPresent(context.branch, Identifier.class)), + new Table(getLocation(context), getQualifiedName(context.qualifiedName())), visit(context.updateAssignment(), UpdateAssignment.class), visitIfPresent(context.booleanExpression(), Expression.class)); } @@ -722,10 +1059,10 @@ public Node visitTruncateTable(SqlBaseParser.TruncateTableContext context) @Override public Node visitMerge(SqlBaseParser.MergeContext context) { - Table table = new Table(getLocation(context), getQualifiedName(context.qualifiedName()), visitIfPresent(context.branch, Identifier.class)); + Table table = new Table(getLocation(context), getQualifiedName(context.qualifiedName())); Relation targetRelation = table; - if (context.alias != null) { - targetRelation = new AliasedRelation(table, (Identifier) visit(context.alias), null); + if (context.identifier() != null) { + targetRelation = new AliasedRelation(table, (Identifier) visit(context.identifier()), null); } return new Merge( getLocation(context), @@ -901,6 +1238,8 @@ public Node visitDropColumn(SqlBaseParser.DropColumnContext context) context.EXISTS().stream().anyMatch(node -> node.getSymbol().getTokenIndex() > context.COLUMN().getSymbol().getTokenIndex())); } + // ********************** query expressions ******************** + @Override public Node visitTableExecute(SqlBaseParser.TableExecuteContext context) { @@ -1002,8 +1341,7 @@ public Node visitCreateBranch(SqlBaseParser.CreateBranchContext context) return new CreateBranch( getLocation(context), getQualifiedName(context.qualifiedName()), - (Identifier) visit(context.branch), - visitIfPresent(context.from, Identifier.class), + (Identifier) visit(context.identifier()), toSaveMode(context.REPLACE(), context.EXISTS()), properties); } @@ -1163,8 +1501,6 @@ public Node visitProperty(SqlBaseParser.PropertyContext context) return new Property(location, name, value); } - // ********************** query expressions ******************** - @Override public Node visitRootQuery(SqlBaseParser.RootQueryContext context) { @@ -1565,6 +1901,8 @@ public Node visitShowStats(SqlBaseParser.ShowStatsContext context) return new ShowStats(getLocation(context), new Table(getQualifiedName(context.qualifiedName()))); } + // ***************** boolean expressions ****************** + @Override public Node visitShowStatsForQuery(SqlBaseParser.ShowStatsForQueryContext context) { @@ -1590,6 +1928,8 @@ public Node visitShowCreateMaterializedView(SqlBaseParser.ShowCreateMaterialized return new ShowCreate(getLocation(context), ShowCreate.Type.MATERIALIZED_VIEW, getQualifiedName(context.qualifiedName())); } + // *************** from clause ***************** + @Override public Node visitShowCreateFunction(SqlBaseParser.ShowCreateFunctionContext context) { @@ -1722,11 +2062,6 @@ public Node visitRevokeRoles(SqlBaseParser.RevokeRolesContext context) visitIfPresent(context.catalog, Identifier.class)); } - private static List extractPrivileges(List privilegesOrRoles) - { - return privilegesOrRoles.stream().map(AstBuilder::privilegeToString).collect(toImmutableList()); - } - private Set extractRoles(List privilegesOrRoles) { privilegesOrRoles.forEach(context -> { @@ -1737,18 +2072,12 @@ private Set extractRoles(List return privilegesOrRoles.stream().map(context -> (Identifier) visit(context.identifier())).collect(toImmutableSet()); } - private static boolean isPrivilegeKeywordContext(SqlBaseParser.PrivilegeOrRoleContext context) - { - return context.CREATE() != null || context.SELECT() != null || context.INSERT() != null || context.UPDATE() != null || context.DELETE() != null; - } - private GrantObject createGrantObject(NodeLocation location, SqlBaseParser.GrantObjectContext context) { return new GrantObject( location, context.entityKind() == null ? Optional.empty() : Optional.of(context.entityKind().getText()), - getQualifiedName(context.qualifiedName()), - getIdentifierIfPresent(context.branch)); + getQualifiedName(context.qualifiedName())); } @Override @@ -1777,7 +2106,7 @@ public Node visitDeny(SqlBaseParser.DenyContext context) } else { privileges = Optional.of(context.privilege().stream() - .map(AstBuilder::privilegeToString) + .map(SqlBaseParser.PrivilegeContext::getText) .collect(toList())); } return new Deny( @@ -1787,16 +2116,6 @@ public Node visitDeny(SqlBaseParser.DenyContext context) getPrincipalSpecification(context.grantee)); } - private static String privilegeToString(RuleContext context) - { - checkArgument(context.getChildCount() >= 1, "Privilege context must have at least one child"); - List children = new ArrayList<>(); - for (int i = 0; i < context.getChildCount(); i++) { - children.add(context.getChild(i).getText()); - } - return String.join(" ", children); - } - @Override public Node visitShowGrants(SqlBaseParser.ShowGrantsContext context) { @@ -1839,7 +2158,7 @@ public Node visitSetTimeZone(SqlBaseParser.SetTimeZoneContext context) return new SetTimeZone(getLocation(context), timeZone); } - // ***************** boolean expressions ****************** + // ********************* predicates ******************* @Override public Node visitLogicalNot(SqlBaseParser.LogicalNotContext context) @@ -1875,31 +2194,6 @@ public Node visitAnd(SqlBaseParser.AndContext context) return new LogicalExpression(getLocation(context), LogicalExpression.Operator.AND, visit(terms, Expression.class)); } - private static List flatten(ParserRuleContext root, Function>> extractChildren) - { - List result = new ArrayList<>(); - Deque pending = new ArrayDeque<>(); - pending.push(root); - - while (!pending.isEmpty()) { - ParserRuleContext next = pending.pop(); - - Optional> children = extractChildren.apply(next); - if (children.isEmpty()) { - result.add(next); - } - else { - for (int i = children.get().size() - 1; i >= 0; i--) { - pending.push(children.get().get(i)); - } - } - } - - return result; - } - - // *************** from clause ***************** - @Override public Node visitJoinRelation(SqlBaseParser.JoinRelationContext context) { @@ -2012,31 +2306,6 @@ public Node visitMeasureDefinition(SqlBaseParser.MeasureDefinitionContext contex return new MeasureDefinition(getLocation(context), (Expression) visit(context.expression()), (Identifier) visit(context.identifier())); } - private static Optional getRowsPerMatch(SqlBaseParser.RowsPerMatchContext context) - { - if (context == null) { - return Optional.empty(); - } - - if (context.ONE() != null) { - return Optional.of(ONE); - } - - if (context.emptyMatchHandling() == null) { - return Optional.of(ALL_SHOW_EMPTY); - } - - if (context.emptyMatchHandling().SHOW() != null) { - return Optional.of(ALL_SHOW_EMPTY); - } - - if (context.emptyMatchHandling().OMIT() != null) { - return Optional.of(ALL_OMIT_EMPTY); - } - - return Optional.of(ALL_WITH_UNMATCHED); - } - @Override public Node visitSkipTo(SqlBaseParser.SkipToContext context) { @@ -2067,6 +2336,8 @@ public Node visitVariableDefinition(SqlBaseParser.VariableDefinitionContext cont return new VariableDefinition(getLocation(context), (Identifier) visit(context.identifier()), (Expression) visit(context.expression())); } + // ************** value expressions ************** + @Override public Node visitAliasedRelation(SqlBaseParser.AliasedRelationContext context) { @@ -2088,7 +2359,7 @@ public Node visitAliasedRelation(SqlBaseParser.AliasedRelationContext context) public Node visitTableName(SqlBaseParser.TableNameContext context) { if (context.queryPeriod() != null) { - return new Table(getLocation(context), getQualifiedName(context.qualifiedName()), (QueryPeriod) visit(context.queryPeriod()), Optional.empty()); + return new Table(getLocation(context), getQualifiedName(context.qualifiedName()), (QueryPeriod) visit(context.queryPeriod())); } return new Table(getLocation(context), getQualifiedName(context.qualifiedName())); } @@ -2117,6 +2388,8 @@ public Node visitTableFunctionInvocation(SqlBaseParser.TableFunctionInvocationCo return visit(context.tableFunctionCall()); } + // ********************* primary expressions ********************** + @Override public Node visitTableFunctionCall(SqlBaseParser.TableFunctionCallContext context) { @@ -2240,8 +2513,6 @@ public Node visitParenthesizedRelation(SqlBaseParser.ParenthesizedRelationContex return visit(context.relation()); } - // ********************* predicates ******************* - @Override public Node visitPredicated(SqlBaseParser.PredicatedContext context) { @@ -2369,8 +2640,6 @@ public Node visitQuantifiedComparison(SqlBaseParser.QuantifiedComparisonContext new SubqueryExpression(getLocation(context.query()), (Query) visit(context.query()))); } - // ************** value expressions ************** - @Override public Node visitArithmeticUnary(SqlBaseParser.ArithmeticUnaryContext context) { @@ -2424,8 +2693,6 @@ public Node visitTimeZoneString(SqlBaseParser.TimeZoneStringContext context) return visit(context.string()); } - // ********************* primary expressions ********************** - @Override public Node visitParenthesizedExpression(SqlBaseParser.ParenthesizedExpressionContext context) { @@ -2489,15 +2756,6 @@ public Node visitCurrentTimestamp(SqlBaseParser.CurrentTimestampContext context) .orElseGet(() -> new CurrentTimestamp(getLocation(context), Optional.empty())); } - private static Optional parsePrecision(Token precision) - { - if (precision == null) { - return Optional.empty(); - } - - return Optional.of(Integer.parseInt(precision.getText())); - } - @Override public Node visitCurrentCatalog(SqlBaseParser.CurrentCatalogContext context) { @@ -2615,16 +2873,6 @@ public Node visitTrim(SqlBaseParser.TrimContext context) visitIfPresent(context.trimChar, Expression.class)); } - private static Trim.Specification toTrimSpecification(Token token) - { - return switch (token.getType()) { - case SqlBaseLexer.BOTH -> Trim.Specification.BOTH; - case SqlBaseLexer.LEADING -> Trim.Specification.LEADING; - case SqlBaseLexer.TRAILING -> Trim.Specification.TRAILING; - default -> throw new IllegalArgumentException("Unsupported trim specification: " + token.getText()); - }; - } - @Override public Node visitJsonExists(SqlBaseParser.JsonExistsContext context) { @@ -2804,20 +3052,6 @@ public Node visitJsonPathInvocation(SqlBaseParser.JsonPathInvocationContext cont return new JsonPathInvocation(getLocation(context), jsonInput, inputFormat, jsonPath, pathName, pathParameters); } - private static JsonFormat getJsonFormat(SqlBaseParser.JsonRepresentationContext context) - { - if (context.UTF8() != null) { - return UTF8; - } - if (context.UTF16() != null) { - return UTF16; - } - if (context.UTF32() != null) { - return UTF32; - } - return JSON; - } - @Override public Node visitJsonArgument(SqlBaseParser.JsonArgumentContext context) { @@ -3134,7 +3368,6 @@ public Node visitColumnDefinition(SqlBaseParser.ColumnDefinitionContext context) getLocation(context), getQualifiedName(context.qualifiedName()), (DataType) visit(context.type()), - visitIfPresent(context.literal(), Expression.class), nullable, properties, comment); @@ -3164,6 +3397,8 @@ public Node visitSortItem(SqlBaseParser.SortItemContext context) .orElse(SortItem.NullOrdering.UNDEFINED)); } + // ************** literals ************** + @Override public Node visitWindowFrame(SqlBaseParser.WindowFrameContext context) { @@ -3262,6 +3497,8 @@ public Node visitPatternVariable(SqlBaseParser.PatternVariableContext context) return new PatternVariable(getLocation(context), (Identifier) visit(context.identifier())); } + // ***************** arguments ***************** + @Override public Node visitEmptyPattern(SqlBaseParser.EmptyPatternContext context) { @@ -3340,8 +3577,6 @@ public Node visitRangeQuantifier(SqlBaseParser.RangeQuantifierContext context) return new RangeQuantifier(getLocation(context), greedy, atLeast, atMost); } - // ************** literals ************** - @Override public Node visitNullLiteral(SqlBaseParser.NullLiteralContext context) { @@ -3433,8 +3668,6 @@ public Node visitParameter(SqlBaseParser.ParameterContext context) return parameter; } - // ***************** arguments ***************** - @Override public Node visitPositionalArgument(SqlBaseParser.PositionalArgumentContext context) { @@ -3476,6 +3709,8 @@ public Node visitRowType(SqlBaseParser.RowTypeContext context) return new RowDataType(getLocation(context), fields); } + // ***************** functions & stored procedures ***************** + @Override public Node visitRowField(SqlBaseParser.RowFieldContext context) { @@ -3814,8 +4049,6 @@ public Node visitJsonTableDefaultPlan(SqlBaseParser.JsonTableDefaultPlanContext return new JsonTableDefaultPlan(getLocation(context), parentChildPlanType, siblingsPlanType); } - // ***************** functions & stored procedures ***************** - @Override public Node visitFunctionSpecification(SqlBaseParser.FunctionSpecificationContext context) { @@ -3884,6 +4117,8 @@ public Node visitLanguageCharacteristic(SqlBaseParser.LanguageCharacteristicCont return new LanguageCharacteristic(getLocation(context), (Identifier) visit(context.identifier())); } + // ***************** helpers ***************** + @Override public Node visitDeterministicCharacteristic(SqlBaseParser.DeterministicCharacteristicContext context) { @@ -3921,7 +4156,7 @@ public Node visitPropertiesCharacteristic(SqlBaseParser.PropertiesCharacteristic { return new PropertiesCharacteristic( getLocation(context), - visit(context.properties().propertyAssignments().property(), Property.class)); + visit(context.properties().propertyAssignments().property(), Property.class)); } @Override @@ -3935,416 +4170,182 @@ public Node visitAssignmentStatement(SqlBaseParser.AssignmentStatementContext co { return new AssignmentStatement( getLocation(context), - (Identifier) visit(context.identifier()), - (Expression) visit(context.expression())); - } - - @Override - public Node visitSimpleCaseStatement(SqlBaseParser.SimpleCaseStatementContext context) - { - return new CaseStatement( - getLocation(context), - visitIfPresent(context.expression(), Expression.class), - visit(context.caseStatementWhenClause(), CaseStatementWhenClause.class), - visitIfPresent(context.elseClause(), ElseClause.class)); - } - - @Override - public Node visitSearchedCaseStatement(SqlBaseParser.SearchedCaseStatementContext context) - { - return new CaseStatement( - getLocation(context), - Optional.empty(), - visit(context.caseStatementWhenClause(), CaseStatementWhenClause.class), - visitIfPresent(context.elseClause(), ElseClause.class)); - } - - @Override - public Node visitCaseStatementWhenClause(SqlBaseParser.CaseStatementWhenClauseContext context) - { - return new CaseStatementWhenClause( - getLocation(context), - (Expression) visit(context.expression()), - visit(context.sqlStatementList().controlStatement(), ControlStatement.class)); - } - - @Override - public Node visitIfStatement(SqlBaseParser.IfStatementContext context) - { - return new IfStatement( - getLocation(context), - (Expression) visit(context.expression()), - visit(context.sqlStatementList().controlStatement(), ControlStatement.class), - visit(context.elseIfClause(), ElseIfClause.class), - visitIfPresent(context.elseClause(), ElseClause.class)); - } - - @Override - public Node visitElseIfClause(SqlBaseParser.ElseIfClauseContext context) - { - return new ElseIfClause( - getLocation(context), - (Expression) visit(context.expression()), - visit(context.sqlStatementList().controlStatement(), ControlStatement.class)); - } - - @Override - public Node visitElseClause(SqlBaseParser.ElseClauseContext context) - { - return new ElseClause( - getLocation(context), - visit(context.sqlStatementList().controlStatement(), ControlStatement.class)); - } - - @Override - public Node visitIterateStatement(SqlBaseParser.IterateStatementContext context) - { - return new IterateStatement( - getLocation(context), - (Identifier) visit(context.identifier())); - } - - @Override - public Node visitLeaveStatement(SqlBaseParser.LeaveStatementContext context) - { - return new LeaveStatement( - getLocation(context), - (Identifier) visit(context.identifier())); - } - - @Override - public Node visitVariableDeclaration(SqlBaseParser.VariableDeclarationContext context) - { - return new VariableDeclaration( - getLocation(context), - visit(context.identifier(), Identifier.class), - (DataType) visit(context.type()), - visitIfPresent(context.valueExpression(), Expression.class)); - } - - @Override - public Node visitCompoundStatement(SqlBaseParser.CompoundStatementContext context) - { - return new CompoundStatement( - getLocation(context), - visit(context.variableDeclaration(), VariableDeclaration.class), - visit(Optional.ofNullable(context.sqlStatementList()) - .map(SqlBaseParser.SqlStatementListContext::controlStatement) - .orElse(ImmutableList.of()), ControlStatement.class)); - } - - @Override - public Node visitLoopStatement(SqlBaseParser.LoopStatementContext context) - { - return new LoopStatement( - getLocation(context), - getIdentifierIfPresent(context.label), - visit(context.sqlStatementList().controlStatement(), ControlStatement.class)); - } - - @Override - public Node visitWhileStatement(SqlBaseParser.WhileStatementContext context) - { - return new WhileStatement( - getLocation(context), - getIdentifierIfPresent(context.label), - (Expression) visit(context.expression()), - visit(context.sqlStatementList().controlStatement(), ControlStatement.class)); - } - - @Override - public Node visitRepeatStatement(SqlBaseParser.RepeatStatementContext context) - { - return new RepeatStatement( - getLocation(context), - getIdentifierIfPresent(context.label), - visit(context.sqlStatementList().controlStatement(), ControlStatement.class), - (Expression) visit(context.expression())); - } - - // ***************** helpers ***************** - - @Override - protected Node defaultResult() - { - return null; - } - - @Override - protected Node aggregateResult(Node aggregate, Node nextResult) - { - if (nextResult == null) { - throw new UnsupportedOperationException("not yet implemented"); - } - - if (aggregate == null) { - return nextResult; - } - - throw new UnsupportedOperationException("not yet implemented"); - } - - private enum UnicodeDecodeState - { - EMPTY, - ESCAPED, - UNICODE_SEQUENCE - } - - private static String decodeUnicodeLiteral(SqlBaseParser.UnicodeStringLiteralContext context) - { - char escape; - if (context.UESCAPE() != null) { - String escapeString = unquote(context.STRING().getText()); - check(!escapeString.isEmpty(), "Empty Unicode escape character", context); - check(escapeString.length() == 1, "Invalid Unicode escape character: " + escapeString, context); - escape = escapeString.charAt(0); - check(isValidUnicodeEscape(escape), "Invalid Unicode escape character: " + escapeString, context); - } - else { - escape = '\\'; - } - - String rawContent = unquote(context.UNICODE_STRING().getText().substring(2)); - StringBuilder unicodeStringBuilder = new StringBuilder(); - StringBuilder escapedCharacterBuilder = new StringBuilder(); - int charactersNeeded = 0; - UnicodeDecodeState state = UnicodeDecodeState.EMPTY; - for (int i = 0; i < rawContent.length(); i++) { - char ch = rawContent.charAt(i); - switch (state) { - case EMPTY -> { - if (ch == escape) { - state = UnicodeDecodeState.ESCAPED; - } - else { - unicodeStringBuilder.append(ch); - } - } - case ESCAPED -> { - if (ch == escape) { - unicodeStringBuilder.append(escape); - state = UnicodeDecodeState.EMPTY; - } - else if (ch == '+') { - state = UnicodeDecodeState.UNICODE_SEQUENCE; - charactersNeeded = 6; - } - else if (isHexDigit(ch)) { - state = UnicodeDecodeState.UNICODE_SEQUENCE; - charactersNeeded = 4; - escapedCharacterBuilder.append(ch); - } - else { - throw parseError("Invalid hexadecimal digit: " + ch, context); - } - } - case UNICODE_SEQUENCE -> { - check(isHexDigit(ch), "Incomplete escape sequence: " + escapedCharacterBuilder, context); - escapedCharacterBuilder.append(ch); - if (charactersNeeded == escapedCharacterBuilder.length()) { - String currentEscapedCode = escapedCharacterBuilder.toString(); - escapedCharacterBuilder.setLength(0); - int codePoint = Integer.parseInt(currentEscapedCode, 16); - check(Character.isValidCodePoint(codePoint), "Invalid escaped character: " + currentEscapedCode, context); - if (Character.isSupplementaryCodePoint(codePoint)) { - unicodeStringBuilder.appendCodePoint(codePoint); - } - else { - char currentCodePoint = (char) codePoint; - if (Character.isSurrogate(currentCodePoint)) { - throw parseError("Invalid escaped character: %s. Escaped character is a surrogate. Use '\\+123456' instead.".formatted(currentEscapedCode), context); - } - unicodeStringBuilder.append(currentCodePoint); - } - state = UnicodeDecodeState.EMPTY; - charactersNeeded = -1; - } - else { - check(charactersNeeded > escapedCharacterBuilder.length(), "Unexpected escape sequence length: " + escapedCharacterBuilder.length(), context); - } - } - default -> throw new UnsupportedOperationException(); - } - } - - check(state == UnicodeDecodeState.EMPTY, "Incomplete escape sequence: " + escapedCharacterBuilder, context); - return unicodeStringBuilder.toString(); - } - - private Optional visitIfPresent(ParserRuleContext context, Class clazz) - { - return Optional.ofNullable(context) - .map(this::visit) - .map(clazz::cast); + (Identifier) visit(context.identifier()), + (Expression) visit(context.expression())); } - private List visit(List contexts, Class clazz) + @Override + public Node visitSimpleCaseStatement(SqlBaseParser.SimpleCaseStatementContext context) { - return contexts.stream() - .map(this::visit) - .map(clazz::cast) - .collect(toList()); + return new CaseStatement( + getLocation(context), + visitIfPresent(context.expression(), Expression.class), + visit(context.caseStatementWhenClause(), CaseStatementWhenClause.class), + visitIfPresent(context.elseClause(), ElseClause.class)); } - @SuppressWarnings("TypeMayBeWeakened") - private StringLiteral visitString(SqlBaseParser.StringContext context) + @Override + public Node visitSearchedCaseStatement(SqlBaseParser.SearchedCaseStatementContext context) { - return (StringLiteral) visit(context); + return new CaseStatement( + getLocation(context), + Optional.empty(), + visit(context.caseStatementWhenClause(), CaseStatementWhenClause.class), + visitIfPresent(context.elseClause(), ElseClause.class)); } - private static String unquote(String value) + @Override + public Node visitCaseStatementWhenClause(SqlBaseParser.CaseStatementWhenClauseContext context) { - return value.substring(1, value.length() - 1) - .replace("''", "'"); + return new CaseStatementWhenClause( + getLocation(context), + (Expression) visit(context.expression()), + visit(context.sqlStatementList().controlStatement(), ControlStatement.class)); } - private static LikeClause.PropertiesOption getPropertiesOption(Token token) + @Override + public Node visitIfStatement(SqlBaseParser.IfStatementContext context) { - return switch (token.getType()) { - case SqlBaseLexer.INCLUDING -> LikeClause.PropertiesOption.INCLUDING; - case SqlBaseLexer.EXCLUDING -> LikeClause.PropertiesOption.EXCLUDING; - default -> throw new IllegalArgumentException("Unsupported LIKE option type: " + token.getText()); - }; + return new IfStatement( + getLocation(context), + (Expression) visit(context.expression()), + visit(context.sqlStatementList().controlStatement(), ControlStatement.class), + visit(context.elseIfClause(), ElseIfClause.class), + visitIfPresent(context.elseClause(), ElseClause.class)); } - private QualifiedName getQualifiedName(SqlBaseParser.QualifiedNameContext context) + @Override + public Node visitElseIfClause(SqlBaseParser.ElseIfClauseContext context) { - return QualifiedName.of(visit(context.identifier(), Identifier.class)); + return new ElseIfClause( + getLocation(context), + (Expression) visit(context.expression()), + visit(context.sqlStatementList().controlStatement(), ControlStatement.class)); } - private static boolean isDistinct(SqlBaseParser.SetQuantifierContext setQuantifier) + @Override + public Node visitElseClause(SqlBaseParser.ElseClauseContext context) { - return setQuantifier != null && setQuantifier.DISTINCT() != null; + return new ElseClause( + getLocation(context), + visit(context.sqlStatementList().controlStatement(), ControlStatement.class)); } - private static boolean isHexDigit(char c) + @Override + public Node visitIterateStatement(SqlBaseParser.IterateStatementContext context) { - return ((c >= '0') && (c <= '9')) || - ((c >= 'A') && (c <= 'F')) || - ((c >= 'a') && (c <= 'f')); + return new IterateStatement( + getLocation(context), + (Identifier) visit(context.identifier())); } - private static boolean isValidUnicodeEscape(char c) + @Override + public Node visitLeaveStatement(SqlBaseParser.LeaveStatementContext context) { - return c < 0x7F && c > 0x20 && !isHexDigit(c) && c != '"' && c != '+' && c != '\''; + return new LeaveStatement( + getLocation(context), + (Identifier) visit(context.identifier())); } - private Optional getIdentifierIfPresent(ParserRuleContext context) + @Override + public Node visitVariableDeclaration(SqlBaseParser.VariableDeclarationContext context) { - return Optional.ofNullable(context).map(c -> (Identifier) visit(c)); + return new VariableDeclaration( + getLocation(context), + visit(context.identifier(), Identifier.class), + (DataType) visit(context.type()), + visitIfPresent(context.valueExpression(), Expression.class)); } - private static ArithmeticBinaryExpression.Operator getArithmeticBinaryOperator(Token operator) + @Override + public Node visitCompoundStatement(SqlBaseParser.CompoundStatementContext context) { - return switch (operator.getType()) { - case SqlBaseLexer.PLUS -> ArithmeticBinaryExpression.Operator.ADD; - case SqlBaseLexer.MINUS -> ArithmeticBinaryExpression.Operator.SUBTRACT; - case SqlBaseLexer.ASTERISK -> ArithmeticBinaryExpression.Operator.MULTIPLY; - case SqlBaseLexer.SLASH -> ArithmeticBinaryExpression.Operator.DIVIDE; - case SqlBaseLexer.PERCENT -> ArithmeticBinaryExpression.Operator.MODULUS; - default -> throw new UnsupportedOperationException("Unsupported operator: " + operator.getText()); - }; + return new CompoundStatement( + getLocation(context), + visit(context.variableDeclaration(), VariableDeclaration.class), + visit(Optional.ofNullable(context.sqlStatementList()) + .map(SqlBaseParser.SqlStatementListContext::controlStatement) + .orElse(ImmutableList.of()), ControlStatement.class)); } - private static ComparisonExpression.Operator getComparisonOperator(Token symbol) + @Override + public Node visitLoopStatement(SqlBaseParser.LoopStatementContext context) { - return switch (symbol.getType()) { - case SqlBaseLexer.EQ -> ComparisonExpression.Operator.EQUAL; - case SqlBaseLexer.NEQ -> ComparisonExpression.Operator.NOT_EQUAL; - case SqlBaseLexer.LT -> ComparisonExpression.Operator.LESS_THAN; - case SqlBaseLexer.LTE -> ComparisonExpression.Operator.LESS_THAN_OR_EQUAL; - case SqlBaseLexer.GT -> ComparisonExpression.Operator.GREATER_THAN; - case SqlBaseLexer.GTE -> ComparisonExpression.Operator.GREATER_THAN_OR_EQUAL; - default -> throw new IllegalArgumentException("Unsupported operator: " + symbol.getText()); - }; + return new LoopStatement( + getLocation(context), + getIdentifierIfPresent(context.label), + visit(context.sqlStatementList().controlStatement(), ControlStatement.class)); } - private static IntervalLiteral.IntervalField getIntervalFieldType(Token token) + @Override + public Node visitWhileStatement(SqlBaseParser.WhileStatementContext context) { - return switch (token.getType()) { - case SqlBaseLexer.YEAR -> IntervalLiteral.IntervalField.YEAR; - case SqlBaseLexer.MONTH -> IntervalLiteral.IntervalField.MONTH; - case SqlBaseLexer.DAY -> IntervalLiteral.IntervalField.DAY; - case SqlBaseLexer.HOUR -> IntervalLiteral.IntervalField.HOUR; - case SqlBaseLexer.MINUTE -> IntervalLiteral.IntervalField.MINUTE; - case SqlBaseLexer.SECOND -> IntervalLiteral.IntervalField.SECOND; - default -> throw new IllegalArgumentException("Unsupported interval field: " + token.getText()); - }; + return new WhileStatement( + getLocation(context), + getIdentifierIfPresent(context.label), + (Expression) visit(context.expression()), + visit(context.sqlStatementList().controlStatement(), ControlStatement.class)); } - private static IntervalLiteral.Sign getIntervalSign(Token token) + @Override + public Node visitRepeatStatement(SqlBaseParser.RepeatStatementContext context) { - return switch (token.getType()) { - case SqlBaseLexer.MINUS -> IntervalLiteral.Sign.NEGATIVE; - case SqlBaseLexer.PLUS -> IntervalLiteral.Sign.POSITIVE; - default -> throw new IllegalArgumentException("Unsupported sign: " + token.getText()); - }; + return new RepeatStatement( + getLocation(context), + getIdentifierIfPresent(context.label), + visit(context.sqlStatementList().controlStatement(), ControlStatement.class), + (Expression) visit(context.expression())); } - private static WindowFrame.Type getFrameType(Token type) + @Override + protected Node defaultResult() { - return switch (type.getType()) { - case SqlBaseLexer.RANGE -> WindowFrame.Type.RANGE; - case SqlBaseLexer.ROWS -> WindowFrame.Type.ROWS; - case SqlBaseLexer.GROUPS -> WindowFrame.Type.GROUPS; - default -> throw new IllegalArgumentException("Unsupported frame type: " + type.getText()); - }; + return null; } - private static FrameBound.Type getBoundedFrameBoundType(Token token) + @Override + protected Node aggregateResult(Node aggregate, Node nextResult) { - return switch (token.getType()) { - case SqlBaseLexer.PRECEDING -> FrameBound.Type.PRECEDING; - case SqlBaseLexer.FOLLOWING -> FrameBound.Type.FOLLOWING; - default -> throw new IllegalArgumentException("Unsupported bound type: " + token.getText()); - }; + if (nextResult == null) { + throw new UnsupportedOperationException("not yet implemented"); + } + + if (aggregate == null) { + return nextResult; + } + + throw new UnsupportedOperationException("not yet implemented"); } - private static FrameBound.Type getUnboundedFrameBoundType(Token token) + private Optional visitIfPresent(ParserRuleContext context, Class clazz) { - return switch (token.getType()) { - case SqlBaseLexer.PRECEDING -> FrameBound.Type.UNBOUNDED_PRECEDING; - case SqlBaseLexer.FOLLOWING -> FrameBound.Type.UNBOUNDED_FOLLOWING; - default -> throw new IllegalArgumentException("Unsupported bound type: " + token.getText()); - }; + return Optional.ofNullable(context) + .map(this::visit) + .map(clazz::cast); } - private static SampledRelation.Type getSamplingMethod(Token token) + private List visit(List contexts, Class clazz) { - return switch (token.getType()) { - case SqlBaseLexer.BERNOULLI -> SampledRelation.Type.BERNOULLI; - case SqlBaseLexer.SYSTEM -> SampledRelation.Type.SYSTEM; - default -> throw new IllegalArgumentException("Unsupported sampling method: " + token.getText()); - }; + return contexts.stream() + .map(this::visit) + .map(clazz::cast) + .collect(toList()); } - private static SortItem.NullOrdering getNullOrderingType(Token token) + @SuppressWarnings("TypeMayBeWeakened") + private StringLiteral visitString(SqlBaseParser.StringContext context) { - return switch (token.getType()) { - case SqlBaseLexer.FIRST -> SortItem.NullOrdering.FIRST; - case SqlBaseLexer.LAST -> SortItem.NullOrdering.LAST; - default -> throw new IllegalArgumentException("Unsupported ordering: " + token.getText()); - }; + return (StringLiteral) visit(context); } - private static SortItem.Ordering getOrderingType(Token token) + private QualifiedName getQualifiedName(SqlBaseParser.QualifiedNameContext context) { - return switch (token.getType()) { - case SqlBaseLexer.ASC -> SortItem.Ordering.ASCENDING; - case SqlBaseLexer.DESC -> SortItem.Ordering.DESCENDING; - default -> throw new IllegalArgumentException("Unsupported ordering: " + token.getText()); - }; + return QualifiedName.of(visit(context.identifier(), Identifier.class)); } - private static QuantifiedComparisonExpression.Quantifier getComparisonQuantifier(Token symbol) + private Optional getIdentifierIfPresent(ParserRuleContext context) { - return switch (symbol.getType()) { - case SqlBaseLexer.ALL -> QuantifiedComparisonExpression.Quantifier.ALL; - case SqlBaseLexer.ANY -> QuantifiedComparisonExpression.Quantifier.ANY; - case SqlBaseLexer.SOME -> QuantifiedComparisonExpression.Quantifier.SOME; - default -> throw new IllegalArgumentException("Unsupported quantifier: " + symbol.getText()); - }; + return Optional.ofNullable(context).map(c -> (Identifier) visit(c)); } private List getPrincipalSpecifications(List principals) @@ -4385,13 +4386,6 @@ private PrincipalSpecification getPrincipalSpecification(SqlBaseParser.Principal throw new IllegalArgumentException("Unsupported principal: " + context); } - private static void check(boolean condition, String message, ParserRuleContext context) - { - if (!condition) { - throw parseError(message, context); - } - } - private NodeLocation getLocation(TerminalNode terminalNode) { requireNonNull(terminalNode, "terminalNode is null"); @@ -4414,27 +4408,10 @@ private NodeLocation getLocation(Token token) .orElse(new NodeLocation(token.getLine(), token.getCharPositionInLine() + 1)); } - private static ParsingException parseError(String message, ParserRuleContext context) - { - return new ParsingException(message, null, context.getStart().getLine(), context.getStart().getCharPositionInLine() + 1); - } - - private static QueryPeriod.RangeType getRangeType(Token token) - { - return switch (token.getType()) { - case SqlBaseLexer.TIMESTAMP -> QueryPeriod.RangeType.TIMESTAMP; - case SqlBaseLexer.VERSION -> QueryPeriod.RangeType.VERSION; - default -> throw new IllegalArgumentException("Unsupported query period range type: " + token.getText()); - }; - } - - private static void validateArgumentAlias(Identifier alias, ParserRuleContext context) + private enum UnicodeDecodeState { - check( - alias.isDelimited() || !alias.getValue().equalsIgnoreCase("COPARTITION"), - "The word \"COPARTITION\" is ambiguous in this context. " + - "To alias an argument, precede the alias with \"AS\". " + - "To specify co-partitioning, change the argument order so that the last argument cannot be aliased.", - context); + EMPTY, + ESCAPED, + UNICODE_SEQUENCE } } diff --git a/docs/src/main/sphinx/connector/teradata.md b/docs/src/main/sphinx/connector/teradata.md index 710adadcdc02..83c463de9a85 100644 --- a/docs/src/main/sphinx/connector/teradata.md +++ b/docs/src/main/sphinx/connector/teradata.md @@ -4,20 +4,24 @@ ``` -The Teradata connector allows querying and creating tables in an external Teradata database. -This can be used to join data between different systems like Teradata and Hive, or between different Teradata instances. +The Teradata connector allows querying and creating tables in an external +[Teradata](https://www.teradata.com/) database. This can be used to join +data between different systems like Teradata and Hive, or between different Teradata instances. ## Requirements To connect to Teradata, you need: - Teradata Database -- Network access from the Trino coordinator and workers to Teradata. Port 1025 is the default port +- Network access from the Trino coordinator and workers to Teradata. Port + 1025 is the default port ## Configuration -To configure the Teradata connector, create a catalog properties file in `etc/catalog` named, for example, `teradata.properties`, to mount the Teradata connector as the `teradata` -catalog. Create the file with the following contents, replacing the connection properties as appropriate for your setup: +To configure the Teradata connector, create a catalog properties file in +`etc/catalog` named, for example, `teradata.properties`, to mount the Teradata +connector as the `teradata` catalog. Create the file with the following +contents, replacing the connection properties as appropriate for your setup: ```properties connector.name=teradata @@ -26,21 +30,26 @@ connection-user=*** connection-password=*** ``` -The `connection-url` defines the connection information and parameters to pass to the Teradata JDBC driver. The supported parameters for the URL are available in -the [Teradata JDBC documentation](https://teradata-docs.s3.amazonaws.com/doc/connectivity/jdbc/reference/current/jdbcug_chapter_2.html#BABJIHBJ). - -For example, the following `connection-url` configures character encoding, transaction mode, and authentication. +The `connection-url` defines the connection information and parameters to pass +to the Teradata JDBC driver. The supported parameters for the URL are +available in the +[Teradata JDBC documentation](https://teradata-docs.s3.amazonaws.com/doc/connectivity/jdbc/reference/current/jdbcug_chapter_2.html#BABJIHBJ). +For example, the following `connection-url` configures character encoding, +transaction mode, and authentication. ```properties connection-url=jdbc:teradata://example.teradata.com/CHARSET=UTF8,TMODE=ANSI,LOGMECH=TD2 ``` -The `connection-user` and `connection-password` are typically required and determine the user credentials for the connection, often a service user. +The `connection-user` and `connection-password` are typically required and +determine the user credentials for the connection, often a service user. ### Connection security -If you have TLS configured with a globally-trusted certificate installed on your data source, you can enable TLS between your cluster and the data source by appending parameters to -the JDBC connection string set in the connection-url catalog configuration property. +If you have TLS configured with a globally-trusted certificate installed on +your data source, you can enable TLS between your cluster and the data +source by appending parameters to the JDBC connection string set in the +connection-url catalog configuration property. For example, to specify SSLMODE: @@ -56,15 +65,18 @@ Teradata [JDBC documentation](https://teradata-docs.s3.amazonaws.com/doc/connect ### Multiple Teradata databases -You can have as many catalogs as you need, so if you have additional Teradata databases, simply add another properties file to etc/catalog with a different name, making sure it -ends in .properties. For example, if you name the property file sales.properties, Trino creates a catalog named sales using the configured connector. +You can have as many catalogs as you need, so if you have additional Teradata +databases, simply add another properties file to etc/catalog with a different +name, making sure it ends in .properties. +For example, if you name the property file sales.properties, Trino creates a +catalog named sales using the configured connector. ## Type mapping Because Trino and Teradata each support types that the other does not, this connector {ref}`modifies some types ` when reading data. -Refer to the following sections for type mapping in when reading data from Teradata -to Trino. +Refer to the following sections for type mapping in when reading data from +Teradata to Trino. ### Teradata type to Trino type mapping @@ -75,144 +87,53 @@ this table: :widths: 30, 30, 40 :header-rows: 1 -* - - Teradata type - - Trino type - - Notes -* - - `TINYINT` - - `TINYINT` - - - -* - - `SMALLINT` - - - `SMALLINT` - - - -* - - `INTEGER` - - - `INTEGER` - - - -* - - `BIGINT` - - - `BIGINT` - - - -* - - `REAL` - - - `DOUBLE` - - - -* - - `DOUBLE` - - - `DOUBLE` - - - -* - - `FLOAT` - - - `DOUBLE` - - - -* - - `NUMBER(p, s)` - - - `DECIMAL(p, s)` - - `DECIMAL(p, s)` is an alias of `NUMERIC(p, s)`. See - [](postgresql-decimal-type-handling) for more information. - -* - - `NUMERIC(p, s)` - - - `DECIMAL(p, s)` - - `DECIMAL(p, s)` is an alias of `NUMERIC(p, s)`. See - [](postgresql-decimal-type-handling) for more information. - -* - - `DECIMAL(p, s)` - - - `DECIMAL(p, s)` - - `DECIMAL(p, s)` is an alias of `NUMERIC(p, s)`. See - [](postgresql-decimal-type-handling) for more information. - -* - - `CHAR(n)` - - - `CHAR(n)` - - - -* - - `CHARACTER(n)` - - - `CHAR(n)` - - - -* - - `VARCHAR(n)` - - - `VARCHAR(n)` - - - -* - - `BINARY` - - - `VARBINARY` - - - -* - - `VARBINARY` - - - `VARBINARY` - - - -* - - `BLOB` - - - `VARBINARY` - - - -* - - `DATE` - - - `DATE` - - - -* - - `TIME(n)` - - - `TIME(n)` - - - -* - - `TIMESTAMP(n)` - - - `TIMESTAMP(n)` - - - -* - - `TIMESTAMP(n) WITH TIME ZONE` - - - `TIMESTAMP(n) WITH TIME ZONE` - - - -* - - `TIME(n) WITH TIME ZONE` - - - `TIME(n) WITH TIME ZONE` - - - -* - - `JSON` - - - `JSON` - - - +* - Teradata type + - Trino type + - Notes +* - `TINYINT` + - `TINYINT` + - +* - `SMALLINT` + - `SMALLINT` + - +* - `INTEGER` + - `INTEGER` + - +* - `BIGINT` + - `BIGINT` + - +* - `REAL` + - `DOUBLE` + - +* - `DOUBLE` + - `DOUBLE` + - +* - `FLOAT` + - `DOUBLE` + - +* - `NUMBER(p, s)` + - `DECIMAL(p, s)` + - `DECIMAL(p, s)` is an alias of `NUMERIC(p, s)`. See + [](teradata-decimal-type-handling) for more information. +* - `NUMERIC(p, s)` + - `DECIMAL(p, s)` + - `DECIMAL(p, s)` is an alias of `NUMERIC(p, s)`. See + [](teradata-decimal-type-handling) for more information. +* - `DECIMAL(p, s)` + - `DECIMAL(p, s)` + - `DECIMAL(p, s)` is an alias of `NUMERIC(p, s)`. See + [](postgresql-decimal-type-handling) for more information. +* - `CHAR(n)` + - `CHAR(n)` + - +* - `CHARACTER(n)` + - `CHAR(n)` + - +* - `VARCHAR(n)` + - `VARCHAR(n)` +* - `DATE` + - `DATE` + - ::: No other types are supported. @@ -222,19 +143,22 @@ No other types are supported. ## Querying Teradata -The Teradata connector provides a schema for every Teradata database. You can see the available Teradata databases by running SHOW SCHEMAS: +The Teradata connector provides a schema for every Teradata database. You can +see the available Teradata databases by running SHOW SCHEMAS: ``` SHOW SCHEMAS FROM teradata; ``` -If you have a Teradata database named sales, you can view the tables in this database by running SHOW TABLES: +If you have a Teradata database named sales, you can view the tables in this +database by running SHOW TABLES: ``` SHOW TABLES FROM teradata.sales; ``` -You can see a list of the columns in the orders table in the sales database using either of the following: +You can see a list of the columns in the orders table in the sales database +using either of the following: ``` DESCRIBE teradata.sales.orders; @@ -249,81 +173,8 @@ SELECT * FROM teradata.sales.orders; ## SQL support -The connector provides read access to data and metadata in the Teradata database. In addition to -the [globally available](https://trino.io/docs/current/language/sql-support.html#globally-available-statements) -and [read operation](https://trino.io/docs/current/language/sql-support.html#read-operations) statements, the connector supports the following features: - -## Performance - -The connector includes a number of performance improvements, detailed in the following sections. - -### Table statistics - -The Teradata connector can use [table and column statistics](https://trino.io/docs/current/optimizer/statistics.html) -for [cost based optimizations](https://trino.io/docs/current/optimizer/cost-based-optimizations.html), to improve query processing performance based on the actual data in the data -source. -The statistics are collected by Teradata and retrieved by the connector. The table and column statistics are based on Teradata's Data Dictionary views. - -You can update statistics in Teradata by running: - -``` -COLLECT STATISTICS COLUMN (regionkey), COLUMN (name) ON trino_test_teradatajdbcconnect.nation; -``` - -Please refer to [Statistics](https://docs.teradata.com/r/Enterprise_IntelliFlex_VMware/SQL-Data-Definition-Language-Syntax-and-Examples/Statistics-Statements) for more information -on Table Statistics. - -### Pushdown - -The connector supports pushdown for a number of operations: - -- {ref}`join-pushdown` -- {ref}`limit-pushdown` -- {ref}`topn-pushdown` - -{ref}`Aggregate pushdown ` for the following functions: - -- {func}`avg` -- {func}`count` -- {func}`max` -- {func}`min` -- {func}`sum` -- {func}`stddev` -- {func}`stddev_pop` -- {func}`stddev_samp` -- {func}`variance` -- {func}`var_pop` -- {func}`var_samp` -- {func}`covar_pop` -- {func}`covar_samp` -- {func}`corr` -- {func}`regr_intercept` -- {func}`regr_slope` - -```{include} join-pushdown-enabled-true.fragment -``` - -### Predicate pushdown support - -Predicates are pushed down for most types, including `UUID` and temporal -types, such as `DATE`. - -The connector does not support pushdown of range predicates, such as `>`, -`<`, or `BETWEEN`, on columns with {ref}`character string types -` like `CHAR` or `VARCHAR`. Equality predicates, such as -`IN` or `=`, and inequality predicates, such as `!=` on columns with -textual types are pushed down. This ensures correctness of results since the -remote data source may sort strings differently than Trino. - -In the following example, the predicate of the first query is not pushed down -since `name` is a column of type `VARCHAR` and `>` is a range predicate. -The other queries are pushed down. - -```sql --- Not pushed down -SELECT * FROM nation WHERE name > 'CANADA'; --- Pushed down -SELECT * FROM nation WHERE name != 'CANADA'; -SELECT * FROM nation WHERE name = 'CANADA'; -``` +The connector provides read access to data and metadata in the Teradata +database. In addition to the [globally available](https://trino.io/docs/current/language/sql-support.html#globally-available-statements) +and [read operation] (https://trino.io/docs/current/language/sql-support. +html#read-operations) statements, the connector supports the following features: diff --git a/plugin/trino-teradata/pom.xml b/plugin/trino-teradata/pom.xml index 8609714bc189..6b0b7b6a2a61 100644 --- a/plugin/trino-teradata/pom.xml +++ b/plugin/trino-teradata/pom.xml @@ -4,7 +4,7 @@ io.trino trino-root - 476 + 478-SNAPSHOT ../../pom.xml @@ -13,33 +13,41 @@ ${project.artifactId} Trino - Teradata connector + + true + + + com.google.guava guava + com.google.inject guice + classes io.airlift configuration + - io.trino - trino-base-jdbc + io.airlift + log io.trino - trino-plugin-toolkit + trino-base-jdbc - org.jdbi - jdbi3-core + io.trino + trino-plugin-toolkit @@ -84,12 +92,6 @@ provided - - com.fasterxml.jackson.core - jackson-databind - runtime - - com.google.errorprone error_prone_annotations @@ -105,7 +107,13 @@ io.airlift - log + concurrent + runtime + + + + io.airlift + json runtime @@ -117,7 +125,13 @@ io.airlift - http-server + units + runtime + + + + io.airlift + configuration-testing test @@ -132,6 +146,13 @@ testing test + + + io.airlift + tracing + test + + io.trino trino-base-jdbc @@ -139,6 +160,25 @@ test + + io.trino + trino-exchange-filesystem + test + + + + io.trino + trino-exchange-filesystem + test-jar + test + + + + io.trino + trino-jmx + test + + io.trino trino-main @@ -154,37 +194,44 @@ io.trino - trino-testing + trino-parser test io.trino - trino-testing-services + trino-plugin-toolkit + test-jar test io.trino - trino-tpch + trino-testing test - io.trino.tpch - tpch + io.trino + trino-testing-containers test - jakarta.servlet - jakarta.servlet-api + io.trino + trino-testing-services test - org.apache.commons - commons-lang3 + io.trino + trino-tpch + test + + + + io.trino.tpch + tpch test @@ -223,6 +270,9 @@ com.fasterxml.jackson.core:jackson-core + com.fasterxml.jackson.core:jackson-databind + com.google.guava:guava + io.airlift:log diff --git a/plugin/trino-teradata/src/main/java/io/trino/plugin/teradata/ImplementAvgBigint.java b/plugin/trino-teradata/src/main/java/io/trino/plugin/teradata/ImplementAvgBigint.java deleted file mode 100644 index b424199366d0..000000000000 --- a/plugin/trino-teradata/src/main/java/io/trino/plugin/teradata/ImplementAvgBigint.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.trino.plugin.teradata; - -import io.trino.plugin.jdbc.aggregation.BaseImplementAvgBigint; - -/** - * Implements the AVG aggregation for BIGINT columns in Teradata. - *

- * Teradata uses FLOAT for double precision floating-point arithmetic. - * This class ensures that the AVG function casts BIGINT values to FLOAT, - * providing correct decimal division semantics. - *

- */ -public class ImplementAvgBigint - extends BaseImplementAvgBigint -{ - /** - * Returns the SQL expression format for rewriting AVG aggregation. - *

- * The expression casts the input to FLOAT before applying AVG, - * ensuring proper floating-point division in Teradata. - *

- * - * @return the SQL format string for AVG aggregation - */ - @Override - public String getRewriteFormatExpression() - { - // Teradata uses FLOAT for double precision floating-point - // CAST to FLOAT ensures proper decimal division for AVG - return "avg(CAST(%s AS FLOAT))"; - } -} diff --git a/plugin/trino-teradata/src/main/java/io/trino/plugin/teradata/LogonMechanism.java b/plugin/trino-teradata/src/main/java/io/trino/plugin/teradata/LogonMechanism.java index b2d1d3c8abc0..18358011ba7e 100644 --- a/plugin/trino-teradata/src/main/java/io/trino/plugin/teradata/LogonMechanism.java +++ b/plugin/trino-teradata/src/main/java/io/trino/plugin/teradata/LogonMechanism.java @@ -11,22 +11,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.teradata; -/** - * Enum representing supported logon mechanisms for Teradata authentication. - *

- * Each mechanism corresponds to a specific authentication method - * such as TD2, JWT, BEARER, or SECRET. - *

- */ public enum LogonMechanism { - TD2("TD2"), - JWT("JWT"), - BEARER("BEARER"), - SECRET("SECRET"); + TD2("TD2"); private final String mechanism; diff --git a/plugin/trino-teradata/src/main/java/io/trino/plugin/teradata/TeradataClient.java b/plugin/trino-teradata/src/main/java/io/trino/plugin/teradata/TeradataClient.java index 79fe99b7efbc..ec6120bd3dd5 100644 --- a/plugin/trino-teradata/src/main/java/io/trino/plugin/teradata/TeradataClient.java +++ b/plugin/trino-teradata/src/main/java/io/trino/plugin/teradata/TeradataClient.java @@ -11,14 +11,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.teradata; import com.google.inject.Inject; import io.airlift.slice.Slice; -import io.trino.plugin.base.aggregation.AggregateFunctionRewriter; -import io.trino.plugin.base.aggregation.AggregateFunctionRule; -import io.trino.plugin.base.expression.ConnectorExpressionRewriter; import io.trino.plugin.base.mapping.IdentifierMapping; import io.trino.plugin.jdbc.BaseJdbcClient; import io.trino.plugin.jdbc.BaseJdbcConfig; @@ -26,129 +22,46 @@ import io.trino.plugin.jdbc.ColumnMapping; import io.trino.plugin.jdbc.ConnectionFactory; import io.trino.plugin.jdbc.JdbcColumnHandle; -import io.trino.plugin.jdbc.JdbcExpression; -import io.trino.plugin.jdbc.JdbcMetadata; import io.trino.plugin.jdbc.JdbcOutputTableHandle; -import io.trino.plugin.jdbc.JdbcSortItem; import io.trino.plugin.jdbc.JdbcStatisticsConfig; import io.trino.plugin.jdbc.JdbcTableHandle; import io.trino.plugin.jdbc.JdbcTypeHandle; -import io.trino.plugin.jdbc.LongReadFunction; -import io.trino.plugin.jdbc.LongWriteFunction; -import io.trino.plugin.jdbc.ObjectReadFunction; -import io.trino.plugin.jdbc.ObjectWriteFunction; import io.trino.plugin.jdbc.PredicatePushdownController; -import io.trino.plugin.jdbc.PreparedQuery; import io.trino.plugin.jdbc.QueryBuilder; import io.trino.plugin.jdbc.RemoteTableName; -import io.trino.plugin.jdbc.SliceReadFunction; import io.trino.plugin.jdbc.SliceWriteFunction; import io.trino.plugin.jdbc.WriteMapping; -import io.trino.plugin.jdbc.aggregation.ImplementAvgDecimal; -import io.trino.plugin.jdbc.aggregation.ImplementAvgFloatingPoint; -import io.trino.plugin.jdbc.aggregation.ImplementCorr; -import io.trino.plugin.jdbc.aggregation.ImplementCount; -import io.trino.plugin.jdbc.aggregation.ImplementCountAll; -import io.trino.plugin.jdbc.aggregation.ImplementCountDistinct; -import io.trino.plugin.jdbc.aggregation.ImplementCovariancePop; -import io.trino.plugin.jdbc.aggregation.ImplementCovarianceSamp; -import io.trino.plugin.jdbc.aggregation.ImplementMinMax; -import io.trino.plugin.jdbc.aggregation.ImplementRegrIntercept; -import io.trino.plugin.jdbc.aggregation.ImplementRegrSlope; -import io.trino.plugin.jdbc.aggregation.ImplementStddevPop; -import io.trino.plugin.jdbc.aggregation.ImplementStddevSamp; -import io.trino.plugin.jdbc.aggregation.ImplementSum; -import io.trino.plugin.jdbc.aggregation.ImplementVariancePop; -import io.trino.plugin.jdbc.aggregation.ImplementVarianceSamp; -import io.trino.plugin.jdbc.expression.ComparisonOperator; -import io.trino.plugin.jdbc.expression.JdbcConnectorExpressionRewriterBuilder; -import io.trino.plugin.jdbc.expression.ParameterizedExpression; -import io.trino.plugin.jdbc.expression.RewriteCaseSensitiveComparison; -import io.trino.plugin.jdbc.expression.RewriteIn; -import io.trino.plugin.jdbc.expression.RewriteLikeEscapeWithCaseSensitivity; -import io.trino.plugin.jdbc.expression.RewriteLikeWithCaseSensitivity; import io.trino.plugin.jdbc.logging.RemoteQueryModifier; import io.trino.spi.TrinoException; -import io.trino.spi.block.Block; -import io.trino.spi.block.BlockBuilder; -import io.trino.spi.connector.AggregateFunction; -import io.trino.spi.connector.ColumnHandle; import io.trino.spi.connector.ColumnMetadata; import io.trino.spi.connector.ColumnPosition; import io.trino.spi.connector.ConnectorSession; -import io.trino.spi.connector.JoinStatistics; -import io.trino.spi.connector.JoinType; import io.trino.spi.connector.SchemaTableName; -import io.trino.spi.expression.ConnectorExpression; -import io.trino.spi.predicate.Domain; -import io.trino.spi.predicate.ValueSet; -import io.trino.spi.statistics.ColumnStatistics; -import io.trino.spi.statistics.Estimate; -import io.trino.spi.statistics.TableStatistics; -import io.trino.spi.type.ArrayType; import io.trino.spi.type.CharType; import io.trino.spi.type.DecimalType; import io.trino.spi.type.Decimals; -import io.trino.spi.type.LongTimestampWithTimeZone; -import io.trino.spi.type.TimeType; -import io.trino.spi.type.TimeZoneKey; -import io.trino.spi.type.TimestampType; -import io.trino.spi.type.TimestampWithTimeZoneType; import io.trino.spi.type.Type; import io.trino.spi.type.TypeManager; import io.trino.spi.type.TypeSignature; import io.trino.spi.type.VarcharType; -import org.jdbi.v3.core.Handle; -import org.jdbi.v3.core.Jdbi; import org.weakref.jmx.$internal.guava.collect.ImmutableMap; -import org.weakref.jmx.$internal.guava.collect.ImmutableSet; -import java.sql.Array; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; -import java.sql.Statement; -import java.sql.Timestamp; import java.sql.Types; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.OffsetDateTime; -import java.time.OffsetTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.temporal.ChronoField; -import java.util.AbstractMap.SimpleEntry; -import java.util.ArrayList; -import java.util.Calendar; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.OptionalLong; -import java.util.Set; -import java.util.function.BiFunction; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Throwables.throwIfInstanceOf; -import static com.google.common.base.Verify.verify; -import static io.airlift.slice.Slices.utf8Slice; -import static io.trino.plugin.base.util.JsonTypeUtil.jsonParse; import static io.trino.plugin.jdbc.CaseSensitivity.CASE_INSENSITIVE; import static io.trino.plugin.jdbc.CaseSensitivity.CASE_SENSITIVE; import static io.trino.plugin.jdbc.JdbcErrorCode.JDBC_ERROR; -import static io.trino.plugin.jdbc.JdbcJoinPushdownUtil.implementJoinCostAware; -import static io.trino.plugin.jdbc.JdbcMetadataSessionProperties.getDomainCompactionThreshold; import static io.trino.plugin.jdbc.PredicatePushdownController.CASE_INSENSITIVE_CHARACTER_PUSHDOWN; -import static io.trino.plugin.jdbc.PredicatePushdownController.DISABLE_PUSHDOWN; import static io.trino.plugin.jdbc.PredicatePushdownController.FULL_PUSHDOWN; import static io.trino.plugin.jdbc.StandardColumnMappings.bigintColumnMapping; import static io.trino.plugin.jdbc.StandardColumnMappings.bigintWriteFunction; @@ -159,7 +72,6 @@ import static io.trino.plugin.jdbc.StandardColumnMappings.decimalColumnMapping; import static io.trino.plugin.jdbc.StandardColumnMappings.doubleColumnMapping; import static io.trino.plugin.jdbc.StandardColumnMappings.doubleWriteFunction; -import static io.trino.plugin.jdbc.StandardColumnMappings.fromTrinoTime; import static io.trino.plugin.jdbc.StandardColumnMappings.integerColumnMapping; import static io.trino.plugin.jdbc.StandardColumnMappings.integerWriteFunction; import static io.trino.plugin.jdbc.StandardColumnMappings.longDecimalWriteFunction; @@ -167,12 +79,8 @@ import static io.trino.plugin.jdbc.StandardColumnMappings.shortDecimalWriteFunction; import static io.trino.plugin.jdbc.StandardColumnMappings.smallintColumnMapping; import static io.trino.plugin.jdbc.StandardColumnMappings.smallintWriteFunction; -import static io.trino.plugin.jdbc.StandardColumnMappings.timestampColumnMapping; -import static io.trino.plugin.jdbc.StandardColumnMappings.timestampWriteFunction; import static io.trino.plugin.jdbc.StandardColumnMappings.tinyintColumnMapping; import static io.trino.plugin.jdbc.StandardColumnMappings.tinyintWriteFunction; -import static io.trino.plugin.jdbc.StandardColumnMappings.varbinaryColumnMapping; -import static io.trino.plugin.jdbc.StandardColumnMappings.varbinaryWriteFunction; import static io.trino.plugin.jdbc.StandardColumnMappings.varcharReadFunction; import static io.trino.plugin.jdbc.StandardColumnMappings.varcharWriteFunction; import static io.trino.plugin.jdbc.TypeHandlingJdbcSessionProperties.getUnsupportedTypeHandling; @@ -180,10 +88,6 @@ import static io.trino.spi.StandardErrorCode.NOT_SUPPORTED; import static io.trino.spi.type.BigintType.BIGINT; import static io.trino.spi.type.CharType.createCharType; -import static io.trino.spi.type.DateTimeEncoding.packDateTimeWithZone; -import static io.trino.spi.type.DateTimeEncoding.packTimeWithTimeZone; -import static io.trino.spi.type.DateTimeEncoding.unpackMillisUtc; -import static io.trino.spi.type.DateTimeEncoding.unpackZoneKey; import static io.trino.spi.type.DateType.DATE; import static io.trino.spi.type.DecimalType.createDecimalType; import static io.trino.spi.type.DoubleType.DOUBLE; @@ -191,298 +95,34 @@ import static io.trino.spi.type.RealType.REAL; import static io.trino.spi.type.SmallintType.SMALLINT; import static io.trino.spi.type.StandardTypes.JSON; -import static io.trino.spi.type.TimeType.createTimeType; -import static io.trino.spi.type.TimeWithTimeZoneType.createTimeWithTimeZoneType; -import static io.trino.spi.type.TimeZoneKey.getTimeZoneKey; -import static io.trino.spi.type.TimestampWithTimeZoneType.createTimestampWithTimeZoneType; -import static io.trino.spi.type.Timestamps.MILLISECONDS_PER_SECOND; -import static io.trino.spi.type.Timestamps.PICOSECONDS_PER_DAY; -import static io.trino.spi.type.Timestamps.PICOSECONDS_PER_NANOSECOND; -import static io.trino.spi.type.Timestamps.round; import static io.trino.spi.type.TinyintType.TINYINT; -import static io.trino.spi.type.VarbinaryType.VARBINARY; import static io.trino.spi.type.VarcharType.createUnboundedVarcharType; import static io.trino.spi.type.VarcharType.createVarcharType; -import static java.lang.Math.floorDiv; import static java.lang.String.format; -import static java.util.Locale.ENGLISH; -import static java.util.Objects.requireNonNull; -import static java.util.stream.Collectors.toMap; -/** - * TeradataClient provides a Trino connector implementation for Teradata databases. - *

- * It handles Teradata-specific SQL translation, type mapping, statistics retrieval, - * and enforces read-only semantics. The class extends {@link BaseJdbcClient} and - * customizes behavior for Teradata's SQL dialect and metadata. - *

- */ public class TeradataClient extends BaseJdbcClient { - /** - * Predicate pushdown controller for Teradata string columns. - * Ensures correct pushdown behavior for case-sensitive and case-insensitive domains. - */ - private static final PredicatePushdownController TERADATA_STRING_PUSHDOWN = (session, domain) -> { - // 1. NULL-only filters are always safe - if (domain.isOnlyNull()) { - return FULL_PUSHDOWN.apply(session, domain); - } - - Domain simplifiedDomain = domain.simplify(getDomainCompactionThreshold(session)); - if (!simplifiedDomain.getValues().isDiscreteSet()) { - // Push down inequality predicate - ValueSet complement = simplifiedDomain.getValues().complement(); - if (complement.isDiscreteSet()) { - return FULL_PUSHDOWN.apply(session, simplifiedDomain); - } - // Domain#simplify can turn a discrete set into a range predicate - // Push down of range predicate for varchar/char types could lead to incorrect results - // when the remote database is case-insensitive - return DISABLE_PUSHDOWN.apply(session, domain); - } - return FULL_PUSHDOWN.apply(session, simplifiedDomain); - }; - /** - * Maximum fallback number of distinct values (NDV) for statistics estimation. - */ - private static final long MAX_FALLBACK_NDV = 1_000_000L; - /** - * Default fallback fraction for NDV estimation (10% of row count). - */ - private static final double DEFAULT_FALLBACK_FRACTION = 0.1; - /** - * Maximum supported timestamp precision in Teradata. - */ - private static final int TERADATA_MAX_SUPPORTED_TIMESTAMP_PRECISION = 6; - /** - * Trino type representing JSON columns. - */ + private static final PredicatePushdownController TERADATA_STRING_PUSHDOWN = FULL_PUSHDOWN; private final Type jsonType; - /** - * Teradata String case sensitivity mode. - */ private final TeradataConfig.TeradataCaseSensitivity teradataJDBCCaseSensitivity; - /** - * Flag indicating if statistics collection is enabled. - */ - private final boolean statisticsEnabled; - /** - * Expression rewriter for translating connector expressions to Teradata SQL. - */ - private ConnectorExpressionRewriter connectorExpressionRewriter; - /** - * Aggregate function rewriter for translating Trino aggregates to Teradata SQL. - */ - private AggregateFunctionRewriter aggregateFunctionRewriter; - /** - * Constructs a new TeradataClient instance. - * - * @param config base JDBC configuration - * @param teradataConfig Teradata-specific configuration - * @param connectionFactory factory to create JDBC connections - * @param queryBuilder query builder for SQL queries - * @param identifierMapping mapping for identifiers such as column names - * @param remoteQueryModifier optional modifier for remote queries - */ @Inject - public TeradataClient(BaseJdbcConfig config, TeradataConfig teradataConfig, JdbcStatisticsConfig statisticsConfig, ConnectionFactory connectionFactory, QueryBuilder queryBuilder, TypeManager typeManager, IdentifierMapping identifierMapping, RemoteQueryModifier remoteQueryModifier) + public TeradataClient( + BaseJdbcConfig config, + TeradataConfig teradataConfig, + JdbcStatisticsConfig statisticsConfig, + ConnectionFactory connectionFactory, + QueryBuilder queryBuilder, + TypeManager typeManager, + IdentifierMapping identifierMapping, + RemoteQueryModifier remoteQueryModifier) { super("\"", connectionFactory, queryBuilder, config.getJdbcTypesMappedToVarchar(), identifierMapping, remoteQueryModifier, true); this.jsonType = typeManager.getType(new TypeSignature(JSON)); this.teradataJDBCCaseSensitivity = teradataConfig.getTeradataCaseSensitivity(); - this.statisticsEnabled = statisticsConfig.isEnabled(); - buildExpressionRewriter(); - buildAggregateRewriter(); - } - - /** - * Creates a ColumnMapping for Teradata TIME type with specified precision. - * - * @param precision fractional seconds precision for the TIME column - * @return ColumnMapping instance for TIME type - */ - public static ColumnMapping timeColumnMapping(int precision) - { - TimeType timeType = createTimeType(precision); - return ColumnMapping.longMapping(timeType, timeReadFunction(timeType), timeWriteFunction(precision), DISABLE_PUSHDOWN); - } - - /** - * Returns a function to read TIME values from JDBC result set, - * converting SQL Timestamp to Trino internal representation. - * - * @param timeType Trino TimeType - * @return LongReadFunction for TIME values - */ - public static LongReadFunction timeReadFunction(TimeType timeType) - { - requireNonNull(timeType, "timeType is null"); - return (resultSet, columnIndex) -> { - Timestamp sqlTimestamp = resultSet.getTimestamp(columnIndex); - LocalTime localTime = sqlTimestamp.toLocalDateTime().toLocalTime(); - long nsOfDay = localTime.toNanoOfDay(); - long picosOfDay = nsOfDay * PICOSECONDS_PER_NANOSECOND; - long rounded = round(picosOfDay, 12 - timeType.getPrecision()); - if (rounded == PICOSECONDS_PER_DAY) { - rounded = 0; - } - return rounded; - }; } - /** - * Returns a function to write TIME values to JDBC PreparedStatement, - * converting Trino internal representation to JDBC object. - * - * @param precision fractional seconds precision - * @return LongWriteFunction for TIME values - */ - public static LongWriteFunction timeWriteFunction(int precision) - { - return LongWriteFunction.of(Types.TIME, (statement, index, picosOfDay) -> { - picosOfDay = round(picosOfDay, 12 - precision); - if (picosOfDay == PICOSECONDS_PER_DAY) { - picosOfDay = 0; - } - statement.setObject(index, fromTrinoTime(picosOfDay)); - }); - } - - /** - * Creates a ColumnMapping for Teradata TIME WITH TIME ZONE type with specified precision. - * - * @param precision fractional seconds precision - * @return ColumnMapping instance for TIME WITH TIME ZONE type - */ - public static ColumnMapping timeWithTimeZoneColumnMapping(int precision) - { - return ColumnMapping.longMapping(createTimeWithTimeZoneType(precision), shortTimeWithTimeZoneReadFunction(), shortTimeWithTimeZoneWriteFunction(), DISABLE_PUSHDOWN); - } - - /** - * Reads TIME WITH TIME ZONE values from JDBC ResultSet. - * - * @return LongReadFunction for TIME WITH TIME ZONE values - */ - private static LongReadFunction shortTimeWithTimeZoneReadFunction() - { - return (resultSet, columnIndex) -> { - Calendar calendar = Calendar.getInstance(); - Timestamp sqlTimestamp = resultSet.getTimestamp(columnIndex, calendar); - LocalDateTime localDateTime = sqlTimestamp.toLocalDateTime(); - ZoneId zone = ZoneId.of(calendar.getTimeZone().getID()); - ZonedDateTime zdt = ZonedDateTime.of(localDateTime, zone); - int offsetMinutes = zdt.getOffset().getTotalSeconds() / 60; - long nanos = localDateTime.getLong(ChronoField.NANO_OF_DAY); - return packTimeWithTimeZone(nanos, offsetMinutes); - }; - } - - /** - * Writes TIME WITH TIME ZONE values to JDBC PreparedStatement. - * - * @return LongWriteFunction for TIME WITH TIME ZONE values - */ - private static LongWriteFunction shortTimeWithTimeZoneWriteFunction() - { - return (statement, index, value) -> { - long millisUtc = unpackMillisUtc(value); - TimeZoneKey timeZoneKey = unpackZoneKey(value); - statement.setObject(index, OffsetTime.ofInstant(Instant.ofEpochMilli(millisUtc), timeZoneKey.getZoneId())); - }; - } - - /** - * Creates a ColumnMapping for Teradata TIMESTAMP WITH TIME ZONE type with specified precision. - * - * @param precision fractional seconds precision - * @return ColumnMapping instance for TIMESTAMP WITH TIME ZONE type - */ - public static ColumnMapping timestampWithTimeZoneColumnMapping(int precision) - { - if (precision <= TimestampWithTimeZoneType.MAX_SHORT_PRECISION) { - return ColumnMapping.longMapping(createTimestampWithTimeZoneType(precision), shortTimestampWithTimeZoneReadFunction(), shortTimestampWithTimeZoneWriteFunction(), DISABLE_PUSHDOWN); - } - return ColumnMapping.objectMapping(createTimestampWithTimeZoneType(precision), longTimestampWithTimeZoneReadFunction(), longTimestampWithTimeZoneWriteFunction(), DISABLE_PUSHDOWN); - } - - /** - * Reads TIMESTAMP WITH TIME ZONE values with short precision from JDBC ResultSet. - * - * @return LongReadFunction for short precision TIMESTAMP WITH TIME ZONE values - */ - private static LongReadFunction shortTimestampWithTimeZoneReadFunction() - { - return (resultSet, columnIndex) -> { - Calendar calendar = Calendar.getInstance(); - Timestamp sqlTimestamp = resultSet.getTimestamp(columnIndex, calendar); - ZonedDateTime zonedDateTime = ZonedDateTime.of(sqlTimestamp.toLocalDateTime(), calendar.getTimeZone().toZoneId()); - return packDateTimeWithZone(zonedDateTime.toInstant().toEpochMilli(), zonedDateTime.getZone().getId()); - }; - } - - /** - * Writes TIMESTAMP WITH TIME ZONE values with short precision to JDBC PreparedStatement. - * - * @return LongWriteFunction for short precision TIMESTAMP WITH TIME ZONE values - */ - private static LongWriteFunction shortTimestampWithTimeZoneWriteFunction() - { - return (statement, index, value) -> { - long millisUtc = unpackMillisUtc(value); - TimeZoneKey timeZoneKey = unpackZoneKey(value); - statement.setObject(index, OffsetDateTime.ofInstant(Instant.ofEpochMilli(millisUtc), timeZoneKey.getZoneId())); - }; - } - - /** - * Reads TIMESTAMP WITH TIME ZONE values with long precision from JDBC ResultSet. - * - * @return ObjectReadFunction for long precision TIMESTAMP WITH TIME ZONE values - */ - private static ObjectReadFunction longTimestampWithTimeZoneReadFunction() - { - return ObjectReadFunction.of(LongTimestampWithTimeZone.class, (resultSet, columnIndex) -> { - Calendar calendar = Calendar.getInstance(); - Timestamp sqlTimestamp = resultSet.getTimestamp(columnIndex, calendar); - ZonedDateTime dateTime = ZonedDateTime.of(sqlTimestamp.toLocalDateTime(), calendar.getTimeZone().toZoneId()); - OffsetDateTime offsetDateTime = dateTime.toOffsetDateTime(); - long picosOfSecond = offsetDateTime.getNano() * ((long) PICOSECONDS_PER_NANOSECOND); - - return LongTimestampWithTimeZone.fromEpochSecondsAndFraction(offsetDateTime.toEpochSecond(), picosOfSecond, getTimeZoneKey(offsetDateTime.toZonedDateTime().getZone().getId())); - }); - } - - /** - * Writes TIMESTAMP WITH TIME ZONE values with long precision to JDBC PreparedStatement. - * - * @return ObjectWriteFunction for long precision TIMESTAMP WITH TIME ZONE values - */ - private static ObjectWriteFunction longTimestampWithTimeZoneWriteFunction() - { - return ObjectWriteFunction.of(LongTimestampWithTimeZone.class, (statement, index, value) -> { - long epochMillis = value.getEpochMillis(); - long epochSeconds = floorDiv(epochMillis, MILLISECONDS_PER_SECOND); - ZoneId zoneId = getTimeZoneKey(value.getTimeZoneKey()).getZoneId(); - Instant instant = Instant.ofEpochSecond(epochSeconds); - statement.setObject(index, OffsetDateTime.ofInstant(instant, zoneId)); - }); - } - - /** - * Creates a ColumnMapping for Teradata CHAR columns. - *

- * If the specified length exceeds the maximum allowed for CHAR, the column is mapped as VARCHAR. - * The mapping also applies the appropriate predicate pushdown controller based on case sensitivity. - *

- * - * @param charLength the length of the CHAR column - * @param isCaseSensitive true if the column is case-sensitive, false otherwise - * @return ColumnMapping for the CHAR or VARCHAR column - */ private static ColumnMapping charColumnMapping(int charLength, boolean isCaseSensitive) { if (charLength > CharType.MAX_LENGTH) { @@ -496,17 +136,6 @@ private static ColumnMapping charColumnMapping(int charLength, boolean isCaseSen isCaseSensitive ? TERADATA_STRING_PUSHDOWN : CASE_INSENSITIVE_CHARACTER_PUSHDOWN); } - /** - * Creates a ColumnMapping for Teradata VARCHAR columns. - *

- * If the specified length exceeds the maximum allowed for VARCHAR, the column is mapped as unbounded VARCHAR. - * The mapping also applies the appropriate predicate pushdown controller based on case sensitivity. - *

- * - * @param varcharLength the length of the VARCHAR column - * @param isCaseSensitive true if the column is case-sensitive, false otherwise - * @return ColumnMapping for the VARCHAR column - */ private static ColumnMapping varcharColumnMapping(int varcharLength, boolean isCaseSensitive) { VarcharType varcharType = varcharLength <= VarcharType.MAX_LENGTH @@ -519,22 +148,6 @@ private static ColumnMapping varcharColumnMapping(int varcharLength, boolean isC isCaseSensitive ? TERADATA_STRING_PUSHDOWN : CASE_INSENSITIVE_CHARACTER_PUSHDOWN); } - /** - * Converts a Trino DecimalType to a JDBC type handle for NUMERIC/DECIMAL columns. - * - * @param decimalType the Trino DecimalType - * @return Optional containing the corresponding JdbcTypeHandle - */ - private static Optional toTypeHandle(DecimalType decimalType) - { - return Optional.of(new JdbcTypeHandle(Types.NUMERIC, Optional.of("decimal"), Optional.of(decimalType.getPrecision()), Optional.of(decimalType.getScale()), Optional.empty(), Optional.empty())); - } - - /** - * Returns a SliceWriteFunction for writing JSON values as typed VARCHAR to JDBC. - * - * @return SliceWriteFunction for JSON columns - */ private static SliceWriteFunction typedVarcharWriteFunction() { String bindExpression = format("CAST(? AS %s)", "JSON"); @@ -560,12 +173,6 @@ public void set(PreparedStatement statement, int index, Slice value) }; } - /** - * Determines case sensitivity for string columns based on configuration and metadata. - * - * @param caseSensitivity the case sensitivity from metadata - * @return true if case-sensitive, false otherwise - */ private boolean deriveCaseSensitivity(CaseSensitivity caseSensitivity) { return switch (teradataJDBCCaseSensitivity) { @@ -575,196 +182,6 @@ private boolean deriveCaseSensitivity(CaseSensitivity caseSensitivity) }; } - @Override - protected Optional> limitFunction() - { - return Optional.of((sql, limit) -> { - return sql.replaceFirst("(?i)^SELECT", "SELECT TOP " + limit); - }); - } - - @Override - public boolean isLimitGuaranteed(ConnectorSession session) - { - return true; - } - - @Override - public boolean isTopNGuaranteed(ConnectorSession session) - { - return true; - } - - @Override - public boolean supportsTopN(ConnectorSession session, JdbcTableHandle handle, List sortOrder) - { - // Teradata supports TOP N with ORDER BY for all data types - return true; - } - - @Override - protected Optional topNFunction() - { - return Optional.of((query, sortItems, limit) -> { - // Collect selected columns - Set selectColumns = new HashSet<>(); - Matcher matcher = Pattern.compile("(?i)SELECT\\s+(.*?)\\s+FROM").matcher(query); - if (matcher.find()) { - String[] cols = matcher.group(1).split(","); - for (String col : cols) { - selectColumns.add(col.trim().replaceAll("\"", "")); - } - } - - // Add missing ORDER BY columns to SELECT - List extraColumns = new ArrayList<>(); - for (JdbcSortItem sortItem : sortItems) { - String columnName = sortItem.column().getColumnName(); - if (!selectColumns.contains(columnName)) { - extraColumns.add("\"" + columnName + "\""); - } - } - - String modifiedQuery = query; - if (!extraColumns.isEmpty()) { - String allColumns = String.join(", ", selectColumns.stream().map(c -> "\"" + c + "\"").toList()); - allColumns += ", " + String.join(", ", extraColumns); - modifiedQuery = query.replaceFirst("(?i)SELECT\\s+(.*?)\\s+FROM", "SELECT " + allColumns + " FROM"); - } - - String orderBy = sortItems.stream() - .map(sortItem -> { - String columnName = quoted(sortItem.column().getColumnName()); - boolean asc = sortItem.sortOrder().isAscending(); - String direction = asc ? "ASC" : "DESC"; - String nullsHandling = sortItem.sortOrder().isNullsFirst() ? "NULLS FIRST" : "NULLS LAST"; - return columnName + " " + direction + " " + nullsHandling; - }) - .collect(Collectors.joining(", ")); - - // Remove schema qualification (e.g. trino.nation → nation) - String baseQuery = modifiedQuery.replaceAll("\\w+\\.\\w+\\.", ""); - - return format("SELECT TOP %d * FROM (%s) AS t ORDER BY %s", limit, baseQuery, orderBy); - }); - } - - @Override - public TableStatistics getTableStatistics(ConnectorSession session, JdbcTableHandle handle) - { - if (!statisticsEnabled) { - return TableStatistics.empty(); - } - if (!handle.isNamedRelation()) { - return TableStatistics.empty(); - } - try { - return readTableStatistics(session, handle); - } - catch (SQLException | RuntimeException e) { - throwIfInstanceOf(e, TrinoException.class); - throw new TrinoException(JDBC_ERROR, "Failed fetching statistics for table: " + handle, e); - } - } - - private TableStatistics readTableStatistics(ConnectorSession session, JdbcTableHandle table) - throws SQLException - { - checkArgument(table.isNamedRelation(), "Relation is not a table: %s", table); - - try (Connection connection = connectionFactory.openConnection(session); - Handle handle = Jdbi.open(connection)) { - TeradataStatisticsDao dao = new TeradataStatisticsDao(handle); - long rowCount = dao.estimateRowCount(table); - - // Fallback to SAMPLE - if (rowCount <= 0) { - OptionalLong fallbackCount = dao.sampleRowCountEstimate(table, connection); - if (fallbackCount.isEmpty()) { - return TableStatistics.empty(); - } - rowCount = fallbackCount.getAsLong(); - } - - Map stats = dao.getColumnIndexStatistics(table); - TableStatistics.Builder tableStats = TableStatistics.builder().setRowCount(Estimate.of(rowCount)); - - for (JdbcColumnHandle column : JdbcMetadata.getColumns(session, this, table)) { - String columnName = column.getColumnName(); - TeradataStatisticsDao.ColumnIndexStatistics stat = stats.get(columnName); - - ColumnStatistics.Builder columnStats = ColumnStatistics.builder(); - - if (stat != null) { - columnStats.setNullsFraction(Estimate.of((double) stat.nullCount() / rowCount)); - - long distinctValues = stat.distinctValues(); - if (distinctValues <= 0) { - // No NDV info from Teradata, fallback - columnStats.setDistinctValuesCount(Estimate.of(computeFallbackNDV(rowCount))); - } - else { - columnStats.setDistinctValuesCount(Estimate.of(distinctValues)); - } - } - else { - // No stats at all for this column, fallback both null fraction and NDV - columnStats.setNullsFraction(Estimate.of(0.0)); - columnStats.setDistinctValuesCount(Estimate.of(computeFallbackNDV(rowCount))); - } - - tableStats.setColumnStatistics(column, columnStats.build()); - } - - return tableStats.build(); - } - } - - /** - * Compute fallback NDV based on table row count. - * - Uses a fraction (e.g., 10%) of rowCount as fallback NDV, - * - capped at MAX_FALLBACK_NDV, - * - minimum fallback of 1 to avoid zero distinct count. - */ - private long computeFallbackNDV(long rowCount) - { - if (rowCount <= 0) { - return 1; // minimal fallback for empty or invalid row count - } - - long fallback = (long) (rowCount * DEFAULT_FALLBACK_FRACTION); - fallback = Math.max(fallback, 1); // at least 1 distinct value - fallback = Math.min(fallback, MAX_FALLBACK_NDV); // cap at max fallback - - return fallback; - } - - @Override - public Optional implementJoin( - ConnectorSession session, - JoinType joinType, - PreparedQuery leftSource, - Map leftProjections, - PreparedQuery rightSource, - Map rightProjections, - List joinConditions, - JoinStatistics statistics) - { - return implementJoinCostAware( - session, - joinType, - leftSource, - rightSource, - statistics, - () -> super.implementJoin(session, joinType, leftSource, leftProjections, rightSource, rightProjections, joinConditions, statistics)); - } - - @Override - public Optional implementAggregation(ConnectorSession session, AggregateFunction aggregate, Map assignments) - { - return aggregateFunctionRewriter.rewrite(session, aggregate, assignments); - } - @Override protected void createSchema(ConnectorSession session, Connection connection, String remoteSchemaName) { @@ -774,7 +191,8 @@ protected void createSchema(ConnectorSession session, Connection connection, Str } @Override - protected void copyTableSchema(ConnectorSession session, Connection connection, String catalogName, String schemaName, String tableName, String newTableName, List columnNames) + protected void copyTableSchema(ConnectorSession session, Connection connection, String catalogName, String schemaName, String tableName, String newTableName, + List columnNames) { String tableCopyFormat = "CREATE TABLE %s AS ( SELECT * FROM %s ) WITH DATA"; String sql = format( @@ -804,7 +222,8 @@ protected void verifyTableName(DatabaseMetaData databaseMetadata, String tableNa throws SQLException { if (tableName.length() > databaseMetadata.getMaxTableNameLength()) { - throw new TrinoException(NOT_SUPPORTED, format("Table name must be shorter than or equal to '%s' characters but got '%s'", databaseMetadata.getMaxTableNameLength(), tableName.length())); + throw new TrinoException(NOT_SUPPORTED, format("Table name must be shorter than or equal to '%s' characters but got '%s'", databaseMetadata.getMaxTableNameLength(), + tableName.length())); } } @@ -813,7 +232,8 @@ protected void verifyColumnName(DatabaseMetaData databaseMetadata, String column throws SQLException { if (columnName.length() > databaseMetadata.getMaxColumnNameLength()) { - throw new TrinoException(NOT_SUPPORTED, format("Column name must be shorter than or equal to '%s' characters but got '%s': '%s'", databaseMetadata.getMaxColumnNameLength(), columnName.length(), columnName)); + throw new TrinoException(NOT_SUPPORTED, format("Column name must be shorter than or equal to '%s' characters but got '%s': '%s'", + databaseMetadata.getMaxColumnNameLength(), columnName.length(), columnName)); } } @@ -834,70 +254,30 @@ public void renameSchema(ConnectorSession session, String schemaName, String new throw new TrinoException(NOT_SUPPORTED, "This connector does not support renaming schema"); } - /** - * Delete operations are not supported by the Teradata connector. - * - * @param session connector session - * @param handle table handle identifying the target table - * @return empty optional indicating no deletion occurred - * @throws TrinoException always thrown with NOT_SUPPORTED error code - */ @Override public OptionalLong delete(ConnectorSession session, JdbcTableHandle handle) { throw new TrinoException(NOT_SUPPORTED, "This connector does not support modifying table rows"); } - /** - * Truncate operations are not supported by the Teradata connector. - * - * @param session connector session - * @param handle table handle identifying the target table - * @throws TrinoException always thrown with NOT_SUPPORTED error code - */ @Override public void truncateTable(ConnectorSession session, JdbcTableHandle handle) { throw new TrinoException(NOT_SUPPORTED, "This connector does not support truncating tables"); } - /** - * Drop column operations are not supported by the Teradata connector. - * - * @param session connector session - * @param handle table handle identifying the target table - * @param column column handle identifying the column to drop - * @throws TrinoException always thrown with NOT_SUPPORTED error code - */ @Override public void dropColumn(ConnectorSession session, JdbcTableHandle handle, JdbcColumnHandle column) { throw new TrinoException(NOT_SUPPORTED, "This connector does not support dropping columns"); } - /** - * Rename column operations are not supported by the Teradata connector. - * - * @param session connector session - * @param handle table handle identifying the target table - * @param jdbcColumn column handle identifying the column to rename - * @param newColumnName new name for the column - * @throws TrinoException always thrown with NOT_SUPPORTED error code - */ @Override public void renameColumn(ConnectorSession session, JdbcTableHandle handle, JdbcColumnHandle jdbcColumn, String newColumnName) { throw new TrinoException(NOT_SUPPORTED, "This connector does not support renaming columns"); } - /** - * Rename table operations are not supported by the Teradata connector. - * - * @param session connector session - * @param handle table handle identifying the target table - * @param newTableName new name for the table - * @throws TrinoException always thrown with NOT_SUPPORTED error code - */ @Override public void renameTable(ConnectorSession session, JdbcTableHandle handle, SchemaTableName newTableName) { @@ -928,115 +308,9 @@ public void dropNotNullConstraint(ConnectorSession session, JdbcTableHandle hand throw new TrinoException(NOT_SUPPORTED, "This connector does not support dropping a not null constraint"); } - /** - * Builds the expression rewriter for translating connector expressions - * into SQL fragments understood by Teradata. - * Currently, supports numeric equality expressions and quoted identifiers. - */ - private void buildExpressionRewriter() - { - this.connectorExpressionRewriter = JdbcConnectorExpressionRewriterBuilder.newBuilder() - .addStandardRules(this::quoted) - .add(new RewriteIn()) - .add(new RewriteLikeWithCaseSensitivity()) - .add(new RewriteLikeEscapeWithCaseSensitivity()) - .withTypeClass("integer_type", ImmutableSet.of("tinyint", "smallint", "integer", "bigint")) - .withTypeClass("numeric_type", ImmutableSet.of("tinyint", "smallint", "integer", "bigint", "decimal", "real", "double")) - .map("$equal(left: numeric_type, right: numeric_type)").to("left = right") - .map("$not_equal(left: numeric_type, right: numeric_type)").to("left <> right") - .map("$less_than(left: numeric_type, right: numeric_type)").to("left < right") - .map("$less_than_or_equal(left: numeric_type, right: numeric_type)").to("left <= right") - .map("$greater_than(left: numeric_type, right: numeric_type)").to("left > right") - .map("$greater_than_or_equal(left: numeric_type, right: numeric_type)").to("left >= right") - .add(new RewriteCaseSensitiveComparison(ImmutableSet.of(ComparisonOperator.EQUAL, ComparisonOperator.NOT_EQUAL))) - .map("$add(left: integer_type, right: integer_type)").to("left + right") - .map("$subtract(left: integer_type, right: integer_type)").to("left - right") - .map("$multiply(left: integer_type, right: integer_type)").to("left * right") - .map("$divide(left: integer_type, right: integer_type)").to("left / right") - .map("$modulus(left: integer_type, right: integer_type)").to("MOD(left, right)") - .map("$negate(value: integer_type)").to("-value") - .map("$not($is_null(value))").to("value IS NOT NULL") - .map("$not(value: boolean)").to("NOT value") - .map("$is_null(value)").to("value IS NULL") - .map("$nullif(first, second)").to("NULLIF(first, second)") - .build(); - } - - /** - * Initializes the aggregate function rewriter for Teradata. - *

- * This method sets up the {@link AggregateFunctionRewriter} with a set of rules for translating Trino aggregate functions - * into SQL expressions supported by Teradata. Supported aggregates include COUNT, SUM, AVG, MIN, MAX, statistical functions, - * and regression/correlation functions. The rewriter uses the connector's expression rewriter for SQL translation. - *

- */ - private void buildAggregateRewriter() - { - JdbcTypeHandle bigintTypeHandle = new JdbcTypeHandle(Types.BIGINT, Optional.of("bigint"), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()); - - this.aggregateFunctionRewriter = new AggregateFunctionRewriter<>( - this.connectorExpressionRewriter, - ImmutableSet.>builder() - // Basic aggregates - .add(new ImplementCountAll(bigintTypeHandle)) - .add(new ImplementCount(bigintTypeHandle)) - .add(new ImplementCountDistinct(bigintTypeHandle, false)) - .add(new ImplementMinMax(false)) - .add(new ImplementSum(TeradataClient::toTypeHandle)) - - // AVG - .add(new ImplementAvgFloatingPoint()) - .add(new ImplementAvgDecimal()) - .add(new ImplementAvgBigint()) - - // Statistical aggregates (numeric types only) - .add(new ImplementStddevSamp()) - .add(new ImplementStddevPop()) - .add(new ImplementVarianceSamp()) - .add(new ImplementVariancePop()) - - // Correlation and regression - .add(new ImplementCovarianceSamp()) - .add(new ImplementCovariancePop()) - .add(new ImplementCorr()) - .add(new ImplementRegrIntercept()) - .add(new ImplementRegrSlope()) - .build()); - } - - /** - * Converts a predicate expression to a parameterized JDBC expression, - * suitable for pushdown to the Teradata database. - * - * @param session connector session - * @param expression connector expression representing the predicate - * @param assignments mapping of column names to handles - * @return optional parameterized expression if conversion is possible - */ @Override - public Optional convertPredicate(ConnectorSession session, ConnectorExpression expression, Map assignments) - { - return this.connectorExpressionRewriter.rewrite(session, expression, assignments); - } - - @Override - public boolean supportsAggregationPushdown(ConnectorSession session, JdbcTableHandle table, List aggregates, Map assignments, List> groupingSets) - { - return preventTextualTypeAggregationPushdown(groupingSets); - } - - /** - * Returns a mapping of column names to their case sensitivity, - * derived from the metadata of a query "SELECT * FROM schema.table WHERE 0=1". - * - * @param session connector session - * @param connection JDBC connection to the Teradata database - * @param schemaTableName schema and table name within the connector - * @param remoteTableName the fully qualified remote table name - * @return map of column name to case sensitivity (case-sensitive or insensitive) - */ - @Override - protected Map getCaseSensitivityForColumns(ConnectorSession session, Connection connection, SchemaTableName schemaTableName, RemoteTableName remoteTableName) + protected Map getCaseSensitivityForColumns(ConnectorSession session, Connection connection, SchemaTableName schemaTableName, + RemoteTableName remoteTableName) { // try to use result set metadata from select * from table to populate the mapping try { @@ -1057,16 +331,6 @@ protected Map getCaseSensitivityForColumns(ConnectorSes } } - /** - * Maps JDBC types and Teradata-specific types to Trino column mappings. - * Handles standard types as well as Teradata-specific types like - * TIMESTAMP WITH TIME ZONE and JSON. - * - * @param session connector session - * @param connection JDBC connection - * @param typeHandle JDBC type handle describing the column type - * @return optional column mapping for the given type - */ @Override public Optional toColumnMapping(ConnectorSession session, Connection connection, JdbcTypeHandle typeHandle) { @@ -1079,20 +343,6 @@ public Optional toColumnMapping(ConnectorSession session, Connect // switch by names as some types overlap other types going by jdbc type alone String jdbcTypeName = typeHandle.jdbcTypeName().orElse("VARCHAR"); - switch (jdbcTypeName.toUpperCase(ENGLISH)) { - case "TIMESTAMP WITH TIME ZONE": - return Optional.of(timestampWithTimeZoneColumnMapping(typeHandle.requiredDecimalDigits())); - case "TIME WITH TIME ZONE": - return Optional.of(timeWithTimeZoneColumnMapping(typeHandle.requiredDecimalDigits())); - case "JSON": - return Optional.of(jsonColumnMapping()); - case "NUMBER": - return numberMapping(typeHandle); - case "CHARACTER": - return Optional.of(charColumnMapping(typeHandle.requiredColumnSize(), deriveCaseSensitivity(typeHandle.caseSensitivity().orElse(null)))); - case "ARRAY": - return Optional.of(arrayColumnMapping()); - } switch (typeHandle.jdbcType()) { case Types.TINYINT: @@ -1118,23 +368,8 @@ public Optional toColumnMapping(ConnectorSession session, Connect case Types.VARCHAR: // see prior note on trino case sensitivity return Optional.of(varcharColumnMapping(typeHandle.requiredColumnSize(), deriveCaseSensitivity(typeHandle.caseSensitivity().orElse(null)))); - case Types.CLOB: - return Optional.of(ColumnMapping.sliceMapping( - createUnboundedVarcharType(), - (resultSet, columnIndex) -> utf8Slice(resultSet.getString(columnIndex)), - varcharWriteFunction(), - DISABLE_PUSHDOWN)); - case Types.BINARY: - case Types.VARBINARY: - case Types.BLOB: - // trino only has varbinary - return Optional.of(varbinaryColumnMapping()); case Types.DATE: return Optional.of(dateColumnMappingUsingLocalDate()); - case Types.TIME: - return Optional.of(timeColumnMapping(typeHandle.requiredDecimalDigits())); - case Types.TIMESTAMP: - return Optional.of(timestampColumnMapping(TimestampType.createTimestampType(typeHandle.requiredDecimalDigits()))); } if (getUnsupportedTypeHandling(session) == CONVERT_TO_VARCHAR) { @@ -1144,13 +379,6 @@ public Optional toColumnMapping(ConnectorSession session, Connect return Optional.empty(); } - /** - * Maps a JDBC NUMERIC/DECIMAL type to a Trino DecimalType column mapping. - * Handles precision and scale constraints. - * - * @param typeHandle the JDBC type handle - * @return Optional containing the ColumnMapping for the decimal type - */ private Optional numberMapping(JdbcTypeHandle typeHandle) { int precision = typeHandle.requiredColumnSize(); @@ -1162,270 +390,32 @@ private Optional numberMapping(JdbcTypeHandle typeHandle) return Optional.of(decimalColumnMapping(createDecimalType(precision, scale))); } - /** - * This connector is read-only and does not support writing to Teradata. - * This method always throws a NOT_SUPPORTED exception. - * - * @param session connector session - * @param type Trino type for the column - * @return never returns normally - * @throws TrinoException always thrown indicating unsupported operation - */ @Override public WriteMapping toWriteMapping(ConnectorSession session, Type type) { return switch (type) { - case Type t when t.equals(jsonType) -> WriteMapping.sliceMapping("JSON", typedVarcharWriteFunction()); - case Type t when t == TINYINT -> WriteMapping.longMapping("smallint", tinyintWriteFunction()); - case Type t when t == SMALLINT -> WriteMapping.longMapping("smallint", smallintWriteFunction()); - case Type t when t == INTEGER -> WriteMapping.longMapping("integer", integerWriteFunction()); - case Type t when t == BIGINT -> WriteMapping.longMapping("bigint", bigintWriteFunction()); - case Type t when t == REAL -> WriteMapping.longMapping("FLOAT", realWriteFunction()); - case Type t when t == DOUBLE -> WriteMapping.doubleMapping("double precision", doubleWriteFunction()); - case Type t when VARBINARY.equals(t) -> WriteMapping.sliceMapping("blob", varbinaryWriteFunction()); - case Type t when t == DATE -> WriteMapping.longMapping("date", dateWriteFunctionUsingLocalDate()); - case DecimalType decimalType -> { - String dataType = format("decimal(%s, %s)", decimalType.getPrecision(), decimalType.getScale()); - if (decimalType.isShort()) { - yield WriteMapping.longMapping(dataType, shortDecimalWriteFunction(decimalType)); + case Type typeInstance when typeInstance == TINYINT -> WriteMapping.longMapping("smallint", tinyintWriteFunction()); + case Type typeInstance when typeInstance == SMALLINT -> WriteMapping.longMapping("smallint", smallintWriteFunction()); + case Type typeInstance when typeInstance == INTEGER -> WriteMapping.longMapping("integer", integerWriteFunction()); + case Type typeInstance when typeInstance == BIGINT -> WriteMapping.longMapping("bigint", bigintWriteFunction()); + case Type typeInstance when typeInstance == REAL -> WriteMapping.longMapping("FLOAT", realWriteFunction()); + case Type typeInstance when typeInstance == DOUBLE -> WriteMapping.doubleMapping("double precision", doubleWriteFunction()); + case Type typeInstance when typeInstance == DATE -> WriteMapping.longMapping("date", dateWriteFunctionUsingLocalDate()); + case DecimalType decimalTypeInstance -> { + String dataType = String.format("decimal(%s, %s)", decimalTypeInstance.getPrecision(), decimalTypeInstance.getScale()); + if (decimalTypeInstance.isShort()) { + yield WriteMapping.longMapping(dataType, shortDecimalWriteFunction(decimalTypeInstance)); } - yield WriteMapping.objectMapping(dataType, longDecimalWriteFunction(decimalType)); + yield WriteMapping.objectMapping(dataType, longDecimalWriteFunction(decimalTypeInstance)); } - case CharType charType -> WriteMapping.sliceMapping("char(" + charType.getLength() + ")", charWriteFunction()); - case VarcharType varcharType -> { - String dataType = varcharType.isUnbounded() ? "clob" : "varchar(" + varcharType.getBoundedLength() + ")"; + case CharType charTypeInstance -> WriteMapping.sliceMapping("char(" + charTypeInstance.getLength() + ")", charWriteFunction()); + case VarcharType varcharTypeInstance -> { + String dataType = varcharTypeInstance.isUnbounded() + ? "clob" + : "varchar(" + varcharTypeInstance.getBoundedLength() + ")"; yield WriteMapping.sliceMapping(dataType, varcharWriteFunction()); } - case TimeType timeType -> { - verify(timeType.getPrecision() <= TERADATA_MAX_SUPPORTED_TIMESTAMP_PRECISION); - yield WriteMapping.longMapping(format("time(%s)", timeType.getPrecision()), timeWriteFunction(timeType.getPrecision())); - } - case TimestampType timestampType -> { - verify(timestampType.getPrecision() <= TERADATA_MAX_SUPPORTED_TIMESTAMP_PRECISION); - yield WriteMapping.longMapping(format("timestamp(%s)", timestampType.getPrecision()), timestampWriteFunction(timestampType)); - } default -> throw new TrinoException(NOT_SUPPORTED, "Unsupported column type: " + type.getDisplayName()); }; } - - /** - * Creates a ColumnMapping for Teradata JSON columns. - * - * @return ColumnMapping for the JSON column - */ - private ColumnMapping jsonColumnMapping() - { - return ColumnMapping.sliceMapping( - jsonType, - jsonReadFunction(), - typedVarcharWriteFunction(), - DISABLE_PUSHDOWN); - } - - /** - * Reads JSON values from a JDBC ResultSet and parses them as Trino slices. - * - * @return SliceReadFunction for JSON columns - */ - private SliceReadFunction jsonReadFunction() - { - return (resultSet, columnIndex) -> { - String json = resultSet.getString(columnIndex); - if (json == null) { - return null; - } - return jsonParse(utf8Slice(json)); - }; - } - - /** - * Creates a ColumnMapping for Teradata ARRAY columns (defaulting to VARCHAR element type). - * - * @return ColumnMapping for the ARRAY column - */ - private ColumnMapping arrayColumnMapping() - { - // Default to VARCHAR element type - you can enhance this to detect actual element type - Type elementType = createUnboundedVarcharType(); - Type arrayType = new ArrayType(elementType); - - return ColumnMapping.objectMapping( - arrayType, - arrayReadFunction(elementType), - arrayWriteFunction(elementType), - DISABLE_PUSHDOWN); - } - - /** - * Reads ARRAY values from a JDBC ResultSet and converts them to Trino blocks. - * - * @param elementType the Trino type of array elements - * @return ObjectReadFunction for ARRAY columns - */ - private ObjectReadFunction arrayReadFunction(Type elementType) - { - return ObjectReadFunction.of(Block.class, (resultSet, columnIndex) -> { - Array sqlArray = resultSet.getArray(columnIndex); - if (sqlArray == null) { - return null; - } - - Object[] elements = (Object[]) sqlArray.getArray(); - BlockBuilder blockBuilder = elementType.createBlockBuilder(null, elements.length); - - for (Object element : elements) { - if (element == null) { - blockBuilder.appendNull(); - } - else { - elementType.writeSlice(blockBuilder, utf8Slice(element.toString())); - } - } - - return blockBuilder.build(); - }); - } - - /** - * Writes ARRAY values from Trino blocks to a JDBC PreparedStatement. - * - * @param elementType the Trino type of array elements - * - */ - private ObjectWriteFunction arrayWriteFunction(Type elementType) - { - return ObjectWriteFunction.of(Block.class, (statement, index, block) -> { - if (block == null) { - statement.setNull(index, Types.ARRAY); - return; - } - - Object[] elements = new Object[block.getPositionCount()]; - for (int i = 0; i < block.getPositionCount(); i++) { - if (block.isNull(i)) { - elements[i] = null; - } - else { - elements[i] = elementType.getSlice(block, i).toStringUtf8(); - } - } - - Array sqlArray = statement.getConnection().createArrayOf("VARCHAR", elements); - statement.setArray(index, sqlArray); - }); - } - - /** - * TeradataStatisticsDao provides methods to retrieve table and column statistics from Teradata's system tables. - *

- * It estimates row counts, retrieves column-level statistics such as null counts and distinct value counts, - * and provides a fallback mechanism for row count estimation using sampling. This class is used internally - * by {@link TeradataClient} to support statistics-related features for query planning and optimization. - *

- */ - private record TeradataStatisticsDao(Handle handle) - { - /** - * Constructs a TeradataStatisticsDao with the provided JDBI handle. - * - * @param handle JDBI handle for database access - */ - private TeradataStatisticsDao(Handle handle) - { - this.handle = requireNonNull(handle, "handle is null"); - } - - /** - * Estimates the row count for a table using the maximum RowCount from DBC.StatsV. - * - * @param table the JDBC table handle - * @return estimated row count, or 0 if unavailable - */ - public long estimateRowCount(JdbcTableHandle table) - { - RemoteTableName remote = table.getRequiredNamedRelation().getRemoteTableName(); - String schema = remote.getSchemaName().orElseThrow(); - String tableName = remote.getTableName(); - - return handle.createQuery( - "SELECT MAX(RowCount) AS est_row_count " + - "FROM DBC.StatsV " + - "WHERE DatabaseName = :schema AND TableName = :table") - .bind("schema", schema) - .bind("table", tableName) - .mapTo(Long.class) - .findOne() - .orElse(0L); - } - - /** - * Retrieves column-level statistics (null count, distinct value count) from DBC.StatsV. - * - * @param table the JDBC table handle - * @return map of column name to column statistics - */ - public Map getColumnIndexStatistics(JdbcTableHandle table) - { - RemoteTableName remote = table.getRequiredNamedRelation().getRemoteTableName(); - String schema = remote.getSchemaName().orElseThrow(); - String tableName = remote.getTableName(); - - return handle.createQuery( - "SELECT ColumnName, NullCount, UniqueValueCount " + - "FROM DBC.StatsV " + - "WHERE DatabaseName = :schema AND TableName = :table") - .bind("schema", schema) - .bind("table", tableName) - .map((rs, _) -> { - String column = rs.getString("ColumnName"); - if (column == null) { - // skip this row by returning null - return null; - } - long nullCount = rs.getLong("NullCount"); - long distinct = rs.getLong("UniqueValueCount"); - - return new SimpleEntry<>( - column.trim(), - new ColumnIndexStatistics(nullCount > 0, distinct, nullCount)); - }) - // Filter out nulls before collecting to map - .filter(Objects::nonNull) - .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); - } - - /** - * Estimates the row count using a SAMPLE query as a fallback when direct statistics are unavailable. - * - * @param table the JDBC table handle - * @param connection JDBC connection - * @return OptionalLong containing the estimated row count, or empty if unavailable - */ - public OptionalLong sampleRowCountEstimate(JdbcTableHandle table, Connection connection) - { - RemoteTableName remote = table.getRequiredNamedRelation().getRemoteTableName(); - String schema = remote.getSchemaName().orElseThrow(); - String tableName = remote.getTableName(); - - String sql = format("SELECT COUNT(*) * 100 AS estimated_count FROM %s.%s SAMPLE 1", schema, tableName); - - try (Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery(sql)) { - if (rs.next()) { - long estimated = rs.getLong("estimated_count"); - return OptionalLong.of(estimated); - } - } - catch (SQLException e) { - throw new TrinoException(JDBC_ERROR, "Sampling fallback failed: " + e); - } - - return OptionalLong.empty(); - } - - /** - * ColumnIndexStatistics holds statistics for a single column, including nullability, distinct value count, and null count. - */ - public record ColumnIndexStatistics(boolean nullable, long distinctValues, long nullCount) {} - } } diff --git a/plugin/trino-teradata/src/main/java/io/trino/plugin/teradata/TeradataClientModule.java b/plugin/trino-teradata/src/main/java/io/trino/plugin/teradata/TeradataClientModule.java index 5ef70e820c68..56f6f5fa570e 100644 --- a/plugin/trino-teradata/src/main/java/io/trino/plugin/teradata/TeradataClientModule.java +++ b/plugin/trino-teradata/src/main/java/io/trino/plugin/teradata/TeradataClientModule.java @@ -11,7 +11,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.teradata; import com.google.inject.Binder; @@ -36,52 +35,9 @@ import static io.airlift.configuration.ConfigBinder.configBinder; -/** - * Guice module for configuring the Teradata JDBC client integration. - *

- * This module binds the {@link TeradataConfig} configuration class, - * the Teradata-specific {@link JdbcClient} implementation, and - * provides a {@link ConnectionFactory} tailored for Teradata connections. - *

- *

- * The {@code ConnectionFactory} is built using Teradata-specific JDBC connection properties, - * including session mode, character set, and authentication mechanism. - *

- *

- * This class uses Google Guice for dependency injection and - * OpenTelemetry for instrumentation of JDBC connections. - *

- * - * @see TeradataConfig - * @see TeradataClient - */ public class TeradataClientModule extends AbstractConfigurationAwareModule { - /** - * Provides a singleton {@link ConnectionFactory} for Teradata JDBC connections. - *

- * This factory is configured with Teradata-specific JDBC properties: - *

- *
    - *
  • {@code TMODE} - Teradata transaction mode
  • - *
  • {@code CHARSET} - Session character set
  • - *
  • {@code LOGMECH} - Logon mechanism
  • - *
  • {@code DATABASE} - Default database (optional)
  • - *
* - *

- * Uses the {@link DriverConnectionFactory} builder to create the connection factory, - * supplying the driver obtained from the JDBC connection URL and credentials via {@link CredentialProvider}. - * OpenTelemetry instrumentation is enabled on the connection factory. - *

- * - * @param config base JDBC configuration with connection URL - * @param teradataConfig Teradata-specific configuration properties - * @param credentialProvider provider for database credentials - * @param openTelemetry OpenTelemetry instance for instrumentation - * @return a singleton {@link ConnectionFactory} for Teradata connections - * @throws SQLException if the JDBC driver cannot be loaded or initialized - */ @Provides @Singleton @ForBaseJdbc @@ -96,53 +52,12 @@ public static ConnectionFactory getConnectionFactory(BaseJdbcConfig config, Tera switch (longMech) { case "TD2": break; - case "BEARER": - clientId = teradataConfig.getOidcClientId(); - if (clientId != null && !clientId.isEmpty()) { - connectionProperties.put("oidc_clientid", clientId); - } - String privateKey = teradataConfig.getOidcJWSPrivateKey(); - if (privateKey != null && !privateKey.isEmpty()) { - connectionProperties.put("jws_private_key", privateKey); - } - String certificate = teradataConfig.getOidcJWSCertificate(); - if (certificate != null && !certificate.isEmpty()) { - connectionProperties.put("jws_cert", certificate); - } - break; - case "JWT": - String token = teradataConfig.getOidcJwtToken(); - if (token != null && !token.trim().isEmpty()) { - token = "token=" + token; - connectionProperties.put("LOGDATA", token); - } - break; - case "SECRET": - clientId = teradataConfig.getOidcClientId(); - if (clientId != null && !clientId.isEmpty()) { - connectionProperties.put("oidc_clientid", clientId); - } - String clientSecret = teradataConfig.getOidcClientSecret(); - if (clientSecret != null && !clientSecret.isEmpty()) { - connectionProperties.put("LOGDATA", clientSecret); - } - break; default: throw new IllegalArgumentException("Unsupported logon mechanism: " + longMech); } return DriverConnectionFactory.builder(driver, config.getConnectionUrl(), credentialProvider).setConnectionProperties(connectionProperties).setOpenTelemetry(openTelemetry).build(); } - /** - * Configures Guice bindings for the Teradata connector. - *

- * Binds the {@link TeradataConfig} configuration, - * and binds {@link JdbcClient} annotated with {@link ForBaseJdbc} to {@link TeradataClient}. - * The client is scoped as a singleton. - *

- * - * @param binder Guice binder used to register bindings - */ @Override public void setup(Binder binder) { diff --git a/plugin/trino-teradata/src/main/java/io/trino/plugin/teradata/TeradataConfig.java b/plugin/trino-teradata/src/main/java/io/trino/plugin/teradata/TeradataConfig.java index fa7e34b702d3..e4ede4a6461a 100644 --- a/plugin/trino-teradata/src/main/java/io/trino/plugin/teradata/TeradataConfig.java +++ b/plugin/trino-teradata/src/main/java/io/trino/plugin/teradata/TeradataConfig.java @@ -11,115 +11,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.teradata; import io.airlift.configuration.Config; import io.airlift.configuration.ConfigDescription; import io.trino.plugin.jdbc.BaseJdbcConfig; -/** - * Configuration class for Teradata connector properties. - *

- * This class holds configuration options related to Teradata JDBC behavior, - * including transaction mode, character set, logon mechanism, case sensitivity, - * and default database. - *

- *

- * Many of these properties correspond to Teradata JDBC connection parameters. - *

- */ public class TeradataConfig extends BaseJdbcConfig { - /** - * OIDC JWT token used for authentication. - */ - private String oidcJWTToken; - - /** - * OIDC client secret used for authentication. - */ - private String oidcClientSecret; - /** - * OIDC JWS certificate for validating JWT signatures. - */ - private String oidcJWSCertificate; - - /** - * OIDC JWS private key for signing JWTs. - */ - private String oidcJWSPrivateKey; - /** - * OIDC client ID used for authentication. - */ - private String oidcClientId; - /** - * Logon mechanism for Teradata authentication (default: TD2). - */ private String logMech = "TD2"; private TeradataCaseSensitivity teradataCaseSensitivity = TeradataCaseSensitivity.CASE_SENSITIVE; - public String getOidcClientId() - { - return oidcClientId; - } - - @Config("oidc.client-id") - public TeradataConfig setOidcClientId(String clientId) - { - this.oidcClientId = clientId; - return this; - } - - public String getOidcJWSPrivateKey() - { - return oidcJWSPrivateKey; - } - - @Config("oidc.jws-private-key") - public TeradataConfig setOidcJWSPrivateKey(String privateKey) - { - this.oidcJWSPrivateKey = privateKey; - return this; - } - - public String getOidcJWSCertificate() - { - return oidcJWSCertificate; - } - - @Config("oidc.jws-certificate") - public TeradataConfig setOidcJWSCertificate(String certificate) - { - this.oidcJWSCertificate = certificate; - return this; - } - - public String getOidcClientSecret() - { - return oidcClientSecret; - } - - @Config("oidc.client-secret") - public TeradataConfig setOidcClientSecret(String clientSecret) - { - this.oidcClientSecret = clientSecret; - return this; - } - - public String getOidcJwtToken() - { - return oidcJWTToken; - } - - @Config("jwt.token") - public TeradataConfig setOidcJwtToken(String jwtToken) - { - this.oidcJWTToken = jwtToken; - return this; - } - public String getLogMech() { return logMech; @@ -133,22 +36,11 @@ public TeradataConfig setLogMech(String logMech) return this; } - /** - * Gets the Teradata case sensitivity setting. - * - * @return the current TeradataCaseSensitivity mode (default: CASE_SPECIFIC) - */ public TeradataCaseSensitivity getTeradataCaseSensitivity() { return teradataCaseSensitivity; } - /** - * Sets how char/varchar columns' case sensitivity will be exposed to Trino. - * - * @param teradataCaseSensitivity the case sensitivity mode - * @return this {@link TeradataConfig} instance for method chaining - */ @Config("teradata.case-sensitivity") @ConfigDescription("How char/varchar columns' case sensitivity will be exposed to Trino (default: CASE_SENSITIVE). Possible values: CASE_INSENSITIVE, CASE_SENSITIVE, AS_DEFINED.") public TeradataConfig setTeradataCaseSensitivity(TeradataCaseSensitivity teradataCaseSensitivity) @@ -157,14 +49,6 @@ public TeradataConfig setTeradataCaseSensitivity(TeradataCaseSensitivity teradat return this; } - /** - * Enum representing Teradata case sensitivity modes for char/varchar columns. - *
    - *
  • CASE_INSENSITIVE - case insensitive
  • - *
  • CASE_SENSITIVE - case sensitive
  • - *
  • AS_DEFINED - as defined by Teradata
  • - *
- */ public enum TeradataCaseSensitivity { CASE_INSENSITIVE, CASE_SENSITIVE, AS_DEFINED diff --git a/plugin/trino-teradata/src/main/java/io/trino/plugin/teradata/TeradataPlugin.java b/plugin/trino-teradata/src/main/java/io/trino/plugin/teradata/TeradataPlugin.java index 93e6213d3000..d11110edfbed 100644 --- a/plugin/trino-teradata/src/main/java/io/trino/plugin/teradata/TeradataPlugin.java +++ b/plugin/trino-teradata/src/main/java/io/trino/plugin/teradata/TeradataPlugin.java @@ -11,31 +11,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.teradata; import io.trino.plugin.jdbc.JdbcPlugin; -/** - * Teradata plugin for Trino. - *

- * This class registers the Teradata connector plugin with Trino, - * extending the base {@link JdbcPlugin} class. - * It provides the connector name ("teradata") and the client module - * to use for Teradata JDBC connectivity. - *

- */ public class TeradataPlugin extends JdbcPlugin { - /** - * Constructs a new TeradataPlugin instance. - *

- * The plugin is identified by the name "teradata" and - * uses the {@link TeradataClientModule} to configure - * the JDBC client for Teradata. - *

- */ public TeradataPlugin() { super("teradata", TeradataClientModule::new); diff --git a/plugin/trino-teradata/src/main/java/io/trino/plugin/teradata/util/TeradataConstants.java b/plugin/trino-teradata/src/main/java/io/trino/plugin/teradata/util/TeradataConstants.java index 4498f200976d..ea406c58cb06 100644 --- a/plugin/trino-teradata/src/main/java/io/trino/plugin/teradata/util/TeradataConstants.java +++ b/plugin/trino-teradata/src/main/java/io/trino/plugin/teradata/util/TeradataConstants.java @@ -11,16 +11,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.teradata.util; -/** - * Defines constants used throughout the Teradata connector. - */ public interface TeradataConstants { - /** - * The maximum length allowed for Teradata object names (e.g., databases, tables, columns). - */ int TERADATA_OBJECT_NAME_LIMIT = 128; } diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/AbstractTeradataJDBCTest.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/AbstractTeradataJDBCTest.java deleted file mode 100644 index 68b097745042..000000000000 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/AbstractTeradataJDBCTest.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.trino.plugin.integration; - -import io.trino.testing.AbstractTestQueryFramework; -import io.trino.testing.QueryRunner; -import io.trino.testing.datatype.CreateAndInsertDataSetup; -import io.trino.testing.datatype.DataSetup; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; - -/** - * Abstract base class for Teradata JDBC integration tests in Trino. - *

- * This class initializes a Teradata test database using a provided {@code databaseName}, - * manages its lifecycle, and provides support for creating and inserting data - * via {@link DataSetup}. - *

- * - *

Derived classes must implement {@link #initTables()} to set up the schema - * and tables needed for testing.

- * - *

Database is created before all tests and dropped after all tests. A unique - * {@code databaseName} can be passed by subclasses to allow isolated parallel test runs.

- */ -abstract class AbstractTeradataJDBCTest - extends AbstractTestQueryFramework -{ - /** - * Teradata database wrapper for executing SQL and retrieving connections. - */ - protected TestingTeradataServer database; - protected String envName; - - /** - * Constructs the test framework with a specific database name. - */ - AbstractTeradataJDBCTest(String envName) - { - this.envName = envName; - } - - /** - * Creates a {@link QueryRunner} instance for Trino. - * - * @return a new QueryRunner for executing Trino queries - * @throws Exception if initialization fails - */ - @Override - protected QueryRunner createQueryRunner() - throws Exception - { - database = new TestingTeradataServer(this.envName); - // Register this specific instance for this test class - return TeradataQueryRunner.builder(database).build(); - } - - /** - * Provides a {@link DataSetup} that creates and inserts test data into a table - * in the configured Teradata schema. - * - * @param tableNamePrefix prefix used for naming the test table - * @return a DataSetup object to create and populate the table - */ - protected DataSetup teradataJDBCCreateAndInsert(String tableNamePrefix) - { - String prefix = String.format("%s.%s", database.getDatabaseName(), tableNamePrefix); - return new CreateAndInsertDataSetup(database, prefix); - } - - /** - * Sets up the test database before all tests run. - * Creates the schema if it does not exist and invokes {@link #initTables()}. - */ - @BeforeAll - public void setup() - { - initTables(); - } - - /** - * Cleans up the test database after all tests have run. - * Deletes and drops the schema used for testing. - */ - - @AfterAll - public void cleanupTestClass() - { - if (database != null) { - database.close(); - } - } - - /** - * Implemented by subclasses to define the schema and tables required for the test. - */ - protected abstract void initTables(); -} diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/AuthenticationConfig.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/AuthenticationConfig.java index e0ce610f62a4..e4dad3ed36ea 100644 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/AuthenticationConfig.java +++ b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/AuthenticationConfig.java @@ -11,74 +11,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.integration; -public class AuthenticationConfig +public record AuthenticationConfig( + String userName, + String password) { - private final String userName; - private final String password; - private final String jwtToken; - private final String jwsPrivateKey; - private final String jwsCertificate; - private final String clientId; - private final String clientSecret; - public AuthenticationConfig() { - this(null, null, null, null, null, null, null); - } - - public AuthenticationConfig(String userName, String password) - { - this(userName, password, null, null, null, null, null); - } - - public AuthenticationConfig(String userName, String password, String jwtToken, String jwsPrivateKey, String jwsCertificate, - String clientId, String clientSecret) - { - this.userName = userName; - this.password = password; - this.jwtToken = jwtToken; - this.jwsPrivateKey = jwsPrivateKey; - this.jwsCertificate = jwsCertificate; - this.clientId = clientId; - this.clientSecret = clientSecret; - } - - // Getters only - public String getUserName() - { - return userName; - } - - public String getPassword() - { - return password; - } - - public String getJwtToken() - { - return jwtToken; - } - - public String getJwsPrivateKey() - { - return jwsPrivateKey; - } - - public String getJwsCertificate() - { - return jwsCertificate; - } - - public String getClientId() - { - return clientId; - } - - public String getClientSecret() - { - return clientSecret; + this(null, null); } } diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/DatabaseConfig.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/DatabaseConfig.java index 1fda33d88087..a0f3e72f3fd2 100644 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/DatabaseConfig.java +++ b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/DatabaseConfig.java @@ -11,7 +11,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.integration; import io.trino.plugin.teradata.LogonMechanism; @@ -59,7 +58,6 @@ public Builder toBuilder() .jdbcProperties(this.jdbcProperties); } - // Getters public String getJdbcUrl() { return jdbcUrl; diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/DatabaseConfigFactory.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/DatabaseConfigFactory.java index 339ba0815964..70ad8f3918de 100644 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/DatabaseConfigFactory.java +++ b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/DatabaseConfigFactory.java @@ -11,12 +11,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.integration; import io.trino.plugin.integration.util.TeradataTestConstants; import io.trino.plugin.teradata.LogonMechanism; -import org.apache.commons.lang3.StringUtils; import java.util.HashMap; import java.util.Map; @@ -41,7 +39,7 @@ public static DatabaseConfig create(String envName) } AuthenticationConfig authConfig = createAuthConfig(userName, password); LogonMechanism logMech = LogonMechanism.fromString(getEnvVar("logMech", DEFAULT_LOG_MECH)); - String databaseName = StringUtils.replace(envName, "-", "_"); + String databaseName = envName.replace("-", "_"); return DatabaseConfig.builder() .hostName(hostName) .databaseName(databaseName) @@ -63,12 +61,7 @@ public static Map getJdbcProperties() private static AuthenticationConfig createAuthConfig(String username, String password) { - return new AuthenticationConfig(username, password, - getEnvVar("jwt_token", null), - getEnvVar("jws_private_key", null), - getEnvVar("jws_cert", null), - getEnvVar("oidc_clientid", null), - getEnvVar("client_secret", null)); + return new AuthenticationConfig(username, password); } private static String getEnvVar(String name, String defaultValue) diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/TeradataConnectorTest.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/TeradataConnectorTest.java new file mode 100644 index 000000000000..0f8f79b4df82 --- /dev/null +++ b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/TeradataConnectorTest.java @@ -0,0 +1,476 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.plugin.integration; + +import io.trino.Session; +import io.trino.plugin.integration.clearscape.ClearScapeEnvironmentUtils; +import io.trino.plugin.jdbc.BaseJdbcConnectorTest; +import io.trino.sql.query.QueryAssertions; +import io.trino.testing.QueryRunner; +import io.trino.testing.TestingConnectorBehavior; +import io.trino.testing.TestingNames; +import io.trino.testing.assertions.TrinoExceptionAssert; +import io.trino.testing.sql.SqlExecutor; +import io.trino.testing.sql.TestTable; +import org.assertj.core.api.AssertProvider; +import org.intellij.lang.annotations.Language; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.function.Consumer; + +import static io.trino.plugin.teradata.util.TeradataConstants.TERADATA_OBJECT_NAME_LIMIT; +import static io.trino.testing.TestingNames.randomNameSuffix; +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; +import static org.assertj.core.api.Assertions.assertThat; + +final class TeradataConnectorTest + extends BaseJdbcConnectorTest +{ + private TestingTeradataServer database; + + @Override + protected SqlExecutor onRemoteDatabase() + { + return database; + } + + @Override + protected QueryRunner createQueryRunner() + throws Exception + { + database = new TestingTeradataServer(ClearScapeEnvironmentUtils.generateUniqueEnvName(getClass())); + // Register this specific instance for this test class + return TeradataQueryRunner.builder(database).setInitialTables(REQUIRED_TPCH_TABLES).build(); + } + + @Override + protected boolean hasBehavior(TestingConnectorBehavior connectorBehavior) + { + return switch (connectorBehavior) { + case SUPPORTS_ADD_COLUMN, + SUPPORTS_AGGREGATION_PUSHDOWN, + SUPPORTS_COMMENT_ON_COLUMN, + SUPPORTS_COMMENT_ON_TABLE, + SUPPORTS_CREATE_MATERIALIZED_VIEW, + SUPPORTS_CREATE_TABLE_WITH_COLUMN_COMMENT, + SUPPORTS_CREATE_TABLE_WITH_TABLE_COMMENT, + SUPPORTS_CREATE_VIEW, + SUPPORTS_DELETE, + SUPPORTS_DEREFERENCE_PUSHDOWN, + SUPPORTS_DROP_COLUMN, + SUPPORTS_DROP_SCHEMA_CASCADE, + SUPPORTS_INSERT, + SUPPORTS_JOIN_PUSHDOWN, + SUPPORTS_JOIN_PUSHDOWN_WITH_DISTINCT_FROM, + SUPPORTS_JOIN_PUSHDOWN_WITH_VARCHAR_INEQUALITY, + SUPPORTS_LIMIT_PUSHDOWN, + SUPPORTS_MAP_TYPE, + SUPPORTS_MERGE, + SUPPORTS_NATIVE_QUERY, + SUPPORTS_NEGATIVE_DATE, + SUPPORTS_PREDICATE_ARITHMETIC_EXPRESSION_PUSHDOWN, + SUPPORTS_PREDICATE_EXPRESSION_PUSHDOWN, + SUPPORTS_PREDICATE_PUSHDOWN, + SUPPORTS_PREDICATE_PUSHDOWN_WITH_VARCHAR_INEQUALITY, + SUPPORTS_RENAME_COLUMN, + SUPPORTS_RENAME_SCHEMA, + SUPPORTS_RENAME_TABLE, + SUPPORTS_ROW_LEVEL_DELETE, + SUPPORTS_ROW_TYPE, + SUPPORTS_SET_COLUMN_TYPE, + SUPPORTS_TOPN_PUSHDOWN, + SUPPORTS_TOPN_PUSHDOWN_WITH_VARCHAR, + SUPPORTS_TRUNCATE, + SUPPORTS_UPDATE -> false; + case SUPPORTS_CREATE_SCHEMA, + SUPPORTS_CREATE_TABLE -> true; + default -> super.hasBehavior(connectorBehavior); + }; + } + + @AfterAll + public void cleanupTestDatabase() + { + database.close(); + } + + @Override + protected OptionalInt maxSchemaNameLength() + { + return OptionalInt.of(TERADATA_OBJECT_NAME_LIMIT); + } + + @Override // Override because the expected error message is different + protected void verifySchemaNameLengthFailurePermissible(Throwable e) + { + assertThat(e).hasMessage(format("Schema name must be shorter than or equal to '%s' characters but got '%s'", TERADATA_OBJECT_NAME_LIMIT, TERADATA_OBJECT_NAME_LIMIT + 1)); + } + + @Override // Override because Teradata Object name limit is 128 characters + protected OptionalInt maxColumnNameLength() + { + return OptionalInt.of(TERADATA_OBJECT_NAME_LIMIT); + } + + @Override // Override because the expected error message is different + protected void verifyColumnNameLengthFailurePermissible(Throwable e) + { + assertThat(e).hasMessageMatching(format("Column name must be shorter than or equal to '%s' characters but got '%s': '.*'", TERADATA_OBJECT_NAME_LIMIT, + TERADATA_OBJECT_NAME_LIMIT + 1)); + } + + @Override // Override to skip the data mapping smoke test + @Test + public void testDataMappingSmokeTest() + { + skipTestUnless(false); + } + + @Override // Override because Teradata Table name limit is 128 characters + protected OptionalInt maxTableNameLength() + { + return OptionalInt.of(TERADATA_OBJECT_NAME_LIMIT); + } + + @Override // Override because the expected error message is different + protected void verifyTableNameLengthFailurePermissible(Throwable e) + { + assertThat(e).hasMessageMatching(format("Table name must be shorter than or equal to '%s' characters but got '%s'", TERADATA_OBJECT_NAME_LIMIT, + TERADATA_OBJECT_NAME_LIMIT + 1)); + } + + @Override // Overriding this test case as Teradata defines varchar with a length. + @Test + public void testVarcharCastToDateInPredicate() + { + String tableName = "varchar_as_date_pred"; + try (TestTable table = newTrinoTable(tableName, "(a varchar(50))", List.of("'999-09-09'", "'1005-09-09'", "'2005-06-06'", "'2005-06-6'", "'2005-6-06'", "'2005-6-6'", "' " + + "2005-06-06'", "'2005-06-06 '", "' +2005-06-06'", "'02005-06-06'", "'2005-09-06'", "'2005-09-6'", "'2005-9-06'", "'2005-9-6'", "' 2005-09-06'", "'2005-09-06 '", + "' +2005-09-06'", "'02005-09-06'", "'2005-09-09'", "'2005-09-9'", "'2005-9-09'", "'2005-9-9'", "' 2005-09-09'", "'2005-09-09 '", "' +2005-09-09'", "'02005-09-09" + + "'", "'2005-09-10'", "'2005-9-10'", "' 2005-09-10'", "'2005-09-10 '", "' +2005-09-10'", "'02005-09-10'", "'2005-09-20'", "'2005-9-20'", "' 2005-09-20'", + "'2005-09-20 '", "' +2005-09-20'", "'02005-09-20'", "'9999-09-09'", "'99999-09-09'"))) { + for (String date : List.of("2005-09-06", "2005-09-09", "2005-09-10")) { + for (String operator : List.of("=", "<=", "<", ">", ">=", "!=", "IS DISTINCT FROM", "IS NOT DISTINCT FROM")) { + assertThat(query("SELECT a FROM %s WHERE CAST(a AS date) %s DATE '%s'".formatted(table.getName(), operator, date))).hasCorrectResultsRegardlessOfPushdown(); + } + } + } + try (TestTable table = newTrinoTable(tableName, "(a varchar(50))", List.of("'2005-06-bad-date'", "'2005-09-10'"))) { + assertThat(query("SELECT a FROM %s WHERE CAST(a AS date) < DATE '2005-09-10'".formatted(table.getName()))).failure().hasMessage("Value cannot be cast to date: " + + "2005-06-bad-date"); + verifyResultOrFailure(query("SELECT a FROM %s WHERE CAST(a AS date) = DATE '2005-09-10'".formatted(table.getName())), + queryAssert -> queryAssert.skippingTypesCheck().matches("VALUES '2005-09-10'"), failureAssert -> failureAssert.hasMessage("Value cannot be cast to date: " + + "2005-06-bad-date")); + } + try (TestTable table = newTrinoTable(tableName, "(a varchar(50))", List.of("'2005-09-10'"))) { + // 2005-09-01, when written as 2005-09-1, is a prefix of an existing data point: 2005-09-10 + assertThat(query("SELECT a FROM %s WHERE CAST(a AS date) != DATE '2005-09-01'".formatted(table.getName()))).skippingTypesCheck().matches("VALUES '2005-09-10'"); + } + } + + // Tests CREATE TABLE AS SELECT functionality with Teradata syntax + // Overridden to handle Teradata's specific "WITH DATA" syntax for table creation + @Override + @Test + public void testCreateTableAsSelect() + { + String tableName = "test_ctas" + randomNameSuffix(); + assertUpdate("CREATE TABLE IF NOT EXISTS " + tableName + " AS SELECT name, regionkey FROM nation", "SELECT count(*) FROM nation"); + assertTableColumnNames(tableName, "name", "regionkey"); + assertThat(getTableComment(tableName)).isNull(); + assertUpdate("DROP TABLE " + tableName); + + // Some connectors support CREATE TABLE AS but not the ordinary CREATE TABLE. Let's test CTAS IF NOT EXISTS with a table that is guaranteed to exist. + assertUpdate("CREATE TABLE IF NOT EXISTS nation AS SELECT nationkey, regionkey FROM nation", 0); + assertTableColumnNames("nation", "nationkey", "name", "regionkey", "comment"); + + assertCreateTableAsSelect("SELECT nationkey, name, regionkey FROM nation", "SELECT count(*) FROM nation"); + + assertCreateTableAsSelect("SELECT mktsegment, sum(acctbal) x FROM customer GROUP BY mktsegment", "SELECT count(DISTINCT mktsegment) FROM customer"); + + assertCreateTableAsSelect("SELECT count(*) x FROM nation JOIN region ON nation.regionkey = region.regionkey", "SELECT 1"); + + assertCreateTableAsSelect("SELECT nationkey FROM nation ORDER BY nationkey LIMIT 10", "SELECT 10"); + + // Tests for CREATE TABLE with UNION ALL: exercises PushTableWriteThroughUnion optimizer + + assertCreateTableAsSelect("SELECT name, nationkey, regionkey FROM nation WHERE nationkey % 2 = 0 UNION ALL " + "SELECT name, nationkey, regionkey FROM nation WHERE " + + "nationkey % 2 = 1", "SELECT name, nationkey, regionkey FROM nation", "SELECT count(*) FROM nation"); + + assertCreateTableAsSelect(Session.builder(getSession()).setSystemProperty("redistribute_writes", "true").build(), "SELECT CAST(nationkey AS BIGINT) nationkey, regionkey " + + "FROM nation UNION ALL " + "SELECT 1234567890, 123", "SELECT nationkey, regionkey FROM nation UNION ALL " + "SELECT 1234567890, 123", "SELECT count(*) + 1 FROM " + + "nation"); + + assertCreateTableAsSelect(Session.builder(getSession()).setSystemProperty("redistribute_writes", "false").build(), "SELECT CAST(nationkey AS BIGINT) nationkey, regionkey" + + " FROM nation UNION ALL " + "SELECT 1234567890, 123", "SELECT nationkey, regionkey FROM nation UNION ALL " + "SELECT 1234567890, 123", "SELECT count(*) + 1 FROM " + + "nation"); + + tableName = "test_ctas" + randomNameSuffix(); + assertThat(query("EXPLAIN ANALYZE CREATE TABLE " + tableName + " AS SELECT name FROM nation")).succeeds(); + assertThat(query("SELECT * from " + tableName)).matches("SELECT name FROM nation"); + assertUpdate("DROP TABLE " + tableName); + } + + @Override // Overriding this test case as Teradata does not support negative dates. + @Test + public void testDateYearOfEraPredicate() + { + assertQuery("SELECT orderdate FROM orders WHERE orderdate = DATE '1997-09-14'", "VALUES DATE '1997-09-14'"); + } + + @Override // Override this test case as Teradata has different syntax for creating tables with AS SELECT statement. + @Test + public void verifySupportsRowLevelUpdateDeclaration() + { + String testTableName = "test_supports_update"; + try (TestTable table = newTrinoTable(testTableName, "AS ( SELECT * FROM nation) WITH DATA")) { + assertQueryFails("UPDATE " + table.getName() + " SET nationkey = nationkey * 100 WHERE regionkey = 2", "This connector does not support modifying table rows"); + } + } + + @Override // Overriding this test case as Teradata doesn't have support to (k, v) AS VALUES in insert statement + @Test + public void testCharVarcharComparison() + { + String testTableName = "test_char_varchar"; + try (TestTable table = newTrinoTable(testTableName, "(k int, v char(3))", List.of("-1, CAST(NULL AS char(3))", "3, CAST(' ' AS char(3))", "6, CAST('x ' AS char(3))"))) { + assertQuery("SELECT k, v FROM " + table.getName() + " WHERE v = CAST(' ' AS varchar(2))", "VALUES (3, ' ')"); + assertQuery("SELECT k, v FROM " + table.getName() + " WHERE v = CAST(' ' AS varchar(4))", "VALUES (3, ' ')"); + assertQuery("SELECT k, v FROM " + table.getName() + " WHERE v = CAST('x ' AS varchar(2))", "VALUES (6, 'x ')"); + } + } + + @Override // Overriding this test case as Teradata doesn't have support to (k, v) AS VALUES in insert statement + @Test + public void testVarcharCharComparison() + { + try (TestTable table = newTrinoTable("test_varchar_char", "(k int, v char(3))", List.of("-1, CAST(NULL AS varchar(3))", "0, CAST('' AS varchar(3))", "1, CAST(' ' AS" + + " varchar(3))", "2, CAST(' ' AS varchar(3))", "3, CAST(' ' AS varchar(3))", "4, CAST('x' AS varchar(3))", "5, CAST('x ' AS varchar(3))", + "6, CAST('x ' AS " + "varchar(3))"))) { + // Teradata's CHAR type automatically pads values with spaces to the defined length + assertQuery("SELECT k, v FROM " + table.getName() + " WHERE v = CAST(' ' AS char(2))", "VALUES (0, ' '), (1, ' '), (2, ' '), (3, ' ')"); + assertQuery("SELECT k, v FROM " + table.getName() + " WHERE v = CAST('x ' AS char(2))", "VALUES (4, 'x '), (5, 'x '), (6, 'x ')"); + } + } + + // Filters data mapping test data for Teradata compatibility + // Overridden to exclude data types that Teradata doesn't support or handles differently + @Override + protected Optional filterDataMappingSmokeTestData(DataMappingTestSetup dataMappingTestSetup) + { + String typeName = dataMappingTestSetup.getTrinoTypeName(); + return switch (typeName) { + // skipping date as during julian->gregorian date is handled differently in Teradata. tinyint, double and varchar with unbounded (need to handle special characters) + // is skipped and will handle it while improving + // write functionalities. + case "boolean", "tinyint", "date", "real", "double", "varchar", "time", "time(6)", "timestamp", "timestamp(6)", "varbinary", "timestamp(3) with time zone", + "timestamp(6) with time zone", "U&'a \\000a newline'" -> Optional.empty(); + default -> Optional.of(dataMappingTestSetup); + }; + } + + @Test + public void testTimestampWithTimeZoneCastToDatePredicate() + { + Assumptions.abort("Skipping as connector does not support Timestamp with Time Zone data type"); + } + + @Test + public void testTimestampWithTimeZoneCastToTimestampPredicate() + { + Assumptions.abort("Skipping as connector does not support Timestamp with Time Zone data type"); + } + + @Override + @Test + public void testRenameSchema() + { + Assumptions.abort("Skipping as connector does not support RENAME SCHEMA"); + } + + @Override + @Test + public void testColumnName() + { + Assumptions.abort("Skipping as connector does not support column level write operations"); + } + + @Override + @Test + public void testCreateTableAsSelectWithUnicode() + { + Assumptions.abort("Skipping as connector does not support creating table with UNICODE characters"); + } + + @Override + @Test + public void testUpdateNotNullColumn() + { + Assumptions.abort("Skipping as connector does not support insert operations"); + } + + @Override + @Test + public void testWriteBatchSizeSessionProperty() + { + Assumptions.abort("Skipping as connector does not support insert operations"); + } + + @Override + @Test + public void testInsertWithoutTemporaryTable() + { + Assumptions.abort("Skipping as connector does not support insert operations"); + } + + @Override + @Test + public void testWriteTaskParallelismSessionProperty() + { + Assumptions.abort("Skipping as connector does not support insert operations"); + } + + @Override + @Test + public void testInsertIntoNotNullColumn() + { + Assumptions.abort("Skipping as connector does not support insert operations"); + } + + @Override + @Test + public void testDropSchemaCascade() + { + Assumptions.abort("Skipping as connector does not support dropping schemas with CASCADE option"); + } + + @Override + @Test + public void testAddColumn() + { + Assumptions.abort("Skipping as connector does not support column level write operations"); + } + + @Override + @Test + public void testDropNonEmptySchemaWithTable() + { + Assumptions.abort("Skipping as connector does not support drop schemas"); + } + + @Override + @Test + public void verifySupportsUpdateDeclaration() + { + Assumptions.abort("Skipping as connector does not support update operations"); + } + + @Override + @Test + public void testDropNotNullConstraint() + { + Assumptions.abort("Skipping as connector does not support dropping a not null constraint"); + } + + @Override + @Test + public void testExecuteProcedureWithInvalidQuery() + { + Assumptions.abort("Skipping as connector does not support execute procedure"); + } + + @Override + @Test + public void testCreateTableAsSelectNegativeDate() + { + Assumptions.abort("Skipping as connector does not support creating table with negative date"); + } + + // Creates CTAS queries with proper session and row count validation + // Overridden to use Teradata's "WITH DATA" syntax for CREATE TABLE AS SELECT statements + @Override + protected void assertCreateTableAsSelect(Session session, String query, String expectedQuery, String rowCountQuery) + { + String table = "test_ctas_" + TestingNames.randomNameSuffix(); + assertUpdate(session, "CREATE TABLE " + table + " AS ( " + query + ") WITH DATA", rowCountQuery); + assertQuery(session, "SELECT * FROM " + table, expectedQuery); + assertUpdate(session, "DROP TABLE " + table); + assertThat(getQueryRunner().tableExists(session, table)).isFalse(); + } + + // Creates new Trino test tables with proper schema handling + // Overridden to handle Teradata's schema.table naming format and table creation syntax + @Override + protected TestTable newTrinoTable(String namePrefix, @Language("SQL") String tableDefinition, List rowsToInsert) + { + String tableName = ""; + + // Check if namePrefix already contains schema (contains a dot) + if (namePrefix.contains(".")) { + // namePrefix already has schema.tablename format + tableName = namePrefix; + } + else { + // Append current schema to namePrefix + String schemaName = getSession().getSchema().orElseThrow(); + tableName = schemaName + "." + namePrefix; + } + return new TestTable(database, tableName, tableDefinition, rowsToInsert); + } + + @Test + public void testTeradataNumberDataType() + { + try (TestTable table = newTrinoTable("test_number", "(id INTEGER, " + "number_col NUMBER(10,2), " + "number_default NUMBER, " + "number_large NUMBER(38,10))", List.of( + "1, CAST(12345.67 AS NUMBER(10,2)), CAST(999999999999999 AS NUMBER), CAST(1234567890123456789012345678.1234567890 AS NUMBER(38,10))", "2, CAST(-99999.99 AS " + + "NUMBER(10,2)), CAST(-123456789012345 AS NUMBER), CAST(-9999999999999999999999999999.9999999999 AS NUMBER(38,10))", + "3, CAST(0.00 AS NUMBER(10,2)), CAST" + "(0 AS NUMBER), CAST(0.0000000000 AS NUMBER(38,10))"))) { + assertThat(query(format("SELECT number_col FROM %s WHERE id = 1", table.getName()))).matches("VALUES CAST(12345.67 AS DECIMAL(10,2))"); + assertThat(query(format("SELECT number_default FROM %s WHERE id = 1", table.getName()))).matches("VALUES CAST(999999999999999 AS DECIMAL(38,0))"); + assertThat(query(format("SELECT number_large FROM %s WHERE id = 1", table.getName()))).matches("VALUES CAST(1234567890123456789012345678.1234567890 AS DECIMAL(38,10)" + + ")"); + assertThat(query(format("SELECT number_col FROM %s WHERE id = 2", table.getName()))).matches("VALUES CAST(-99999.99 AS DECIMAL(10,2))"); + assertThat(query(format("SELECT number_col FROM %s WHERE id = 3", table.getName()))).matches("VALUES CAST(0.00 AS DECIMAL(10,2))"); + } + } + + @Test + public void testTeradataCharacterDataType() + { + try (TestTable table = newTrinoTable("test_character", "(id INTEGER, " + "char_col CHARACTER(5), " + "char_default CHARACTER, " + "char_large CHARACTER(100))", List.of( + "1, CAST('HELLO' AS CHARACTER(5)), CAST('A' AS CHARACTER), CAST('TERADATA' AS CHARACTER(100))", + "2, CAST('WORLD' AS CHARACTER(5)), CAST('B' AS CHARACTER), CAST" + "('CHARACTER' AS CHARACTER(100))", "3, CAST('' AS CHARACTER(5)), CAST('C' AS CHARACTER), CAST" + + "('' AS CHARACTER(100))"))) { + assertThat(query(format("SELECT char_col FROM %s WHERE id = 1", table.getName()))).matches("VALUES CAST('HELLO' AS CHAR(5))"); + assertThat(query(format("SELECT char_default FROM %s WHERE id = 1", table.getName()))).matches("VALUES CAST('A' AS CHAR(1))"); + assertThat(query(format("SELECT char_large FROM %s WHERE id = 1", table.getName()))).matches("VALUES CAST('TERADATA' AS CHAR(100))"); + assertThat(query(format("SELECT char_col FROM %s WHERE id = 3", table.getName()))).matches("VALUES CAST('' AS CHAR(5))"); + } + } + + private static void verifyResultOrFailure(AssertProvider queryAssertProvider, Consumer verifyResults, + Consumer verifyFailure) + { + requireNonNull(verifyResults, "verifyResults is null"); + requireNonNull(verifyFailure, "verifyFailure is null"); + QueryAssertions.QueryAssert queryAssert = assertThat(queryAssertProvider); + verifyResults.accept(queryAssert); + } +} diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/TeradataJdbcConnectorTest.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/TeradataJdbcConnectorTest.java deleted file mode 100644 index cebab64371cc..000000000000 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/TeradataJdbcConnectorTest.java +++ /dev/null @@ -1,942 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.trino.plugin.integration; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.MoreCollectors; -import io.trino.Session; -import io.trino.plugin.integration.clearscape.ClearScapeEnvironmentUtils; -import io.trino.plugin.jdbc.BaseJdbcConnectorTest; -import io.trino.plugin.jdbc.JoinOperator; -import io.trino.spi.connector.JoinCondition; -import io.trino.sql.planner.plan.AggregationNode; -import io.trino.sql.planner.plan.FilterNode; -import io.trino.sql.query.QueryAssertions; -import io.trino.testing.MaterializedResult; -import io.trino.testing.MaterializedRow; -import io.trino.testing.QueryFailedException; -import io.trino.testing.QueryRunner; -import io.trino.testing.TestingConnectorBehavior; -import io.trino.testing.TestingNames; -import io.trino.testing.assertions.TrinoExceptionAssert; -import io.trino.testing.sql.SqlExecutor; -import io.trino.testing.sql.TestTable; -import org.assertj.core.api.AssertProvider; -import org.assertj.core.api.Assertions; -import org.intellij.lang.annotations.Language; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Assumptions; -import org.junit.jupiter.api.Test; - -import java.util.List; -import java.util.Optional; -import java.util.OptionalInt; -import java.util.function.Consumer; -import java.util.regex.Pattern; -import java.util.stream.Stream; - -import static com.google.common.collect.ImmutableList.toImmutableList; -import static io.trino.plugin.teradata.util.TeradataConstants.TERADATA_OBJECT_NAME_LIMIT; -import static io.trino.testing.TestingConnectorBehavior.SUPPORTS_CREATE_TABLE_WITH_DATA; -import static io.trino.testing.TestingNames.randomNameSuffix; -import static java.lang.String.format; -import static java.util.Objects.requireNonNull; -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Integration test class for Teradata JDBC Connector. - * Sets up schema and tables before tests and cleans up afterwards. - */ -public class TeradataJdbcConnectorTest - extends BaseJdbcConnectorTest -{ - protected TestingTeradataServer database; - - public TeradataJdbcConnectorTest() - { - } - - private static void verifyResultOrFailure(AssertProvider queryAssertProvider, Consumer verifyResults, Consumer verifyFailure) - { - requireNonNull(verifyResults, "verifyResults is null"); - requireNonNull(verifyFailure, "verifyFailure is null"); - QueryAssertions.QueryAssert queryAssert = Assertions.assertThat(queryAssertProvider); - verifyResults.accept(queryAssert); - } - - @Override - protected SqlExecutor onRemoteDatabase() - { - return database; - } - - @Override - protected boolean hasBehavior(TestingConnectorBehavior connectorBehavior) - { - return switch (connectorBehavior) { - case SUPPORTS_CREATE_VIEW, - SUPPORTS_CREATE_MATERIALIZED_VIEW, - SUPPORTS_DELETE, - SUPPORTS_INSERT, - SUPPORTS_UPDATE, - SUPPORTS_ADD_COLUMN, - SUPPORTS_DROP_COLUMN, - SUPPORTS_RENAME_COLUMN, - SUPPORTS_RENAME_TABLE, - SUPPORTS_TRUNCATE, - SUPPORTS_MERGE, - SUPPORTS_COMMENT_ON_TABLE, - SUPPORTS_COMMENT_ON_COLUMN, - SUPPORTS_CREATE_TABLE_WITH_TABLE_COMMENT, - SUPPORTS_CREATE_TABLE_WITH_COLUMN_COMMENT, - SUPPORTS_RENAME_SCHEMA, - SUPPORTS_SET_COLUMN_TYPE, - SUPPORTS_ROW_LEVEL_DELETE, - SUPPORTS_DROP_SCHEMA_CASCADE, - SUPPORTS_NATIVE_QUERY, - SUPPORTS_JOIN_PUSHDOWN_WITH_DISTINCT_FROM, - SUPPORTS_JOIN_PUSHDOWN_WITH_VARCHAR_INEQUALITY, - SUPPORTS_PREDICATE_PUSHDOWN_WITH_VARCHAR_INEQUALITY, - SUPPORTS_ROW_TYPE, - SUPPORTS_MAP_TYPE, - SUPPORTS_DEREFERENCE_PUSHDOWN, - SUPPORTS_NEGATIVE_DATE -> false; - case SUPPORTS_CREATE_SCHEMA, - SUPPORTS_CREATE_TABLE, - SUPPORTS_TOPN_PUSHDOWN, - SUPPORTS_PREDICATE_PUSHDOWN, - SUPPORTS_AGGREGATION_PUSHDOWN, - SUPPORTS_JOIN_PUSHDOWN, - SUPPORTS_LIMIT_PUSHDOWN, - SUPPORTS_TOPN_PUSHDOWN_WITH_VARCHAR, - SUPPORTS_PREDICATE_ARITHMETIC_EXPRESSION_PUSHDOWN, - SUPPORTS_PREDICATE_EXPRESSION_PUSHDOWN -> true; - default -> super.hasBehavior(connectorBehavior); - }; - } - - @Override - protected QueryRunner createQueryRunner() - throws Exception - { - database = new TestingTeradataServer(ClearScapeEnvironmentUtils.generateUniqueEnvName(this.getClass())); - // Register this specific instance for this test class - return TeradataQueryRunner.builder(database).setInitialTables(REQUIRED_TPCH_TABLES).build(); - } - - @AfterAll - public void cleanupTestDatabase() - { - database.close(); - } - - @Override - protected OptionalInt maxSchemaNameLength() - { - return OptionalInt.of(TERADATA_OBJECT_NAME_LIMIT); - } - - @Override - protected void verifySchemaNameLengthFailurePermissible(Throwable e) - { - assertThat(e).hasMessage(format("Schema name must be shorter than or equal to '%s' characters but got '%s'", TERADATA_OBJECT_NAME_LIMIT, TERADATA_OBJECT_NAME_LIMIT + 1)); - } - - @Override - protected OptionalInt maxColumnNameLength() - { - return OptionalInt.of(TERADATA_OBJECT_NAME_LIMIT); - } - - @Override - protected void verifyColumnNameLengthFailurePermissible(Throwable e) - { - assertThat(e).hasMessageMatching(format("Column name must be shorter than or equal to '%s' characters but got '%s': '.*'", TERADATA_OBJECT_NAME_LIMIT, TERADATA_OBJECT_NAME_LIMIT + 1)); - } - - @Override - @Test - public void testDataMappingSmokeTest() - { - // Skipping the Data Mapping smoke test as this is consuming more time to complete all data type mapping on Teradata ClearScape instance. Will enable once we fix timeout error when running tests on Teradata ClearScape instance - skipTestUnless(false); - } - - @Override - protected OptionalInt maxTableNameLength() - { - return OptionalInt.of(TERADATA_OBJECT_NAME_LIMIT); - } - - @Override - protected void verifyTableNameLengthFailurePermissible(Throwable e) - { - assertThat(e).hasMessageMatching(format("Table name must be shorter than or equal to '%s' characters but got '%s'", TERADATA_OBJECT_NAME_LIMIT, TERADATA_OBJECT_NAME_LIMIT + 1)); - } - - @Override - @Test - public void testDistinctLimit() - { - assertQuery("SELECT COUNT(*) FROM (SELECT DISTINCT orderstatus, custkey FROM orders LIMIT 10)"); - assertQuery("SELECT DISTINCT custkey, orderstatus FROM orders WHERE custkey = 1268 LIMIT 2"); - assertQuery("SELECT DISTINCT x " + - "FROM (VALUES 1) t(x) JOIN (VALUES 10, 20) u(a) ON t.x < u.a " + - "LIMIT 100", - "SELECT 1"); - } - - /* Overriding the method as Teradata avg calculations are slightly different than trino so Skipping the results check for avg - Expecting actual: (111.660, 111728394.9938271616, 1.117283945E8, 111.6605) to contain exactly in any order: [(111.661, 111728394.9938271605, 1.117283945E8, 111.6605)] */ - @Override - @Test - public void testNumericAggregationPushdown() - { - if (!this.hasBehavior(TestingConnectorBehavior.SUPPORTS_AGGREGATION_PUSHDOWN)) { - Assertions.assertThat(this.query("SELECT min(nationkey) FROM nation")).isNotFullyPushedDown(AggregationNode.class); - Assertions.assertThat(this.query("SELECT max(nationkey) FROM nation")).isNotFullyPushedDown(AggregationNode.class); - Assertions.assertThat(this.query("SELECT sum(nationkey) FROM nation")).isNotFullyPushedDown(AggregationNode.class); - Assertions.assertThat(this.query("SELECT avg(nationkey) FROM nation")).isNotFullyPushedDown(AggregationNode.class); - } - else { - try (TestTable emptyTable = this.createAggregationTestTable("test_num_agg_pd", ImmutableList.of())) { - Assertions.assertThat(this.query("SELECT min(short_decimal), min(long_decimal), min(a_bigint), min(t_double) FROM " + emptyTable.getName())).isFullyPushedDown(); - Assertions.assertThat(this.query("SELECT max(short_decimal), max(long_decimal), max(a_bigint), max(t_double) FROM " + emptyTable.getName())).isFullyPushedDown(); - Assertions.assertThat(this.query("SELECT sum(short_decimal), sum(long_decimal), sum(a_bigint), sum(t_double) FROM " + emptyTable.getName())).isFullyPushedDown(); - Assertions.assertThat(this.query("SELECT avg(short_decimal), avg(long_decimal), avg(a_bigint), avg(t_double) FROM " + emptyTable.getName())).skipResultsCorrectnessCheckForPushdown().isFullyPushedDown(); - } - - try (TestTable testTable = this.createAggregationTestTable("test_num_agg_pd", ImmutableList.of("100.000, 100000000.000000000, 100.000, 100000000", "123.321, 123456789.987654321, 123.321, 123456789"))) { - Assertions.assertThat(this.query("SELECT min(short_decimal), min(long_decimal), min(a_bigint), min(t_double) FROM " + testTable.getName())).isFullyPushedDown(); - Assertions.assertThat(this.query("SELECT max(short_decimal), max(long_decimal), max(a_bigint), max(t_double) FROM " + testTable.getName())).isFullyPushedDown(); - Assertions.assertThat(this.query("SELECT sum(short_decimal), sum(long_decimal), sum(a_bigint), sum(t_double) FROM " + testTable.getName())).isFullyPushedDown(); - Assertions.assertThat(this.query("SELECT avg(short_decimal), avg(long_decimal), avg(a_bigint), avg(t_double) FROM " + testTable.getName())).skipResultsCorrectnessCheckForPushdown().isFullyPushedDown(); - Assertions.assertThat(this.query("SELECT min(short_decimal), min(long_decimal) FROM " + testTable.getName() + " WHERE short_decimal < 110 AND long_decimal < 124")).isFullyPushedDown(); - Assertions.assertThat(this.query("SELECT min(long_decimal) FROM " + testTable.getName() + " WHERE short_decimal < 110")).isFullyPushedDown(); - Assertions.assertThat(this.query("SELECT short_decimal, min(long_decimal) FROM " + testTable.getName() + " GROUP BY short_decimal")).isFullyPushedDown(); - Assertions.assertThat(this.query("SELECT short_decimal, min(long_decimal) FROM " + testTable.getName() + " WHERE short_decimal < 110 AND long_decimal < 124 GROUP BY short_decimal")).isFullyPushedDown(); - Assertions.assertThat(this.query("SELECT short_decimal, min(long_decimal) FROM " + testTable.getName() + " WHERE short_decimal < 110 GROUP BY short_decimal")).isFullyPushedDown(); - Assertions.assertThat(this.query("SELECT short_decimal, min(long_decimal) FROM " + testTable.getName() + " WHERE long_decimal < 124 GROUP BY short_decimal")).isFullyPushedDown(); - } - } - } - - // Overriding this test case as Teradata defines varchar with a length. - @Override - @Test - public void testVarcharCastToDateInPredicate() - { - skipTestUnless(hasBehavior(SUPPORTS_CREATE_TABLE_WITH_DATA)); - String tableName = "varchar_as_date_pred"; - try (TestTable table = newTrinoTable( - tableName, - "(a varchar(50))", - List.of( - "'999-09-09'", - "'1005-09-09'", - "'2005-06-06'", "'2005-06-6'", "'2005-6-06'", "'2005-6-6'", "' 2005-06-06'", "'2005-06-06 '", "' +2005-06-06'", "'02005-06-06'", - "'2005-09-06'", "'2005-09-6'", "'2005-9-06'", "'2005-9-6'", "' 2005-09-06'", "'2005-09-06 '", "' +2005-09-06'", "'02005-09-06'", - "'2005-09-09'", "'2005-09-9'", "'2005-9-09'", "'2005-9-9'", "' 2005-09-09'", "'2005-09-09 '", "' +2005-09-09'", "'02005-09-09'", - "'2005-09-10'", "'2005-9-10'", "' 2005-09-10'", "'2005-09-10 '", "' +2005-09-10'", "'02005-09-10'", - "'2005-09-20'", "'2005-9-20'", "' 2005-09-20'", "'2005-09-20 '", "' +2005-09-20'", "'02005-09-20'", - "'9999-09-09'", - "'99999-09-09'"))) { - for (String date : List.of("2005-09-06", "2005-09-09", "2005-09-10")) { - for (String operator : List.of("=", "<=", "<", ">", ">=", "!=", "IS DISTINCT FROM", "IS NOT DISTINCT FROM")) { - assertThat(query("SELECT a FROM %s WHERE CAST(a AS date) %s DATE '%s'".formatted(table.getName(), operator, date))) - .hasCorrectResultsRegardlessOfPushdown(); - } - } - } - try (TestTable table = newTrinoTable(tableName, - "(a varchar(50))", - List.of("'2005-06-bad-date'", "'2005-09-10'"))) { - assertThat(query("SELECT a FROM %s WHERE CAST(a AS date) < DATE '2005-09-10'".formatted(table.getName()))) - .failure().hasMessage("Value cannot be cast to date: 2005-06-bad-date"); - verifyResultOrFailure( - query("SELECT a FROM %s WHERE CAST(a AS date) = DATE '2005-09-10'".formatted(table.getName())), - queryAssert -> queryAssert - .skippingTypesCheck() - .matches("VALUES '2005-09-10'"), - failureAssert -> failureAssert - .hasMessage("Value cannot be cast to date: 2005-06-bad-date")); - // This failure isn't guaranteed: a row may be filtered out on the connector side with a derived predicate on a varchar column. - verifyResultOrFailure( - query("SELECT a FROM %s WHERE CAST(a AS date) != DATE '2005-9-1'".formatted(table.getName())), - queryAssert -> queryAssert - .skippingTypesCheck() - .matches("VALUES '2005-09-10'"), - failureAssert -> failureAssert - .hasMessage("Value cannot be cast to date: 2005-06-bad-date")); - // This failure isn't guaranteed: a row may be filtered out on the connector side with a derived predicate on a varchar column. - verifyResultOrFailure( - query("SELECT a FROM %s WHERE CAST(a AS date) > DATE '2022-08-10'".formatted(table.getName())), - queryAssert -> queryAssert - .skippingTypesCheck() - .returnsEmptyResult(), - failureAssert -> failureAssert - .hasMessage("Value cannot be cast to date: 2005-06-bad-date")); - } - try (TestTable table = newTrinoTable( - tableName, - "(a varchar(50))", - List.of("'2005-09-10'"))) { - // 2005-09-01, when written as 2005-09-1, is a prefix of an existing data point: 2005-09-10 - assertThat(query("SELECT a FROM %s WHERE CAST(a AS date) != DATE '2005-09-01'".formatted(table.getName()))) - .skippingTypesCheck() - .matches("VALUES '2005-09-10'"); - } - } - - // Overriding this test case as Teradata raises different error message for division by zero. - @Override - @Test - public void testArithmeticPredicatePushdown() - { - if (!this.hasBehavior(TestingConnectorBehavior.SUPPORTS_PREDICATE_ARITHMETIC_EXPRESSION_PUSHDOWN)) { - Assertions.assertThat(this.query("SELECT shippriority FROM orders WHERE shippriority % 4 = 0")).isNotFullyPushedDown(FilterNode.class); - } - else { - Assertions.assertThat(this.query("SELECT shippriority FROM orders WHERE shippriority % 4 = 0")).isFullyPushedDown(); - Assertions.assertThat(this.query("SELECT nationkey, name, regionkey FROM nation WHERE nationkey > 0 AND (nationkey - regionkey) % nationkey = 2")).isFullyPushedDown().matches("VALUES (BIGINT '3', CAST('CANADA' AS varchar(25)), BIGINT '1')"); - Assertions.assertThat(this.query("SELECT nationkey, name, regionkey FROM nation WHERE nationkey > 0 AND (nationkey - regionkey) % -nationkey = 2")).isFullyPushedDown().matches("VALUES (BIGINT '3', CAST('CANADA' AS varchar(25)), BIGINT '1')"); - Assertions.assertThat(this.query("SELECT nationkey, name, regionkey FROM nation WHERE nationkey > 0 AND (nationkey - regionkey) % 0 = 2")).failure().hasMessageContaining("Operation Error"); - Assertions.assertThat(this.query("SELECT nationkey, name, regionkey FROM nation WHERE nationkey > 0 AND (nationkey - regionkey) % (regionkey - 1) = 2")).failure().hasMessageContaining("Operation Error"); - } - } - - @Override - @Test - public void testCreateTableAsSelect() - { - String tableName = "test_ctas" + randomNameSuffix(); - if (!hasBehavior(SUPPORTS_CREATE_TABLE_WITH_DATA)) { - assertQueryFails("CREATE TABLE IF NOT EXISTS " + tableName + " AS SELECT name, regionkey FROM nation", "This connector does not support creating tables with data"); - return; - } - assertUpdate("CREATE TABLE IF NOT EXISTS " + tableName + " AS SELECT name, regionkey FROM nation", "SELECT count(*) FROM nation"); - assertTableColumnNames(tableName, "name", "regionkey"); - assertThat(getTableComment(tableName)).isNull(); - assertUpdate("DROP TABLE " + tableName); - - // Some connectors support CREATE TABLE AS but not the ordinary CREATE TABLE. Let's test CTAS IF NOT EXISTS with a table that is guaranteed to exist. - assertUpdate("CREATE TABLE IF NOT EXISTS nation AS SELECT nationkey, regionkey FROM nation", 0); - assertTableColumnNames("nation", "nationkey", "name", "regionkey", "comment"); - - assertCreateTableAsSelect( - "SELECT nationkey, name, regionkey FROM nation", - "SELECT count(*) FROM nation"); - - assertCreateTableAsSelect( - "SELECT mktsegment, sum(acctbal) x FROM customer GROUP BY mktsegment", - "SELECT count(DISTINCT mktsegment) FROM customer"); - - assertCreateTableAsSelect( - "SELECT count(*) x FROM nation JOIN region ON nation.regionkey = region.regionkey", - "SELECT 1"); - - assertCreateTableAsSelect( - "SELECT nationkey FROM nation ORDER BY nationkey LIMIT 10", - "SELECT 10"); - - // Tests for CREATE TABLE with UNION ALL: exercises PushTableWriteThroughUnion optimizer - - assertCreateTableAsSelect( - "SELECT name, nationkey, regionkey FROM nation WHERE nationkey % 2 = 0 UNION ALL " + - "SELECT name, nationkey, regionkey FROM nation WHERE nationkey % 2 = 1", - "SELECT name, nationkey, regionkey FROM nation", - "SELECT count(*) FROM nation"); - - assertCreateTableAsSelect( - Session.builder(getSession()).setSystemProperty("redistribute_writes", "true").build(), - "SELECT CAST(nationkey AS BIGINT) nationkey, regionkey FROM nation UNION ALL " + - "SELECT 1234567890, 123", - "SELECT nationkey, regionkey FROM nation UNION ALL " + - "SELECT 1234567890, 123", - "SELECT count(*) + 1 FROM nation"); - - assertCreateTableAsSelect( - Session.builder(getSession()).setSystemProperty("redistribute_writes", "false").build(), - "SELECT CAST(nationkey AS BIGINT) nationkey, regionkey FROM nation UNION ALL " + - "SELECT 1234567890, 123", - "SELECT nationkey, regionkey FROM nation UNION ALL " + - "SELECT 1234567890, 123", - "SELECT count(*) + 1 FROM nation"); - - tableName = "test_ctas" + randomNameSuffix(); - assertExplainAnalyze("EXPLAIN ANALYZE CREATE TABLE " + tableName + " AS SELECT name FROM nation"); - assertQuery("SELECT * from " + tableName, "SELECT name FROM nation"); - assertUpdate("DROP TABLE " + tableName); - } - - // Overriding this test case as Teradata does not support negative dates. - @Override - @Test - public void testDateYearOfEraPredicate() - { - this.assertQuery("SELECT orderdate FROM orders WHERE orderdate = DATE '1997-09-14'", "VALUES DATE '1997-09-14'"); - } - - // Override this test case as Teradata has different syntax for creating tables with AS SELECT statement. - @Override - @Test - public void verifySupportsRowLevelUpdateDeclaration() - { - if (!this.hasBehavior(TestingConnectorBehavior.SUPPORTS_ROW_LEVEL_UPDATE)) { - skipTestUnless(this.hasBehavior(TestingConnectorBehavior.SUPPORTS_CREATE_TABLE_WITH_DATA)); - String testTableName = "test_supports_update"; - try (TestTable table = this.newTrinoTable(testTableName, "AS ( SELECT * FROM nation) WITH DATA")) { - this.assertQueryFails("UPDATE " + table.getName() + " SET nationkey = nationkey * 100 WHERE regionkey = 2", "This connector does not support modifying table rows"); - } - } - } - - // Override this test case as Teradata has different syntax for creating tables with AS SELECT statement. - // TODO Will handle this while Teradata connector supporting WRITE operations. - @Override - @Test - public void testJoinPushdown() - { - Session session = this.joinPushdownEnabled(this.getSession()); - if (!this.hasBehavior(TestingConnectorBehavior.SUPPORTS_JOIN_PUSHDOWN)) { - Assertions.assertThat(this.query(session, "SELECT r.name, n.name FROM nation n JOIN region r ON n.regionkey = r.regionkey")).joinIsNotFullyPushedDown(); - } - else { - String testTableName = "nation_lowercase"; - try (TestTable nationLowercaseTable = this.newTrinoTable(testTableName, "AS ( SELECT nationkey, lower(name) name, regionkey FROM nation ) WITH DATA")) { - for (JoinOperator joinOperator : JoinOperator.values()) { - if (joinOperator == JoinOperator.FULL_JOIN && !this.hasBehavior(TestingConnectorBehavior.SUPPORTS_JOIN_PUSHDOWN_WITH_FULL_JOIN)) { - Assertions.assertThat(this.query(session, "SELECT r.name, n.name FROM nation n FULL JOIN region r ON n.regionkey = r.regionkey")).joinIsNotFullyPushedDown(); - } - else { - Session withoutDynamicFiltering = Session.builder(session).setSystemProperty("enable_dynamic_filtering", "false").build(); - List nonEqualities = Stream.concat(Stream.of(JoinCondition.Operator.values()).filter((operatorx) -> operatorx != JoinCondition.Operator.EQUAL && operatorx != JoinCondition.Operator.IDENTICAL).map(JoinCondition.Operator::getValue), Stream.of("IS DISTINCT FROM", "IS NOT DISTINCT FROM")).collect(toImmutableList()); - Assertions.assertThat(this.query(session, String.format("SELECT r.name, n.name FROM nation n %s region r ON n.regionkey = r.regionkey", joinOperator))).skipResultsCorrectnessCheckForPushdown().isFullyPushedDown(); - Assertions.assertThat(this.query(session, String.format("SELECT r.name, n.name FROM nation n %s region r ON n.nationkey = r.regionkey", joinOperator))).skipResultsCorrectnessCheckForPushdown().isFullyPushedDown(); - Assertions.assertThat(this.query(session, String.format("SELECT r.name, n.name FROM nation n %s region r USING(regionkey)", joinOperator))).skipResultsCorrectnessCheckForPushdown().isFullyPushedDown(); - this.assertJoinConditionallyPushedDown(session, String.format("SELECT n.name, n2.regionkey FROM nation n %s nation n2 ON n.name = n2.name", joinOperator), this.hasBehavior(TestingConnectorBehavior.SUPPORTS_JOIN_PUSHDOWN_WITH_VARCHAR_EQUALITY)).skipResultsCorrectnessCheckForPushdown(); - this.assertJoinConditionallyPushedDown(session, String.format("SELECT n.name, nl.regionkey FROM nation n %s %s nl ON n.name = nl.name", joinOperator, nationLowercaseTable.getName()), this.hasBehavior(TestingConnectorBehavior.SUPPORTS_JOIN_PUSHDOWN_WITH_VARCHAR_EQUALITY)).skipResultsCorrectnessCheckForPushdown(); - Assertions.assertThat(this.query(session, String.format("SELECT n.name, c.name FROM nation n %s customer c ON n.nationkey = c.nationkey and n.regionkey = c.custkey", joinOperator))).skipResultsCorrectnessCheckForPushdown().isFullyPushedDown(); - for (String operator : nonEqualities) { - this.assertJoinConditionallyPushedDown(withoutDynamicFiltering, String.format("SELECT r.name, n.name FROM nation n %s region r ON n.regionkey %s r.regionkey", joinOperator, operator), this.expectJoinPushdown(operator) && this.expectJoinPushdownOnInequalityOperator(joinOperator)).skipResultsCorrectnessCheckForPushdown(); - this.assertJoinConditionallyPushedDown(withoutDynamicFiltering, String.format("SELECT n.name, nl.name FROM nation n %s %s nl ON n.name %s nl.name", joinOperator, nationLowercaseTable.getName(), operator), this.expectVarcharJoinPushdown(operator) && this.expectJoinPushdownOnInequalityOperator(joinOperator)).skipResultsCorrectnessCheckForPushdown(); - this.assertJoinConditionallyPushedDown(session, String.format("SELECT n.name, c.name FROM nation n %s customer c ON n.nationkey = c.nationkey AND n.regionkey %s c.custkey", joinOperator, operator), this.expectJoinPushdown(operator)).skipResultsCorrectnessCheckForPushdown(); - } - for (String operator : nonEqualities) { - this.assertJoinConditionallyPushedDown(session, String.format("SELECT n.name, nl.name FROM nation n %s %s nl ON n.regionkey = nl.regionkey AND n.name %s nl.name", joinOperator, nationLowercaseTable.getName(), operator), this.expectVarcharJoinPushdown(operator)).skipResultsCorrectnessCheckForPushdown(); - } - Assertions.assertThat(this.query(session, String.format("SELECT c.name, n.name FROM (SELECT * FROM customer WHERE acctbal > 8000) c %s nation n ON c.custkey = n.nationkey", joinOperator))).isFullyPushedDown().skipResultsCorrectnessCheckForPushdown(); - this.assertJoinConditionallyPushedDown(session, String.format("SELECT c.name, n.name FROM (SELECT * FROM customer WHERE address = 'TcGe5gaZNgVePxU5kRrvXBfkasDTea') c %s nation n ON c.custkey = n.nationkey", joinOperator), this.hasBehavior(TestingConnectorBehavior.SUPPORTS_PREDICATE_PUSHDOWN_WITH_VARCHAR_EQUALITY)).skipResultsCorrectnessCheckForPushdown(); - this.assertJoinConditionallyPushedDown(session, String.format("SELECT c.name, n.name FROM (SELECT * FROM customer WHERE address < 'TcGe5gaZNgVePxU5kRrvXBfkasDTea') c %s nation n ON c.custkey = n.nationkey", joinOperator), this.hasBehavior(TestingConnectorBehavior.SUPPORTS_PREDICATE_PUSHDOWN_WITH_VARCHAR_INEQUALITY)).skipResultsCorrectnessCheckForPushdown(); - this.assertJoinConditionallyPushedDown(session, String.format("SELECT * FROM (SELECT regionkey rk, count(nationkey) c FROM nation GROUP BY regionkey) n %s region r ON n.rk = r.regionkey", joinOperator), this.hasBehavior(TestingConnectorBehavior.SUPPORTS_AGGREGATION_PUSHDOWN)).skipResultsCorrectnessCheckForPushdown(); - this.assertJoinConditionallyPushedDown(session, String.format("SELECT * FROM (SELECT nationkey FROM nation LIMIT 30) n %s region r ON n.nationkey = r.regionkey", joinOperator), this.hasBehavior(TestingConnectorBehavior.SUPPORTS_LIMIT_PUSHDOWN)).skipResultsCorrectnessCheckForPushdown(); - this.assertJoinConditionallyPushedDown(session, String.format("SELECT * FROM (SELECT nationkey FROM nation ORDER BY regionkey LIMIT 5) n %s region r ON n.nationkey = r.regionkey", joinOperator), this.hasBehavior(TestingConnectorBehavior.SUPPORTS_TOPN_PUSHDOWN)).skipResultsCorrectnessCheckForPushdown(); - Assertions.assertThat(this.query(session, "SELECT * FROM nation n, region r, customer c WHERE n.regionkey = r.regionkey AND r.regionkey = c.custkey")).isFullyPushedDown().skipResultsCorrectnessCheckForPushdown(); - } - } - } - catch (Throwable e) { - throw new RuntimeException(e); - } - } - } - - @Override - @Test - public void testCharVarcharComparison() - { - skipTestUnless(this.hasBehavior(TestingConnectorBehavior.SUPPORTS_CREATE_TABLE)); - String testTableName = "test_char_varchar"; - try (TestTable table = newTrinoTable(testTableName, "(k int, v char(3))", List.of("-1, CAST(NULL AS char(3))", "3, CAST(' ' AS char(3))", "6, CAST('x ' AS char(3))"))) { - this.assertQuery("SELECT k, v FROM " + table.getName() + " WHERE v = CAST(' ' AS varchar(2))", "VALUES (3, ' ')"); - this.assertQuery("SELECT k, v FROM " + table.getName() + " WHERE v = CAST(' ' AS varchar(4))", "VALUES (3, ' ')"); - this.assertQuery("SELECT k, v FROM " + table.getName() + " WHERE v = CAST('x ' AS varchar(2))", "VALUES (6, 'x ')"); - } - } - - @Test - public void testJsonColumnMapping() - { - String testTableName = "test_json_table"; - try (TestTable table = newTrinoTable(testTableName, "(id INTEGER, json_data JSON)", List.of("1, '{\"name\": \"Alice\", \"age\": 30}'", "2, '{\"name\": \"Bob\", \"age\": 25, \"active\": true}'", "3, NULL"))) { - // Test JSON reading - assertQuery( - format("SELECT id, json_data FROM %s ORDER BY id", table.getName()), - "VALUES " + - "(1, JSON '{\"name\": \"Alice\", \"age\": 30}'), " + - "(2, JSON '{\"name\": \"Bob\", \"age\": 25, \"active\": true}'), " + - "(3, CAST(NULL AS JSON))"); - - // Test JSON extraction - assertQuery( - format("SELECT JSON_EXTRACT_SCALAR(json_data, '$.name') FROM %s WHERE id = 1", table.getName()), - "VALUES 'Alice'"); - - assertQuery( - format("SELECT JSON_EXTRACT_SCALAR(json_data, '$.age') FROM %s WHERE id = 2", table.getName()), - "VALUES '25'"); - } - } - - @Test - public void testJsonColumnMappingTypeMapping() - { - String testTableName = "test_json_type_mapping"; - try (TestTable table = newTrinoTable(testTableName, "(id INTEGER, json_col JSON)", List.of("1, '{\"test\": \"value\"}'"))) { - // Verify the column type is mapped correctly - MaterializedResult result = computeActual(format("DESCRIBE %s", table.getName())); - - boolean jsonColumnFound = false; - for (MaterializedRow row : result.getMaterializedRows()) { - String columnName = (String) row.getField(0); - String columnType = (String) row.getField(1); - - if ("json_col".equals(columnName)) { - org.junit.jupiter.api.Assertions.assertEquals("json", columnType); - jsonColumnFound = true; - break; - } - } - assertThat(jsonColumnFound).isTrue(); - } - } - - @Test - public void testJsonColumnMappingComplexData() - { - String testTableName = "test_json_complex"; - try (TestTable table = newTrinoTable(testTableName, "(id INTEGER, json_data JSON)", - List.of("1, '{\"user\": {\"name\": \"John\", \"addresses\": [{\"city\": \"NYC\", \"zip\": \"10001\"}, {\"city\": \"LA\", \"zip\": \"90210\"}]}}'", - "2, '{\"numbers\": [1, 2, 3, 4, 5], \"mixed\": [\"text\", 42, true, null]}'", - "3, '{\"empty_object\": {}, \"empty_array\": [], \"null_value\": null}'"))) { - // Test nested object extraction - assertQuery( - format("SELECT JSON_EXTRACT_SCALAR(json_data, '$.user.name') FROM %s WHERE id = 1", table.getName()), - "VALUES 'John'"); - - // Test array element extraction - assertQuery( - format("SELECT JSON_EXTRACT_SCALAR(json_data, '$.user.addresses[0].city') FROM %s WHERE id = 1", table.getName()), - "VALUES 'NYC'"); - - // Test array element from numbers array - assertQuery( - format("SELECT JSON_EXTRACT_SCALAR(json_data, '$.numbers[2]') FROM %s WHERE id = 2", table.getName()), - "VALUES '3'"); - - // Test JSON_EXTRACT for object/array values - assertQuery( - format("SELECT JSON_EXTRACT(json_data, '$.user.addresses') FROM %s WHERE id = 1", table.getName()), - "VALUES JSON '[{\"city\": \"NYC\", \"zip\": \"10001\"}, {\"city\": \"LA\", \"zip\": \"90210\"}]'"); - } - } - - @Test - public void testJsonArrayWithNullValues() - { - String testTableName = "test_json_array_nulls"; - - try (TestTable table = newTrinoTable(testTableName, "(id INTEGER, json_data JSON)", - List.of("1, '{\"array\": [1, null, 3, null]}'"))) { - // Extract specific array elements - assertQuery( - format("SELECT JSON_EXTRACT_SCALAR(json_data, '$.array[1]') FROM %s WHERE id = 1", table.getName()), - "VALUES CAST(NULL AS VARCHAR)"); // Second element is null - - assertQuery( - format("SELECT JSON_EXTRACT_SCALAR(json_data, '$.array[2]') FROM %s WHERE id = 1", table.getName()), - "VALUES '3'"); // Third element is 3 - - // Extract the entire array - assertQuery( - format("SELECT JSON_EXTRACT(json_data, '$.array') FROM %s WHERE id = 1", table.getName()), - "VALUES JSON '[1, null, 3, null]'"); - } - } - - // Overriding this test case as Teradata doesn't have support to (k, v) AS VALUES in insert statement - @Override - @Test - public void testVarcharCharComparison() - { - skipTestUnless(this.hasBehavior(TestingConnectorBehavior.SUPPORTS_CREATE_TABLE)); - - try (TestTable table = this.newTrinoTable("test_varchar_char", "(k int, v char(3))", List.of("-1, CAST(NULL AS varchar(3))", "0, CAST('' AS varchar(3))", "1, CAST(' ' AS varchar(3))", "2, CAST(' ' AS varchar(3))", "3, CAST(' ' AS varchar(3))", "4, CAST('x' AS varchar(3))", "5, CAST('x ' AS varchar(3))", "6, CAST('x ' AS varchar(3))"))) { - // Teradata's CHAR type automatically pads values with spaces to the defined length - this.assertQuery("SELECT k, v FROM " + table.getName() + " WHERE v = CAST(' ' AS char(2))", "VALUES (0, ' '), (1, ' '), (2, ' '), (3, ' ')"); - this.assertQuery("SELECT k, v FROM " + table.getName() + " WHERE v = CAST('x ' AS char(2))", "VALUES (4, 'x '), (5, 'x '), (6, 'x ')"); - } - } - - // Overriding this test case as Teradata supports timezone in different way. - @Override - @Test - public void testTimestampWithTimeZoneCastToDatePredicate() - { - skipTestUnless(this.hasBehavior(TestingConnectorBehavior.SUPPORTS_CREATE_TABLE_WITH_DATA)); - TestTable table; - try { - table = this.newTrinoTable("timestamptz_to_date", "(i varchar(20), t TIMESTAMP)", - List.of( - "'UTC', CAST(TIMESTAMP '2005-09-10 00:12:34.000+00:00' AT TIME ZONE INTERVAL '0:00' HOUR TO MINUTE AS TIMESTAMP)", - "'Warsaw', CAST(TIMESTAMP '2005-09-10 00:12:34.000+02:00' AT TIME ZONE INTERVAL '2:00' HOUR TO MINUTE AS TIMESTAMP)", - "'Los Angeles', CAST(TIMESTAMP '2005-09-10 00:12:34.000-07:00' AT TIME ZONE - INTERVAL '7:00' HOUR TO MINUTE AS TIMESTAMP)")); - } - catch (QueryFailedException e) { - this.verifyUnsupportedTypeException(e, "timestamp(3) with time zone"); - return; - } - - TestTable e = table; - - try { - Assertions.assertThat(this.query("SELECT i FROM " + table.getName() + " WHERE CAST(t AS date) = DATE '2005-09-10'")).hasCorrectResultsRegardlessOfPushdown(); - } - catch (Throwable var7) { - if (table != null) { - try { - e.close(); - } - catch (Throwable var5) { - var7.addSuppressed(var5); - } - } - - throw var7; - } - table.close(); - } - - @Override - @Test - public void testTimestampWithTimeZoneCastToTimestampPredicate() - { - skipTestUnless(this.hasBehavior(TestingConnectorBehavior.SUPPORTS_CREATE_TABLE_WITH_DATA)); - TestTable table; - try { - table = this.newTrinoTable( - "timestamptz_to_ts", - "(i varchar(20), t TIMESTAMP)", - List.of( - "'UTC', CAST(TIMESTAMP '2005-09-10 13:00:00.000+00:00' AT TIME ZONE INTERVAL '0:00' HOUR TO MINUTE AS TIMESTAMP)", - "'Warsaw', CAST(TIMESTAMP '2005-09-10 13:00:00.000+02:00' AT TIME ZONE INTERVAL '2:00' HOUR TO MINUTE AS TIMESTAMP)", - "'Los Angeles', CAST(TIMESTAMP '2005-09-10 13:00:00.000-07:00' AT TIME ZONE - INTERVAL '7:00' HOUR TO MINUTE AS TIMESTAMP)")); - } - catch (QueryFailedException e) { - this.verifyUnsupportedTypeException(e, "timestamp(3) with time zone"); - return; - } - - TestTable e = table; - - try { - Assertions.assertThat(this.query("SELECT i FROM " + table.getName() + " WHERE CAST(t AS timestamp(0)) = TIMESTAMP '2005-09-10 13:00:00'")).hasCorrectResultsRegardlessOfPushdown(); - } - catch (Throwable var7) { - if (table != null) { - try { - e.close(); - } - catch (Throwable var5) { - var7.addSuppressed(var5); - } - } - - throw var7; - } - table.close(); - } - - @Override - @Test - public void testJoinPushdownWithLongIdentifiers() - { - skipTestUnless(this.hasBehavior(TestingConnectorBehavior.SUPPORTS_CREATE_TABLE) && this.hasBehavior(TestingConnectorBehavior.SUPPORTS_JOIN_PUSHDOWN)); - int maxLength = this.maxColumnNameLength().orElse(65541); - String validColumnName = "z".repeat(maxLength - 5); - - try (TestTable left = this.newTrinoTable("test_long_id_l", String.format("(%s BIGINT)", validColumnName)); - TestTable right = this.newTrinoTable("test_long_id_r", String.format("(%s BIGINT)", validColumnName))) { - Assertions.assertThat(this.query(this.joinPushdownEnabled(this.getSession()), "SELECT l.%1$s, r.%1$s\nFROM %2$s l JOIN %3$s r ON l.%1$s = r.%1$s".formatted(validColumnName, left.getName(), right.getName()))).isFullyPushedDown(); - } - } - - @Override - protected Optional filterDataMappingSmokeTestData(DataMappingTestSetup dataMappingTestSetup) - { - String typeName = dataMappingTestSetup.getTrinoTypeName(); - return switch (typeName) { - // skipping date as during julian->gregorian date is handled differently in Teradata. tinyint, double and varchar with unbounded (need to handle special characters) is skipped and will handle it while improving - // write functionalities. - case "boolean", "tinyint", "date", "real", "double", "varchar", "timestamp(3) with time zone", "timestamp(6) with time zone", - "U&'a \\000a newline'" -> Optional.empty(); - default -> Optional.of(dataMappingTestSetup); - }; - } - - @Override - @Test - public void testRenameSchema() - { - Assumptions.abort("Skipping as connector does not support RENAME SCHEMA"); - } - - @Override - @Test - public void testColumnName() - { - Assumptions.abort("Skipping as connector does not support column level write operations"); - } - - @Override - @Test - public void testCreateTableAsSelectWithUnicode() - { - Assumptions.abort("Skipping as connector does not support creating table with UNICODE characters"); - } - - @Override - @Test - public void testUpdateNotNullColumn() - { - Assumptions.abort("Skipping as connector does not support insert operations"); - } - - @Override - @Test - public void testWriteBatchSizeSessionProperty() - { - Assumptions.abort("Skipping as connector does not support insert operations"); - } - - @Override - @Test - public void testInsertWithoutTemporaryTable() - { - Assumptions.abort("Skipping as connector does not support insert operations"); - } - - @Override - @Test - public void testWriteTaskParallelismSessionProperty() - { - Assumptions.abort("Skipping as connector does not support insert operations"); - } - - @Override - @Test - public void testInsertIntoNotNullColumn() - { - Assumptions.abort("Skipping as connector does not support insert operations"); - } - - @Override - @Test - public void testDropSchemaCascade() - { - Assumptions.abort("Skipping as connector does not support dropping schemas with CASCADE option"); - } - - @Override - @Test - public void testAddColumn() - { - Assumptions.abort("Skipping as connector does not support column level write operations"); - } - - @Override - @Test - public void testDropNonEmptySchemaWithTable() - { - Assumptions.abort("Skipping as connector does not support drop schemas"); - } - - @Override - @Test - public void verifySupportsUpdateDeclaration() - { - Assumptions.abort("Skipping as connector does not support update operations"); - } - - @Override - @Test - public void testDropNotNullConstraint() - { - Assumptions.abort("Skipping as connector does not support dropping a not null constraint"); - } - - @Override - @Test - public void testExecuteProcedureWithInvalidQuery() - { - Assumptions.abort("Skipping as connector does not support execute procedure"); - } - - @Override - @Test - public void testCreateTableAsSelectNegativeDate() - { - Assumptions.abort("Skipping as connector does not support creating table with negative date"); - } - - @Override - protected void assertCreateTableAsSelect(Session session, String query, String expectedQuery, String rowCountQuery) - { - String table = "test_ctas_" + TestingNames.randomNameSuffix(); - this.assertUpdate(session, "CREATE TABLE " + table + " AS ( " + query + ") WITH DATA", rowCountQuery); - this.assertQuery(session, "SELECT * FROM " + table, expectedQuery); - this.assertUpdate(session, "DROP TABLE " + table); - Assertions.assertThat(this.getQueryRunner().tableExists(session, table)).isFalse(); - } - - @Override - protected Session joinPushdownEnabled(Session session) - { - return Session.builder(super.joinPushdownEnabled(session)) - // strategy is AUTOMATIC by default and would not work for certain test cases (even if statistics are collected) - .setCatalogSessionProperty(session.getCatalog().orElseThrow(), "join_pushdown_strategy", "EAGER") - .build(); - } - - @Override - protected TestTable newTrinoTable(String namePrefix, @Language("SQL") String tableDefinition, List rowsToInsert) - { - String tableName = ""; - - // Check if namePrefix already contains schema (contains a dot) - if (namePrefix.contains(".")) { - // namePrefix already has schema.tablename format - tableName = namePrefix; - } - else { - // Append current schema to namePrefix - String schemaName = this.getSession().getSchema().orElseThrow(); - tableName = schemaName + "." + namePrefix; - } - return new TestTable(database, tableName, tableDefinition, rowsToInsert); - } - - private boolean expectVarcharJoinPushdown(String operator) - throws Throwable - { - if ("IS DISTINCT FROM".equals(operator)) { - return this.hasBehavior(TestingConnectorBehavior.SUPPORTS_JOIN_PUSHDOWN_WITH_DISTINCT_FROM) && this.hasBehavior(TestingConnectorBehavior.SUPPORTS_JOIN_PUSHDOWN_WITH_VARCHAR_EQUALITY); - } - else { - boolean var10000; - switch (this.toJoinConditionOperator(operator)) { - case EQUAL: - case NOT_EQUAL: - var10000 = this.hasBehavior(TestingConnectorBehavior.SUPPORTS_JOIN_PUSHDOWN_WITH_VARCHAR_EQUALITY); - break; - case LESS_THAN: - case LESS_THAN_OR_EQUAL: - case GREATER_THAN: - case GREATER_THAN_OR_EQUAL: - var10000 = this.hasBehavior(TestingConnectorBehavior.SUPPORTS_JOIN_PUSHDOWN_WITH_VARCHAR_INEQUALITY); - break; - case IDENTICAL: - var10000 = this.hasBehavior(TestingConnectorBehavior.SUPPORTS_JOIN_PUSHDOWN_WITH_DISTINCT_FROM) && this.hasBehavior(TestingConnectorBehavior.SUPPORTS_JOIN_PUSHDOWN_WITH_VARCHAR_EQUALITY); - break; - default: - throw new MatchException(null, null); - } - return var10000; - } - } - - private JoinCondition.Operator toJoinConditionOperator(String operator) - throws Throwable - { - return operator.equals("IS NOT DISTINCT FROM") ? JoinCondition.Operator.IDENTICAL : (JoinCondition.Operator) ((Optional) Stream.of(JoinCondition.Operator.values()).filter((joinOperator) -> joinOperator.getValue().equals(operator)).collect(MoreCollectors.toOptional())).orElseThrow(() -> new IllegalArgumentException("Not found: " + operator)); - } - - private void verifyUnsupportedTypeException(Throwable exception, String trinoTypeName) - { - String typeNameBase = trinoTypeName.replaceFirst("\\(.*", ""); - String expectedMessagePart = String.format("(%1$s.*not (yet )?supported)|((?i)unsupported.*%1$s)|((?i)not supported.*%1$s)", Pattern.quote(typeNameBase)); - Assertions.assertThat(exception).hasMessageFindingMatch(expectedMessagePart).satisfies((e) -> Assertions.assertThat(io.trino.testing.QueryAssertions.getTrinoExceptionCause(e)).hasMessageFindingMatch(expectedMessagePart)); - } - - @Test - public void testTeradataNumberDataType() - { - try (TestTable table = newTrinoTable("test_number", "(id INTEGER, " + "number_col NUMBER(10,2), " + "number_default NUMBER, " + "number_large NUMBER(38,10))", List.of("1, CAST(12345.67 AS NUMBER(10,2)), CAST(999999999999999 AS NUMBER), CAST(1234567890123456789012345678.1234567890 AS NUMBER(38,10))", "2, CAST(-99999.99 AS NUMBER(10,2)), CAST(-123456789012345 AS NUMBER), CAST(-9999999999999999999999999999.9999999999 AS NUMBER(38,10))", "3, CAST(0.00 AS NUMBER(10,2)), CAST(0 AS NUMBER), CAST(0.0000000000 AS NUMBER(38,10))"))) { - assertThat(query(format("SELECT number_col FROM %s WHERE id = 1", table.getName()))).matches("VALUES CAST(12345.67 AS DECIMAL(10,2))"); - assertThat(query(format("SELECT number_default FROM %s WHERE id = 1", table.getName()))).matches("VALUES CAST(999999999999999 AS DECIMAL(38,0))"); - assertThat(query(format("SELECT number_large FROM %s WHERE id = 1", table.getName()))).matches("VALUES CAST(1234567890123456789012345678.1234567890 AS DECIMAL(38,10))"); - assertThat(query(format("SELECT number_col FROM %s WHERE id = 2", table.getName()))).matches("VALUES CAST(-99999.99 AS DECIMAL(10,2))"); - assertThat(query(format("SELECT number_col FROM %s WHERE id = 3", table.getName()))).matches("VALUES CAST(0.00 AS DECIMAL(10,2))"); - } - } - - @Test - public void testTeradataCharacterDataType() - { - try (TestTable table = newTrinoTable("test_character", "(id INTEGER, " + "char_col CHARACTER(5), " + "char_default CHARACTER, " + "char_large CHARACTER(100))", List.of("1, CAST('HELLO' AS CHARACTER(5)), CAST('A' AS CHARACTER), CAST('TERADATA' AS CHARACTER(100))", "2, CAST('WORLD' AS CHARACTER(5)), CAST('B' AS CHARACTER), CAST('CHARACTER' AS CHARACTER(100))", "3, CAST('' AS CHARACTER(5)), CAST('C' AS CHARACTER), CAST('' AS CHARACTER(100))"))) { - assertThat(query(format("SELECT char_col FROM %s WHERE id = 1", table.getName()))).matches("VALUES CAST('HELLO' AS CHAR(5))"); - assertThat(query(format("SELECT char_default FROM %s WHERE id = 1", table.getName()))).matches("VALUES CAST('A' AS CHAR(1))"); - assertThat(query(format("SELECT char_large FROM %s WHERE id = 1", table.getName()))).matches("VALUES CAST('TERADATA' AS CHAR(100))"); - assertThat(query(format("SELECT char_col FROM %s WHERE id = 3", table.getName()))).matches("VALUES CAST('' AS CHAR(5))"); - } - } - - @Test - public void testTeradataTimeWithTimeZoneDataType() - { - try (TestTable table = newTrinoTable("test_time_with_timezone", "(id INTEGER, " + "time_tz_default TIME WITH TIME ZONE, " + "time_tz_precision TIME(3) WITH TIME ZONE, " + "time_tz_max TIME(6) WITH TIME ZONE)", List.of("1, CAST('10:30:45.000000+05:30' AS TIME WITH TIME ZONE), CAST('14:25:30.123+00:00' AS TIME(3) WITH TIME ZONE), CAST('09:15:20.123456-08:00' AS TIME(6) WITH TIME ZONE)", "2, CAST('23:59:59.000000-07:00' AS TIME WITH TIME ZONE), CAST('00:00:00.000+01:00' AS TIME(3) WITH TIME ZONE), CAST('12:30:45.999999+09:00' AS TIME(6) WITH TIME ZONE)", "3, CAST('06:45:30.000000+00:00' AS TIME WITH TIME ZONE), CAST('18:20:15.567+03:00' AS TIME(3) WITH TIME ZONE), CAST('21:10:05.000001-05:00' AS TIME(6) WITH TIME ZONE)"))) { - assertThat(query(format("SELECT time_tz_default FROM %s WHERE id = 1", table.getName()))).matches("VALUES CAST('10:30:45.000000+05:30' AS TIME(6) WITH TIME ZONE)"); - } - } - - @Test - public void testTeradataTimestampWithTimeZoneDataType() - { - try (TestTable table = newTrinoTable("test_timestamp_with_timezone", "(id INTEGER, " + "ts_tz_default TIMESTAMP WITH TIME ZONE, " + "ts_tz_precision TIMESTAMP(3) WITH TIME ZONE, " + "ts_tz_max TIMESTAMP(6) WITH TIME ZONE)", List.of("1, CAST('2023-05-15 10:30:45.000000+05:30' AS TIMESTAMP WITH TIME ZONE), CAST('2023-05-15 14:25:30.123+00:00' AS TIMESTAMP(3) WITH TIME ZONE), CAST('2023-05-15 09:15:20.123456-08:00' AS TIMESTAMP(6) WITH TIME ZONE)", "2, CAST('2023-12-31 23:59:59.000000-07:00' AS TIMESTAMP WITH TIME ZONE), CAST('2023-01-01 00:00:00.000+01:00' AS TIMESTAMP(3) WITH TIME ZONE), CAST('2023-06-15 12:30:45.999999+09:00' AS TIMESTAMP(6) WITH TIME ZONE)", "3, CAST('2023-07-04 06:45:30.000000+00:00' AS TIMESTAMP WITH TIME ZONE), CAST('2023-11-25 18:20:15.567+03:00' AS TIMESTAMP(3) WITH TIME ZONE), CAST('2023-03-10 21:10:05.000001-05:00' AS TIMESTAMP(6) WITH TIME ZONE)"))) { - assertThat(query(format("SELECT ts_tz_default FROM %s WHERE id = 1", table.getName()))).matches("VALUES CAST('2023-05-15 10:30:45.000000+05:30' AS TIMESTAMP(6) WITH TIME ZONE)"); - assertThat(query(format("SELECT ts_tz_precision FROM %s WHERE id = 1", table.getName()))).matches("VALUES CAST('2023-05-15 14:25:30.123+00:00' AS TIMESTAMP(3) WITH TIME ZONE)"); - assertThat(query(format("SELECT ts_tz_max FROM %s WHERE id = 1", table.getName()))).matches("VALUES CAST('2023-05-15 09:15:20.123456-08:00' AS TIMESTAMP(6) WITH TIME ZONE)"); - assertThat(query(format("SELECT ts_tz_default FROM %s WHERE id = 2", table.getName()))).matches("VALUES CAST('2023-12-31 23:59:59.000000-07:00' AS TIMESTAMP(6) WITH TIME ZONE)"); - assertThat(query(format("SELECT ts_tz_precision FROM %s WHERE id = 2", table.getName()))).matches("VALUES CAST('2023-01-01 00:00:00.000+01:00' AS TIMESTAMP(3) WITH TIME ZONE)"); - assertThat(query(format("SELECT ts_tz_max FROM %s WHERE id = 3", table.getName()))).matches("VALUES CAST('2023-03-10 21:10:05.000001-05:00' AS TIMESTAMP(6) WITH TIME ZONE)"); - assertThat(query(format("SELECT ts_tz_default FROM %s WHERE id = 3", table.getName()))).matches("VALUES CAST('2023-07-04 06:45:30.000000+00:00' AS TIMESTAMP(6) WITH TIME ZONE)"); - } - } - - @Test - public void testArrayAsVarcharColumnMapping() - { - String testTableName = "test_array_table"; - - try (TestTable table = newTrinoTable(testTableName, "(id INTEGER, array_data VARCHAR(1000))", List.of("1, 'ARRAY[\"Alice\", \"Bob\", \"Charlie\"]'", "2, 'ARRAY[\"John\", \"Jane\"]'", "3, NULL", "4, 'ARRAY[]'"))) { - assertQuery(format("SELECT id, array_data FROM %s ORDER BY id", table.getName()), "VALUES " + "(1, 'ARRAY[\"Alice\", \"Bob\", \"Charlie\"]'), " + "(2, 'ARRAY[\"John\", \"Jane\"]'), " + "(3, CAST(NULL AS VARCHAR)), " + "(4, 'ARRAY[]')"); - assertQuery(format("SELECT array_data FROM %s WHERE id = 1", table.getName()), "VALUES 'ARRAY[\"Alice\", \"Bob\", \"Charlie\"]'"); - assertQuery(format("SELECT array_data FROM %s WHERE id = 2", table.getName()), "VALUES 'ARRAY[\"John\", \"Jane\"]'"); - } - } - - @Test - public void testArrayAsVarcharColumnMappingWithNullElements() - { - String testTableName = "test_array_nulls"; - - try (TestTable table = newTrinoTable(testTableName, "(id INTEGER, array_data VARCHAR(1000))", List.of("1, 'ARRAY[\"first\", null, \"third\", null]'", "2, 'ARRAY[null, \"second\"]'", "3, 'ARRAY[null, null, null]'"))) { - assertQuery(format("SELECT id, array_data FROM %s ORDER BY id", table.getName()), "VALUES " + "(1, 'ARRAY[\"first\", null, \"third\", null]'), " + "(2, 'ARRAY[null, \"second\"]'), " + "(3, 'ARRAY[null, null, null]')"); - assertQuery(format("SELECT array_data FROM %s WHERE id = 1", table.getName()), "VALUES 'ARRAY[\"first\", null, \"third\", null]'"); - assertQuery(format("SELECT array_data FROM %s WHERE id = 2", table.getName()), "VALUES 'ARRAY[null, \"second\"]'"); - assertQuery(format("SELECT array_data FROM %s WHERE id = 3", table.getName()), "VALUES 'ARRAY[null, null, null]'"); - } - } -} diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/TeradataQueryRunner.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/TeradataQueryRunner.java index b4707c6ebda6..91fb609bb657 100644 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/TeradataQueryRunner.java +++ b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/TeradataQueryRunner.java @@ -11,7 +11,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.integration; import com.google.common.collect.ImmutableList; @@ -38,44 +37,15 @@ import static io.trino.testing.TestingSession.testSessionBuilder; import static java.util.Objects.requireNonNull; -/** - * Sets up a QueryRunner for Teradata connector integration testing. - */ public final class TeradataQueryRunner { - private TeradataQueryRunner() - { - // private constructor to prevent instantiation - } + private TeradataQueryRunner() {} public static Builder builder(TestingTeradataServer server) { return new Builder(server); } - /** - * Starts a QueryRunner server for Teradata connector on port 8080. - * - * @param args unused - * @throws Exception on error - */ - public static void main(String[] args) - throws Exception - { - Logging logger = Logging.initialize(); - logger.setLevel("io.trino.plugin.teradata", Level.DEBUG); - logger.setLevel("io.trino", Level.INFO); - TestingTeradataServer server = new TestingTeradataServer(ClearScapeEnvironmentUtils.generateUniqueEnvName(TeradataQueryRunner.class)); - QueryRunner queryRunner = builder(server).addCoordinatorProperty("http-server.http.port", "8080").setInitialTables(TpchTable.getTables()).build(); - - Logger log = Logger.get(TeradataQueryRunner.class); - log.info("======== SERVER STARTED ========"); - log.info("\n====\n%s\n====", queryRunner.getCoordinator().getBaseUrl()); - } - - /** - * Builder class for constructing DistributedQueryRunner with Teradata connector. - */ public static class Builder extends DistributedQueryRunner.Builder { @@ -139,4 +109,18 @@ public DistributedQueryRunner build() return super.build(); } } + + public static void main(String[] args) + throws Exception + { + Logging logger = Logging.initialize(); + logger.setLevel("io.trino.plugin.teradata", Level.DEBUG); + logger.setLevel("io.trino", Level.INFO); + TestingTeradataServer server = new TestingTeradataServer(ClearScapeEnvironmentUtils.generateUniqueEnvName(TeradataQueryRunner.class)); + QueryRunner queryRunner = builder(server).addCoordinatorProperty("http-server.http.port", "8080").setInitialTables(TpchTable.getTables()).build(); + + Logger log = Logger.get(TeradataQueryRunner.class); + log.info("======== SERVER STARTED ========"); + log.info("\n====\n%s\n====", queryRunner.getCoordinator().getBaseUrl()); + } } diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/TestTeradataJDBCTypeMapping.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/TestTeradataJDBCTypeMapping.java deleted file mode 100644 index 2233764d0da7..000000000000 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/TestTeradataJDBCTypeMapping.java +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.trino.plugin.integration; - -import io.trino.plugin.integration.clearscape.ClearScapeEnvironmentUtils; -import io.trino.testing.datatype.SqlDataTypeTest; -import org.junit.jupiter.api.Test; - -import java.sql.SQLException; - -import static io.trino.spi.type.BigintType.BIGINT; -import static io.trino.spi.type.CharType.createCharType; -import static io.trino.spi.type.DateType.DATE; -import static io.trino.spi.type.DecimalType.createDecimalType; -import static io.trino.spi.type.DoubleType.DOUBLE; -import static io.trino.spi.type.IntegerType.INTEGER; -import static io.trino.spi.type.SmallintType.SMALLINT; -import static io.trino.spi.type.TimeType.createTimeType; -import static io.trino.spi.type.TimeWithTimeZoneType.createTimeWithTimeZoneType; -import static io.trino.spi.type.TimestampType.createTimestampType; -import static io.trino.spi.type.TinyintType.TINYINT; -import static io.trino.spi.type.VarbinaryType.VARBINARY; -import static io.trino.spi.type.VarcharType.createVarcharType; -import static io.trino.type.JsonType.JSON; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; - -public class TestTeradataJDBCTypeMapping - extends AbstractTeradataJDBCTest -{ - public TestTeradataJDBCTypeMapping() - { - super(ClearScapeEnvironmentUtils.generateUniqueEnvName(TestTeradataJDBCTypeMapping.class)); - } - - static String padBinaryString(String prefix, int length) - { - // simple function to help with byte test cases - StringBuilder result = new StringBuilder(prefix); - while (result.length() < length * 2) { - result.append("0"); - } - return "X'" + result + "'"; - } - - @Override - protected void initTables() - { - } - - @Test - public void testByteint() - { - SqlDataTypeTest.create().addRoundTrip("byteint", "0", TINYINT, "CAST(0 AS TINYINT)").addRoundTrip("byteint", "127", TINYINT, "CAST(127 AS TINYINT)").addRoundTrip("byteint", "-128", TINYINT, "CAST(-128 AS TINYINT)").addRoundTrip("byteint", "null", TINYINT, "CAST(null AS TINYINT)").execute(getQueryRunner(), teradataJDBCCreateAndInsert("byteint")); - } - - @Test - public void testSmallint() - { - SqlDataTypeTest.create().addRoundTrip("smallint", "0", SMALLINT, "CAST(0 AS SMALLINT)").addRoundTrip("smallint", "32767", SMALLINT, "CAST(32767 AS SMALLINT)").addRoundTrip("smallint", "-32768", SMALLINT, "CAST(-32768 AS SMALLINT)").addRoundTrip("smallint", "null", SMALLINT, "CAST(null AS SMALLINT)").execute(getQueryRunner(), teradataJDBCCreateAndInsert("smallint")); - } - - @Test - public void testInteger() - { - SqlDataTypeTest.create().addRoundTrip("integer", "0", INTEGER, "0").addRoundTrip("integer", "2147483647", INTEGER, "2147483647").addRoundTrip("integer", "-2147483648", INTEGER, "-2147483648").addRoundTrip("integer", "NULL", INTEGER, "CAST(NULL AS INTEGER)").execute(getQueryRunner(), teradataJDBCCreateAndInsert("integer")); - } - - @Test - public void testBigint() - { - SqlDataTypeTest.create().addRoundTrip("bigint", "0", BIGINT, "CAST(0 AS BIGINT)").addRoundTrip("bigint", "9223372036854775807", BIGINT, "9223372036854775807").addRoundTrip("bigint", "-9223372036854775808", BIGINT, "-9223372036854775808").addRoundTrip("bigint", "NULL", BIGINT, "CAST(NULL AS BIGINT)").execute(getQueryRunner(), teradataJDBCCreateAndInsert("bigint")); - } - - @Test - public void testFloat() - { - SqlDataTypeTest.create().addRoundTrip("float", "0", DOUBLE, "CAST(0 AS DOUBLE)").addRoundTrip("real", "0", DOUBLE, "CAST(0 AS DOUBLE)").addRoundTrip("double precision", "0", DOUBLE, "CAST(0 AS DOUBLE)").addRoundTrip("float", "1.797e308", DOUBLE, "1.797e308").addRoundTrip("real", "1.797e308", DOUBLE, "1.797e308").addRoundTrip("double precision", "1.797e308", DOUBLE, "1.797e308").addRoundTrip("float", "2.226e-308", DOUBLE, "2.226e-308").addRoundTrip("real", "2.226e-308", DOUBLE, "2.226e-308").addRoundTrip("double precision", "2.226e-308", DOUBLE, "2.226e-308").addRoundTrip("float", "NULL", DOUBLE, "CAST(NULL AS DOUBLE)").addRoundTrip("real", "NULL", DOUBLE, "CAST(NULL AS DOUBLE)").addRoundTrip("double precision", "NULL", DOUBLE, "CAST(NULL AS DOUBLE)").execute(getQueryRunner(), teradataJDBCCreateAndInsert("float")); - } - - @Test - public void testDecimal() - { - SqlDataTypeTest.create().addRoundTrip("decimal(3, 0)", "0", createDecimalType(3, 0), "CAST('0' AS decimal(3, 0))").addRoundTrip("numeric(3, 0)", "0", createDecimalType(3, 0), "CAST('0' AS decimal(3, 0))").addRoundTrip("decimal(3, 1)", "0.0", createDecimalType(3, 1), "CAST('0.0' AS decimal(3, 1))").addRoundTrip("numeric(3, 1)", "0.0", createDecimalType(3, 1), "CAST('0.0' AS decimal(3, 1))").addRoundTrip("decimal(1, 0)", "1", createDecimalType(1, 0), "CAST('1' AS decimal(1, 0))").addRoundTrip("numeric(1, 0)", "1", createDecimalType(1, 0), "CAST('1' AS decimal(1, 0))").addRoundTrip("decimal(1, 0)", "-1", createDecimalType(1, 0), "CAST('-1' AS decimal(1, 0))").addRoundTrip("numeric(1, 0)", "-1", createDecimalType(1, 0), "CAST('-1' AS decimal(1, 0))").addRoundTrip("decimal(3, 0)", "1", createDecimalType(3, 0), "CAST('1' AS decimal(3, 0))").addRoundTrip("numeric(3, 0)", "1", createDecimalType(3, 0), "CAST('1' AS decimal(3, 0))").addRoundTrip("decimal(3, 0)", "-1", createDecimalType(3, 0), "CAST('-1' AS decimal(3, 0))").addRoundTrip("numeric(3, 0)", "-1", createDecimalType(3, 0), "CAST('-1' AS decimal(3, 0))").addRoundTrip("decimal(3, 0)", "123", createDecimalType(3, 0), "CAST('123' AS decimal(3, 0))").addRoundTrip("numeric(3, 0)", "123", createDecimalType(3, 0), "CAST('123' AS decimal(3, 0))").addRoundTrip("decimal(3, 0)", "-123", createDecimalType(3, 0), "CAST('-123' AS decimal(3, 0))").addRoundTrip("numeric(3, 0)", "-123", createDecimalType(3, 0), "CAST('-123' AS decimal(3, 0))").addRoundTrip("decimal(3, 1)", "10.0", createDecimalType(3, 1), "CAST('10.0' AS decimal(3, 1))").addRoundTrip("numeric(3, 1)", "10.0", createDecimalType(3, 1), "CAST('10.0' AS decimal(3, 1))").addRoundTrip("decimal(3, 1)", "12.3", createDecimalType(3, 1), "CAST('12.3' AS decimal(3, 1))").addRoundTrip("numeric(3, 1)", "12.3", createDecimalType(3, 1), "CAST('12.3' AS decimal(3, 1))").addRoundTrip("decimal(3, 1)", "-12.3", createDecimalType(3, 1), "CAST('-12.3' AS decimal(3, 1))").addRoundTrip("numeric(3, 1)", "-12.3", createDecimalType(3, 1), "CAST('-12.3' AS decimal(3, 1))").addRoundTrip("decimal(38, 0)", "12345678901234567890123456789012345678", createDecimalType(38, 0), "CAST('12345678901234567890123456789012345678' AS decimal(38, 0))").addRoundTrip("numeric(38, 0)", "12345678901234567890123456789012345678", createDecimalType(38, 0), "CAST('12345678901234567890123456789012345678' AS decimal(38, 0))").addRoundTrip("decimal(38, 0)", "-12345678901234567890123456789012345678", createDecimalType(38, 0), "CAST('-12345678901234567890123456789012345678' AS decimal(38, 0))").addRoundTrip("numeric(38, 0)", "-12345678901234567890123456789012345678", createDecimalType(38, 0), "CAST('-12345678901234567890123456789012345678' AS decimal(38, 0))").addRoundTrip("decimal(1, 0)", "null", createDecimalType(1, 0), "CAST(null AS decimal(1, 0))").execute(getQueryRunner(), teradataJDBCCreateAndInsert("decimal")); - } - - @Test - public void testNumber() - { - SqlDataTypeTest.create().addRoundTrip("numeric(3)", "0", createDecimalType(3, 0), "CAST('0' AS decimal(3, 0))").addRoundTrip("number(5,2)", "0", createDecimalType(5, 2), "CAST('0' AS decimal(5, 2))").addRoundTrip("number(38)", "0", createDecimalType(38, 0), "CAST('0' AS decimal(38, 0))").addRoundTrip("number(38,2)", "123456789012345678901234567890123456.78", createDecimalType(38, 2), "CAST('123456789012345678901234567890123456.78' AS decimal(38, 2))").addRoundTrip("numeric(38)", "12345678901234567890123456789012345678", createDecimalType(38, 0), "CAST('12345678901234567890123456789012345678' AS decimal(38, 0))").addRoundTrip("numeric(3)", "null", createDecimalType(3, 0), "CAST(null AS decimal(3, 0))").execute(getQueryRunner(), teradataJDBCCreateAndInsert("number")); - } - - @Test - public void testChar() - { - SqlDataTypeTest.create().addRoundTrip("char(3)", "''", createCharType(3), "CAST('' AS char(3))").addRoundTrip("char(3)", "' '", createCharType(3), "CAST(' ' AS char(3))").addRoundTrip("char(3)", "' '", createCharType(3), "CAST(' ' AS char(3))").addRoundTrip("char(3)", "' '", createCharType(3), "CAST(' ' AS char(3))").addRoundTrip("char(3)", "'A'", createCharType(3), "CAST('A' AS char(3))").addRoundTrip("char(3)", "'A '", createCharType(3), "CAST('A ' AS char(3))").addRoundTrip("char(3)", "' B '", createCharType(3), "CAST(' B ' AS char(3))").addRoundTrip("char(3)", "' C'", createCharType(3), "CAST(' C' AS char(3))").addRoundTrip("char(3)", "'AB'", createCharType(3), "CAST('AB' AS char(3))").addRoundTrip("char(3)", "'ABC'", createCharType(3), "CAST('ABC' AS char(3))").addRoundTrip("char(3)", "'A C'", createCharType(3), "CAST('A C' AS char(3))").addRoundTrip("char(3)", "' BC'", createCharType(3), "CAST(' BC' AS char(3))").addRoundTrip("char(3)", "null", createCharType(3), "CAST(null AS char(3))").execute(getQueryRunner(), teradataJDBCCreateAndInsert("char")); - String tmode = database.getTMode(); - if (tmode.equals("TERA")) { - // truncation - SqlDataTypeTest.create().addRoundTrip("char(3)", "'ABCD'", createCharType(3), "CAST('ABCD' AS char(3))").execute(getQueryRunner(), teradataJDBCCreateAndInsert("chart")); - } - else { - // Error on truncation - assertThatThrownBy(() -> - SqlDataTypeTest.create().addRoundTrip("char(3)", "'ABCD'", createCharType(3), "CAST('ABCD' AS char(3))").execute(getQueryRunner(), teradataJDBCCreateAndInsert("chart"))) - .isInstanceOf(RuntimeException.class) - .hasCauseInstanceOf(SQLException.class) - .cause() - .hasMessageContaining("Right truncation of string data"); - } - // max-size - SqlDataTypeTest.create().addRoundTrip("char(64000)", "'max'", createCharType(64000), "CAST('max' AS char(64000))").execute(getQueryRunner(), teradataJDBCCreateAndInsert("charl")); - } - - @Test - public void testVarchar() - { - SqlDataTypeTest.create().addRoundTrip("varchar(32)", "''", createVarcharType(32), "CAST('' AS varchar(32))").addRoundTrip("varchar(32)", "' '", createVarcharType(32), "CAST(' ' AS varchar(32))").addRoundTrip("varchar(32)", "' '", createVarcharType(32), "CAST(' ' AS varchar(32))").addRoundTrip("varchar(32)", "' '", createVarcharType(32), "CAST(' ' AS varchar(32))").addRoundTrip("varchar(32)", "' '", createVarcharType(32), "CAST(' ' AS varchar(32))").addRoundTrip("varchar(32)", "'A'", createVarcharType(32), "CAST('A' AS varchar(32))").addRoundTrip("varchar(32)", "'A '", createVarcharType(32), "CAST('A ' AS varchar(32))").addRoundTrip("varchar(32)", "' B '", createVarcharType(32), "CAST(' B ' AS varchar(32))").addRoundTrip("varchar(32)", "' C'", createVarcharType(32), "CAST(' C' AS varchar(32))").addRoundTrip("varchar(32)", "'AB'", createVarcharType(32), "CAST('AB' AS varchar(32))").addRoundTrip("varchar(32)", "'ABC'", createVarcharType(32), "CAST('ABC' AS varchar(32))").addRoundTrip("varchar(32)", "'A C'", createVarcharType(32), "CAST('A C' AS varchar(32))").addRoundTrip("varchar(32)", "' BC'", createVarcharType(32), "CAST(' BC' AS varchar(32))").addRoundTrip("varchar(32)", "null", createVarcharType(32), "CAST(null AS varchar(32))").execute(getQueryRunner(), teradataJDBCCreateAndInsert("varchar")); - String tmode = database.getTMode(); - if (tmode.equals("TERA")) { - // truncation - SqlDataTypeTest.create().addRoundTrip("varchar(3)", "'ABCD'", createVarcharType(3), "CAST('ABCD' AS varchar(3))").execute(getQueryRunner(), teradataJDBCCreateAndInsert("varchart")); - } - else { - // Error on truncation - assertThatThrownBy(() -> - SqlDataTypeTest.create().addRoundTrip("varchar(3)", "'ABCD'", createVarcharType(3), "CAST('ABCD' AS varchar(3))").execute(getQueryRunner(), teradataJDBCCreateAndInsert("varchart"))) - .isInstanceOf(RuntimeException.class) - .hasCauseInstanceOf(SQLException.class) - .cause() - .hasMessageContaining("Right truncation of string data"); - } - // max-size - SqlDataTypeTest.create().addRoundTrip("long varchar", "'max'", createVarcharType(64000), "CAST('max' AS varchar(64000))").execute(getQueryRunner(), teradataJDBCCreateAndInsert("varcharl")); - } - - @Test - public void testByte() - { - SqlDataTypeTest.create().addRoundTrip("byte(3)", "'000000'XB", VARBINARY, "X'000000'").addRoundTrip("byte(3)", "'012345'XB", VARBINARY, "X'012345'").addRoundTrip("byte(3)", "'FEDCBA'XB", VARBINARY, "X'FEDCBA'").addRoundTrip("byte(3)", "'AA'XB", VARBINARY, padBinaryString("AA", 3)).addRoundTrip("byte(3)", "'00AA'XB", VARBINARY, padBinaryString("00AA", 3)).addRoundTrip("byte(3)", "'AA00'XB", VARBINARY, padBinaryString("AA00", 3)).addRoundTrip("byte(3)", "null", VARBINARY, "CAST(null AS varbinary)").execute(getQueryRunner(), teradataJDBCCreateAndInsert("byte")); - String tmode = database.getTMode(); - if (tmode.equals("TERA")) { - // truncation - SqlDataTypeTest.create().addRoundTrip("byte(3)", "'01234567'XB", VARBINARY, "X'012345'").execute(getQueryRunner(), teradataJDBCCreateAndInsert("bytet")); - } - else { - // Error on truncation - assertThatThrownBy(() -> - SqlDataTypeTest.create().addRoundTrip("byte(3)", "'01234567'XB", VARBINARY, "X'012345'").execute(getQueryRunner(), teradataJDBCCreateAndInsert("bytet"))) - .isInstanceOf(RuntimeException.class) - .hasCauseInstanceOf(SQLException.class) - .cause() - .hasMessageContaining("Right truncation of string data"); - } - // max-size - SqlDataTypeTest.create().addRoundTrip("byte(64000)", "'FF'XB", VARBINARY, padBinaryString("FF", 64000)).execute(getQueryRunner(), teradataJDBCCreateAndInsert("bytel")); - } - - @Test - public void testVarbyte() - { - SqlDataTypeTest.create().addRoundTrip("varbyte(32)", "'000000'XB", VARBINARY, "X'000000'").addRoundTrip("varbyte(32)", "'012345'XB", VARBINARY, "X'012345'").addRoundTrip("varbyte(32)", "'FEDCBA'XB", VARBINARY, "X'FEDCBA'").addRoundTrip("varbyte(32)", "'AA'XB", VARBINARY, "X'AA'").addRoundTrip("varbyte(32)", "'00AA'XB", VARBINARY, "X'00AA'").addRoundTrip("varbyte(32)", "'AA00'XB", VARBINARY, "X'AA00'").addRoundTrip("varbyte(32)", "null", VARBINARY, "CAST(null AS varbinary)").execute(getQueryRunner(), teradataJDBCCreateAndInsert("varbyte")); - String tmode = database.getTMode(); - if (tmode.equals("TERA")) { - // truncation - SqlDataTypeTest.create().addRoundTrip("varbyte(3)", "'01234567'XB", VARBINARY, "X'012345'").execute(getQueryRunner(), teradataJDBCCreateAndInsert("varbytet")); - } - else { - // Error on truncation - assertThatThrownBy(() -> - SqlDataTypeTest.create().addRoundTrip("varbyte(3)", "'01234567'XB", VARBINARY, "X'012345'").execute(getQueryRunner(), teradataJDBCCreateAndInsert("varbytet"))) - .isInstanceOf(RuntimeException.class) - .hasCauseInstanceOf(SQLException.class) - .cause() - .hasMessageContaining("Right truncation of string data"); - } - // max-size - SqlDataTypeTest.create().addRoundTrip("varbyte(64000)", "'FF'XB", VARBINARY, "X'FF'").execute(getQueryRunner(), teradataJDBCCreateAndInsert("varbytel")); - } - - @Test - public void testDate() - { - SqlDataTypeTest.create().addRoundTrip("date", "DATE '0001-01-01'", DATE, "DATE '0001-01-01'").addRoundTrip("date", "DATE '0012-12-12'", DATE, "DATE '0012-12-12'").addRoundTrip("date", "DATE '1500-01-01'", DATE, "DATE '1500-01-01'").addRoundTrip("date", "DATE '1582-10-04'", DATE, "DATE '1582-10-04'").addRoundTrip("date", "DATE '1582-10-15'", DATE, "DATE '1582-10-15'").addRoundTrip("date", "DATE '1952-04-03'", DATE, "DATE '1952-04-03'").addRoundTrip("date", "DATE '1970-01-01'", DATE, "DATE '1970-01-01'").addRoundTrip("date", "DATE '1970-02-03'", DATE, "DATE '1970-02-03'").addRoundTrip("date", "DATE '1970-01-01'", DATE, "DATE '1970-01-01'").addRoundTrip("date", "DATE '1983-04-01'", DATE, "DATE '1983-04-01'").addRoundTrip("date", "DATE '1983-10-01'", DATE, "DATE '1983-10-01'").addRoundTrip("date", "DATE '2017-07-01'", DATE, "DATE '2017-07-01'").addRoundTrip("date", "DATE '2017-01-01'", DATE, "DATE '2017-01-01'").addRoundTrip("date", "DATE '2024-02-29'", DATE, "DATE '2024-02-29'").addRoundTrip("date", "DATE '9999-12-30'", DATE, "DATE '9999-12-30'").addRoundTrip("date", "NULL", DATE, "CAST(NULL AS DATE)").execute(getQueryRunner(), teradataJDBCCreateAndInsert("date")); - } - - @Test - public void testTime() - { - SqlDataTypeTest.create().addRoundTrip("time", "time '00:00:00'", createTimeType(6), "CAST('00:00:00' AS TIME(6))").addRoundTrip("time(0)", "time '00:00:00'", createTimeType(0), "CAST('00:00:00' AS TIME(0))").addRoundTrip("time(2)", "time '00:00:00.00'", createTimeType(2), "CAST('00:00:00.00' AS TIME(2))").addRoundTrip("time(3)", "time '00:00:00.000'", createTimeType(3), "TIME '00:00:00.000'").addRoundTrip("time(6)", "time '00:00:00.000000'", createTimeType(6), "CAST('00:00:00.000000' AS TIME(6))").addRoundTrip("time", "time '23:59:59'", createTimeType(6), "CAST('23:59:59' AS TIME(6))").addRoundTrip("time(0)", "time '23:59:59'", createTimeType(0), "CAST('23:59:59' AS TIME(0))").addRoundTrip("time(2)", "time '23:59:59.99'", createTimeType(2), "CAST('23:59:59.99' AS TIME(2))").addRoundTrip("time(3)", "time '23:59:59.999'", createTimeType(3), "TIME '23:59:59.999'").addRoundTrip("time(6)", "time '23:59:59.999999'", createTimeType(6), "CAST('23:59:59.999999' AS TIME(6))").execute(getQueryRunner(), teradataJDBCCreateAndInsert("time")); - } - - @Test - public void testTimeWithTimeZone() - { - SqlDataTypeTest.create().addRoundTrip("time(0) with time zone", "time '00:00:00-00:00'", createTimeWithTimeZoneType(0), "CAST('00:00:00-00:00' AS TIME(0) WITH TIME ZONE)").addRoundTrip("time(0) with time zone", "time '23:59:59-00:00'", createTimeWithTimeZoneType(0), "CAST('23:59:59-00:00' AS TIME(0) WITH TIME ZONE)").addRoundTrip("time(3) with time zone", "time '01:23:45.678-08:00'", createTimeWithTimeZoneType(3), "CAST('01:23:45.678-08:00' AS TIME WITH TIME ZONE)").addRoundTrip("time(6) with time zone", "time '00:00:00.000000-00:00'", createTimeWithTimeZoneType(6), "CAST('00:00:00.000000-00:00' AS TIME(6) WITH TIME ZONE)").addRoundTrip("time(6) with time zone", "time '23:59:59.999999-00:00'", createTimeWithTimeZoneType(6), "CAST('23:59:59.999999-00:00' AS TIME(6) WITH TIME ZONE)").execute(getQueryRunner(), teradataJDBCCreateAndInsert("time_tz1")); - } - - @Test - public void testTimestamp() - { - SqlDataTypeTest.create().addRoundTrip("timestamp", "timestamp '0001-01-01 00:00:00'", createTimestampType(6), "CAST('0001-01-01 00:00:00' AS TIMESTAMP(6))").addRoundTrip("timestamp", "timestamp '0001-01-01 23:59:59.999999'", createTimestampType(6), "CAST('0001-01-01 23:59:59.999999' AS TIMESTAMP(6))").addRoundTrip("timestamp(2)", "timestamp '0001-01-01 23:59:59.99'", createTimestampType(2), "CAST('0001-01-01 23:59:59.99' AS TIMESTAMP(2))").addRoundTrip("timestamp(3)", "timestamp '0001-01-01 23:59:59.999'", createTimestampType(3), "TIMESTAMP '0001-01-01 23:59:59.999'").addRoundTrip("timestamp", "timestamp '9999-12-30 23:59:59'", createTimestampType(6), "CAST('9999-12-30 23:59:59' AS TIMESTAMP(6))").addRoundTrip("timestamp", "timestamp '9999-12-30 23:59:59.999999'", createTimestampType(6), "CAST('9999-12-30 23:59:59.999999' AS TIMESTAMP(6))").addRoundTrip("timestamp(2)", "timestamp '9999-12-30 23:59:59.99'", createTimestampType(2), "CAST('9999-12-30 23:59:59.99' AS TIMESTAMP(2))").addRoundTrip("timestamp(3)", "timestamp '9999-12-30 23:59:59.999'", createTimestampType(3), "TIMESTAMP '9999-12-30 23:59:59.999'").execute(getQueryRunner(), teradataJDBCCreateAndInsert("timestamp")); - } - - @Test - public void testJson() - { - SqlDataTypeTest.create() - .addRoundTrip("byteint", "0", TINYINT, "CAST(0 AS TINYINT)") - .addRoundTrip("JSON", "'{\"name\": \"Alice\", \"age\": 30}'", JSON, "JSON '{\"name\": \"Alice\", \"age\": 30}'") - .addRoundTrip("JSON", "NULL", JSON, "CAST(NULL AS JSON)") - .execute(getQueryRunner(), teradataJDBCCreateAndInsert("json")); - } -} diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/TestTeradataTypeMapping.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/TestTeradataTypeMapping.java new file mode 100644 index 000000000000..6f4ce543da1e --- /dev/null +++ b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/TestTeradataTypeMapping.java @@ -0,0 +1,289 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.plugin.integration; + +import io.trino.plugin.integration.clearscape.ClearScapeEnvironmentUtils; +import io.trino.testing.AbstractTestQueryFramework; +import io.trino.testing.QueryRunner; +import io.trino.testing.datatype.CreateAndInsertDataSetup; +import io.trino.testing.datatype.DataSetup; +import io.trino.testing.datatype.SqlDataTypeTest; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import java.sql.SQLException; + +import static io.trino.spi.type.BigintType.BIGINT; +import static io.trino.spi.type.CharType.createCharType; +import static io.trino.spi.type.DateType.DATE; +import static io.trino.spi.type.DecimalType.createDecimalType; +import static io.trino.spi.type.DoubleType.DOUBLE; +import static io.trino.spi.type.IntegerType.INTEGER; +import static io.trino.spi.type.SmallintType.SMALLINT; +import static io.trino.spi.type.TinyintType.TINYINT; +import static io.trino.spi.type.VarcharType.createVarcharType; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +final class TestTeradataTypeMapping + extends AbstractTestQueryFramework +{ + private TestingTeradataServer database; + private String envName; + + public TestTeradataTypeMapping() + { + envName = ClearScapeEnvironmentUtils.generateUniqueEnvName(TestTeradataTypeMapping.class); + } + + @Override + protected QueryRunner createQueryRunner() + throws Exception + { + database = new TestingTeradataServer(envName); + // Register this specific instance for this test class + return TeradataQueryRunner.builder(database).build(); + } + + @AfterAll + void cleanupTestClass() + { + if (database != null) { + database.close(); + } + } + + @Test + void testByteint() + { + SqlDataTypeTest.create() + .addRoundTrip("byteint", "0", TINYINT, "CAST(0 AS TINYINT)") + .addRoundTrip("byteint", "127", TINYINT, "CAST(127 AS TINYINT)") + .addRoundTrip("byteint", "-128", TINYINT, "CAST(-128 AS TINYINT)") + .addRoundTrip("byteint", "null", TINYINT, "CAST(null AS TINYINT)") + .execute(getQueryRunner(), teradataJDBCCreateAndInsert("byteint")); + } + + @Test + void testSmallint() + { + SqlDataTypeTest.create() + .addRoundTrip("smallint", "0", SMALLINT, "CAST(0 AS SMALLINT)") + .addRoundTrip("smallint", "32767", SMALLINT, "CAST(32767 AS SMALLINT)") + .addRoundTrip("smallint", "-32768", SMALLINT, "CAST(-32768 AS SMALLINT)") + .addRoundTrip("smallint", "null", SMALLINT, "CAST(null AS SMALLINT)") + .execute(getQueryRunner(), teradataJDBCCreateAndInsert("smallint")); + } + + @Test + void testInteger() + { + SqlDataTypeTest.create() + .addRoundTrip("integer", "0", INTEGER, "0") + .addRoundTrip("integer", "2147483647", INTEGER, "2147483647") + .addRoundTrip("integer", "-2147483648", INTEGER, "-2147483648") + .addRoundTrip("integer", "NULL", INTEGER, "CAST(NULL AS INTEGER)") + .execute(getQueryRunner(), teradataJDBCCreateAndInsert("integer")); + } + + @Test + void testBigint() + { + SqlDataTypeTest.create() + .addRoundTrip("bigint", "0", BIGINT, "CAST(0 AS BIGINT)") + .addRoundTrip("bigint", "9223372036854775807", BIGINT, "9223372036854775807") + .addRoundTrip("bigint", "-9223372036854775808", BIGINT, "-9223372036854775808") + .addRoundTrip("bigint", "NULL", BIGINT, "CAST(NULL AS BIGINT)") + .execute(getQueryRunner(), teradataJDBCCreateAndInsert("bigint")); + } + + @Test + void testFloat() + { + SqlDataTypeTest.create() + .addRoundTrip("float", "0", DOUBLE, "CAST(0 AS DOUBLE)") + .addRoundTrip("real", "0", DOUBLE, "CAST(0 AS DOUBLE)") + .addRoundTrip("double precision", "0", DOUBLE, "CAST(0 AS DOUBLE)") + .addRoundTrip("float", "1.797e308", DOUBLE, "1.797e308") + .addRoundTrip("real", "1.797e308", DOUBLE, "1.797e308") + .addRoundTrip("double precision", "1.797e308", DOUBLE, "1.797e308") + .addRoundTrip("float", "2.226e-308", DOUBLE, "2.226e-308") + .addRoundTrip("real", "2.226e-308", DOUBLE, "2.226e-308") + .addRoundTrip("double precision", "2.226e-308", DOUBLE, "2.226e-308") + .addRoundTrip("float", "NULL", DOUBLE, "CAST(NULL AS DOUBLE)") + .addRoundTrip("real", "NULL", DOUBLE, "CAST(NULL AS DOUBLE)") + .addRoundTrip("double precision", "NULL", DOUBLE, "CAST(NULL AS DOUBLE)") + .execute(getQueryRunner(), teradataJDBCCreateAndInsert("float")); + } + + @Test + void testDecimal() + { + SqlDataTypeTest.create() + .addRoundTrip("decimal(3, 0)", "0", createDecimalType(3, 0), "CAST('0' AS decimal(3, 0))") + .addRoundTrip("numeric(3, 0)", "0", createDecimalType(3, 0), "CAST('0' AS decimal(3, 0))") + .addRoundTrip("decimal(3, 1)", "0.0", createDecimalType(3, 1), "CAST('0.0' AS decimal(3, 1))") + .addRoundTrip("numeric(3, 1)", "0.0", createDecimalType(3, 1), "CAST('0.0' AS decimal(3, 1))") + .addRoundTrip("decimal(1, 0)", "1", createDecimalType(1, 0), "CAST('1' AS decimal(1, 0))") + .addRoundTrip("numeric(1, 0)", "1", createDecimalType(1, 0), "CAST('1' AS decimal(1, 0))") + .addRoundTrip("decimal(1, 0)", "-1", createDecimalType(1, 0), "CAST('-1' AS decimal(1, 0))") + .addRoundTrip("numeric(1, 0)", "-1", createDecimalType(1, 0), "CAST('-1' AS decimal(1, 0))") + .addRoundTrip("decimal(3, 0)", "1", createDecimalType(3, 0), "CAST('1' AS decimal(3, 0))") + .addRoundTrip("numeric(3, 0)", "1", createDecimalType(3, 0), "CAST('1' AS decimal(3, 0))") + .addRoundTrip("decimal(3, 0)", "-1", createDecimalType(3, 0), "CAST('-1' AS decimal(3, 0))") + .addRoundTrip("numeric(3, 0)", "-1", createDecimalType(3, 0), "CAST('-1' AS decimal(3, 0))") + .addRoundTrip("decimal(3, 0)", "123", createDecimalType(3, 0), "CAST('123' AS decimal(3, 0))") + .addRoundTrip("numeric(3, 0)", "123", createDecimalType(3, 0), "CAST('123' AS decimal(3, 0))") + .addRoundTrip("decimal(3, 0)", "-123", createDecimalType(3, 0), "CAST('-123' AS decimal(3, 0))") + .addRoundTrip("numeric(3, 0)", "-123", createDecimalType(3, 0), "CAST('-123' AS decimal(3, 0))") + .addRoundTrip("decimal(3, 1)", "10.0", createDecimalType(3, 1), "CAST('10.0' AS decimal(3, 1))") + .addRoundTrip("numeric(3, 1)", "10.0", createDecimalType(3, 1), "CAST('10.0' AS decimal(3, 1))") + .addRoundTrip("decimal(3, 1)", "12.3", createDecimalType(3, 1), "CAST('12.3' AS decimal(3, 1))") + .addRoundTrip("numeric(3, 1)", "12.3", createDecimalType(3, 1), "CAST('12.3' AS decimal(3, 1))") + .addRoundTrip("decimal(3, 1)", "-12.3", createDecimalType(3, 1), "CAST('-12.3' AS decimal(3, 1))") + .addRoundTrip("numeric(3, 1)", "-12.3", createDecimalType(3, 1), "CAST('-12.3' AS decimal(3, 1))") + .addRoundTrip("decimal(38, 0)", "12345678901234567890123456789012345678", createDecimalType(38, 0), "CAST('12345678901234567890123456789012345678' AS decimal(38, 0))") + .addRoundTrip("numeric(38, 0)", "12345678901234567890123456789012345678", createDecimalType(38, 0), "CAST('12345678901234567890123456789012345678' AS decimal(38, 0))") + .addRoundTrip("decimal(38, 0)", "-12345678901234567890123456789012345678", createDecimalType(38, 0), "CAST('-12345678901234567890123456789012345678' AS decimal(38, 0))") + .addRoundTrip("numeric(38, 0)", "-12345678901234567890123456789012345678", createDecimalType(38, 0), "CAST('-12345678901234567890123456789012345678' AS decimal(38, 0))") + .addRoundTrip("decimal(1, 0)", "null", createDecimalType(1, 0), "CAST(null AS decimal(1, 0))") + .execute(getQueryRunner(), teradataJDBCCreateAndInsert("decimal")); + } + + @Test + void testNumber() + { + SqlDataTypeTest.create() + .addRoundTrip("numeric(3)", "0", createDecimalType(3, 0), "CAST('0' AS decimal(3, 0))") + .addRoundTrip("number(5,2)", "0", createDecimalType(5, 2), "CAST('0' AS decimal(5, 2))") + .addRoundTrip("number(38)", "0", createDecimalType(38, 0), "CAST('0' AS decimal(38, 0))") + .addRoundTrip("number(38,2)", "123456789012345678901234567890123456.78", createDecimalType(38, 2), "CAST('123456789012345678901234567890123456.78' AS decimal(38, 2))") + .addRoundTrip("numeric(38)", "12345678901234567890123456789012345678", createDecimalType(38, 0), "CAST('12345678901234567890123456789012345678' AS decimal(38, 0))") + .addRoundTrip("numeric(3)", "null", createDecimalType(3, 0), "CAST(null AS decimal(3, 0))") + .execute(getQueryRunner(), teradataJDBCCreateAndInsert("number")); + } + + @Test + void testChar() + { + SqlDataTypeTest.create() + .addRoundTrip("char(3)", "''", createCharType(3), "CAST('' AS char(3))") + .addRoundTrip("char(3)", "' '", createCharType(3), "CAST(' ' AS char(3))") + .addRoundTrip("char(3)", "' '", createCharType(3), "CAST(' ' AS char(3))") + .addRoundTrip("char(3)", "' '", createCharType(3), "CAST(' ' AS char(3))") + .addRoundTrip("char(3)", "'A'", createCharType(3), "CAST('A' AS char(3))") + .addRoundTrip("char(3)", "'A '", createCharType(3), "CAST('A ' AS char(3))") + .addRoundTrip("char(3)", "' B '", createCharType(3), "CAST(' B ' AS char(3))") + .addRoundTrip("char(3)", "' C'", createCharType(3), "CAST(' C' AS char(3))") + .addRoundTrip("char(3)", "'AB'", createCharType(3), "CAST('AB' AS char(3))") + .addRoundTrip("char(3)", "'ABC'", createCharType(3), "CAST('ABC' AS char(3))") + .addRoundTrip("char(3)", "'A C'", createCharType(3), "CAST('A C' AS char(3))") + .addRoundTrip("char(3)", "' BC'", createCharType(3), "CAST(' BC' AS char(3))") + .addRoundTrip("char(3)", "null", createCharType(3), "CAST(null AS char(3))") + .execute(getQueryRunner(), teradataJDBCCreateAndInsert("char")); + String tmode = database.getTMode(); + if (tmode.equals("TERA")) { + // truncation + SqlDataTypeTest.create() + .addRoundTrip("char(3)", "'ABCD'", createCharType(3), "CAST('ABCD' AS char(3))") + .execute(getQueryRunner(), teradataJDBCCreateAndInsert("chart")); + } + else { + // Error on truncation + assertThatThrownBy(() -> + SqlDataTypeTest.create() + .addRoundTrip("char(3)", "'ABCD'", createCharType(3), "CAST('ABCD' AS char(3))") + .execute(getQueryRunner(), teradataJDBCCreateAndInsert("chart"))) + .isInstanceOf(RuntimeException.class) + .hasCauseInstanceOf(SQLException.class) + .cause() + .hasMessageContaining("Right truncation of string data"); + } + // max-size + SqlDataTypeTest.create() + .addRoundTrip("char(64000)", "'max'", createCharType(64000), "CAST('max' AS char(64000))") + .execute(getQueryRunner(), teradataJDBCCreateAndInsert("charl")); + } + + @Test + void testVarchar() + { + SqlDataTypeTest.create() + .addRoundTrip("varchar(32)", "''", createVarcharType(32), "CAST('' AS varchar(32))") + .addRoundTrip("varchar(32)", "' '", createVarcharType(32), "CAST(' ' AS varchar(32))") + .addRoundTrip("varchar(32)", "' '", createVarcharType(32), "CAST(' ' AS varchar(32))") + .addRoundTrip("varchar(32)", "' '", createVarcharType(32), "CAST(' ' AS varchar(32))") + .addRoundTrip("varchar(32)", "' '", createVarcharType(32), "CAST(' ' AS varchar(32))") + .addRoundTrip("varchar(32)", "'A'", createVarcharType(32), "CAST('A' AS varchar(32))") + .addRoundTrip("varchar(32)", "'A '", createVarcharType(32), "CAST('A ' AS varchar(32))") + .addRoundTrip("varchar(32)", "' B '", createVarcharType(32), "CAST(' B ' AS varchar(32))") + .addRoundTrip("varchar(32)", "' C'", createVarcharType(32), "CAST(' C' AS varchar(32))") + .addRoundTrip("varchar(32)", "'AB'", createVarcharType(32), "CAST('AB' AS varchar(32))") + .addRoundTrip("varchar(32)", "'ABC'", createVarcharType(32), "CAST('ABC' AS varchar(32))") + .addRoundTrip("varchar(32)", "'A C'", createVarcharType(32), "CAST('A C' AS varchar(32))") + .addRoundTrip("varchar(32)", "' BC'", createVarcharType(32), "CAST(' BC' AS varchar(32))") + .addRoundTrip("varchar(32)", "null", createVarcharType(32), "CAST(null AS varchar(32))") + .execute(getQueryRunner(), teradataJDBCCreateAndInsert("varchar")); + String tmode = database.getTMode(); + if (tmode.equals("TERA")) { + // truncation + SqlDataTypeTest.create() + .addRoundTrip("varchar(3)", "'ABCD'", createVarcharType(3), "CAST('ABCD' AS varchar(3))") + .execute(getQueryRunner(), teradataJDBCCreateAndInsert("varchart")); + } + else { + // Error on truncation + assertThatThrownBy(() -> + SqlDataTypeTest.create() + .addRoundTrip("varchar(3)", "'ABCD'", createVarcharType(3), "CAST('ABCD' AS varchar(3))") + .execute(getQueryRunner(), teradataJDBCCreateAndInsert("varchart"))) + .isInstanceOf(RuntimeException.class) + .hasCauseInstanceOf(SQLException.class) + .cause() + .hasMessageContaining("Right truncation of string data"); + } + // max-size + SqlDataTypeTest.create() + .addRoundTrip("long varchar", "'max'", createVarcharType(64000), "CAST('max' AS varchar(64000))") + .execute(getQueryRunner(), teradataJDBCCreateAndInsert("varcharl")); + } + + @Test + void testDate() + { + SqlDataTypeTest.create() + .addRoundTrip("date", "DATE '0001-01-01'", DATE, "DATE '0001-01-01'") + .addRoundTrip("date", "DATE '0012-12-12'", DATE, "DATE '0012-12-12'") + .addRoundTrip("date", "DATE '1500-01-01'", DATE, "DATE '1500-01-01'") + .addRoundTrip("date", "DATE '1582-10-04'", DATE, "DATE '1582-10-04'") + .addRoundTrip("date", "DATE '1582-10-15'", DATE, "DATE '1582-10-15'") + .addRoundTrip("date", "DATE '1952-04-03'", DATE, "DATE '1952-04-03'") + .addRoundTrip("date", "DATE '1970-01-01'", DATE, "DATE '1970-01-01'") + .addRoundTrip("date", "DATE '1970-02-03'", DATE, "DATE '1970-02-03'") + .addRoundTrip("date", "DATE '1970-01-01'", DATE, "DATE '1970-01-01'") + .addRoundTrip("date", "DATE '1983-04-01'", DATE, "DATE '1983-04-01'") + .addRoundTrip("date", "DATE '1983-10-01'", DATE, "DATE '1983-10-01'") + .addRoundTrip("date", "DATE '2017-07-01'", DATE, "DATE '2017-07-01'") + .addRoundTrip("date", "DATE '2017-01-01'", DATE, "DATE '2017-01-01'") + .addRoundTrip("date", "DATE '2024-02-29'", DATE, "DATE '2024-02-29'") + .addRoundTrip("date", "DATE '9999-12-30'", DATE, "DATE '9999-12-30'") + .addRoundTrip("date", "NULL", DATE, "CAST(NULL AS DATE)") + .execute(getQueryRunner(), teradataJDBCCreateAndInsert("date")); + } + + private DataSetup teradataJDBCCreateAndInsert(String tableNamePrefix) + { + String prefix = String.format("%s.%s", database.getDatabaseName(), tableNamePrefix); + return new CreateAndInsertDataSetup(database, prefix); + } +} diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/TestingTeradataServer.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/TestingTeradataServer.java index 13539e914b8d..9ca24af71a0a 100644 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/TestingTeradataServer.java +++ b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/TestingTeradataServer.java @@ -11,11 +11,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.integration; import io.trino.plugin.integration.clearscape.ClearScapeSetup; import io.trino.plugin.integration.clearscape.Model; +import io.trino.plugin.integration.clearscape.Region; +import io.trino.plugin.integration.util.TeradataTestConstants; +import io.trino.plugin.teradata.LogonMechanism; import io.trino.testing.sql.SqlExecutor; import java.sql.Connection; @@ -24,10 +26,13 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Properties; +import static java.util.Objects.requireNonNull; + public class TestingTeradataServer implements AutoCloseable, SqlExecutor { @@ -47,11 +52,16 @@ public TestingTeradataServer(String envName) if (System.getenv("CLEARSCAPE_DESTORY_ENV") != null) { destoryEnv = Boolean.parseBoolean(System.getenv("CLEARSCAPE_DESTORY_ENV")); } + String region = System.getenv("CLEARSCAPE_REGION"); + if (!isValidRegion(region)) { + region = TeradataTestConstants.ENV_CLEARSCAPE_REGION; + } this.clearScapeSetup = new ClearScapeSetup( System.getenv("CLEARSCAPE_TOKEN"), System.getenv("CLEARSCAPE_PASSWORD"), config.getClearScapeEnvName(), - destoryEnv); + destoryEnv, + region); Model model = this.clearScapeSetup.initialize(); hostName = model.getHostName(); } @@ -64,6 +74,15 @@ public TestingTeradataServer(String envName) createTestDatabaseIfAbsent(); } + public static boolean isValidRegion(String region) + { + if (region == null || region.isBlank()) { + return false; + } + return Arrays.stream(Region.values()) + .anyMatch(r -> r.name().equalsIgnoreCase(region)); + } + private String buildJdbcUrl(String hostName) { String baseUrl = String.format("jdbc:teradata://%s/", hostName); @@ -104,27 +123,13 @@ private Properties buildConnectionProperties() Properties props = new Properties(); props.put("logmech", config.getLogMech().getMechanism()); - switch (config.getLogMech()) { - case TD2: - AuthenticationConfig auth = config.getAuthConfig(); - props.put("username", auth.getUserName()); - props.put("password", auth.getPassword()); - break; - case BEARER: - auth = config.getAuthConfig(); - props.put("jws_private_key", auth.getJwsPrivateKey()); - props.put("jws_cert", auth.getJwsCertificate()); - props.put("oidc_clientid", auth.getClientId()); - break; - case JWT: - props.put("logdata", "token=" + config.getAuthConfig().getJwtToken()); - break; - case SECRET: - props.put("oidc_clientid", config.getAuthConfig().getClientId()); - props.put("logdata", config.getAuthConfig().getClientSecret()); - break; - default: - throw new IllegalArgumentException("Unsupported logon mechanism: " + config.getLogMech()); + if (requireNonNull(config.getLogMech()) == LogonMechanism.TD2) { + AuthenticationConfig auth = config.getAuthConfig(); + props.put("username", auth.userName()); + props.put("password", auth.password()); + } + else { + throw new IllegalArgumentException("Unsupported logon mechanism: " + config.getLogMech()); } return props; @@ -136,25 +141,10 @@ public Map getCatalogProperties() properties.put("connection-url", config.getJdbcUrl()); properties.put("logon-mechanism", config.getLogMech().getMechanism()); - switch (config.getLogMech()) { - case TD2: - AuthenticationConfig auth = config.getAuthConfig(); - properties.put("connection-user", auth.getUserName()); - properties.put("connection-password", auth.getPassword()); - break; - case BEARER: - auth = config.getAuthConfig(); - properties.put("oidc.client-id", auth.getClientId()); - properties.put("oidc.jws-private-key", auth.getJwsPrivateKey()); - properties.put("oidc.jws-certificate", auth.getJwsCertificate()); - break; - case JWT: - properties.put("jwt.token", config.getAuthConfig().getJwtToken()); - break; - case SECRET: - properties.put("oidc.client-id", config.getAuthConfig().getClientId()); - properties.put("oidc.client-secret", config.getAuthConfig().getClientSecret()); - break; + if (requireNonNull(config.getLogMech()) == LogonMechanism.TD2) { + AuthenticationConfig auth = config.getAuthConfig(); + properties.put("connection-user", auth.userName()); + properties.put("connection-password", auth.password()); } return properties; @@ -256,7 +246,6 @@ public void execute(String sql) } } - // Getters public String getDatabaseName() { return config.getDatabaseName(); diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/BaseException.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/BaseException.java index 619541aaafdd..3eceeaf9c586 100644 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/BaseException.java +++ b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/BaseException.java @@ -11,7 +11,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.integration.clearscape; public class BaseException @@ -25,12 +24,6 @@ public BaseException(int statusCode, String body) this.statusCode = statusCode; } - public BaseException(int statusCode, String body, String reason) - { - super(body); - this.statusCode = statusCode; - } - public int getStatusCode() { return statusCode; diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/ClearScapeEnvironmentUtils.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/ClearScapeEnvironmentUtils.java index 34238935972c..3b9f45b3da9a 100644 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/ClearScapeEnvironmentUtils.java +++ b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/ClearScapeEnvironmentUtils.java @@ -11,36 +11,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.integration.clearscape; import static java.util.Locale.ENGLISH; -public class ClearScapeEnvironmentUtils +public final class ClearScapeEnvironmentUtils { - /** - * Utility class for generating unique environment names for ClearScape tests. - * The names are prefixed with "trino-test-" and truncated to fit within the - * maximum length allowed by ClearScape. - */ private static final String PREFIX = "trino-test-"; private static final int MAX_ENV_NAME_LENGTH = 30; // Adjust based on ClearScape limits private ClearScapeEnvironmentUtils() { - // Prevent instantiation } public static String generateUniqueEnvName(Class testClass) { String className = testClass.getSimpleName().toLowerCase(ENGLISH); String envName = PREFIX + className; - // Truncate if too long if (envName.length() > MAX_ENV_NAME_LENGTH) { envName = envName.substring(0, MAX_ENV_NAME_LENGTH); } - return envName; } } diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/ClearScapeManager.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/ClearScapeManager.java index cdd0705777a9..f4bb6574d6cb 100644 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/ClearScapeManager.java +++ b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/ClearScapeManager.java @@ -11,7 +11,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.integration.clearscape; import io.airlift.log.Logger; @@ -20,14 +19,6 @@ import java.net.URISyntaxException; import java.util.regex.Pattern; -/** - * Manager class responsible for provisioning, starting, and tearing down ClearScape environments - * using the Teradata Environment API. - * This class reads configuration from a JSON file, uses the environment configuration to call the - * Teradata ClearScape HTTP API, and sets up the necessary JDBC parameters for connecting to a - * Teradata instance. - */ - public class ClearScapeManager { private static final Logger log = Logger.get(ClearScapeManager.class); @@ -39,23 +30,11 @@ public ClearScapeManager() { } - /** - * Validates that the provided URL matches the expected Clearscape Teradata API pattern. - * - * @param url the environment URL to validate - * @return true if the URL is valid, false otherwise - */ private boolean isValidUrl(String url) { return ALLOWED_URL_PATTERN.matcher(url).matches(); } - /** - * Creates a new instance of {@link TeradataHttpClient} using the environment URL from the config. - * - * @return an initialized {@link TeradataHttpClient} instance - * @throws URISyntaxException if the environment URL is invalid - */ private TeradataHttpClient getTeradataHttpClient() throws URISyntaxException { @@ -78,28 +57,16 @@ public void setup() createAndStartClearScapeInstance(); } - /** - * Public method to stop the clearscape instance - */ public void stop() { stopClearScapeInstance(); } - /** - * Public method to shut down and delete the ClearScape environment instance. Should be called - * to clean up resources after usage. - */ public void teardown() { shutdownAndDestroyClearScapeInstance(); } - /** - * Handles the logic for creating and starting a ClearScape environment instance. If the - * environment already exists and is stopped, it is started. If it doesn't exist, a new - * environment is created. Updates the {@code configJSON} with host/IP and authentication info. - */ private void createAndStartClearScapeInstance() { try { @@ -118,7 +85,7 @@ private void createAndStartClearScapeInstance() if (response == null || response.ip() == null) { CreateEnvironmentRequest request = new CreateEnvironmentRequest( name, - TeradataTestConstants.ENV_CLEARSCAPE_REGION, + model.getRegion(), model.getPassword()); response = teradataHttpClient.createEnvironment(request, token).get(); } @@ -136,9 +103,6 @@ else if (response.state() == EnvironmentResponse.State.STOPPED) { } } - /** - * Handles the logic for stopping a ClearScape environment instance. - */ private void stopClearScapeInstance() { try { @@ -165,10 +129,6 @@ private void stopClearScapeInstance() } } - /** - * Handles the logic for shutting down and deleting a ClearScape environment instance. Logs a - * warning if the environment is not available. - */ private void shutdownAndDestroyClearScapeInstance() { try { diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/ClearScapeSetup.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/ClearScapeSetup.java index 556d778c2918..7915ea0e61d6 100644 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/ClearScapeSetup.java +++ b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/ClearScapeSetup.java @@ -11,7 +11,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.integration.clearscape; import io.trino.plugin.integration.util.TeradataTestConstants; @@ -21,15 +20,22 @@ public class ClearScapeSetup private final String token; private final String password; private final String envName; + private final String region; private final boolean destoryEnv; private ClearScapeManager manager; - public ClearScapeSetup(String token, String password, String envName, boolean destoryEnv) + public ClearScapeSetup( + String token, + String password, + String envName, + boolean destroyEnv, + String region) { this.token = token; this.password = password; this.envName = envName; - this.destoryEnv = destoryEnv; + this.region = region; + this.destoryEnv = destroyEnv; } public Model initialize() @@ -54,6 +60,7 @@ private Model createModel() model.setPassword(password); model.setDatabaseName(TeradataTestConstants.ENV_CLEARSCAPE_USERNAME); model.setToken(token); + model.setRegion(region); return model; } diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/CreateEnvironmentRequest.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/CreateEnvironmentRequest.java index 82e1ffdb205f..553d0ca1338e 100644 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/CreateEnvironmentRequest.java +++ b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/CreateEnvironmentRequest.java @@ -11,14 +11,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.integration.clearscape; public record CreateEnvironmentRequest( - String name, - String region, - String password ) {} diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/DeleteEnvironmentRequest.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/DeleteEnvironmentRequest.java index 01e75e22d340..2bbf60e69b42 100644 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/DeleteEnvironmentRequest.java +++ b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/DeleteEnvironmentRequest.java @@ -11,11 +11,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.integration.clearscape; public record DeleteEnvironmentRequest( - String name - ) {} diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/EnvironmentRequest.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/EnvironmentRequest.java index e25db3322196..76bb75e1d2ad 100644 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/EnvironmentRequest.java +++ b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/EnvironmentRequest.java @@ -11,12 +11,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.integration.clearscape; public record EnvironmentRequest( - String name, - OperationRequest request ) {} diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/EnvironmentResponse.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/EnvironmentResponse.java index 6766eab55b41..88159e72cce2 100644 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/EnvironmentResponse.java +++ b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/EnvironmentResponse.java @@ -18,49 +18,28 @@ public record EnvironmentResponse( State state, - String region, - - // Use for subsequent environment operations i.e GET, DELETE, etc String name, - - // Use for connecting with JDBC driver String ip, - String dnsName, - String owner, - String type, - List services) { public enum State { - PROVISIONING, - INITIALIZING, RUNNING, - STARTING, - STOPPING, STOPPED, - TERMINATING, - TERMINATED, - REPAIRING } record Service( - List credentials, - String name, - String url ) {} record Credential( - String name, - String value ) {} } diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/Error4xxException.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/Error4xxException.java index f0263c8117ca..338f0669efe7 100644 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/Error4xxException.java +++ b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/Error4xxException.java @@ -11,12 +11,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.integration.clearscape; public class Error4xxException extends BaseException { + public Error4xxException(int statusCode, String body, String reason) + { + super(statusCode, body, reason); + } + public Error4xxException(int statusCode, String body) { super(statusCode, body); diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/Error5xxException.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/Error5xxException.java index 7b472b6448cb..355e1db480dd 100644 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/Error5xxException.java +++ b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/Error5xxException.java @@ -11,17 +11,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.integration.clearscape; public class Error5xxException extends BaseException { - public Error5xxException(int statusCode, String body, String reason) - { - super(statusCode, body, reason); - } - public Error5xxException(int statusCode, String body) { super(statusCode, body); diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/GetEnvironmentRequest.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/GetEnvironmentRequest.java index 1ffa0b1dfe5b..04a75c64e9f9 100644 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/GetEnvironmentRequest.java +++ b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/GetEnvironmentRequest.java @@ -11,11 +11,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.integration.clearscape; public record GetEnvironmentRequest( - String name - ) {} diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/Headers.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/Headers.java index 80effc44bfdf..c9d02efe1cab 100644 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/Headers.java +++ b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/Headers.java @@ -11,7 +11,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.integration.clearscape; public class Headers diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/Model.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/Model.java index 13fc3ff220dc..7258ed8e435e 100644 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/Model.java +++ b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/Model.java @@ -11,7 +11,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.integration.clearscape; public class Model @@ -22,7 +21,6 @@ public class Model String password; String databaseName; String token; - String jdbcUrl; String region; public String getEnvName() @@ -45,11 +43,6 @@ public void setHostName(String hostName) this.hostName = hostName; } - public String getUserName() - { - return userName; - } - public void setUserName(String userName) { this.userName = userName; @@ -65,11 +58,6 @@ public void setPassword(String password) this.password = password; } - public String getDatabaseName() - { - return databaseName; - } - public void setDatabaseName(String databaseName) { this.databaseName = databaseName; @@ -85,16 +73,6 @@ public void setToken(String token) this.token = token; } - public String getJdbcUrl() - { - return jdbcUrl; - } - - public void setJdbcUrl(String jdbcUrl) - { - this.jdbcUrl = jdbcUrl; - } - public String getRegion() { return region; diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/OperationRequest.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/OperationRequest.java index 0ab1b1ea1e45..a8724424ceee 100644 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/OperationRequest.java +++ b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/OperationRequest.java @@ -11,7 +11,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.integration.clearscape; public record OperationRequest( diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/Region.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/Region.java new file mode 100644 index 000000000000..e7f2af83e0a8 --- /dev/null +++ b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/Region.java @@ -0,0 +1,26 @@ +package io.trino.plugin.integration.clearscape; + +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +public enum Region { + US_CENTRAL, + US_EAST, + US_WEST, + SOUTHAMERICA_EAST, + EUROPE_WEST, + ASIA_SOUTH, + ASIA_NORTHEAST, + ASIA_SOUTHEAST, + AUSTRALIA_SOUTHEAST +} diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/TeradataHttpClient.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/TeradataHttpClient.java index 10ad5693c4b6..ae93d199c9c9 100644 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/TeradataHttpClient.java +++ b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/clearscape/TeradataHttpClient.java @@ -11,7 +11,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.integration.clearscape; import com.fasterxml.jackson.core.type.TypeReference; @@ -36,9 +35,7 @@ public class TeradataHttpClient { private final String baseUrl; - private final HttpClient httpClient; - private final ObjectMapper objectMapper; public TeradataHttpClient(String baseUrl) @@ -46,7 +43,9 @@ public TeradataHttpClient(String baseUrl) this(HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(), baseUrl); } - public TeradataHttpClient(HttpClient httpClient, String baseUrl) + public TeradataHttpClient( + HttpClient httpClient, + String baseUrl) { this.httpClient = httpClient; this.baseUrl = baseUrl; @@ -61,26 +60,16 @@ public CompletableFuture createEnvironment(CreateEnvironmen String token) { var requestBody = handleCheckedException(() -> objectMapper.writeValueAsString(createEnvironmentRequest)); - var httpRequest = HttpRequest.newBuilder(URI.create(baseUrl.concat("/environments"))) .headers( AUTHORIZATION, BEARER + token, CONTENT_TYPE, APPLICATION_JSON) .POST(HttpRequest.BodyPublishers.ofString(requestBody)) .build(); - return httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString()) .thenApply(httpResponse -> handleHttpResponse(httpResponse, new TypeReference<>() {})); } - // Avoids long connections and the risk of connection termination by intermediary - public CompletableFuture pollingCreateEnvironment( - CreateEnvironmentRequest createEnvironmentRequest, - String token) - { - throw new UnsupportedOperationException(); - } - public EnvironmentResponse getEnvironment(GetEnvironmentRequest getEnvironmentRequest, String token) { var httpRequest = HttpRequest.newBuilder(URI.create(baseUrl @@ -89,7 +78,6 @@ public EnvironmentResponse getEnvironment(GetEnvironmentRequest getEnvironmentRe .headers(AUTHORIZATION, BEARER + token) .GET() .build(); - var httpResponse = handleCheckedException(() -> httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString())); return handleHttpResponse(httpResponse, new TypeReference<>() {}); @@ -109,19 +97,19 @@ public CompletableFuture deleteEnvironment(DeleteEnvironmentRequest delete .thenApply(httpResponse -> handleHttpResponse(httpResponse, new TypeReference<>() {})); } - public CompletableFuture startEnvironment(EnvironmentRequest environmentRequest, String token) + public void startEnvironment(EnvironmentRequest environmentRequest, String token) { var requestBody = handleCheckedException(() -> objectMapper.writeValueAsString(environmentRequest.request())); - return getVoidCompletableFuture(environmentRequest.name(), token, requestBody); + getVoidCompletableFuture(environmentRequest.name(), token, requestBody); } - public CompletableFuture stopEnvironment(EnvironmentRequest environmentRequest, String token) + public void stopEnvironment(EnvironmentRequest environmentRequest, String token) { var requestBody = handleCheckedException(() -> objectMapper.writeValueAsString(environmentRequest.request())); - return getVoidCompletableFuture(environmentRequest.name(), token, requestBody); + getVoidCompletableFuture(environmentRequest.name(), token, requestBody); } - private CompletableFuture getVoidCompletableFuture(String name, String token, String jsonPayLoadString) + private void getVoidCompletableFuture(String name, String token, String jsonPayLoadString) { HttpRequest.BodyPublisher publisher = HttpRequest.BodyPublishers.ofString(jsonPayLoadString); var httpRequest = HttpRequest.newBuilder(URI.create(baseUrl @@ -133,7 +121,7 @@ private CompletableFuture getVoidCompletableFuture(String name, String tok .build(); var httpResponse = handleCheckedException(() -> httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString())); - return handleHttpResponse(httpResponse, new TypeReference<>() {}); + handleHttpResponse(httpResponse, new TypeReference<>() {}); } private T handleHttpResponse(HttpResponse httpResponse, TypeReference typeReference) diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/util/TeradataTestConstants.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/util/TeradataTestConstants.java index 2658d9752f24..5e1a7af7097f 100644 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/util/TeradataTestConstants.java +++ b/plugin/trino-teradata/src/test/java/io/trino/plugin/integration/util/TeradataTestConstants.java @@ -11,7 +11,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.integration.util; public interface TeradataTestConstants diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/unit/TestImplementAvgBigint.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/unit/TestImplementAvgBigint.java deleted file mode 100644 index 3da994cf5d88..000000000000 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/unit/TestImplementAvgBigint.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.trino.plugin.unit; - -import io.trino.plugin.teradata.ImplementAvgBigint; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -public class TestImplementAvgBigint -{ - @Test - public void testGetRewriteFormatExpression() - { - ImplementAvgBigint implementAvgBigint = new ImplementAvgBigint(); - String formatExpression = implementAvgBigint.getRewriteFormatExpression(); - // Replace with the expected SQL expression for Teradata - assertThat(formatExpression).isEqualTo("avg(CAST(%s AS FLOAT))"); - } -} diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/unit/TestLogonMechanism.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/unit/TestLogonMechanism.java index d8c8983d5a81..08943c3b3841 100644 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/unit/TestLogonMechanism.java +++ b/plugin/trino-teradata/src/test/java/io/trino/plugin/unit/TestLogonMechanism.java @@ -11,7 +11,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.unit; import io.trino.plugin.teradata.LogonMechanism; @@ -26,22 +25,13 @@ public class TestLogonMechanism public void testFromStringValidValues() { assertThat(LogonMechanism.fromString("TD2")).isEqualTo(LogonMechanism.TD2); - assertThat(LogonMechanism.fromString("JWT")).isEqualTo(LogonMechanism.JWT); - assertThat(LogonMechanism.fromString("BEARER")).isEqualTo(LogonMechanism.BEARER); - assertThat(LogonMechanism.fromString("SECRET")).isEqualTo(LogonMechanism.SECRET); - - // Case-insensitive assertThat(LogonMechanism.fromString("td2")).isEqualTo(LogonMechanism.TD2); - assertThat(LogonMechanism.fromString("jwt")).isEqualTo(LogonMechanism.JWT); } @Test public void testGetMechanism() { assertThat(LogonMechanism.TD2.getMechanism()).isEqualTo("TD2"); - assertThat(LogonMechanism.JWT.getMechanism()).isEqualTo("JWT"); - assertThat(LogonMechanism.BEARER.getMechanism()).isEqualTo("BEARER"); - assertThat(LogonMechanism.SECRET.getMechanism()).isEqualTo("SECRET"); } @Test diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/unit/TestTeradataConfig.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/unit/TestTeradataConfig.java index d18dfa11d2b9..c5f3a5c2cc95 100644 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/unit/TestTeradataConfig.java +++ b/plugin/trino-teradata/src/test/java/io/trino/plugin/unit/TestTeradataConfig.java @@ -11,7 +11,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.unit; import io.trino.plugin.teradata.TeradataConfig; @@ -25,11 +24,6 @@ public class TestTeradataConfig public void testDefaults() { TeradataConfig config = new TeradataConfig(); - assertThat(config.getOidcClientId()).isNull(); - assertThat(config.getOidcClientSecret()).isNull(); - assertThat(config.getOidcJWSCertificate()).isNull(); - assertThat(config.getOidcJWSPrivateKey()).isNull(); - assertThat(config.getOidcJwtToken()).isNull(); assertThat(config.getLogMech()).isEqualTo("TD2"); assertThat(config.getTeradataCaseSensitivity()).isEqualTo(TeradataConfig.TeradataCaseSensitivity.CASE_SENSITIVE); } @@ -38,20 +32,9 @@ public void testDefaults() public void testSetters() { TeradataConfig config = new TeradataConfig() - .setOidcClientId("clientId") - .setOidcClientSecret("clientSecret") - .setOidcJWSCertificate("certificate") - .setOidcJWSPrivateKey("privateKey") - .setOidcJwtToken("jwtToken") - .setLogMech("LDAP") + .setLogMech("TD2") .setTeradataCaseSensitivity(TeradataConfig.TeradataCaseSensitivity.CASE_INSENSITIVE); - - assertThat(config.getOidcClientId()).contains("clientId"); - assertThat(config.getOidcClientSecret()).contains("clientSecret"); - assertThat(config.getOidcJWSCertificate()).contains("certificate"); - assertThat(config.getOidcJWSPrivateKey()).contains("privateKey"); - assertThat(config.getOidcJwtToken()).contains("jwtToken"); - assertThat(config.getLogMech()).isEqualTo("LDAP"); + assertThat(config.getLogMech()).isEqualTo("TD2"); assertThat(config.getTeradataCaseSensitivity()).isEqualTo(TeradataConfig.TeradataCaseSensitivity.CASE_INSENSITIVE); } diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/unit/TestTeradataConstants.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/unit/TestTeradataConstants.java index a45389b9dd58..9eb651699432 100644 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/unit/TestTeradataConstants.java +++ b/plugin/trino-teradata/src/test/java/io/trino/plugin/unit/TestTeradataConstants.java @@ -11,7 +11,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.unit; import io.trino.plugin.teradata.util.TeradataConstants; diff --git a/plugin/trino-teradata/src/test/java/io/trino/plugin/unit/TestTeradataPlugin.java b/plugin/trino-teradata/src/test/java/io/trino/plugin/unit/TestTeradataPlugin.java index 13cd9469ad20..504bd9a2f2bf 100644 --- a/plugin/trino-teradata/src/test/java/io/trino/plugin/unit/TestTeradataPlugin.java +++ b/plugin/trino-teradata/src/test/java/io/trino/plugin/unit/TestTeradataPlugin.java @@ -11,7 +11,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.trino.plugin.unit; import io.trino.plugin.jdbc.JdbcConnectorFactory; diff --git a/testing/trino-server-dev/etc/catalog/teradata.properties b/testing/trino-server-dev/etc/catalog/teradata.properties deleted file mode 100644 index f3b822e1b35d..000000000000 --- a/testing/trino-server-dev/etc/catalog/teradata.properties +++ /dev/null @@ -1,4 +0,0 @@ -connector.name=teradata -connection-url=jdbc:teradata://rtdm-tdvm-smp-0279/TMODE=ANSI,CHARSET=UTF8 -connection-user=dbc -connection-password=dbc diff --git a/testing/trino-server-dev/etc/config.properties b/testing/trino-server-dev/etc/config.properties index 61e28efe4039..fb94f8208060 100644 --- a/testing/trino-server-dev/etc/config.properties +++ b/testing/trino-server-dev/etc/config.properties @@ -46,7 +46,7 @@ plugin.bundles=\ ../../plugin/trino-sqlserver/pom.xml, \ ../../plugin/trino-prometheus/pom.xml, \ ../../plugin/trino-postgresql/pom.xml, \ - ../../plugin/trino-teradata/pom.xml, \ + ../../plugin/trino-teradata/pom.xml, \ ../../plugin/trino-thrift/pom.xml, \ ../../plugin/trino-tpcds/pom.xml, \ ../../plugin/trino-google-sheets/pom.xml, \