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 f5908e641..64227fafb 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -578,7 +578,7 @@ public enum Feature { * * @see CreateSequence */ - createSequence, + createSequence, createAccessMethod, createAccessMethodTable, /** Publication and subscription definitions. */ createForeignDataWrapper, alterForeignDataWrapper, createServer, alterServer, createUserMapping, alterUserMapping, diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java index 9e9933dbd..033f9183d 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java @@ -9,6 +9,8 @@ */ package net.sf.jsqlparser.statement; +import net.sf.jsqlparser.statement.create.accessmethod.CreateAccessMethod; + import net.sf.jsqlparser.statement.oracle.OracleBlock; import net.sf.jsqlparser.statement.oracle.OracleAssignment; import net.sf.jsqlparser.statement.oracle.OracleNullStatement; @@ -1152,4 +1154,9 @@ public Void visit(OracleNullStatement statement, S context) { return null; } + + @Override + public Void visit(CreateAccessMethod statement, S context) { + return schemaOnly(); + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java index 1b75dbad5..767ae9a1b 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -9,6 +9,8 @@ */ package net.sf.jsqlparser.statement; +import net.sf.jsqlparser.statement.create.accessmethod.CreateAccessMethod; + import net.sf.jsqlparser.statement.create.fdw.CreateForeignDataWrapper; import net.sf.jsqlparser.statement.alter.AlterForeignDataWrapper; import net.sf.jsqlparser.statement.create.server.CreateServer; @@ -808,6 +810,14 @@ default void visit(AlterUserMapping statement) { visit(statement, null); } + default T visit(CreateAccessMethod statement, S context) { + return null; + } + + default void visit(CreateAccessMethod statement) { + visit(statement, null); + } + default T visit(CreateTextSearchConfiguration statement, S context) { return null; } diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java index eb98ad568..efb294f5f 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java @@ -9,6 +9,8 @@ */ package net.sf.jsqlparser.statement; +import net.sf.jsqlparser.statement.create.accessmethod.CreateAccessMethod; + import net.sf.jsqlparser.statement.oracle.OracleBlock; import net.sf.jsqlparser.statement.oracle.OracleAssignment; @@ -897,4 +899,9 @@ public T visit(OracleAssignment assignment, S context) { return null; } + + @Override + public T visit(CreateAccessMethod statement, S context) { + return null; + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/accessmethod/CreateAccessMethod.java b/src/main/java/net/sf/jsqlparser/statement/create/accessmethod/CreateAccessMethod.java new file mode 100644 index 000000000..21959d7fe --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/accessmethod/CreateAccessMethod.java @@ -0,0 +1,63 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.create.accessmethod; + +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** A PostgreSQL table or index access method, whose handler names a function, not a table. */ +public class CreateAccessMethod implements Statement { + public enum Type { + TABLE, INDEX + } + + private String name; + private Type type; + private String handler; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Type getType() { + return type; + } + + public void setType(Type type) { + this.type = type; + } + + public String getHandler() { + return handler; + } + + public void setHandler(String handler) { + this.handler = handler; + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } + + public StringBuilder appendTo(StringBuilder builder) { + return builder.append("CREATE ACCESS METHOD ").append(name) + .append(" TYPE ").append(type).append(" HANDLER ").append(handler); + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index ba4804f81..320fc0143 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -9,6 +9,8 @@ */ package net.sf.jsqlparser.util; +import net.sf.jsqlparser.statement.create.accessmethod.CreateAccessMethod; + import net.sf.jsqlparser.statement.create.fdw.CreateForeignDataWrapper; import net.sf.jsqlparser.statement.alter.AlterForeignDataWrapper; import net.sf.jsqlparser.statement.create.server.CreateServer; @@ -3027,6 +3029,11 @@ public Void visit(AlterUserMapping statement, S context) { return null; } + @Override + public Void visit(CreateAccessMethod statement, S context) { + return null; + } + @Override public Void visit(CreateTextSearchConfiguration statement, S context) { return null; diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java index cbd24953a..f1402feca 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -9,6 +9,8 @@ */ package net.sf.jsqlparser.util.deparser; +import net.sf.jsqlparser.statement.create.accessmethod.CreateAccessMethod; + import net.sf.jsqlparser.statement.create.fdw.CreateForeignDataWrapper; import net.sf.jsqlparser.statement.alter.AlterForeignDataWrapper; import net.sf.jsqlparser.statement.create.server.CreateServer; @@ -893,6 +895,11 @@ public StringBuilder visit(AlterUserMapping statement, S context) { expression -> expression.accept(expressionDeParser, context)); } + @Override + public StringBuilder visit(CreateAccessMethod statement, S context) { + return statement.appendTo(builder); + } + @Override public StringBuilder visit(CreateTextSearchConfiguration statement, S context) { return statement.appendTo(builder); diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java index c5561f8e6..b044cf22b 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java @@ -105,6 +105,7 @@ public class FeaturesAllowed implements FeatureSetValidation, ModifyableFeatureS Feature.createCollation, Feature.createSchema, Feature.createSequence, Feature.createTable, Feature.createForeignDataWrapper, Feature.createServer, Feature.createUserMapping, + Feature.createAccessMethod, Feature.createAccessMethodTable, Feature.createTableUnlogged, Feature.createTableCreateOptionStrings, Feature.createTableTableOptionStrings, Feature.createTableIfNotExists, Feature.createTableRowMovement, 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 1555fd246..2fceb8c8e 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 @@ -97,6 +97,7 @@ public enum PostgresqlVersion implements Version { // https://www.postgresql.org/docs/current/sql-createsequence.html Feature.createSequence, + Feature.createAccessMethod, Feature.createForeignDataWrapper, Feature.alterForeignDataWrapper, Feature.createServer, Feature.alterServer, Feature.createUserMapping, Feature.alterUserMapping, @@ -158,10 +159,13 @@ public enum PostgresqlVersion implements Version { Feature.commit, Feature.startTransaction, Feature.releaseSavepoint, Feature.transactionIsolationLevel, Feature.transactionDeferrable)), V11("11", V10.copy().getFeatures()), V12("12", - V11.copy().add(Feature.transactionChain).getFeatures()), V13( - "13", - V12.copy().getFeatures()), V14("14", - V13.copy().getFeatures()); + V11.copy() + .add(Feature.createAccessMethodTable, + Feature.transactionChain) + .getFeatures()), V13( + "13", + V12.copy().getFeatures()), V14("14", + V13.copy().getFeatures()); private Set features; private String versionString; diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java index 5bd622fe5..9d9193090 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java @@ -9,6 +9,8 @@ */ package net.sf.jsqlparser.util.validation.validator; +import net.sf.jsqlparser.statement.create.accessmethod.CreateAccessMethod; + import net.sf.jsqlparser.statement.create.fdw.CreateForeignDataWrapper; import net.sf.jsqlparser.statement.alter.AlterForeignDataWrapper; import net.sf.jsqlparser.statement.create.server.CreateServer; @@ -1115,6 +1117,15 @@ public Void visit(AlterUserMapping statement, S context) { return null; } + @Override + public Void visit(CreateAccessMethod statement, S context) { + validateFeature(Feature.createAccessMethod); + if (statement.getType() == CreateAccessMethod.Type.TABLE) { + validateFeature(Feature.createAccessMethodTable); + } + return null; + } + @Override public Void visit(CreateTextSearchConfiguration statement, S context) { validateFeature(Feature.createTextSearchConfiguration); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index b58adf55f..aa1cbc672 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -51,6 +51,7 @@ import net.sf.jsqlparser.statement.analyze.*; import net.sf.jsqlparser.statement.alter.*; import net.sf.jsqlparser.statement.create.fdw.*; import net.sf.jsqlparser.statement.create.server.CreateServer; +import net.sf.jsqlparser.statement.create.accessmethod.CreateAccessMethod; import net.sf.jsqlparser.statement.create.usermapping.CreateUserMapping; import net.sf.jsqlparser.statement.create.textsearch.CreateTextSearchConfiguration; import net.sf.jsqlparser.statement.create.collation.CreateCollation; @@ -19949,6 +19950,10 @@ Statement Create(): { [ { isUsingOrReplace = true; } ] ( + LOOKAHEAD({ isKeywordAhead("ACCESS") && "METHOD".equalsIgnoreCase(getToken(2).image) }) + { requireDdlSyntax(!isUsingOrReplace, "CREATE ACCESS METHOD does not support OR REPLACE"); } + statement = CreateAccessMethod() + | LOOKAHEAD( ) { requireDdlSyntax(!isUsingOrReplace, "OR REPLACE is not supported for foreign-data objects"); } statement = CreateForeignDataWrapper() | LOOKAHEAD({ isKeywordAhead("SERVER") }) { requireDdlSyntax(!isUsingOrReplace, "OR REPLACE is not supported for foreign-data objects"); } statement = CreateServer() @@ -21386,6 +21391,20 @@ AlterUserMapping AlterUserMapping(): options=ForeignDataOptions(true) { statement.setOptions(options); return statement; } } +CreateAccessMethod CreateAccessMethod(): +{ + CreateAccessMethod statement = new CreateAccessMethod(); + String name; +} +{ + ContextualKeyword("ACCESS") ContextualKeyword("METHOD") name=RelObjectName() + { statement.setName(name); } + ( { statement.setType(CreateAccessMethod.Type.TABLE); } + | { statement.setType(CreateAccessMethod.Type.INDEX); } ) + ContextualKeyword("HANDLER") name=AccessQualifiedName() + { statement.setHandler(name); return statement; } +} + CreateTextSearchConfiguration CreateTextSearchConfiguration(): { CreateTextSearchConfiguration statement = new CreateTextSearchConfiguration(); String name; } { diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlAccessMethodTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlAccessMethodTest.java new file mode 100644 index 000000000..58b6ddc3c --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlAccessMethodTest.java @@ -0,0 +1,85 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.create; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.List; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.create.accessmethod.CreateAccessMethod; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +import net.sf.jsqlparser.util.validation.feature.PostgresqlVersion; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlAccessMethodTest { + @ParameterizedTest + @ValueSource(strings = {"TABLE", "INDEX"}) + void preservesAccessMethodKindAndDoesNotInventTableReferences(String type) + throws JSQLParserException { + String sql = "CREATE ACCESS METHOD am TYPE " + type + " HANDLER public.handlerfn"; + CreateAccessMethod statement = assertInstanceOf(CreateAccessMethod.class, + CCJSqlParserUtil.parse(sql)); + assertEquals("am", statement.getName()); + assertEquals(CreateAccessMethod.Type.valueOf(type), statement.getType()); + assertEquals("public.handlerfn", statement.getHandler()); + assertEquals(sql, statement.toString()); + assertTrue(new TablesNamesFinder().getTables(statement).isEmpty()); + assertTrue(statement.getFeatures().modifiesSchema()); + assertFalse(statement.getFeatures().returnsResultSet()); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output)); + assertEquals(sql, output.toString()); + assertInstanceOf(CreateAccessMethod.class, CCJSqlParserUtil.parse(output.toString())); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1").size()); + assertTrue(new Validation(List.of(FeaturesAllowed.DDL), sql).validate().isEmpty()); + assertTrue(new Validation(List.of(PostgresqlVersion.V12), sql).validate().isEmpty()); + assertFalse(new Validation(List.of(FeaturesAllowed.SELECT), sql).validate().isEmpty()); + assertEquals(type.equals("INDEX"), + new Validation(List.of(PostgresqlVersion.V10), sql).validate().isEmpty()); + } + + @Test + void quotedNamesRemainMutableAndVisitorsReceiveContext() throws JSQLParserException { + CreateAccessMethod statement = assertInstanceOf(CreateAccessMethod.class, + CCJSqlParserUtil.parse( + "CREATE ACCESS METHOD \"my method\" TYPE TABLE HANDLER \"my schema\".\"my handler\"")); + assertEquals("\"my method\"", statement.getName()); + assertEquals("\"my schema\".\"my handler\"", statement.getHandler()); + String actual = statement.accept(new StatementVisitorAdapter() { + @Override + public String visit(CreateAccessMethod method, S context) { + return context + ":" + method.getName(); + } + }, "visited"); + assertEquals("visited:\"my method\"", actual); + statement.setName("other_method"); + statement.setType(CreateAccessMethod.Type.INDEX); + statement.setHandler("other_schema.handler"); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output)); + assertEquals("CREATE ACCESS METHOD other_method TYPE INDEX HANDLER other_schema.handler", + output.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE ACCESS METHOD am TYPE TABLE", + "CREATE ACCESS METHOD am TYPE VIEW HANDLER fn", + "CREATE OR REPLACE ACCESS METHOD am TYPE TABLE HANDLER fn", + "CREATE ACCESS METHOD am TYPE TABLE HANDLER fn()"}) + void rejectsIncompleteOrInvalidAccessMethodDefinitions(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } +}