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 9d2131555..49fdc5fc8 100644
--- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java
+++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java
@@ -644,6 +644,8 @@ public enum Feature {
* @see UseStatement
*/
use,
+ /** SQL Server session permission to insert explicit identity values into a table. */
+ setIdentityInsert,
/**
* @see Grant
*/
diff --git a/src/main/java/net/sf/jsqlparser/statement/SetIdentityInsertStatement.java b/src/main/java/net/sf/jsqlparser/statement/SetIdentityInsertStatement.java
new file mode 100644
index 000000000..0c8c925fb
--- /dev/null
+++ b/src/main/java/net/sf/jsqlparser/statement/SetIdentityInsertStatement.java
@@ -0,0 +1,59 @@
+/*-
+ * #%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;
+
+import java.util.Objects;
+import java.util.function.Consumer;
+import net.sf.jsqlparser.schema.Table;
+
+/** SQL Server SET IDENTITY_INSERT [database.][schema.]table ON | OFF. */
+public final class SetIdentityInsertStatement implements Statement {
+ private Table table;
+ private boolean on;
+
+ public SetIdentityInsertStatement(Table table, boolean on) {
+ setTable(table);
+ this.on = on;
+ }
+
+ public Table getTable() {
+ return table;
+ }
+
+ public void setTable(Table table) {
+ this.table = Objects.requireNonNull(table, "table");
+ }
+
+ public boolean isOn() {
+ return on;
+ }
+
+ public void setOn(boolean on) {
+ this.on = on;
+ }
+
+ /** Shares statement rendering while allowing deparsers to visit the existing Table AST. */
+ public StringBuilder appendTo(StringBuilder builder, Consumer
tableRenderer) {
+ builder.append("SET IDENTITY_INSERT ");
+ tableRenderer.accept(table);
+ return builder.append(on ? " ON" : " OFF");
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ return appendTo(builder, builder::append).toString();
+ }
+
+ @Override
+ public T accept(StatementVisitor statementVisitor, S context) {
+ return statementVisitor.visit(this, context);
+ }
+}
diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java
index 7f13f5efc..70c80a672 100644
--- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java
+++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java
@@ -707,6 +707,11 @@ public Void visit(ResetStatement reset, S context) {
return sessionOnly();
}
+ @Override
+ public Void visit(SetIdentityInsertStatement statement, S context) {
+ return sessionOnly();
+ }
+
@Override
public Void visit(UseStatement use, S context) {
return sessionOnly();
diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java
index 2001533ae..c36344804 100644
--- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java
+++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java
@@ -270,6 +270,14 @@ default void visit(Upsert upsert) {
this.visit(upsert, null);
}
+ default T visit(SetIdentityInsertStatement statement, S context) {
+ return null;
+ }
+
+ default void visit(SetIdentityInsertStatement statement) {
+ visit(statement, null);
+ }
+
T visit(UseStatement use, S context);
default void visit(UseStatement use) {
diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java
index e633adebb..0f0eccdf2 100644
--- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java
+++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java
@@ -478,6 +478,11 @@ public T visit(Upsert upsert, S context) {
return null;
}
+ @Override
+ public T visit(SetIdentityInsertStatement statement, S context) {
+ return statement.getTable().accept(fromItemVisitor, context);
+ }
+
@Override
public T visit(UseStatement use, S context) {
return null;
diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java
index 3c596371b..365043197 100644
--- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java
+++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java
@@ -110,6 +110,7 @@
import net.sf.jsqlparser.statement.Statements;
import net.sf.jsqlparser.statement.UnsupportedStatement;
import net.sf.jsqlparser.statement.UseStatement;
+import net.sf.jsqlparser.statement.SetIdentityInsertStatement;
import net.sf.jsqlparser.statement.alter.Alter;
import net.sf.jsqlparser.statement.alter.AlterSession;
import net.sf.jsqlparser.statement.alter.AlterSystemStatement;
@@ -1938,6 +1939,11 @@ public void visit(Upsert upsert) {
StatementVisitor.super.visit(upsert);
}
+ @Override
+ public Void visit(SetIdentityInsertStatement statement, S context) {
+ return statement.getTable().accept(this, context);
+ }
+
@Override
public Void visit(UseStatement use, 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 d85dbddd2..cee46569f 100644
--- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java
+++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java
@@ -54,6 +54,7 @@
import net.sf.jsqlparser.statement.Statements;
import net.sf.jsqlparser.statement.UnsupportedStatement;
import net.sf.jsqlparser.statement.UseStatement;
+import net.sf.jsqlparser.statement.SetIdentityInsertStatement;
import net.sf.jsqlparser.statement.alter.Alter;
import net.sf.jsqlparser.statement.alter.AlterSession;
import net.sf.jsqlparser.statement.alter.AlterSystemStatement;
@@ -378,6 +379,11 @@ public StringBuilder visit(Upsert upsert, S context) {
return builder;
}
+ @Override
+ public StringBuilder visit(SetIdentityInsertStatement statement, S context) {
+ return statement.appendTo(builder, table -> table.accept(selectDeParser, context));
+ }
+
@Override
public StringBuilder visit(UseStatement use, S context) {
new UseStatementDeParser(builder).deParse(use);
diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/SqlServerVersion.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/SqlServerVersion.java
index 863fbc839..7863341c2 100644
--- a/src/main/java/net/sf/jsqlparser/util/validation/feature/SqlServerVersion.java
+++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/SqlServerVersion.java
@@ -108,7 +108,7 @@ public enum SqlServerVersion implements Version {
Feature.commit, // special sql-server features
// https://docs.microsoft.com/en-us/sql/relational-databases/xml/for-xml-sql-server?view=sql-server-ver15
Feature.selectForXmlPath,
- Feature.use, Feature.allowSquareBracketQuotation, //
+ Feature.use, Feature.setIdentityInsert, Feature.allowSquareBracketQuotation, //
Feature.pivot, Feature.unpivot, Feature.pivotXml,
Feature.selectGroupByGroupingSets));
diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/SetIdentityInsertValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/SetIdentityInsertValidator.java
new file mode 100644
index 000000000..9bb0b4953
--- /dev/null
+++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/SetIdentityInsertValidator.java
@@ -0,0 +1,22 @@
+/*-
+ * #%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.util.validation.validator;
+
+import net.sf.jsqlparser.parser.feature.Feature;
+import net.sf.jsqlparser.statement.SetIdentityInsertStatement;
+import net.sf.jsqlparser.util.validation.metadata.NamedObject;
+
+public class SetIdentityInsertValidator extends AbstractValidator {
+ @Override
+ public void validate(SetIdentityInsertStatement statement) {
+ validateFeatureAndName(Feature.setIdentityInsert, NamedObject.table,
+ statement.getTable().getFullyQualifiedName());
+ }
+}
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 282e30172..69bec7bfe 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
@@ -53,6 +53,7 @@
import net.sf.jsqlparser.statement.Statements;
import net.sf.jsqlparser.statement.UnsupportedStatement;
import net.sf.jsqlparser.statement.UseStatement;
+import net.sf.jsqlparser.statement.SetIdentityInsertStatement;
import net.sf.jsqlparser.statement.alter.Alter;
import net.sf.jsqlparser.statement.alter.AlterSession;
import net.sf.jsqlparser.statement.alter.AlterSystemStatement;
@@ -269,6 +270,12 @@ public Void visit(Upsert upsert, S context) {
return null;
}
+ @Override
+ public Void visit(SetIdentityInsertStatement statement, S context) {
+ getValidator(SetIdentityInsertValidator.class).validate(statement);
+ return null;
+ }
+
@Override
public Void visit(UseStatement use, S context) {
getValidator(UseStatementValidator.class).validate(use);
diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
index 1720fff54..6d67790e8 100644
--- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
+++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
@@ -2785,6 +2785,12 @@ Statement SingleStatement() :
LOOKAHEAD({ getToken(1).kind == K_DO && Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)) })
stm = DoStatement()
|
+ LOOKAHEAD({ Dialect.SQLSERVER.name().equals(getAsString(Feature.dialect))
+ && getToken(1).kind == K_SET && getToken(2).kind == S_IDENTIFIER
+ && "IDENTITY_INSERT".equalsIgnoreCase(getToken(2).image)
+ && !"=".equals(getToken(3).image) && !".".equals(getToken(3).image) })
+ stm = SetIdentityInsert()
+ |
stm = Set()
|
stm = Reset()
@@ -4387,6 +4393,22 @@ List ExplainStatementOptions():
}
}
+SetIdentityInsertStatement SetIdentityInsert(): {
+ Table table;
+ boolean on;
+}
+{
+ AccessKeyword("IDENTITY_INSERT") table=Table()
+ {
+ requireAccessSyntax(table.getNameParts().size() <= 3,
+ "IDENTITY_INSERT requires a table name with at most three parts");
+ }
+ ( { on = true; } | { on = false; } )
+ {
+ return new SetIdentityInsertStatement(table, on);
+ }
+}
+
UseStatement Use(): {
String name;
boolean hasSchemaKeyword = false;
diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst
index b77dc87e4..cf08f7ac3 100644
--- a/src/site/sphinx/usage.rst
+++ b/src/site/sphinx/usage.rst
@@ -1006,6 +1006,17 @@ Parse procedure definitions one SQL Server batch at a time: a procedure consumes
remaining batch, including SQL after an ``END``. Client-side ``GO`` batch splitting is
not performed by this routine declaration parser.
+SQL Server identity inserts
+---------------------------
+
+With ``Dialect.SQLSERVER``, ``SET IDENTITY_INSERT dbo.actor ON`` uses
+``SetIdentityInsertStatement``. ``getTable()`` reuses the qualified ``Table`` AST;
+``isOn()`` and ``setOn()`` expose the session setting. Table names may include a
+database and schema, including SQL Server bracket-quoted identifiers. Table
+visitors, both SQL renderers and metadata validation use this structured target.
+Feature analysis reports ``MODIFIES_SESSION``; the directive itself inserts no rows.
+The dedicated validation capability is ``setIdentityInsert``.
+
Legacy MySQL GROUP BY ordering
==============================
diff --git a/src/test/java/net/sf/jsqlparser/statement/SetIdentityInsertTest.java b/src/test/java/net/sf/jsqlparser/statement/SetIdentityInsertTest.java
new file mode 100644
index 000000000..b83de91b5
--- /dev/null
+++ b/src/test/java/net/sf/jsqlparser/statement/SetIdentityInsertTest.java
@@ -0,0 +1,170 @@
+/*-
+ * #%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;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.parser.feature.Feature;
+import net.sf.jsqlparser.schema.Table;
+import net.sf.jsqlparser.statement.select.FromItemVisitorAdapter;
+import net.sf.jsqlparser.util.TablesNamesFinder;
+import net.sf.jsqlparser.util.deparser.ExpressionDeParser;
+import net.sf.jsqlparser.util.deparser.SelectDeParser;
+import net.sf.jsqlparser.util.deparser.StatementDeParser;
+import net.sf.jsqlparser.util.validation.ValidationContext;
+import net.sf.jsqlparser.util.validation.ValidationTestAsserts;
+import net.sf.jsqlparser.util.validation.feature.PostgresqlVersion;
+import net.sf.jsqlparser.util.validation.feature.SqlServerVersion;
+import net.sf.jsqlparser.util.validation.metadata.DatabaseMetaDataValidation;
+import net.sf.jsqlparser.util.validation.metadata.Named;
+import net.sf.jsqlparser.util.validation.metadata.NamedObject;
+import net.sf.jsqlparser.util.validation.validator.StatementValidator;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class SetIdentityInsertTest {
+ private SetIdentityInsertStatement parse(String sql) throws JSQLParserException {
+ return (SetIdentityInsertStatement) CCJSqlParserUtil.parse(sql,
+ p -> p.withDialect(Dialect.SQLSERVER).withUnsupportedStatements(false));
+ }
+
+ private void assertRoundTrip(SetIdentityInsertStatement set, String sql) throws Exception {
+ assertEquals(sql, set.toString());
+ StringBuilder buffer = new StringBuilder();
+ set.accept(new StatementDeParser(buffer), null);
+ assertEquals(sql, buffer.toString());
+ SetIdentityInsertStatement reparsed = parse(buffer.toString());
+ assertEquals(set.isOn(), reparsed.isOn());
+ assertEquals(set.getTable().getFullyQualifiedName(),
+ reparsed.getTable().getFullyQualifiedName());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"language", "country", "city", "address", "actor", "staff",
+ "store", "category", "film", "inventory", "customer", "rental", "payment"})
+ void parsesSakilaIdentityDirectives(String table) throws Exception {
+ for (boolean on : new boolean[] {true, false}) {
+ String sql = "SET IDENTITY_INSERT " + table + (on ? " ON" : " OFF");
+ SetIdentityInsertStatement set = parse(sql);
+ assertEquals(table, set.getTable().getName());
+ assertEquals(on, set.isOn());
+ assertRoundTrip(set, sql);
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"dbo.actor", "sakila.dbo.actor", "sakila..actor",
+ "[sakila].[dbo].[order]", "[table with spaces]", "#staging"})
+ void reusesQualifiedTableNames(String table) throws Exception {
+ SetIdentityInsertStatement set = parse("set identity_insert " + table + " on");
+ assertEquals(table, set.getTable().getFullyQualifiedName());
+ assertEquals(Collections.singleton(table), new TablesNamesFinder().getTables(set));
+ assertRoundTrip(set, "SET IDENTITY_INSERT " + table + " ON");
+ }
+
+ @Test
+ void exposesEditableTargetAndStateWithoutClaimingDataChanges() throws Exception {
+ SetIdentityInsertStatement set = parse("SET IDENTITY_INSERT dbo.actor ON");
+ set.getTable().setName("customer");
+ set.setOn(false);
+ assertRoundTrip(set, "SET IDENTITY_INSERT dbo.customer OFF");
+ set.setTable(new Table("replacement"));
+ assertRoundTrip(set, "SET IDENTITY_INSERT replacement OFF");
+ assertEquals(Collections.singleton(StmtFeature.MODIFIES_SESSION),
+ set.getFeatures().getCertain());
+ assertTrue(set.getFeatures().getUncertain().isEmpty());
+ assertEquals(3, CCJSqlParserUtil.parseStatements(
+ "SET IDENTITY_INSERT dbo.actor ON; INSERT INTO dbo.actor VALUES (1); "
+ + "SET IDENTITY_INSERT dbo.actor OFF;",
+ p -> p.withDialect(Dialect.SQLSERVER)).size());
+ }
+
+ @Test
+ void traversesTheTargetWithVisitorContext() throws Exception {
+ SetIdentityInsertStatement set = parse("SET IDENTITY_INSERT dbo.actor ON");
+ Object context = new Object();
+ List visited = new ArrayList<>();
+ FromItemVisitorAdapter tables = new FromItemVisitorAdapter() {
+ @Override
+ public Void visit(Table table, S value) {
+ assertSame(context, value);
+ visited.add(table);
+ return null;
+ }
+ };
+ set.accept(new StatementVisitorAdapter(null, null, null, tables, null, null),
+ context);
+ assertEquals(Collections.singletonList(set.getTable()), visited);
+
+ StringBuilder buffer = new StringBuilder();
+ SelectDeParser selects = new SelectDeParser() {
+ @Override
+ public StringBuilder visit(Table table, S value) {
+ assertSame(context, value);
+ return getBuilder().append("mapped_table");
+ }
+ };
+ set.accept(new StatementDeParser(new ExpressionDeParser(), selects, buffer), context);
+ assertEquals("SET IDENTITY_INSERT mapped_table ON", buffer.toString());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"SET IDENTITY_INSERT", "SET IDENTITY_INSERT ON",
+ "SET IDENTITY_INSERT dbo.actor", "SET IDENTITY_INSERT dbo.actor TRUE",
+ "SET IDENTITY_INSERT dbo.actor = ON", "SET IDENTITY_INSERT dbo.actor ON OFF",
+ "SET IDENTITY_INSERT server.sakila.dbo.actor ON",
+ "SET IDENTITY_INSERT dbo.actor AS a ON", "SET IDENTITY_INSERT @actor ON"})
+ void rejectsMalformedDirectives(String sql) {
+ assertThrows(JSQLParserException.class, () -> parse(sql));
+ }
+
+ @Test
+ void preservesGenericSetAndRequiresSqlServerDialect() throws Exception {
+ String sql = "SET IDENTITY_INSERT dbo.actor ON";
+ assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql));
+ assertThrows(JSQLParserException.class,
+ () -> CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)));
+ for (String generic : new String[] {"SET IDENTITY_INSERT = 1",
+ "SET IDENTITY_INSERT.value = 1", "SET @flag = 1"}) {
+ assertInstanceOf(SetStatement.class, CCJSqlParserUtil.parse(generic,
+ p -> p.withDialect(Dialect.SQLSERVER)));
+ }
+ }
+
+ @Test
+ void validatesTheCapabilityAndExistingTargetTable() throws Exception {
+ SetIdentityInsertStatement set = parse("SET IDENTITY_INSERT dbo.actor OFF");
+ List visited = new ArrayList<>();
+ DatabaseMetaDataValidation metadata = name -> {
+ visited.add(name);
+ return true;
+ };
+ StatementValidator validator = new StatementValidator();
+ validator.setContext(new ValidationContext().setCapabilities(
+ Arrays.asList(SqlServerVersion.V2019, PostgresqlVersion.V10, metadata)));
+ set.accept(validator, null);
+ assertFalse(validator.getValidationErrors().containsKey(SqlServerVersion.V2019));
+ assertFalse(validator.getValidationErrors().containsKey(metadata));
+ ValidationTestAsserts.assertNotSupported(
+ validator.getValidationErrors().get(PostgresqlVersion.V10),
+ Feature.setIdentityInsert);
+ assertEquals(1, visited.size());
+ assertEquals(NamedObject.table, visited.get(0).getNamedObject());
+ assertEquals("dbo.actor", visited.get(0).getFqn());
+ }
+}