diff --git a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java
index 0d3d080fb..6bb03b9c9 100644
--- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java
+++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java
@@ -647,6 +647,33 @@ public enum Feature {
/** Additional structured catalog targets of COMMENT ON. */
commentOnIndex, commentOnSchema, commentOnSequence, commentOnDomain, commentOnType, commentOnMaterializedView, commentOnFunction, commentOnConstraint,
+ /** COMMENT ON TRIGGER, POLICY or RULE within a table. */
+ commentOnTrigger, commentOnPolicy, commentOnRule,
+
+ /** COMMENT ON PROCEDURE, ROUTINE, AGGREGATE or CAST. */
+ commentOnProcedure, commentOnRoutine, commentOnAggregate, commentOnCast,
+
+ /** COMMENT ON COLLATION, FOREIGN TABLE or STATISTICS. */
+ commentOnCollation, commentOnForeignTable, commentOnStatistics,
+
+ /** COMMENT ON DATABASE, ROLE or TABLESPACE. */
+ commentOnDatabase, commentOnRole, commentOnTablespace,
+
+ /** COMMENT ON ACCESS METHOD, EXTENSION, FOREIGN DATA WRAPPER, LANGUAGE or SERVER. */
+ commentOnAccessMethod, commentOnExtension, commentOnForeignDataWrapper, commentOnLanguage, commentOnServer,
+
+ /** COMMENT ON EVENT TRIGGER, PUBLICATION or SUBSCRIPTION. */
+ commentOnEventTrigger, commentOnPublication, commentOnSubscription,
+
+ /** COMMENT ON OPERATOR, OPERATOR CLASS or OPERATOR FAMILY. */
+ commentOnOperator, commentOnOperatorClass, commentOnOperatorFamily,
+
+ /** COMMENT ON CONVERSION, LARGE OBJECT or TRANSFORM. */
+ commentOnConversion, commentOnLargeObject, commentOnTransform,
+
+ /** COMMENT ON TEXT SEARCH CONFIGURATION, DICTIONARY, PARSER or TEMPLATE. */
+ commentOnTextSearchConfiguration, commentOnTextSearchDictionary, commentOnTextSearchParser, commentOnTextSearchTemplate,
+
/**
* SQL "DESCRIBE" statement is allowed
*
diff --git a/src/main/java/net/sf/jsqlparser/statement/comment/CommentTarget.java b/src/main/java/net/sf/jsqlparser/statement/comment/CommentTarget.java
index 7269d29a6..5cd3e0009 100644
--- a/src/main/java/net/sf/jsqlparser/statement/comment/CommentTarget.java
+++ b/src/main/java/net/sf/jsqlparser/statement/comment/CommentTarget.java
@@ -13,11 +13,46 @@
import java.util.function.Consumer;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.RoutineReference;
+import net.sf.jsqlparser.statement.create.table.ColDataType;
+import net.sf.jsqlparser.statement.create.table.ExclusionOperator;
/** A catalog object addressed by COMMENT, rather than a function invocation or a query. */
public class CommentTarget implements Serializable {
public enum Kind {
- INDEX, SCHEMA, SEQUENCE, DOMAIN, TYPE, MATERIALIZED_VIEW, FUNCTION, CONSTRAINT
+ INDEX, SCHEMA, SEQUENCE, DOMAIN, TYPE, MATERIALIZED_VIEW, FUNCTION, CONSTRAINT,
+
+ TRIGGER, POLICY, RULE, PROCEDURE, ROUTINE, AGGREGATE, CAST,
+
+ COLLATION, FOREIGN_TABLE, STATISTICS, DATABASE, ROLE, TABLESPACE, ACCESS_METHOD, EXTENSION,
+
+ FOREIGN_DATA_WRAPPER, LANGUAGE, SERVER, EVENT_TRIGGER, PUBLICATION, SUBSCRIPTION,
+
+ OPERATOR, OPERATOR_CLASS, OPERATOR_FAMILY, LARGE_OBJECT, CONVERSION, TRANSFORM,
+
+ TEXT_SEARCH_CONFIGURATION, TEXT_SEARCH_DICTIONARY, TEXT_SEARCH_PARSER, TEXT_SEARCH_TEMPLATE;
+
+ /**
+ * Whether the target is a routine signature, exposed by {@link CommentTarget#getRoutine()}.
+ */
+ public boolean isRoutine() {
+ return this == FUNCTION || this == PROCEDURE || this == ROUTINE || this == AGGREGATE;
+ }
+
+ /**
+ * Whether the target is named within the table or domain of
+ * {@link CommentTarget#getRelation()}.
+ */
+ public boolean isOwnedByRelation() {
+ return this == CONSTRAINT || this == TRIGGER || this == POLICY || this == RULE;
+ }
+
+ /**
+ * Whether the target is named within the index access method of
+ * {@link CommentTarget#getAccessMethod()}.
+ */
+ public boolean isOwnedByAccessMethod() {
+ return this == OPERATOR_CLASS || this == OPERATOR_FAMILY;
+ }
}
private Kind kind;
@@ -25,6 +60,16 @@ public enum Kind {
private RoutineReference routine;
private Table relation;
private boolean onDomain;
+ private boolean procedural;
+ private ColDataType sourceType;
+ private ColDataType targetType;
+ private ExclusionOperator operator;
+ private ColDataType leftOperandType;
+ private ColDataType rightOperandType;
+ private String accessMethod;
+ private Long largeObjectOid;
+ private ColDataType transformType;
+ private String language;
public Kind getKind() {
return kind;
@@ -34,7 +79,10 @@ public void setKind(Kind kind) {
this.kind = kind;
}
- /** The object's identifier; using Table preserves the individual name components. */
+ /**
+ * The object's identifier; using Table preserves the individual name components. Routine, cast,
+ * operator, large object and transform targets have no name.
+ */
public Table getName() {
return name;
}
@@ -51,7 +99,10 @@ public void setRoutine(RoutineReference routine) {
this.routine = routine;
}
- /** The table or domain owning a constraint, distinguished by {@link #isOnDomain()}. */
+ /**
+ * The table owning a constraint, trigger, policy or rule, or the domain owning a constraint,
+ * distinguished by {@link #isOnDomain()}.
+ */
public Table getRelation() {
return relation;
}
@@ -68,34 +119,155 @@ public void setOnDomain(boolean onDomain) {
this.onDomain = onDomain;
}
- /** Returns only an explicitly named table/view, never an index, type, function or domain. */
+ /** Whether a {@link Kind#LANGUAGE} target was written as PROCEDURAL LANGUAGE. */
+ public boolean isProcedural() {
+ return procedural;
+ }
+
+ public void setProcedural(boolean procedural) {
+ this.procedural = procedural;
+ }
+
+ /** The source type of a {@link Kind#CAST} target. */
+ public ColDataType getSourceType() {
+ return sourceType;
+ }
+
+ public void setSourceType(ColDataType sourceType) {
+ this.sourceType = sourceType;
+ }
+
+ /** The target type of a {@link Kind#CAST} target. */
+ public ColDataType getTargetType() {
+ return targetType;
+ }
+
+ public void setTargetType(ColDataType targetType) {
+ this.targetType = targetType;
+ }
+
+ /**
+ * The possibly schema-qualified operator of an {@link Kind#OPERATOR} target, read by the
+ * operator reference shared with exclusion constraints and ALTER EXTENSION.
+ */
+ public ExclusionOperator getOperator() {
+ return operator;
+ }
+
+ public void setOperator(ExclusionOperator operator) {
+ this.operator = operator;
+ }
+
+ /** The left operand type of an {@link Kind#OPERATOR} target; null stands for NONE. */
+ public ColDataType getLeftOperandType() {
+ return leftOperandType;
+ }
+
+ public void setLeftOperandType(ColDataType leftOperandType) {
+ this.leftOperandType = leftOperandType;
+ }
+
+ /** The right operand type of an {@link Kind#OPERATOR} target; null stands for NONE. */
+ public ColDataType getRightOperandType() {
+ return rightOperandType;
+ }
+
+ public void setRightOperandType(ColDataType rightOperandType) {
+ this.rightOperandType = rightOperandType;
+ }
+
+ /**
+ * The index access method of an {@link Kind#OPERATOR_CLASS} or {@link Kind#OPERATOR_FAMILY}
+ * target.
+ */
+ public String getAccessMethod() {
+ return accessMethod;
+ }
+
+ public void setAccessMethod(String accessMethod) {
+ this.accessMethod = accessMethod;
+ }
+
+ /** The OID of a {@link Kind#LARGE_OBJECT} target. */
+ public Long getLargeObjectOid() {
+ return largeObjectOid;
+ }
+
+ public void setLargeObjectOid(Long largeObjectOid) {
+ this.largeObjectOid = largeObjectOid;
+ }
+
+ /** The data type of a {@link Kind#TRANSFORM} target. */
+ public ColDataType getTransformType() {
+ return transformType;
+ }
+
+ public void setTransformType(ColDataType transformType) {
+ this.transformType = transformType;
+ }
+
+ /** The procedural language of a {@link Kind#TRANSFORM} target. */
+ public String getLanguage() {
+ return language;
+ }
+
+ public void setLanguage(String language) {
+ this.language = language;
+ }
+
+ /**
+ * Returns only an explicitly named table, foreign table or view, never an index, type,
+ * function, statistics object or domain.
+ */
public Table getReferencedRelation() {
- if (kind == Kind.MATERIALIZED_VIEW) {
+ if (namesRelation()) {
return name;
}
- return kind == Kind.CONSTRAINT && !onDomain ? relation : null;
+ return kind != null && kind.isOwnedByRelation() && !onDomain ? relation : null;
+ }
+
+ private boolean namesRelation() {
+ return kind == Kind.MATERIALIZED_VIEW || kind == Kind.FOREIGN_TABLE;
}
public StringBuilder appendTo(StringBuilder builder, Consumer
relationWriter) {
+ if (kind == Kind.LANGUAGE && procedural) {
+ builder.append("PROCEDURAL ");
+ }
builder.append(kind.name().replace('_', ' ')).append(' ');
- if (kind == Kind.FUNCTION) {
+ if (kind.isRoutine()) {
builder.append(routine);
- } else if (kind == Kind.MATERIALIZED_VIEW) {
+ } else if (kind == Kind.CAST) {
+ builder.append('(').append(sourceType).append(" AS ").append(targetType).append(')');
+ } else if (kind == Kind.OPERATOR) {
+ builder.append(operator).append(" (").append(operandType(leftOperandType))
+ .append(", ").append(operandType(rightOperandType)).append(')');
+ } else if (kind == Kind.LARGE_OBJECT) {
+ builder.append(largeObjectOid);
+ } else if (kind == Kind.TRANSFORM) {
+ builder.append("FOR ").append(transformType).append(" LANGUAGE ").append(language);
+ } else if (namesRelation()) {
relationWriter.accept(name);
} else {
builder.append(name);
- if (kind == Kind.CONSTRAINT) {
+ if (kind.isOwnedByRelation()) {
builder.append(" ON ");
if (onDomain) {
builder.append("DOMAIN ").append(relation);
} else {
relationWriter.accept(relation);
}
+ } else if (kind.isOwnedByAccessMethod()) {
+ builder.append(" USING ").append(accessMethod);
}
}
return builder;
}
+ private static Object operandType(ColDataType type) {
+ return type == null ? "NONE" : type;
+ }
+
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ExclusionOperator.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ExclusionOperator.java
index 5ae3529a9..182b1e124 100644
--- a/src/main/java/net/sf/jsqlparser/statement/create/table/ExclusionOperator.java
+++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ExclusionOperator.java
@@ -11,7 +11,10 @@
import java.io.Serializable;
-/** An exclusion operator, optionally qualified and enclosed in PostgreSQL OPERATOR(...). */
+/**
+ * An exclusion operator, optionally qualified and enclosed in PostgreSQL OPERATOR(...). COMMENT ON
+ * OPERATOR uses the same reference without the OPERATOR keyword.
+ */
public class ExclusionOperator implements Serializable {
private String schemaName;
private String name;
diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java
index 023a8c0e1..9dea017f3 100644
--- a/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java
+++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java
@@ -95,6 +95,22 @@ public enum PostgresqlVersion implements Version {
Feature.commentOnDomain, Feature.commentOnType,
Feature.commentOnMaterializedView,
Feature.commentOnFunction, Feature.commentOnConstraint,
+ Feature.commentOnTrigger, Feature.commentOnPolicy, Feature.commentOnRule,
+ Feature.commentOnProcedure, Feature.commentOnRoutine,
+ Feature.commentOnAggregate, Feature.commentOnCast,
+ Feature.commentOnCollation, Feature.commentOnForeignTable,
+ Feature.commentOnStatistics, Feature.commentOnDatabase, Feature.commentOnRole,
+ Feature.commentOnTablespace, Feature.commentOnAccessMethod,
+ Feature.commentOnExtension, Feature.commentOnForeignDataWrapper,
+ Feature.commentOnLanguage, Feature.commentOnServer,
+ Feature.commentOnEventTrigger, Feature.commentOnPublication,
+ Feature.commentOnSubscription,
+ Feature.commentOnOperator, Feature.commentOnOperatorClass,
+ Feature.commentOnOperatorFamily, Feature.commentOnConversion,
+ Feature.commentOnLargeObject, Feature.commentOnTransform,
+ Feature.commentOnTextSearchConfiguration,
+ Feature.commentOnTextSearchDictionary, Feature.commentOnTextSearchParser,
+ Feature.commentOnTextSearchTemplate,
// https://www.postgresql.org/docs/current/sql-createsequence.html
Feature.createSequence,
diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/CommentValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/CommentValidator.java
index b5ad37f8b..dcffd3e4a 100644
--- a/src/main/java/net/sf/jsqlparser/util/validation/validator/CommentValidator.java
+++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/CommentValidator.java
@@ -46,6 +46,68 @@ private static Feature targetFeature(CommentTarget.Kind kind) {
return Feature.commentOnFunction;
case CONSTRAINT:
return Feature.commentOnConstraint;
+ case TRIGGER:
+ return Feature.commentOnTrigger;
+ case POLICY:
+ return Feature.commentOnPolicy;
+ case RULE:
+ return Feature.commentOnRule;
+ case PROCEDURE:
+ return Feature.commentOnProcedure;
+ case ROUTINE:
+ return Feature.commentOnRoutine;
+ case AGGREGATE:
+ return Feature.commentOnAggregate;
+ case CAST:
+ return Feature.commentOnCast;
+ case COLLATION:
+ return Feature.commentOnCollation;
+ case FOREIGN_TABLE:
+ return Feature.commentOnForeignTable;
+ case STATISTICS:
+ return Feature.commentOnStatistics;
+ case DATABASE:
+ return Feature.commentOnDatabase;
+ case ROLE:
+ return Feature.commentOnRole;
+ case TABLESPACE:
+ return Feature.commentOnTablespace;
+ case ACCESS_METHOD:
+ return Feature.commentOnAccessMethod;
+ case EXTENSION:
+ return Feature.commentOnExtension;
+ case FOREIGN_DATA_WRAPPER:
+ return Feature.commentOnForeignDataWrapper;
+ case LANGUAGE:
+ return Feature.commentOnLanguage;
+ case SERVER:
+ return Feature.commentOnServer;
+ case EVENT_TRIGGER:
+ return Feature.commentOnEventTrigger;
+ case PUBLICATION:
+ return Feature.commentOnPublication;
+ case SUBSCRIPTION:
+ return Feature.commentOnSubscription;
+ case OPERATOR:
+ return Feature.commentOnOperator;
+ case OPERATOR_CLASS:
+ return Feature.commentOnOperatorClass;
+ case OPERATOR_FAMILY:
+ return Feature.commentOnOperatorFamily;
+ case CONVERSION:
+ return Feature.commentOnConversion;
+ case LARGE_OBJECT:
+ return Feature.commentOnLargeObject;
+ case TRANSFORM:
+ return Feature.commentOnTransform;
+ case TEXT_SEARCH_CONFIGURATION:
+ return Feature.commentOnTextSearchConfiguration;
+ case TEXT_SEARCH_DICTIONARY:
+ return Feature.commentOnTextSearchDictionary;
+ case TEXT_SEARCH_PARSER:
+ return Feature.commentOnTextSearchParser;
+ case TEXT_SEARCH_TEMPLATE:
+ return Feature.commentOnTextSearchTemplate;
default:
throw new IllegalArgumentException("Unknown COMMENT target: " + kind);
}
diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
index 2210dbe9d..6199d7329 100644
--- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
+++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
@@ -20291,38 +20291,137 @@ Table CommentObjectName():
: new Table(names.getNames()); }
}
+/** The object kind decides the shape of the reference that follows it. */
CommentTarget CommentTarget():
{
CommentTarget result = new CommentTarget();
+ CommentTarget.Kind kind;
Table name;
- String constraintName;
+ String objectName;
RoutineReference routine;
+ ColDataType type;
+ ExclusionOperator operator;
+ Long oid;
}
{
+ kind=CommentTargetKind(result) { result.setKind(kind); }
(
- routine=RoutineReference(false)
- { result.setKind(CommentTarget.Kind.FUNCTION); result.setRoutine(routine); }
+ LOOKAHEAD({ kind.isRoutine() })
+ routine=RoutineReference(kind == CommentTarget.Kind.AGGREGATE) { result.setRoutine(routine); }
|
- constraintName=RelObjectName()
- { result.setKind(CommentTarget.Kind.CONSTRAINT); result.setName(new Table(constraintName, false)); }
+ LOOKAHEAD({ kind.isOwnedByRelation() })
+ objectName=RelObjectName() { result.setName(new Table(objectName, false)); }
- [ LOOKAHEAD({ getToken(1).kind == K_DOMAIN && !".".equals(getToken(2).image)
- && getToken(2).kind != K_IS }) { result.setOnDomain(true); } ]
+ [ LOOKAHEAD({ kind == CommentTarget.Kind.CONSTRAINT && getToken(1).kind == K_DOMAIN
+ && !".".equals(getToken(2).image) && getToken(2).kind != K_IS })
+ { result.setOnDomain(true); } ]
name=CommentObjectName() { result.setRelation(name); }
|
- (
- { result.setKind(CommentTarget.Kind.INDEX); }
- | { result.setKind(CommentTarget.Kind.SCHEMA); }
- | { result.setKind(CommentTarget.Kind.SEQUENCE); }
- | { result.setKind(CommentTarget.Kind.MATERIALIZED_VIEW); }
- | { result.setKind(CommentTarget.Kind.DOMAIN); }
- | { result.setKind(CommentTarget.Kind.TYPE); }
- )
+ LOOKAHEAD({ kind == CommentTarget.Kind.CAST })
+ "(" type=ColDataType() { result.setSourceType(type); }
+ type=ColDataType() { result.setTargetType(type); } ")"
+ |
+ LOOKAHEAD({ kind == CommentTarget.Kind.OPERATOR })
+ operator=PostgreSqlOperatorReference() { result.setOperator(operator); }
+ "(" type=ExtensionOperatorType() { result.setLeftOperandType(type); }
+ "," type=ExtensionOperatorType() { result.setRightOperandType(type); } ")"
+ { requireTypeDdlSyntax(result.getLeftOperandType() != null || result.getRightOperandType() != null,
+ "An operator signature needs an operand type"); }
+ |
+ LOOKAHEAD({ kind.isOwnedByAccessMethod() })
+ name=CommentObjectName() { result.setName(name); }
+ objectName=RelObjectName() { result.setAccessMethod(objectName); }
+ |
+ LOOKAHEAD({ kind == CommentTarget.Kind.LARGE_OBJECT })
+ oid=ObjectIdentifier() { result.setLargeObjectOid(oid); }
+ |
+ LOOKAHEAD({ kind == CommentTarget.Kind.TRANSFORM })
+ type=ColDataType() { result.setTransformType(type); }
+ ContextualKeyword("LANGUAGE") objectName=RelObjectName() { result.setLanguage(objectName); }
+ |
name=CommentObjectName() { result.setName(name); }
)
{ return result; }
}
+/** Kind words without a token are matched contextually and remain ordinary identifiers. */
+CommentTarget.Kind CommentTargetKind(CommentTarget target):
+{ CommentTarget.Kind kind; }
+{
+ (
+ { kind = CommentTarget.Kind.INDEX; }
+ | { kind = CommentTarget.Kind.SCHEMA; }
+ | { kind = CommentTarget.Kind.SEQUENCE; }
+ | { kind = CommentTarget.Kind.MATERIALIZED_VIEW; }
+ | { kind = CommentTarget.Kind.DOMAIN; }
+ | { kind = CommentTarget.Kind.TYPE; }
+ | { kind = CommentTarget.Kind.FUNCTION; }
+ | { kind = CommentTarget.Kind.CONSTRAINT; }
+ | { kind = CommentTarget.Kind.TRIGGER; }
+ | { kind = CommentTarget.Kind.POLICY; }
+ | LOOKAHEAD({ isKeywordAhead("RULE") }) ContextualKeyword("RULE") { kind = CommentTarget.Kind.RULE; }
+ | { kind = CommentTarget.Kind.PROCEDURE; }
+ | LOOKAHEAD({ isKeywordAhead("ROUTINE") }) ContextualKeyword("ROUTINE") { kind = CommentTarget.Kind.ROUTINE; }
+ | { kind = CommentTarget.Kind.AGGREGATE; }
+ | { kind = CommentTarget.Kind.CAST; }
+ | LOOKAHEAD({ isKeywordAhead("ACCESS") }) ContextualKeyword("ACCESS") ContextualKeyword("METHOD")
+ { kind = CommentTarget.Kind.ACCESS_METHOD; }
+ | LOOKAHEAD({ isKeywordAhead("COLLATION") }) ContextualKeyword("COLLATION") { kind = CommentTarget.Kind.COLLATION; }
+ | { kind = CommentTarget.Kind.DATABASE; }
+ | { kind = CommentTarget.Kind.EVENT_TRIGGER; }
+ | LOOKAHEAD({ isKeywordAhead("EXTENSION") }) ContextualKeyword("EXTENSION") { kind = CommentTarget.Kind.EXTENSION; }
+ |
+ (
+ ContextualKeyword("WRAPPER") { kind = CommentTarget.Kind.FOREIGN_DATA_WRAPPER; }
+ | { kind = CommentTarget.Kind.FOREIGN_TABLE; }
+ )
+ | LOOKAHEAD({ isKeywordAhead("PROCEDURAL") || isKeywordAhead("LANGUAGE") })
+ [ LOOKAHEAD({ isKeywordAhead("PROCEDURAL") }) ContextualKeyword("PROCEDURAL") { target.setProcedural(true); } ]
+ ContextualKeyword("LANGUAGE") { kind = CommentTarget.Kind.LANGUAGE; }
+ | LOOKAHEAD({ isKeywordAhead("PUBLICATION") }) ContextualKeyword("PUBLICATION") { kind = CommentTarget.Kind.PUBLICATION; }
+ | LOOKAHEAD({ isKeywordAhead("ROLE") }) ContextualKeyword("ROLE") { kind = CommentTarget.Kind.ROLE; }
+ | LOOKAHEAD({ isKeywordAhead("SERVER") }) ContextualKeyword("SERVER") { kind = CommentTarget.Kind.SERVER; }
+ | LOOKAHEAD({ isKeywordAhead("STATISTICS") }) ContextualKeyword("STATISTICS") { kind = CommentTarget.Kind.STATISTICS; }
+ | LOOKAHEAD({ isKeywordAhead("SUBSCRIPTION") }) ContextualKeyword("SUBSCRIPTION") { kind = CommentTarget.Kind.SUBSCRIPTION; }
+ | { kind = CommentTarget.Kind.TABLESPACE; }
+ | LOOKAHEAD({ isKeywordAhead("OPERATOR") }) ContextualKeyword("OPERATOR") { kind = CommentTarget.Kind.OPERATOR; }
+ // a schema named CLASS or FAMILY may qualify an operator, as in class.+
+ [ LOOKAHEAD({ (isKeywordAhead("CLASS") || isKeywordAhead("FAMILY")) && !".".equals(getToken(2).image) })
+ ( LOOKAHEAD({ isKeywordAhead("CLASS") }) ContextualKeyword("CLASS") { kind = CommentTarget.Kind.OPERATOR_CLASS; }
+ | ContextualKeyword("FAMILY") { kind = CommentTarget.Kind.OPERATOR_FAMILY; } ) ]
+ | LOOKAHEAD({ isKeywordAhead("LARGE") }) ContextualKeyword("LARGE") ContextualKeyword("OBJECT")
+ { kind = CommentTarget.Kind.LARGE_OBJECT; }
+ | LOOKAHEAD({ isKeywordAhead("CONVERSION") }) ContextualKeyword("CONVERSION") { kind = CommentTarget.Kind.CONVERSION; }
+ | LOOKAHEAD({ isKeywordAhead("TRANSFORM") }) ContextualKeyword("TRANSFORM") { kind = CommentTarget.Kind.TRANSFORM; }
+ | LOOKAHEAD({ isKeywordAhead("TEXT") })
+ (
+ { kind = CommentTarget.Kind.TEXT_SEARCH_PARSER; }
+ | LOOKAHEAD({ isKeywordAhead("CONFIGURATION") }) ContextualKeyword("CONFIGURATION")
+ { kind = CommentTarget.Kind.TEXT_SEARCH_CONFIGURATION; }
+ | LOOKAHEAD({ isKeywordAhead("DICTIONARY") }) ContextualKeyword("DICTIONARY")
+ { kind = CommentTarget.Kind.TEXT_SEARCH_DICTIONARY; }
+ | ContextualKeyword("TEMPLATE") { kind = CommentTarget.Kind.TEXT_SEARCH_TEMPLATE; }
+ )
+ )
+ { return kind; }
+}
+
+/** An unsigned 32-bit PostgreSQL object identifier, such as the OID of a large object. */
+Long ObjectIdentifier():
+{ Token token; long value; }
+{
+ token=
+ {
+ try {
+ value = Long.parseLong(token.image);
+ } catch (NumberFormatException exception) {
+ value = -1;
+ }
+ requireTypeDdlSyntax(value >= 0 && value <= 0xFFFFFFFFL, "OID out of range: " + token.image);
+ return value;
+ }
+}
+
/** Consumes a keyword that remains an ordinary identifier outside this grammar context. */
void ContextualKeyword(String expected):
{ Token keyword; }
@@ -21427,7 +21526,9 @@ RoutineReference RoutineReference(boolean aggregate):
(
"*" { requireTypeDdlSyntax(aggregate, "Only aggregate signatures accept *"); result.setAllArguments(true); }
|
- [ argument=RoutineSignatureArgument() { arguments.add(argument); }
+ // ORDER is non-reserved, so ORDER BY without direct arguments is not an argument name
+ [ LOOKAHEAD(1, { getToken(1).kind != K_ORDER || getToken(2).kind != K_BY })
+ argument=RoutineSignatureArgument() { arguments.add(argument); }
( "," argument=RoutineSignatureArgument() { arguments.add(argument); } )* ]
[ { requireTypeDdlSyntax(aggregate, "ORDER BY requires an aggregate signature"); orderBy = new ArrayList(); }
argument=RoutineSignatureArgument() { orderBy.add(argument); }
@@ -21445,15 +21546,49 @@ String ExtensionOperatorName():
/** Shared schema-qualified operator spelling used in DDL references. */
ExclusionOperator PostgreSqlOperatorReference():
-{ String schema = null; Token token; }
+{ String schema = null; String name; }
{
[ LOOKAHEAD(RelObjectName() ".") schema=RelObjectName() "." ]
- ( token="+" | token="-" | token="*" | token="/" | token="%" | token="="
- | token= | token= | token="<" | token=">"
- | token= | token= | token="&&" | token="@>" | token="<@" | token="~"
- | token="~*" | token="!~" | token="!~*" | token="?" | token="?|" | token="?&"
- | token= )
- { return new ExclusionOperator().withSchemaName(schema).withName(token.image); }
+ name=PostgreSqlOperatorName()
+ { return new ExclusionOperator().withSchemaName(schema).withName(name); }
+}
+
+/**
+ * The lexer splits some PostgreSQL operators, such as <<= or -|-, into several tokens. Parts that
+ * touch each other are joined, because PostgreSQL reads them as one operator.
+ */
+String PostgreSqlOperatorName():
+{ StringBuilder name = new StringBuilder(); Token token; }
+{
+ token=PostgreSqlOperatorPart() { name.append(token.image); }
+ (
+ LOOKAHEAD(1, { getToken(1).specialToken == null && getToken(1).beginLine == getToken(0).endLine
+ && getToken(1).beginColumn == getToken(0).endColumn + 1 })
+ token=PostgreSqlOperatorPart() { name.append(token.image); }
+ )*
+ { return name.toString(); }
+}
+
+Token PostgreSqlOperatorPart():
+{ Token token; }
+{
+ (
+ token="+" | token="-" | token="*" | token="/" | token="%" | token= | token=
+ | token="<" | token=">" | token="=" | token="==" | token= | token=
+ | token= | token= | token= | token="*=" | token="=*"
+ | token= | token= | token= | token="|>" | token="&" | token=
+ | token= | token= | token= | token=
+ | token="~" | token="~*" | token="!~" | token="!~*" | token="~~" | token="~~*" | token="!~~" | token="!~~*"
+ | token="~~~" | token="!" | token= | token="@>" | token="<@" | token="@@" | token=
+ | token="?" | token="?|" | token="?&" | token="->" | token="->>" | token= | token="#>"
+ | token="#>>" | token= | token="-#" | token="<->" | token="<#>"
+ )
+ {
+ // PostgreSQL reads "< =" as two operators, while the lexer tolerates whitespace inside it
+ requireTypeDdlSyntax(!token.image.matches("(?s).*\\s.*"),
+ "Unexpected whitespace in operator " + token.image);
+ return token;
+ }
}
ColDataType ExtensionOperatorType():
diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst
index 901608d62..15fc423bc 100644
--- a/src/site/sphinx/usage.rst
+++ b/src/site/sphinx/usage.rst
@@ -1913,36 +1913,78 @@ replacing the expression list clears them. Validators report the separate
PostgreSQL COMMENT targets
=========================
-``COMMENT ON`` supports ``INDEX``, ``SCHEMA``, ``SEQUENCE``, ``DOMAIN``, ``TYPE``,
-``MATERIALIZED VIEW``, ``FUNCTION`` and ``CONSTRAINT`` in addition to the existing
-``TABLE``, ``COLUMN`` and ``VIEW`` forms. These unambiguous target forms also parse
-without a dialect preset. Tagged dollar strings still require
-``Dialect.POSTGRESQL`` or ``withDollarQuotedStringTags(true)``.
+``COMMENT ON`` supports these PostgreSQL object kinds in addition to the existing
+``TABLE``, ``COLUMN`` and ``VIEW`` forms:
+
+- ``INDEX``, ``SCHEMA``, ``SEQUENCE``, ``DOMAIN``, ``TYPE``, ``MATERIALIZED VIEW``,
+ ``FOREIGN TABLE``, ``COLLATION``, ``STATISTICS``, ``CONVERSION`` and
+ ``TEXT SEARCH {CONFIGURATION | DICTIONARY | PARSER | TEMPLATE}``
+- ``ACCESS METHOD``, ``DATABASE``, ``EVENT TRIGGER``, ``EXTENSION``,
+ ``FOREIGN DATA WRAPPER``, ``[PROCEDURAL] LANGUAGE``, ``PUBLICATION``, ``ROLE``,
+ ``SERVER``, ``SUBSCRIPTION`` and ``TABLESPACE``
+- ``FUNCTION``, ``PROCEDURE``, ``ROUTINE`` and ``AGGREGATE`` signatures
+- ``CONSTRAINT``, ``TRIGGER``, ``POLICY`` and ``RULE`` with their owning table
+- ``CAST (source_type AS target_type)``
+- ``OPERATOR [schema.]op (left_type, right_type)``, where ``NONE`` marks the missing
+ operand of a unary operator
+- ``OPERATOR CLASS`` and ``OPERATOR FAMILY`` with ``USING access_method``
+- ``LARGE OBJECT oid`` and ``TRANSFORM FOR type_name LANGUAGE lang_name``
+
+These unambiguous target forms also parse without a dialect preset. Kind words such as
+``ROLE``, ``SERVER``, ``RULE``, ``OPERATOR`` or ``CONVERSION`` are not reserved and
+remain ordinary identifiers.
+Tagged dollar strings still require ``Dialect.POSTGRESQL`` or
+``withDollarQuotedStringTags(true)``.
The additional targets are exposed through ``Comment.getTarget()`` as a
``CommentTarget``. Its ``Kind`` identifies the object, and ``getName()`` preserves
the individual identifier components. ``Table`` is used as the name container;
an index or type name does not thereby represent a table dependency.
+``isProcedural()`` records ``PROCEDURAL LANGUAGE``.
The original ``Comment.getTable()``, ``getColumn()`` and ``getView()`` accessors
continue to describe their respective existing forms.
``COMMENT ON FUNCTION app.f(IN value integer) IS 'description'`` uses the shared
-``RoutineReference`` in ``getTarget().getRoutine()``. Argument mode, optional name
+``RoutineReference`` in ``getTarget().getRoutine()``; ``Kind.isRoutine()`` identifies
+these kinds and ``getName()`` is null. Argument mode, optional name
and ``ColDataType`` are preserved. An omitted signature has null arguments;
-``f()`` has an empty argument list. The signature identifies a function and is not
-a function call.
+``f()`` has an empty argument list. The signature identifies a routine and is not
+a function call. An ``AGGREGATE`` requires a signature and also accepts ``(*)`` and
+ordered-set signatures such as ``(float8 ORDER BY integer)`` or ``(ORDER BY integer)``.
For ``COMMENT ON CONSTRAINT ck ON app.t IS NULL``, ``getName()`` is the constraint
-name and ``getRelation()`` is its owning table. ``ON DOMAIN app.d`` sets
-``isOnDomain()`` and stores the domain as the owner instead. ``NULL`` removes the
-comment and remains represented by a null ``Comment.getComment()``.
-
-Table discovery visits tables, column owners, views, materialized views and
-table-owned constraints. It does not infer an index's table or treat domains,
-sequences, types or functions as tables. Statement visitors visit comment
-literals, and custom SQL deparsers can replace the explicit relation or literal.
-Feature analysis reports a schema modification. Validation exposes a separate
-``commentOn...`` capability for each additional target kind.
+name and ``getRelation()`` is its owning table. ``TRIGGER``, ``POLICY`` and ``RULE``
+targets use the same fields; ``Kind.isOwnedByRelation()`` identifies these kinds.
+Only a constraint accepts ``ON DOMAIN app.d``, which sets ``isOnDomain()`` and stores
+the domain as the owner instead. A ``CAST`` target exposes ``getSourceType()`` and
+``getTargetType()``. ``NULL`` removes the comment and remains represented by a null
+``Comment.getComment()``.
+
+``COMMENT ON OPERATOR app.-> (jsonb, NONE)`` reads the operator with the reference
+shared by exclusion constraints and ``ALTER EXTENSION``: ``getOperator()`` returns an
+``ExclusionOperator`` with ``getSchemaName()`` and the operator symbol in ``getName()``.
+``getLeftOperandType()`` and ``getRightOperandType()`` hold the operand types, and null
+stands for ``NONE``; at least one operand type is required. The lexer reads some
+PostgreSQL operators, such as ``<<=`` or ``-|-``, as several tokens. Parts that touch
+each other are joined into one operator, while whitespace or a comment separates two
+operators as in PostgreSQL. Exclusion constraints and ``ALTER EXTENSION`` accept the same
+operators. Operators that the lexer reads as part of an identifier, such as ``##``
+without ``Dialect.POSTGRESQL``, are not supported.
+
+``OPERATOR CLASS`` and ``OPERATOR FAMILY`` targets keep their name in ``getName()``
+and the index access method in ``getAccessMethod()``; ``Kind.isOwnedByAccessMethod()``
+identifies these kinds. ``LARGE OBJECT`` exposes ``getLargeObjectOid()``, which
+accepts an unsigned 32-bit integer. ``TRANSFORM`` exposes ``getTransformType()`` and
+``getLanguage()``. Each ``TEXT SEARCH`` object type has its own ``Kind``, for example
+``TEXT_SEARCH_DICTIONARY``.
+
+Table discovery visits tables, column owners, views, materialized views, foreign
+tables and the tables owning constraints, triggers, policies and rules. It does not
+infer an index's table or treat domains, sequences, types, routines or other catalog
+objects as tables. Statement visitors visit comment literals, and custom SQL deparsers
+can replace the explicit relation or literal. Feature analysis reports a schema
+modification. Validation exposes a separate ``commentOn...`` capability for each
+additional target kind.
Current date/time expression metadata
=====================================
diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlExclusionOperatorTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlExclusionOperatorTest.java
index 8be4883a1..91490616b 100644
--- a/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlExclusionOperatorTest.java
+++ b/src/test/java/net/sf/jsqlparser/statement/alter/PostgreSqlExclusionOperatorTest.java
@@ -26,7 +26,7 @@
class PostgreSqlExclusionOperatorTest {
@ParameterizedTest
@ValueSource(strings = {"OPERATOR(pg_catalog.&&)", "OPERATOR(&&)", "OPERATOR(\"My.Schema\".&&)",
- "OPERATOR(pg_catalog.=)", "&&"})
+ "OPERATOR(pg_catalog.=)", "OPERATOR(pg_catalog.-|-)", "OPERATOR(<<=)", "&&"})
void supportsSharedCreateAndAlterGrammar(String operator) throws JSQLParserException {
for (String sql : new String[] {
"CREATE TABLE t (c INT4RANGE, CONSTRAINT ex EXCLUDE USING gist (c WITH " + operator
diff --git a/src/test/java/net/sf/jsqlparser/statement/comment/CommentTargetTest.java b/src/test/java/net/sf/jsqlparser/statement/comment/CommentTargetTest.java
index 67f2dddf3..e48148fac 100644
--- a/src/test/java/net/sf/jsqlparser/statement/comment/CommentTargetTest.java
+++ b/src/test/java/net/sf/jsqlparser/statement/comment/CommentTargetTest.java
@@ -14,7 +14,9 @@
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
import java.util.ArrayList;
+import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
@@ -29,6 +31,8 @@
import net.sf.jsqlparser.statement.StatementVisitorAdapter;
import net.sf.jsqlparser.statement.Statements;
import net.sf.jsqlparser.statement.StmtFeature;
+import net.sf.jsqlparser.statement.create.table.ColDataType;
+import net.sf.jsqlparser.statement.create.table.ExclusionOperator;
import net.sf.jsqlparser.statement.select.SelectVisitorAdapter;
import net.sf.jsqlparser.util.TablesNamesFinder;
import net.sf.jsqlparser.util.deparser.ExpressionDeParser;
@@ -41,15 +45,32 @@
import org.junit.jupiter.params.provider.ValueSource;
class CommentTargetTest {
+ // PostgreSQL accepts schema-qualified names only for these kinds
+ private static final Set SCHEMA_QUALIFIED = EnumSet.of(
+ CommentTarget.Kind.INDEX, CommentTarget.Kind.SEQUENCE, CommentTarget.Kind.DOMAIN,
+ CommentTarget.Kind.TYPE, CommentTarget.Kind.MATERIALIZED_VIEW,
+ CommentTarget.Kind.FOREIGN_TABLE, CommentTarget.Kind.COLLATION,
+ CommentTarget.Kind.STATISTICS, CommentTarget.Kind.CONVERSION,
+ CommentTarget.Kind.TEXT_SEARCH_CONFIGURATION, CommentTarget.Kind.TEXT_SEARCH_DICTIONARY,
+ CommentTarget.Kind.TEXT_SEARCH_PARSER, CommentTarget.Kind.TEXT_SEARCH_TEMPLATE);
+
+ private static final Set UNQUALIFIED = EnumSet.of(
+ CommentTarget.Kind.SCHEMA, CommentTarget.Kind.ACCESS_METHOD,
+ CommentTarget.Kind.DATABASE, CommentTarget.Kind.EVENT_TRIGGER,
+ CommentTarget.Kind.EXTENSION, CommentTarget.Kind.FOREIGN_DATA_WRAPPER,
+ CommentTarget.Kind.LANGUAGE, CommentTarget.Kind.PUBLICATION, CommentTarget.Kind.ROLE,
+ CommentTarget.Kind.SERVER, CommentTarget.Kind.SUBSCRIPTION,
+ CommentTarget.Kind.TABLESPACE);
+
static Stream names() {
List cases = new ArrayList<>();
- for (CommentTarget.Kind kind : List.of(CommentTarget.Kind.INDEX, CommentTarget.Kind.SCHEMA,
- CommentTarget.Kind.SEQUENCE, CommentTarget.Kind.DOMAIN, CommentTarget.Kind.TYPE,
- CommentTarget.Kind.MATERIALIZED_VIEW)) {
+ Set kinds = EnumSet.copyOf(SCHEMA_QUALIFIED);
+ kinds.addAll(UNQUALIFIED);
+ for (CommentTarget.Kind kind : kinds) {
for (String name : List.of("obj", "\"a.b\"", "\"a\"\"b.c\"", "\"한.글\"", "\".\"")) {
cases.add(Arguments.of(kind, name, null, name));
}
- if (kind != CommentTarget.Kind.SCHEMA) {
+ if (SCHEMA_QUALIFIED.contains(kind)) {
cases.add(Arguments.of(kind, "public.\"a.b\"", "public", "\"a.b\""));
cases.add(Arguments.of(kind, "\"s.p\".\"a.b\"", "\"s.p\"", "\"a.b\""));
}
@@ -82,14 +103,27 @@ void preservesNamedTargetsAndTheirComponents(CommentTarget.Kind kind, String nam
}
}
+ static Stream signatures() {
+ List cases = new ArrayList<>();
+ for (CommentTarget.Kind kind : List.of(CommentTarget.Kind.FUNCTION,
+ CommentTarget.Kind.PROCEDURE, CommentTarget.Kind.ROUTINE)) {
+ for (String signature : List.of("f", "f()", "f(integer)", "f(IN arg integer)",
+ "f(OUT result text)", "f(INOUT arg integer)", "f(VARIADIC args text[])",
+ "\"s.p\".\"f.x\"(IN \"arg.x\" pg_catalog.int4[])",
+ "f(timestamp with time zone, double precision)")) {
+ cases.add(Arguments.of(kind, signature));
+ }
+ }
+ return cases.stream();
+ }
+
@ParameterizedTest
- @ValueSource(strings = {"f", "f()", "f(integer)", "f(IN arg integer)",
- "f(OUT result text)", "f(INOUT arg integer)", "f(VARIADIC args text[])",
- "\"s.p\".\"f.x\"(IN \"arg.x\" pg_catalog.int4[])",
- "f(timestamp with time zone, double precision)"})
- void preservesRoutineSignaturesWithoutTreatingThemAsCalls(String signature) throws Exception {
- for (Comment comment : roundTrip("COMMENT ON FUNCTION " + signature + " IS 'body'")) {
- assertEquals(CommentTarget.Kind.FUNCTION, comment.getTarget().getKind());
+ @MethodSource("signatures")
+ void preservesRoutineSignaturesWithoutTreatingThemAsCalls(CommentTarget.Kind kind,
+ String signature) throws Exception {
+ for (Comment comment : roundTrip("COMMENT ON " + kind + " " + signature + " IS 'body'")) {
+ assertEquals(kind, comment.getTarget().getKind());
+ assertNull(comment.getTarget().getName());
RoutineReference routine = comment.getTarget().getRoutine();
assertEquals(
signature.contains("(") ? signature.substring(0, signature.indexOf('('))
@@ -117,6 +151,41 @@ void exposesArgumentModeNameAndDataType() throws Exception {
assertEquals("text[]", arguments.get(1).getDataType().toString());
}
+ @Test
+ void preservesAggregateSignatures() throws Exception {
+ RoutineReference star = aggregate("app.agg(*)");
+ assertEquals("app.agg", star.getName());
+ assertTrue(star.isAllArguments());
+ assertNull(star.getOrderByArguments());
+
+ RoutineReference direct = aggregate("agg(integer, text)");
+ assertFalse(direct.isAllArguments());
+ assertEquals(2, direct.getArguments().size());
+ assertNull(direct.getOrderByArguments());
+
+ RoutineReference ordered = aggregate("rank(VARIADIC \"any\" ORDER BY VARIADIC \"any\")");
+ assertEquals(RoutineReference.Argument.Mode.VARIADIC,
+ ordered.getArguments().get(0).getMode());
+ assertEquals(RoutineReference.Argument.Mode.VARIADIC,
+ ordered.getOrderByArguments().get(0).getMode());
+
+ // ORDER is not reserved, so it must not be taken for the name of a direct argument
+ RoutineReference orderedOnly = aggregate("mode(ORDER BY anyelement)");
+ assertTrue(orderedOnly.getArguments().isEmpty());
+ assertEquals("anyelement",
+ orderedOnly.getOrderByArguments().get(0).getDataType().toString());
+ }
+
+ private static RoutineReference aggregate(String signature) throws JSQLParserException {
+ List comments = roundTrip("COMMENT ON AGGREGATE " + signature + " IS NULL");
+ for (Comment comment : comments) {
+ assertEquals(CommentTarget.Kind.AGGREGATE, comment.getTarget().getKind());
+ assertEquals(signature, comment.getTarget().getRoutine().toString());
+ assertTrue(new TablesNamesFinder().getTables(comment).isEmpty());
+ }
+ return comments.get(0).getTarget().getRoutine();
+ }
+
@ParameterizedTest
@ValueSource(strings = {"t", "\"t.x\"", "public.\"t.x\"", "\"s.p\".\"t.x\""})
void distinguishesConstraintTablesFromDomains(String owner) throws Exception {
@@ -146,12 +215,355 @@ void domainKeywordCanNameTheConstraintTable(String name) throws Exception {
}
}
+ static Stream relationOwnedTargets() {
+ List cases = new ArrayList<>();
+ for (CommentTarget.Kind kind : List.of(CommentTarget.Kind.TRIGGER,
+ CommentTarget.Kind.POLICY, CommentTarget.Kind.RULE)) {
+ for (String owner : List.of("t", "\"t.x\"", "public.\"t.x\"", "\"s.p\".\"t.x\"",
+ "domain")) {
+ cases.add(Arguments.of(kind, owner));
+ }
+ }
+ return cases.stream();
+ }
+
+ @ParameterizedTest
+ @MethodSource("relationOwnedTargets")
+ void namesTriggersPoliciesAndRulesWithinTheirTable(CommentTarget.Kind kind, String owner)
+ throws Exception {
+ for (Comment comment : roundTrip(
+ "COMMENT ON " + kind + " \"o.n\" ON " + owner + " IS 'body'")) {
+ CommentTarget target = comment.getTarget();
+ assertEquals(kind, target.getKind());
+ assertEquals("\"o.n\"", target.getName().getName());
+ assertNull(target.getName().getSchemaName());
+ assertFalse(target.isOnDomain());
+ assertEquals(owner, target.getRelation().getFullyQualifiedName());
+ assertEquals(Set.of(owner), new TablesNamesFinder().getTables(comment));
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"bigint AS int4", "character varying AS text",
+ "text[] AS app.custom_type", "timestamp with time zone AS date",
+ "double precision AS time without time zone"})
+ void preservesCastSourceAndTargetTypes(String types) throws Exception {
+ for (Comment comment : roundTrip("COMMENT ON CAST (" + types + ") IS 'body'")) {
+ CommentTarget target = comment.getTarget();
+ assertEquals(CommentTarget.Kind.CAST, target.getKind());
+ assertNull(target.getName());
+ assertNull(target.getRoutine());
+ assertEquals(types, target.getSourceType() + " AS " + target.getTargetType());
+ assertTrue(new TablesNamesFinder().getTables(comment).isEmpty());
+ }
+ }
+
+ @Test
+ void recordsProceduralLanguageSpelling() throws Exception {
+ for (boolean procedural : List.of(false, true)) {
+ String sql = "COMMENT ON " + (procedural ? "PROCEDURAL " : "")
+ + "LANGUAGE plpgsql IS 'body'";
+ for (Comment comment : roundTrip(sql)) {
+ assertEquals(CommentTarget.Kind.LANGUAGE, comment.getTarget().getKind());
+ assertEquals(procedural, comment.getTarget().isProcedural());
+ assertEquals("plpgsql", comment.getTarget().getName().getName());
+ assertEquals(sql, comment.toString());
+ }
+ }
+ }
+
+ static Stream operators() {
+ List cases = new ArrayList<>();
+ // arithmetic, comparison, bitwise, string, pattern, JSON, array, range and geometric
+ for (String operator : List.of("+", "-", "*", "/", "%", "^", "|/", "||/", "@", "<", ">",
+ "=", "<=", ">=", "<>", "!=", "&", "|", "#", "~", "<<", ">>", "||", "~~", "~~*",
+ "!~~", "!~~*", "~*", "!~", "!~*", "^@", "->", "->>", "#>", "#>>", "#-", "@>", "<@",
+ "?", "?|", "?&", "@?", "@@", "@@@", "&&", "<->", "<#>", "<=>", "-|-", "<<=", ">>=",
+ "&<", "&>", "<<|", "|>>", "&<|", "|&>", "<^", ">^", "?#", "?-|", "?||", "~=",
+ "@-@", "##", "!!", "*=", "*<>", "~>=~")) {
+ cases.add(Arguments.of(operator, null, operator));
+ }
+ cases.add(Arguments.of("app.+", "app", "+"));
+ cases.add(Arguments.of("pg_catalog.||", "pg_catalog", "||"));
+ cases.add(Arguments.of("\"My.Schema\".->>", "\"My.Schema\"", "->>"));
+ // CLASS and FAMILY only start an operator class or family when no dot follows
+ cases.add(Arguments.of("class.<<=", "class", "<<="));
+ cases.add(Arguments.of("family.-|-", "family", "-|-"));
+ return cases.stream();
+ }
+
+ @ParameterizedTest
+ @MethodSource("operators")
+ void preservesOperatorNamesAndOperandTypes(String operator, String schema, String symbol)
+ throws Exception {
+ for (Comment comment : roundTrip(
+ "COMMENT ON OPERATOR " + operator + " (integer, app.t[]) IS 'body'")) {
+ CommentTarget target = comment.getTarget();
+ assertEquals(CommentTarget.Kind.OPERATOR, target.getKind());
+ assertNull(target.getName());
+ assertEquals(schema, target.getOperator().getSchemaName());
+ assertEquals(symbol, target.getOperator().getName());
+ assertFalse(target.getOperator().isUseOperatorKeyword());
+ assertEquals("integer", target.getLeftOperandType().toString());
+ assertEquals("app.t[]", target.getRightOperandType().toString());
+ assertTrue(new TablesNamesFinder().getTables(comment).isEmpty());
+ }
+ }
+
+ @Test
+ void joinsOnlyOperatorPartsThatTouch() throws Exception {
+ assertEquals("COMMENT ON OPERATOR <<= (inet, inet) IS NULL",
+ parse("COMMENT ON OPERATOR <<=(inet,inet) IS NULL").toString());
+ assertEquals("COMMENT ON OPERATOR - (NONE, integer) IS NULL",
+ parse("COMMENT ON OPERATOR -/* prefix */(NONE, integer) IS NULL").toString());
+ for (String operator : List.of("< =", "< >", "> =", "! =", "+ -", "+/**/-", "-\n|-")) {
+ assertThrows(JSQLParserException.class,
+ () -> parse("COMMENT ON OPERATOR " + operator + " (integer, integer) IS NULL"),
+ operator);
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"NONE, integer", "integer, NONE", "\"NONE\", none",
+ "timestamp with time zone, double precision", "NONE, character varying"})
+ void representsMissingOperandsAsNull(String operands) throws Exception {
+ String[] types = operands.split(", ");
+ for (Comment comment : roundTrip("COMMENT ON OPERATOR - (" + operands + ") IS NULL")) {
+ CommentTarget target = comment.getTarget();
+ assertEquals(types[0].equals("NONE") ? null : types[0],
+ target.getLeftOperandType() == null ? null
+ : target.getLeftOperandType().toString());
+ assertEquals(types[1].equalsIgnoreCase("NONE") ? null : types[1],
+ target.getRightOperandType() == null ? null
+ : target.getRightOperandType().toString());
+ assertEquals("OPERATOR - (" + operands.replace("none", "NONE") + ")",
+ target.toString());
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"int4_ops USING btree", "app.\"my.ops\" USING gist",
+ "\"s.p\".ops USING \"my am\"", "class USING family"})
+ void namesOperatorClassesAndFamiliesWithinTheirAccessMethod(String reference)
+ throws Exception {
+ String name = reference.substring(0, reference.indexOf(" USING "));
+ String method = reference.substring(reference.indexOf(" USING ") + 7);
+ for (CommentTarget.Kind kind : List.of(CommentTarget.Kind.OPERATOR_CLASS,
+ CommentTarget.Kind.OPERATOR_FAMILY)) {
+ String sql = "COMMENT ON " + kind.name().replace('_', ' ') + " " + reference
+ + " IS 'body'";
+ for (Comment comment : roundTrip(sql)) {
+ CommentTarget target = comment.getTarget();
+ assertEquals(kind, target.getKind());
+ assertEquals(name, target.getName().getFullyQualifiedName());
+ assertEquals(method, target.getAccessMethod());
+ assertNull(target.getRelation());
+ assertNull(target.getOperator());
+ assertEquals(sql, comment.toString());
+ assertTrue(new TablesNamesFinder().getTables(comment).isEmpty());
+ }
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"0", "4242", "0004242", "4294967295"})
+ void storesLargeObjectOids(String oid) throws Exception {
+ for (Comment comment : roundTrip("COMMENT ON LARGE OBJECT " + oid + " IS 'lo'")) {
+ CommentTarget target = comment.getTarget();
+ assertEquals(CommentTarget.Kind.LARGE_OBJECT, target.getKind());
+ assertEquals(Long.valueOf(oid), target.getLargeObjectOid());
+ assertNull(target.getName());
+ assertEquals("COMMENT ON LARGE OBJECT " + Long.valueOf(oid) + " IS 'lo'",
+ comment.toString());
+ assertTrue(new TablesNamesFinder().getTables(comment).isEmpty());
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"int LANGUAGE sql", "hstore LANGUAGE plpython3u",
+ "app.\"my.type\"[] LANGUAGE \"my lang\"",
+ "timestamp with time zone LANGUAGE plperl", "language LANGUAGE language"})
+ void preservesTransformTypeAndLanguage(String reference) throws Exception {
+ String type = reference.substring(0, reference.indexOf(" LANGUAGE "));
+ String language = reference.substring(reference.indexOf(" LANGUAGE ") + 10);
+ for (Comment comment : roundTrip("COMMENT ON TRANSFORM FOR " + reference + " IS NULL")) {
+ CommentTarget target = comment.getTarget();
+ assertEquals(CommentTarget.Kind.TRANSFORM, target.getKind());
+ assertEquals(type, target.getTransformType().toString());
+ assertEquals(language, target.getLanguage());
+ assertNull(target.getName());
+ assertNull(target.getSourceType());
+ assertTrue(new TablesNamesFinder().getTables(comment).isEmpty());
+ }
+ }
+
+ @Test
+ void kindPredicatesDescribeTheReferenceShape() {
+ Set routines = EnumSet.noneOf(CommentTarget.Kind.class);
+ Set owned = EnumSet.noneOf(CommentTarget.Kind.class);
+ Set accessMethod = EnumSet.noneOf(CommentTarget.Kind.class);
+ for (CommentTarget.Kind kind : CommentTarget.Kind.values()) {
+ if (kind.isRoutine()) {
+ routines.add(kind);
+ }
+ if (kind.isOwnedByRelation()) {
+ owned.add(kind);
+ }
+ if (kind.isOwnedByAccessMethod()) {
+ accessMethod.add(kind);
+ }
+ }
+ assertEquals(EnumSet.of(CommentTarget.Kind.FUNCTION, CommentTarget.Kind.PROCEDURE,
+ CommentTarget.Kind.ROUTINE, CommentTarget.Kind.AGGREGATE), routines);
+ assertEquals(EnumSet.of(CommentTarget.Kind.CONSTRAINT, CommentTarget.Kind.TRIGGER,
+ CommentTarget.Kind.POLICY, CommentTarget.Kind.RULE), owned);
+ assertEquals(EnumSet.of(CommentTarget.Kind.OPERATOR_CLASS,
+ CommentTarget.Kind.OPERATOR_FAMILY), accessMethod);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"COMMENT ON TRIGGER trg ON users IS 'x'",
+ "COMMENT ON POLICY p1 ON users IS 'x'", "COMMENT ON RULE r1 ON app.v IS NULL",
+ "COMMENT ON EXTENSION postgis IS 'x'", "COMMENT ON DATABASE mydb IS 'x'",
+ "COMMENT ON ROLE app_read IS 'x'", "COMMENT ON TABLESPACE pg_default IS ''",
+ "COMMENT ON EVENT TRIGGER et IS 'x'", "COMMENT ON PUBLICATION pub IS 'x'",
+ "COMMENT ON SUBSCRIPTION sub IS 'x'", "COMMENT ON SERVER srv IS 'x'",
+ "COMMENT ON FOREIGN DATA WRAPPER postgres_fdw IS 'x'",
+ "COMMENT ON FOREIGN TABLE app.ft IS 'x'",
+ "COMMENT ON PROCEDURAL LANGUAGE plpgsql IS 'x'",
+ "COMMENT ON COLLATION app.\"de-DE\" IS 'x'", "COMMENT ON ACCESS METHOD heap2 IS 'x'",
+ "COMMENT ON STATISTICS app.st IS 'x'",
+ "COMMENT ON PROCEDURE app.p(IN a integer, INOUT b text) IS 'x'",
+ "COMMENT ON ROUTINE r() IS 'x'",
+ "COMMENT ON AGGREGATE percentile_disc(float8 ORDER BY anyelement) IS 'x'",
+ "COMMENT ON CAST (bigint AS int4) IS 'x'",
+ "COMMENT ON OPERATOR + (integer, integer) IS 'op'",
+ "COMMENT ON OPERATOR - (NONE, integer) IS 'prefix op'",
+ "COMMENT ON OPERATOR pg_catalog.->> (jsonb, text) IS 'x'",
+ "COMMENT ON OPERATOR app.-|- (int4range, int4range) IS NULL",
+ "COMMENT ON OPERATOR CLASS int4_ops USING btree IS 'opclass'",
+ "COMMENT ON OPERATOR FAMILY integer_ops USING btree IS 'opfamily'",
+ "COMMENT ON LARGE OBJECT 4242 IS 'lo'",
+ "COMMENT ON TEXT SEARCH CONFIGURATION english IS 'tscfg'",
+ "COMMENT ON TEXT SEARCH DICTIONARY english_stem IS 'tsdict'",
+ "COMMENT ON TEXT SEARCH PARSER \"default\" IS 'tsparser'",
+ "COMMENT ON TEXT SEARCH TEMPLATE simple IS 'tstemplate'",
+ "COMMENT ON CONVERSION utf8_to_iso_8859_1 IS 'conversion'",
+ "COMMENT ON TRANSFORM FOR int LANGUAGE sql IS 'transform'"})
+ void parsesAndDeparsesWithAndWithoutPostgreSqlDialect(String sql) throws Exception {
+ assertSqlCanBeParsedAndDeparsed(sql, false,
+ parser -> parser.withDialect(Dialect.POSTGRESQL));
+ assertSqlCanBeParsedAndDeparsed(sql, false, null);
+ }
+
+ @Test
+ void kindWordsRemainIdentifiers() throws Exception {
+ assertSqlCanBeParsedAndDeparsed("SELECT role, rule, server, extension, routine, "
+ + "statistics, collation, publication, subscription, language, procedural, "
+ + "access, method, wrapper FROM extension");
+ Comment rule = roundTrip("COMMENT ON RULE rule ON rule IS NULL").get(0);
+ assertEquals("rule", rule.getTarget().getName().getName());
+ assertEquals("rule", rule.getTarget().getRelation().getName());
+ assertEquals("server", roundTrip("COMMENT ON SERVER server IS NULL").get(0).getTarget()
+ .getName().getName());
+ assertEquals("role", roundTrip("COMMENT ON TABLE role IS NULL").get(0).getTable()
+ .getName());
+ assertEquals("server.language", roundTrip("COMMENT ON COLUMN server.language IS NULL")
+ .get(0).getColumn().getFullyQualifiedName());
+ }
+
+ @Test
+ void operatorAndTextSearchKindWordsRemainIdentifiers() throws Exception {
+ assertSqlCanBeParsedAndDeparsed("SELECT operator, class, family, large, object, "
+ + "conversion, transform, configuration, dictionary, parser, template, search, "
+ + "none FROM operator");
+ assertEquals("operator", roundTrip("COMMENT ON TABLE operator IS NULL").get(0).getTable()
+ .getName());
+ assertEquals("conversion.transform",
+ roundTrip("COMMENT ON COLUMN conversion.transform IS NULL").get(0).getColumn()
+ .getFullyQualifiedName());
+ assertEquals("template", roundTrip("COMMENT ON TEXT SEARCH TEMPLATE template IS NULL")
+ .get(0).getTarget().getName().getName());
+ assertEquals("conversion", roundTrip("COMMENT ON CONVERSION conversion IS NULL").get(0)
+ .getTarget().getName().getName());
+ assertEquals("operator",
+ roundTrip("COMMENT ON OPERATOR operator.+ (operator, NONE) IS NULL")
+ .get(0).getTarget().getOperator().getSchemaName());
+ }
+
+ @Test
+ void rendersProgrammaticTargets() {
+ CommentTarget trigger = new CommentTarget();
+ trigger.setKind(CommentTarget.Kind.TRIGGER);
+ trigger.setName(new Table("trg"));
+ trigger.setRelation(new Table("app", "t"));
+ assertEquals("COMMENT ON TRIGGER trg ON app.t IS 'body'",
+ new Comment().withTarget(trigger).withComment(new StringValue("body")).toString());
+
+ CommentTarget cast = new CommentTarget();
+ cast.setKind(CommentTarget.Kind.CAST);
+ cast.setSourceType(new ColDataType("integer"));
+ cast.setTargetType(new ColDataType("text"));
+ assertEquals("COMMENT ON CAST (integer AS text) IS NULL",
+ new Comment().withTarget(cast).toString());
+
+ CommentTarget named = new CommentTarget();
+ named.setKind(CommentTarget.Kind.ROLE);
+ named.setName(new Table("r"));
+ named.setProcedural(true);
+ assertEquals("ROLE r", named.toString());
+ named.setKind(CommentTarget.Kind.LANGUAGE);
+ assertEquals("PROCEDURAL LANGUAGE r", named.toString());
+
+ CommentTarget operator = new CommentTarget();
+ operator.setKind(CommentTarget.Kind.OPERATOR);
+ operator.setOperator(new ExclusionOperator().withSchemaName("app").withName("->>"));
+ operator.setLeftOperandType(new ColDataType("jsonb"));
+ assertEquals("OPERATOR app.->> (jsonb, NONE)", operator.toString());
+
+ CommentTarget operatorClass = new CommentTarget();
+ operatorClass.setKind(CommentTarget.Kind.OPERATOR_FAMILY);
+ operatorClass.setName(new Table("app", "ops"));
+ operatorClass.setAccessMethod("gist");
+ assertEquals("OPERATOR FAMILY app.ops USING gist", operatorClass.toString());
+
+ CommentTarget largeObject = new CommentTarget();
+ largeObject.setKind(CommentTarget.Kind.LARGE_OBJECT);
+ largeObject.setLargeObjectOid(4242L);
+ assertEquals("COMMENT ON LARGE OBJECT 4242 IS NULL",
+ new Comment().withTarget(largeObject).toString());
+
+ CommentTarget transform = new CommentTarget();
+ transform.setKind(CommentTarget.Kind.TRANSFORM);
+ transform.setTransformType(new ColDataType("hstore"));
+ transform.setLanguage("plpython3u");
+ assertEquals("TRANSFORM FOR hstore LANGUAGE plpython3u", transform.toString());
+ }
+
@ParameterizedTest
@ValueSource(strings = {"INDEX", "MATERIALIZED obj", "MATERIALIZED VIEW", "FUNCTION f(*)",
"FUNCTION f(integer ORDER BY integer)", "FUNCTION f(IN)",
"FUNCTION f(x integer DEFAULT 1)",
"CONSTRAINT ck", "CONSTRAINT ck ON",
"CONSTRAINT public.ck ON t",
+ "TRIGGER trg", "TRIGGER trg ON", "TRIGGER app.trg ON t", "TRIGGER trg ON DOMAIN d",
+ "POLICY p", "RULE r ON DOMAIN d", "PROCEDURE p(*)",
+ "ROUTINE r(integer ORDER BY integer)", "FUNCTION f(ORDER BY integer)",
+ "AGGREGATE agg", "CAST (integer)", "CAST integer AS text", "CAST (integer AS text",
+ "FOREIGN ft", "FOREIGN DATA fdw", "EVENT et", "ACCESS heap", "ACCESS METHOD",
+ "PROCEDURAL plpgsql", "LANGUAGE", "ROLE",
+ "OPERATOR", "OPERATOR +", "OPERATOR + ()", "OPERATOR + (integer)",
+ "OPERATOR + (NONE, NONE)", "OPERATOR + (integer, integer, integer)",
+ "OPERATOR + integer, integer", "OPERATOR app (integer, integer)",
+ "OPERATOR app. (integer, integer)", "OPERATOR => (integer, integer)",
+ "OPERATOR := (integer, integer)", "OPERATOR OPERATOR(pg_catalog.+) (integer, integer)",
+ "OPERATOR CLASS ops", "OPERATOR CLASS ops USING", "OPERATOR FAMILY ops USING a.b",
+ "OPERATOR CLASS USING btree", "LARGE OBJECT", "LARGE 42", "LARGE OBJECT lo",
+ "LARGE OBJECT -1", "LARGE OBJECT 4294967296", "LARGE OBJECT 99999999999999999999",
+ "LARGE OBJECT 1.5", "LARGE OBJECT 1, 2", "TEXT SEARCH", "TEXT SEARCH english",
+ "TEXT SEARCH INDEX english", "TEXT CONFIGURATION english",
+ "TINYTEXT SEARCH PARSER p", "CONVERSION", "TRANSFORM", "TRANSFORM int LANGUAGE sql",
+ "TRANSFORM FOR int", "TRANSFORM FOR int LANGUAGE", "TRANSFORM FOR int LANGUAGE a.b",
"UNKNOWN obj"})
void rejectsIncompleteOrInvalidTargets(String target) {
assertThrows(JSQLParserException.class, () -> parse("COMMENT ON " + target + " IS NULL"));
@@ -160,12 +572,21 @@ void rejectsIncompleteOrInvalidTargets(String target) {
@Test
void findsOnlyRelationsExplicitlyNamedByComments() throws Exception {
for (String target : List.of("INDEX idx", "SCHEMA app", "SEQUENCE seq", "DOMAIN d",
- "TYPE ty", "FUNCTION f(int)")) {
+ "TYPE ty", "FUNCTION f(int)", "PROCEDURE app.p(int)", "ROUTINE r",
+ "AGGREGATE a(*)", "CAST (integer AS text)", "COLLATION app.c",
+ "STATISTICS app.st", "ACCESS METHOD am", "DATABASE d", "EVENT TRIGGER et",
+ "EXTENSION e", "FOREIGN DATA WRAPPER w", "LANGUAGE l", "PUBLICATION p", "ROLE r",
+ "SERVER s", "SUBSCRIPTION s", "TABLESPACE ts", "OPERATOR app.+ (integer, NONE)",
+ "OPERATOR CLASS app.t USING btree", "OPERATOR FAMILY app.t USING btree",
+ "LARGE OBJECT 42", "CONVERSION app.t", "TEXT SEARCH CONFIGURATION app.t",
+ "TEXT SEARCH DICTIONARY app.t", "TEXT SEARCH PARSER app.t",
+ "TEXT SEARCH TEMPLATE app.t", "TRANSFORM FOR app.t LANGUAGE sql")) {
assertTrue(new TablesNamesFinder().getTables(parse("COMMENT ON " + target + " IS NULL"))
.isEmpty());
}
for (String target : List.of("TABLE app.t", "COLUMN app.t.c", "VIEW app.t",
- "MATERIALIZED VIEW app.t", "CONSTRAINT ck ON app.t")) {
+ "MATERIALIZED VIEW app.t", "CONSTRAINT ck ON app.t", "FOREIGN TABLE app.t",
+ "TRIGGER trg ON app.t", "POLICY p ON app.t", "RULE r ON app.t")) {
assertEquals(Set.of("app.t"),
new TablesNamesFinder().getTables(parse("COMMENT ON " + target + " IS NULL")));
}
@@ -223,9 +644,11 @@ public Void visit(StringValue value, S context) {
assertEquals(List.of("body"), values);
}
- @Test
- void customDeparsersCanRewriteRelationsAndLiterals() throws Exception {
- Comment comment = parse("COMMENT ON CONSTRAINT ck ON app.t IS 'body'");
+ @ParameterizedTest
+ @ValueSource(strings = {"CONSTRAINT ck ON", "TRIGGER trg ON", "POLICY p ON", "RULE r ON",
+ "FOREIGN TABLE", "MATERIALIZED VIEW"})
+ void customDeparsersCanRewriteRelationsAndLiterals(String target) throws Exception {
+ Comment comment = parse("COMMENT ON " + target + " app.t IS 'body'");
StringBuilder builder = new StringBuilder();
ExpressionDeParser expressions = new ExpressionDeParser() {
@Override
@@ -242,8 +665,8 @@ public StringBuilder visit(Table table, S context) {
}
};
comment.accept(new StatementDeParser(expressions, selects, builder), "context");
- assertEquals("COMMENT ON CONSTRAINT ck ON app.new_t IS 'changed'", builder.toString());
- assertEquals("COMMENT ON CONSTRAINT ck ON app.t IS 'body'", comment.toString());
+ assertEquals("COMMENT ON " + target + " app.new_t IS 'changed'", builder.toString());
+ assertEquals("COMMENT ON " + target + " app.t IS 'body'", comment.toString());
}
private static Comment parse(String sql) throws JSQLParserException {
diff --git a/src/test/java/net/sf/jsqlparser/util/validation/validator/CommentValidatorTest.java b/src/test/java/net/sf/jsqlparser/util/validation/validator/CommentValidatorTest.java
index 276e416e9..5efa97751 100644
--- a/src/test/java/net/sf/jsqlparser/util/validation/validator/CommentValidatorTest.java
+++ b/src/test/java/net/sf/jsqlparser/util/validation/validator/CommentValidatorTest.java
@@ -9,8 +9,15 @@
*/
package net.sf.jsqlparser.util.validation.validator;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import java.util.EnumSet;
+import java.util.Set;
+import java.util.stream.Collectors;
import java.util.stream.Stream;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.parser.feature.Feature;
+import net.sf.jsqlparser.statement.comment.Comment;
+import net.sf.jsqlparser.statement.comment.CommentTarget;
import net.sf.jsqlparser.util.validation.ValidationTestAsserts;
import net.sf.jsqlparser.util.validation.feature.DatabaseType;
import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed;
@@ -29,7 +36,51 @@ static Stream targets() {
Arguments.of("MATERIALIZED VIEW mv", Feature.commentOnMaterializedView),
Arguments.of("FUNCTION f(IN x integer)", Feature.commentOnFunction),
Arguments.of("CONSTRAINT ck ON t", Feature.commentOnConstraint),
- Arguments.of("CONSTRAINT ck ON DOMAIN d", Feature.commentOnConstraint));
+ Arguments.of("CONSTRAINT ck ON DOMAIN d", Feature.commentOnConstraint),
+ Arguments.of("TRIGGER trg ON t", Feature.commentOnTrigger),
+ Arguments.of("POLICY p ON t", Feature.commentOnPolicy),
+ Arguments.of("RULE r ON v", Feature.commentOnRule),
+ Arguments.of("PROCEDURE p(integer)", Feature.commentOnProcedure),
+ Arguments.of("ROUTINE r", Feature.commentOnRoutine),
+ Arguments.of("AGGREGATE a(*)", Feature.commentOnAggregate),
+ Arguments.of("CAST (integer AS text)", Feature.commentOnCast),
+ Arguments.of("ACCESS METHOD am", Feature.commentOnAccessMethod),
+ Arguments.of("COLLATION c", Feature.commentOnCollation),
+ Arguments.of("DATABASE d", Feature.commentOnDatabase),
+ Arguments.of("EVENT TRIGGER et", Feature.commentOnEventTrigger),
+ Arguments.of("EXTENSION e", Feature.commentOnExtension),
+ Arguments.of("FOREIGN DATA WRAPPER w", Feature.commentOnForeignDataWrapper),
+ Arguments.of("FOREIGN TABLE ft", Feature.commentOnForeignTable),
+ Arguments.of("PROCEDURAL LANGUAGE l", Feature.commentOnLanguage),
+ Arguments.of("PUBLICATION p", Feature.commentOnPublication),
+ Arguments.of("ROLE r", Feature.commentOnRole),
+ Arguments.of("SERVER s", Feature.commentOnServer),
+ Arguments.of("STATISTICS st", Feature.commentOnStatistics),
+ Arguments.of("SUBSCRIPTION s", Feature.commentOnSubscription),
+ Arguments.of("TABLESPACE ts", Feature.commentOnTablespace),
+ Arguments.of("OPERATOR - (NONE, integer)", Feature.commentOnOperator),
+ Arguments.of("OPERATOR CLASS ops USING btree", Feature.commentOnOperatorClass),
+ Arguments.of("OPERATOR FAMILY ops USING gist", Feature.commentOnOperatorFamily),
+ Arguments.of("CONVERSION conv", Feature.commentOnConversion),
+ Arguments.of("LARGE OBJECT 4242", Feature.commentOnLargeObject),
+ Arguments.of("TRANSFORM FOR hstore LANGUAGE plpython3u",
+ Feature.commentOnTransform),
+ Arguments.of("TEXT SEARCH CONFIGURATION cfg",
+ Feature.commentOnTextSearchConfiguration),
+ Arguments.of("TEXT SEARCH DICTIONARY dict", Feature.commentOnTextSearchDictionary),
+ Arguments.of("TEXT SEARCH PARSER prs", Feature.commentOnTextSearchParser),
+ Arguments.of("TEXT SEARCH TEMPLATE tmpl", Feature.commentOnTextSearchTemplate));
+ }
+
+ @Test
+ void coversEveryTargetKind() throws Exception {
+ Set kinds = EnumSet.noneOf(CommentTarget.Kind.class);
+ for (Arguments arguments : targets().collect(Collectors.toList())) {
+ Comment comment = (Comment) CCJSqlParserUtil
+ .parse("COMMENT ON " + arguments.get()[0] + " IS NULL");
+ kinds.add(comment.getTarget().getKind());
+ }
+ assertEquals(EnumSet.allOf(CommentTarget.Kind.class), kinds);
}
@ParameterizedTest
diff --git a/src/test/resources/postgresql/type-ddl.sql b/src/test/resources/postgresql/type-ddl.sql
index 375b79446..943260e4d 100644
--- a/src/test/resources/postgresql/type-ddl.sql
+++ b/src/test/resources/postgresql/type-ddl.sql
@@ -82,3 +82,5 @@ ALTER EXTENSION hstore ADD TRANSFORM FOR hstore LANGUAGE plpython3u
ALTER EXTENSION hstore ADD OPERATOR app.&& (integer, integer)
ALTER EXTENSION hstore DROP OPERATOR = (hstore, hstore)
ALTER EXTENSION hstore ADD OPERATOR - (NONE, integer)
+ALTER EXTENSION hstore ADD OPERATOR app.-> (hstore, text)
+ALTER EXTENSION hstore DROP OPERATOR <<= (inet, inet)