From c1d22b3a35863461c7a861efcec46be25d6a2464 Mon Sep 17 00:00:00 2001 From: minleejae Date: Thu, 24 Sep 2026 21:40:21 +0900 Subject: [PATCH] fix: preserve PostgreSQL identity sequence names Signed-off-by: minleejae --- .../net/sf/jsqlparser/schema/Sequence.java | 19 ++++- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 18 ++++- .../create/IdentitySequenceNameTest.java | 76 +++++++++++++++++++ 3 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/statement/create/IdentitySequenceNameTest.java diff --git a/src/main/java/net/sf/jsqlparser/schema/Sequence.java b/src/main/java/net/sf/jsqlparser/schema/Sequence.java index efff62a970..6125fce3af 100644 --- a/src/main/java/net/sf/jsqlparser/schema/Sequence.java +++ b/src/main/java/net/sf/jsqlparser/schema/Sequence.java @@ -187,7 +187,7 @@ public Sequence addParameters(Collection parameters) { * The available parameters to a sequence */ public enum ParameterType { - INCREMENT_BY, INCREMENT, START_WITH, START, RESTART_WITH, MAXVALUE, NOMAXVALUE, MINVALUE, NOMINVALUE, CYCLE, NOCYCLE, CACHE, NOCACHE, ORDER, NOORDER, KEEP, NOKEEP, SESSION, GLOBAL, NO_MINVALUE, NO_MAXVALUE, NO_CYCLE; + INCREMENT_BY, INCREMENT, START_WITH, START, RESTART_WITH, MAXVALUE, NOMAXVALUE, MINVALUE, NOMINVALUE, CYCLE, NOCYCLE, CACHE, NOCACHE, ORDER, NOORDER, KEEP, NOKEEP, SESSION, GLOBAL, NO_MINVALUE, NO_MAXVALUE, NO_CYCLE, SEQUENCE_NAME; public static ParameterType from(String type) { return Enum.valueOf(ParameterType.class, type.toUpperCase(Locale.ROOT)); @@ -201,6 +201,7 @@ public static class Parameter implements java.io.Serializable { private final ParameterType option; private Long value; + private Sequence sequenceName; public Parameter(ParameterType option) { this.option = option; @@ -214,6 +215,20 @@ public ParameterType getOption() { return option; } + /** The sequence named by an identity declaration's SEQUENCE NAME option. */ + public Sequence getSequenceName() { + return sequenceName; + } + + public void setSequenceName(Sequence sequenceName) { + this.sequenceName = sequenceName; + } + + public Parameter withSequenceName(Sequence sequenceName) { + setSequenceName(sequenceName); + return this; + } + @Override public String toString() { return formatParameter(); @@ -225,6 +240,8 @@ public void setValue(Long value) { public String formatParameter() { switch (option) { + case SEQUENCE_NAME: + return "SEQUENCE NAME " + sequenceName.getFullyQualifiedName(); case INCREMENT_BY: return prefix("INCREMENT BY"); case INCREMENT: diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 0df99f424a..973326e92c 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -14217,7 +14217,9 @@ IdentityDefinition IdentityDefinition(): Token token; IdentityDefinition.GenerationMode mode; IdentityDefinition identity; - List parameters; + List parameters = new ArrayList(); + Sequence.Parameter parameter; + Sequence sequenceName; } { token= { @@ -14226,8 +14228,18 @@ IdentityDefinition IdentityDefinition(): ( { mode = IdentityDefinition.GenerationMode.ALWAYS; } | { mode = IdentityDefinition.GenerationMode.BY_DEFAULT; } ) { identity = new IdentityDefinition(mode); } - [ LOOKAHEAD(2) "(" parameters=SequenceParameters() ")" { - requireDdlSyntax(!parameters.isEmpty(), "Expected an identity sequence parameter"); + [ LOOKAHEAD(1) "(" + ( + ( + LOOKAHEAD( ) + sequenceName=Sequence() + { parameter = new Sequence.Parameter(Sequence.ParameterType.SEQUENCE_NAME) + .withSequenceName(sequenceName); } + | parameter=SequenceParameter() + ) + { parameters.add(parameter); } + )+ + ")" { identity.setParameters(parameters); } ] { return identity; } diff --git a/src/test/java/net/sf/jsqlparser/statement/create/IdentitySequenceNameTest.java b/src/test/java/net/sf/jsqlparser/statement/create/IdentitySequenceNameTest.java new file mode 100644 index 0000000000..be75c34061 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/IdentitySequenceNameTest.java @@ -0,0 +1,76 @@ +/*- + * #%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.schema.Sequence; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.Alter; +import net.sf.jsqlparser.statement.create.table.CreateTable; +import net.sf.jsqlparser.statement.create.table.IdentityDefinition; +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 IdentitySequenceNameTest { + @ParameterizedTest + @ValueSource(strings = {"ids", "public.ids", "\"schema.name\".\"seq.name\""}) + void sharesIdentityOptionsAcrossCreateAndAlter(String name) throws JSQLParserException { + String definition = "GENERATED BY DEFAULT AS IDENTITY (START 4 SEQUENCE NAME " + name + + " CACHE 2 NO CYCLE)"; + CreateTable create = (CreateTable) parse("CREATE TABLE t (id INT " + definition + ")"); + IdentityDefinition identity = create.getColumnDefinitions().get(0).getColumnOptions().get(0) + .getIdentityDefinition(); + Alter alter = (Alter) parse("ALTER TABLE t ALTER COLUMN id ADD " + definition); + IdentityDefinition altered = alter.getAlterExpressions().get(0).getColDataTypeList().get(0) + .getIdentityAlterations().get(0).getIdentityDefinition(); + for (IdentityDefinition current : new IdentityDefinition[] {identity, altered}) { + assertEquals(4, current.getParameters().size()); + Sequence.Parameter named = current.getParameters().get(1); + assertEquals(Sequence.ParameterType.SEQUENCE_NAME, named.getOption()); + assertEquals(name, named.getSequenceName().getFullyQualifiedName()); + assertNull(named.getValue()); + named.getSequenceName().setName("renamed"); + } + assertTrue(create.toString().contains("renamed")); + assertTrue(alter.toString().contains("renamed")); + roundTrip(create); + roundTrip(alter); + } + + @Test + void preservesNameOnlyAndDoesNotExtendStandaloneSequenceOptions() throws JSQLParserException { + String sql = "CREATE TABLE t (id INT GENERATED ALWAYS AS IDENTITY (SEQUENCE NAME ids))"; + assertEquals(sql, parse(sql).toString()); + roundTrip(parse(sql)); + for (String invalid : new String[] { + "CREATE TABLE t (id INT GENERATED ALWAYS AS IDENTITY (SEQUENCE NAME))", + "CREATE TABLE t (id INT GENERATED ALWAYS AS IDENTITY ())", + "CREATE SEQUENCE ids SEQUENCE NAME other_ids", + "ALTER TABLE t ALTER COLUMN id SET SEQUENCE NAME other_ids"}) { + assertThrows(JSQLParserException.class, () -> parse(invalid), invalid); + } + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void roundTrip(Statement statement) throws JSQLParserException { + StringBuilder out = new StringBuilder(); + statement.accept(new StatementDeParser(out)); + assertEquals(statement.toString(), out.toString()); + assertEquals(out.toString(), parse(out.toString()).toString()); + } +}