From f9785649d2a4eb49c034ad06cce7eed445937b60 Mon Sep 17 00:00:00 2001 From: minleejae Date: Fri, 25 Sep 2026 01:42:28 +0900 Subject: [PATCH] feat: share PostgreSQL sequence options and model alteration actions Signed-off-by: minleejae --- .../alter/sequence/AlterSequence.java | 76 ++++++++++- .../create/sequence/CreateSequence.java | 39 +++++- .../util/deparser/AlterSequenceDeParser.java | 3 +- .../util/deparser/CreateSequenceDeParser.java | 3 +- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 95 ++++++++++---- .../create/PostgreSqlSequenceDdlTest.java | 124 ++++++++++++++++++ 6 files changed, 306 insertions(+), 34 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlSequenceDdlTest.java diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/sequence/AlterSequence.java b/src/main/java/net/sf/jsqlparser/statement/alter/sequence/AlterSequence.java index a9869f8ed6..ccd3a52600 100644 --- a/src/main/java/net/sf/jsqlparser/statement/alter/sequence/AlterSequence.java +++ b/src/main/java/net/sf/jsqlparser/statement/alter/sequence/AlterSequence.java @@ -18,7 +18,79 @@ */ public class AlterSequence implements Statement { + public enum Action { + PARAMETERS, RENAME, OWNER, SET_SCHEMA, SET_LOGGED, SET_UNLOGGED + } + public Sequence sequence; + private boolean ifExists; + private Action action = Action.PARAMETERS; + private String newName; + private String owner; + private String schemaName; + + public boolean isIfExists() { + return ifExists; + } + + public void setIfExists(boolean ifExists) { + this.ifExists = ifExists; + } + + public Action getAction() { + return action; + } + + public void setAction(Action action) { + this.action = java.util.Objects.requireNonNull(action, "action"); + } + + public String getNewName() { + return newName; + } + + public void setNewName(String newName) { + this.newName = newName; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public String getSchemaName() { + return schemaName; + } + + public void setSchemaName(String schemaName) { + this.schemaName = schemaName; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("ALTER SEQUENCE "); + if (ifExists) { + builder.append("IF EXISTS "); + } + builder.append(action == Action.PARAMETERS ? sequence : sequence.getFullyQualifiedName()); + switch (action) { + case RENAME: + return builder.append(" RENAME TO ").append(newName); + case OWNER: + return builder.append(" OWNER TO ").append(owner); + case SET_SCHEMA: + return builder.append(" SET SCHEMA ").append(schemaName); + case SET_LOGGED: + return builder.append(" SET LOGGED"); + case SET_UNLOGGED: + return builder.append(" SET UNLOGGED"); + default: + return builder; + } + } + public Sequence getSequence() { return sequence; @@ -35,9 +107,7 @@ public T accept(StatementVisitor statementVisitor, S context) { @Override public String toString() { - String sql; - sql = "ALTER SEQUENCE " + sequence; - return sql; + return appendTo(new StringBuilder()).toString(); } public AlterSequence withSequence(Sequence sequence) { diff --git a/src/main/java/net/sf/jsqlparser/statement/create/sequence/CreateSequence.java b/src/main/java/net/sf/jsqlparser/statement/create/sequence/CreateSequence.java index 0d1c2b7f75..6c3ed901cd 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/sequence/CreateSequence.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/sequence/CreateSequence.java @@ -18,7 +18,42 @@ */ public class CreateSequence implements Statement { + public enum Persistence { + TEMP, TEMPORARY, UNLOGGED, LOCAL_TEMP, LOCAL_TEMPORARY, GLOBAL_TEMP, GLOBAL_TEMPORARY + } + public Sequence sequence; + private Persistence persistence; + private boolean ifNotExists; + + public Persistence getPersistence() { + return persistence; + } + + public void setPersistence(Persistence persistence) { + this.persistence = persistence; + } + + public boolean isIfNotExists() { + return ifNotExists; + } + + public void setIfNotExists(boolean ifNotExists) { + this.ifNotExists = ifNotExists; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("CREATE "); + if (persistence != null) { + builder.append(persistence.name().replace('_', ' ')).append(' '); + } + builder.append("SEQUENCE "); + if (ifNotExists) { + builder.append("IF NOT EXISTS "); + } + return builder.append(sequence); + } + public Sequence getSequence() { return sequence; @@ -35,9 +70,7 @@ public T accept(StatementVisitor statementVisitor, S context) { @Override public String toString() { - String sql; - sql = "CREATE SEQUENCE " + sequence; - return sql; + return appendTo(new StringBuilder()).toString(); } public CreateSequence withSequence(Sequence sequence) { diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/AlterSequenceDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/AlterSequenceDeParser.java index b9c743bdce..f857a5caf1 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/AlterSequenceDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/AlterSequenceDeParser.java @@ -26,7 +26,6 @@ public AlterSequenceDeParser(StringBuilder buffer) { @Override public void deParse(AlterSequence statement) { - builder.append("ALTER SEQUENCE "); - builder.append(statement.getSequence()); + statement.appendTo(builder); } } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/CreateSequenceDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/CreateSequenceDeParser.java index d4f4750987..f2b1859dc2 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/CreateSequenceDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/CreateSequenceDeParser.java @@ -26,7 +26,6 @@ public CreateSequenceDeParser(StringBuilder buffer) { @Override public void deParse(CreateSequence statement) { - builder.append("CREATE SEQUENCE "); - builder.append(statement.getSequence()); + statement.appendTo(builder); } } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 6c42e69e66..d058deb2e0 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -18812,40 +18812,86 @@ SequenceOwnership SequenceOwnership(): { return ownership; } } +/** AS, ownership and numeric parameters share the same sequence-option boundary. */ +void SequenceDefinitionOptions(Sequence sequence): +{ + List parameters = new ArrayList(); + Sequence.Parameter parameter; + SequenceOwnership ownership; + Token dataType; +} +{ + ( + LOOKAHEAD(2) + ( + ( dataType= | dataType= ) { + requireDdlSyntax(sequence.getDataType() == null, "Duplicate sequence AS clause"); + sequence.setDataType(dataType.image); + } + | + LOOKAHEAD({ isKeywordAhead("OWNED") }) ownership=SequenceOwnership() { + requireDdlSyntax(sequence.getOwnership() == null, "Duplicate sequence OWNED BY clause"); + sequence.setOwnership(ownership); + } + | + parameter=SequenceParameter() { parameters.add(parameter); } + ) + )* + { sequence.setParameters(parameters); } +} + CreateSequence CreateSequence(): { - CreateSequence createSequence = new CreateSequence(); - Sequence sequence; - List sequenceParameters; - Token dataType = null; - SequenceOwnership ownership; + CreateSequence result = new CreateSequence(); + Sequence sequence; + Token persistence; + Token scope = null; } { - sequence=Sequence() { createSequence.setSequence(sequence); } - [ ( dataType= | dataType= ) { sequence.setDataType(dataType.image); } ] - sequenceParameters = SequenceParameters() - [ LOOKAHEAD({ isKeywordAhead("OWNED") }) ownership=SequenceOwnership() { sequence.setOwnership(ownership); } ] - { - sequence.setParameters(sequenceParameters); - return createSequence; - } + [ + ( { result.setPersistence(CreateSequence.Persistence.UNLOGGED); } + | + [ ( scope= | scope= ) ] + ( persistence= | persistence= ) { + result.setPersistence(CreateSequence.Persistence.valueOf( + (scope == null ? "" : scope.image.toUpperCase(Locale.ROOT) + "_") + + persistence.image.toUpperCase(Locale.ROOT))); + } ) + ] + + [ LOOKAHEAD(3) { result.setIfNotExists(true); } ] + sequence=Sequence() { result.setSequence(sequence); } + SequenceDefinitionOptions(sequence) + { return result; } } AlterSequence AlterSequence(): { - AlterSequence alterSequence = new AlterSequence(); - Sequence sequence; - List sequenceParameters; - SequenceOwnership ownership; + AlterSequence result = new AlterSequence(); + Sequence sequence; + String name; } { - sequence=Sequence() { alterSequence.setSequence(sequence); } - sequenceParameters = SequenceParameters() - [ LOOKAHEAD({ isKeywordAhead("OWNED") }) ownership=SequenceOwnership() { sequence.setOwnership(ownership); } ] - { - sequence.setParameters(sequenceParameters); - return alterSequence; - } + [ LOOKAHEAD(2) { result.setIfExists(true); } ] + sequence=Sequence() { result.setSequence(sequence); } + ( + name=RelObjectName() + { result.setAction(AlterSequence.Action.RENAME); result.setNewName(name); } + | + LOOKAHEAD({ isKeywordAhead("OWNER") }) ContextualKeyword("OWNER") name=RelObjectName() + { result.setAction(AlterSequence.Action.OWNER); result.setOwner(name); } + | + + ( + name=RelObjectName() + { result.setAction(AlterSequence.Action.SET_SCHEMA); result.setSchemaName(name); } + | { result.setAction(AlterSequence.Action.SET_UNLOGGED); } + | ContextualKeyword("LOGGED") { result.setAction(AlterSequence.Action.SET_LOGGED); } + ) + | + SequenceDefinitionOptions(sequence) + ) + { return result; } } Statement Create(): @@ -18897,6 +18943,7 @@ Statement Create(): | statement = CreateDatabase() | + LOOKAHEAD([ | [ | ] ( | ) ] ) statement = CreateSequence() | statement = CreateSynonym(isUsingOrReplace) diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlSequenceDdlTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlSequenceDdlTest.java new file mode 100644 index 0000000000..811ba5a67f --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlSequenceDdlTest.java @@ -0,0 +1,124 @@ +/*- + * #%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 net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.sequence.AlterSequence; +import net.sf.jsqlparser.statement.create.sequence.CreateSequence; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlSequenceDdlTest { + @ParameterizedTest + @ValueSource(strings = { + "CREATE SEQUENCE IF NOT EXISTS s AS INTEGER START WITH 3", + "CREATE TEMP SEQUENCE s AS INTEGER START WITH 3", + "CREATE TEMP SEQUENCE IF NOT EXISTS s AS INTEGER START WITH 3", + "CREATE TEMPORARY SEQUENCE s AS INTEGER START WITH 3", + "CREATE TEMPORARY SEQUENCE IF NOT EXISTS s AS INTEGER START WITH 3", + "CREATE UNLOGGED SEQUENCE s AS INTEGER START WITH 3", + "CREATE UNLOGGED SEQUENCE IF NOT EXISTS s AS INTEGER START WITH 3", + "CREATE SEQUENCE s OWNED BY t.id CACHE 5", + "ALTER SEQUENCE s AS SMALLINT", + "ALTER SEQUENCE s OWNED BY t.id INCREMENT BY 3", + "ALTER SEQUENCE s SET UNLOGGED", + "ALTER SEQUENCE s SET LOGGED", + "ALTER SEQUENCE s OWNER TO CURRENT_USER", + "ALTER SEQUENCE s RENAME TO s2", + "ALTER SEQUENCE s SET SCHEMA ddl_aux", + "ALTER SEQUENCE IF EXISTS s AS SMALLINT", + "ALTER SEQUENCE IF EXISTS s RESTART", + "ALTER SEQUENCE IF EXISTS s RESTART WITH 7", + "ALTER SEQUENCE IF EXISTS s INCREMENT BY 2", + "ALTER SEQUENCE IF EXISTS s MINVALUE -5 MAXVALUE 100", + "ALTER SEQUENCE IF EXISTS s CACHE 3 NO CYCLE", + "ALTER SEQUENCE IF EXISTS s OWNED BY t.id", + "ALTER SEQUENCE IF EXISTS s OWNED BY t.id INCREMENT BY 3", + "ALTER SEQUENCE IF EXISTS s SET UNLOGGED", + "ALTER SEQUENCE IF EXISTS s SET LOGGED", + "ALTER SEQUENCE IF EXISTS s OWNER TO CURRENT_USER", + "ALTER SEQUENCE IF EXISTS s RENAME TO s2", + "ALTER SEQUENCE IF EXISTS s SET SCHEMA ddl_aux", + "CREATE SEQUENCE IF NOT EXISTS sequence_test", + "CREATE TEMP SEQUENCE myseq2", + "CREATE TEMP SEQUENCE myseq3", + "CREATE UNLOGGED SEQUENCE sequence_test_unlogged", + "CREATE TEMPORARY SEQUENCE sequence_test_temp1"}) + void postgresFormsRoundTrip(String sql) throws JSQLParserException { + Statement statement = parse(sql); + assertTrue(statement instanceof CreateSequence || statement instanceof AlterSequence); + assertRoundTrip(statement); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1", + p -> p.withDialect(Dialect.POSTGRESQL)).size()); + } + + @Test + void createOptionsCanBeModifiedWithoutStaleTokens() throws JSQLParserException { + CreateSequence statement = (CreateSequence) parse( + "CREATE TEMP SEQUENCE IF NOT EXISTS s OWNED BY t.id CACHE 5 AS INTEGER"); + assertTrue(statement.isIfNotExists()); + assertEquals(CreateSequence.Persistence.TEMP, statement.getPersistence()); + assertEquals("t.id", statement.getSequence().getOwnership().getColumn().toString()); + assertEquals("INTEGER", statement.getSequence().getDataType()); + statement.setPersistence(CreateSequence.Persistence.UNLOGGED); + statement.setIfNotExists(false); + statement.getSequence().setName("renamed"); + statement.getSequence().setOwnership(null); + statement.getSequence().getParameters().get(0).setValue(10L); + assertEquals("CREATE UNLOGGED SEQUENCE renamed AS INTEGER CACHE 10", statement.toString()); + assertRoundTrip(statement); + } + + @Test + void alterOptionsAndActionsRemainSeparate() throws JSQLParserException { + AlterSequence statement = (AlterSequence) parse( + "ALTER SEQUENCE IF EXISTS s OWNED BY t.id INCREMENT BY 3 AS SMALLINT"); + assertTrue(statement.isIfExists()); + assertEquals(AlterSequence.Action.PARAMETERS, statement.getAction()); + assertEquals("SMALLINT", statement.getSequence().getDataType()); + assertEquals(3L, statement.getSequence().getParameters().get(0).getValue()); + statement.setAction(AlterSequence.Action.RENAME); + statement.setNewName("new_s"); + assertEquals("ALTER SEQUENCE IF EXISTS s RENAME TO new_s", statement.toString()); + assertRoundTrip(statement); + } + + @ParameterizedTest + @ValueSource(strings = {"LOCAL TEMP", "LOCAL TEMPORARY", "GLOBAL TEMP", "GLOBAL TEMPORARY"}) + void scopedTemporarySequence(String prefix) throws JSQLParserException { + String sql = "CREATE " + prefix + " SEQUENCE s"; + assertEquals(sql, parse(sql).toString()); + } + + @Test + void duplicateScalarOptionsFail() { + assertThrows(JSQLParserException.class, + () -> parse("CREATE SEQUENCE s AS INT AS BIGINT")); + assertThrows(JSQLParserException.class, + () -> parse("ALTER SEQUENCE s OWNED BY NONE OWNED BY t.id")); + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void assertRoundTrip(Statement statement) throws JSQLParserException { + StringBuilder sql = new StringBuilder(); + statement.accept(new StatementDeParser(sql), null); + assertEquals(statement.toString(), sql.toString()); + assertEquals(statement.toString(), parse(sql.toString()).toString()); + } +}