();
diff --git a/src/test/java/net/sf/jsqlparser/expression/DateUnitExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/DateUnitExpressionTest.java
index 054b25e9a..164c9e112 100644
--- a/src/test/java/net/sf/jsqlparser/expression/DateUnitExpressionTest.java
+++ b/src/test/java/net/sf/jsqlparser/expression/DateUnitExpressionTest.java
@@ -1,3 +1,12 @@
+/*-
+ * #%L
+ * JSQLParser library
+ * %%
+ * Copyright (C) 2004 - 2025 JSQLParser
+ * %%
+ * Dual licensed under GNU LGPL 2.1 or Apache License 2.0
+ * #L%
+ */
package net.sf.jsqlparser.expression;
import net.sf.jsqlparser.JSQLParserException;
diff --git a/src/test/java/net/sf/jsqlparser/statement/alter/AlterRowLevelSecurityTest.java b/src/test/java/net/sf/jsqlparser/statement/alter/AlterRowLevelSecurityTest.java
new file mode 100644
index 000000000..d91cd6341
--- /dev/null
+++ b/src/test/java/net/sf/jsqlparser/statement/alter/AlterRowLevelSecurityTest.java
@@ -0,0 +1,115 @@
+/*-
+ * #%L
+ * JSQLParser library
+ * %%
+ * Copyright (C) 2004 - 2025 JSQLParser
+ * %%
+ * Dual licensed under GNU LGPL 2.1 or Apache License 2.0
+ * #L%
+ */
+package net.sf.jsqlparser.statement.alter;
+
+import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.statement.Statement;
+import org.junit.jupiter.api.Test;
+
+import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests for PostgreSQL ALTER TABLE ... ROW LEVEL SECURITY statements
+ */
+public class AlterRowLevelSecurityTest {
+
+ @Test
+ public void testEnableRowLevelSecurity() throws JSQLParserException {
+ String sql = "ALTER TABLE table1 ENABLE ROW LEVEL SECURITY";
+ assertSqlCanBeParsedAndDeparsed(sql, true);
+
+ Statement stmt = CCJSqlParserUtil.parse(sql);
+ assertInstanceOf(Alter.class, stmt);
+ Alter alter = (Alter) stmt;
+ assertEquals("table1", alter.getTable().getName());
+ assertEquals(AlterOperation.ENABLE_ROW_LEVEL_SECURITY,
+ alter.getAlterExpressions().get(0).getOperation());
+ }
+
+ @Test
+ public void testEnableRowLevelSecurityWithSchema() throws JSQLParserException {
+ String sql = "ALTER TABLE customer_custom_data.phone_opt_out ENABLE ROW LEVEL SECURITY";
+ assertSqlCanBeParsedAndDeparsed(sql, true);
+
+ Alter alter = (Alter) CCJSqlParserUtil.parse(sql);
+ assertEquals("customer_custom_data.phone_opt_out",
+ alter.getTable().getFullyQualifiedName());
+ assertEquals(AlterOperation.ENABLE_ROW_LEVEL_SECURITY,
+ alter.getAlterExpressions().get(0).getOperation());
+ }
+
+ @Test
+ public void testDisableRowLevelSecurity() throws JSQLParserException {
+ String sql = "ALTER TABLE table1 DISABLE ROW LEVEL SECURITY";
+ assertSqlCanBeParsedAndDeparsed(sql, true);
+
+ Alter alter = (Alter) CCJSqlParserUtil.parse(sql);
+ assertEquals(AlterOperation.DISABLE_ROW_LEVEL_SECURITY,
+ alter.getAlterExpressions().get(0).getOperation());
+ }
+
+ @Test
+ public void testForceRowLevelSecurity() throws JSQLParserException {
+ String sql = "ALTER TABLE table1 FORCE ROW LEVEL SECURITY";
+ assertSqlCanBeParsedAndDeparsed(sql, true);
+
+ Alter alter = (Alter) CCJSqlParserUtil.parse(sql);
+ assertEquals(AlterOperation.FORCE_ROW_LEVEL_SECURITY,
+ alter.getAlterExpressions().get(0).getOperation());
+ }
+
+ @Test
+ public void testNoForceRowLevelSecurity() throws JSQLParserException {
+ String sql = "ALTER TABLE table1 NO FORCE ROW LEVEL SECURITY";
+ assertSqlCanBeParsedAndDeparsed(sql, true);
+
+ Alter alter = (Alter) CCJSqlParserUtil.parse(sql);
+ assertEquals(AlterOperation.NO_FORCE_ROW_LEVEL_SECURITY,
+ alter.getAlterExpressions().get(0).getOperation());
+ }
+
+ @Test
+ public void testMultipleStatements() throws JSQLParserException {
+ // Test CREATE POLICY followed by ENABLE RLS
+ String sql = "CREATE POLICY policy1 ON table1 USING (id = user_id()); " +
+ "ALTER TABLE table1 ENABLE ROW LEVEL SECURITY";
+
+ net.sf.jsqlparser.statement.Statements stmts = CCJSqlParserUtil.parseStatements(sql);
+ assertEquals(2, stmts.getStatements().size());
+
+ assertInstanceOf(net.sf.jsqlparser.statement.create.policy.CreatePolicy.class,
+ stmts.getStatements().get(0));
+ assertInstanceOf(Alter.class, stmts.getStatements().get(1));
+ }
+
+ @Test
+ public void testEnableKeysStillWorks() throws JSQLParserException {
+ // Ensure our changes don't break existing ENABLE KEYS syntax
+ String sql = "ALTER TABLE table1 ENABLE KEYS";
+ assertSqlCanBeParsedAndDeparsed(sql, true);
+
+ Alter alter = (Alter) CCJSqlParserUtil.parse(sql);
+ assertEquals(AlterOperation.ENABLE_KEYS,
+ alter.getAlterExpressions().get(0).getOperation());
+ }
+
+ @Test
+ public void testDisableKeysStillWorks() throws JSQLParserException {
+ // Ensure our changes don't break existing DISABLE KEYS syntax
+ String sql = "ALTER TABLE table1 DISABLE KEYS";
+ assertSqlCanBeParsedAndDeparsed(sql, true);
+
+ Alter alter = (Alter) CCJSqlParserUtil.parse(sql);
+ assertEquals(AlterOperation.DISABLE_KEYS,
+ alter.getAlterExpressions().get(0).getOperation());
+ }
+}
diff --git a/src/test/java/net/sf/jsqlparser/statement/create/CreatePolicyTablesFinderTest.java b/src/test/java/net/sf/jsqlparser/statement/create/CreatePolicyTablesFinderTest.java
new file mode 100644
index 000000000..031c86b6e
--- /dev/null
+++ b/src/test/java/net/sf/jsqlparser/statement/create/CreatePolicyTablesFinderTest.java
@@ -0,0 +1,263 @@
+/*-
+ * #%L
+ * JSQLParser library
+ * %%
+ * Copyright (C) 2004 - 2025 JSQLParser
+ * %%
+ * Dual licensed under GNU LGPL 2.1 or Apache License 2.0
+ * #L%
+ */
+package net.sf.jsqlparser.statement.create;
+
+import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.statement.Statement;
+import net.sf.jsqlparser.util.TablesNamesFinder;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests for TablesNamesFinder integration with PostgreSQL CREATE POLICY statements.
+ *
+ *
+ * These tests verify that TablesNamesFinder correctly identifies ALL tables referenced in a CREATE
+ * POLICY statement, including:
+ *
+ * - The policy's target table
+ * - Tables in USING expression subqueries
+ * - Tables in WITH CHECK expression subqueries
+ * - Tables in complex expressions (JOINs, CTEs, nested subqueries)
+ *
+ *
+ *
+ * Current Status: These tests will FAIL until
+ * TablesNamesFinder.visit(CreatePolicy) is updated to traverse USING and WITH CHECK expressions.
+ * This is incomplete feature support, not a regression - CREATE POLICY parsing works correctly, but
+ * analysis tools don't yet have complete integration.
+ *
+ *
+ * Expected Behavior: Once fixed, TablesNamesFinder should find tables in policy
+ * expressions using the same pattern as other statements (CreateView, Insert, Update).
+ */
+public class CreatePolicyTablesFinderTest {
+
+ // =========================================================================
+ // Helper Methods
+ // =========================================================================
+
+ /**
+ * Parse SQL and extract table names using TablesNamesFinder.
+ */
+ private List getTablesFromSQL(String sql) throws JSQLParserException {
+ Statement stmt = CCJSqlParserUtil.parse(sql);
+ TablesNamesFinder finder = new TablesNamesFinder();
+ return finder.getTableList(stmt);
+ }
+
+ /**
+ * Assert that the actual table list contains exactly the expected tables.
+ */
+ private void assertContainsAllTables(List actual, String... expected) {
+ assertEquals(expected.length, actual.size(),
+ "Expected " + expected.length + " tables but found " + actual.size() + ". " +
+ "Expected: " + java.util.Arrays.toString(expected) + ", " +
+ "Actual: " + actual);
+
+ for (String table : expected) {
+ assertTrue(actual.contains(table),
+ "Expected to find table '" + table + "' but it was missing. " +
+ "Found tables: " + actual);
+ }
+ }
+
+ // =========================================================================
+ // Simple Subqueries - Basic USE Cases
+ // =========================================================================
+
+ @Test
+ public void testTablesFinderWithSubqueryInUsing() throws JSQLParserException {
+ String sql = "CREATE POLICY tenant_policy ON documents " +
+ "USING (tenant_id IN (SELECT tenant_id FROM tenant_access))";
+
+ List tables = getTablesFromSQL(sql);
+
+ // Should find: target table + table in USING subquery
+ assertContainsAllTables(tables, "documents", "tenant_access");
+ }
+
+ @Test
+ public void testTablesFinderWithSubqueryInWithCheck() throws JSQLParserException {
+ String sql = "CREATE POLICY data_policy ON user_data " +
+ "WITH CHECK (status IN (SELECT allowed_status FROM status_config))";
+
+ List tables = getTablesFromSQL(sql);
+
+ // Should find: target table + table in WITH CHECK subquery
+ assertContainsAllTables(tables, "user_data", "status_config");
+ }
+
+ @Test
+ public void testTablesFinderWithBothUsingAndWithCheck() throws JSQLParserException {
+ String sql = "CREATE POLICY dual_check_policy ON records " +
+ "USING (user_id IN (SELECT id FROM active_users)) " +
+ "WITH CHECK (status IN (SELECT status FROM valid_statuses))";
+
+ List tables = getTablesFromSQL(sql);
+
+ // Should find: target table + table in USING + table in WITH CHECK
+ assertContainsAllTables(tables, "records", "active_users", "valid_statuses");
+ }
+
+ // =========================================================================
+ // Complex Expressions - Multiple/Nested Subqueries
+ // =========================================================================
+
+ @Test
+ public void testTablesFinderWithMultipleSubqueries() throws JSQLParserException {
+ String sql = "CREATE POLICY complex_policy ON documents " +
+ "USING (" +
+ " tenant_id IN (SELECT tenant_id FROM tenant_access) " +
+ " AND status IN (SELECT status FROM allowed_statuses) " +
+ " AND department_id = (SELECT id FROM departments WHERE name = 'Engineering')" +
+ ")";
+
+ List tables = getTablesFromSQL(sql);
+
+ // Should find: target table + 3 tables from subqueries
+ assertContainsAllTables(tables, "documents", "tenant_access", "allowed_statuses",
+ "departments");
+ }
+
+ @Test
+ public void testTablesFinderWithNestedSubqueries() throws JSQLParserException {
+ String sql = "CREATE POLICY nested_policy ON orders " +
+ "USING (customer_id IN (" +
+ " SELECT customer_id FROM customer_access " +
+ " WHERE region_id IN (SELECT id FROM regions WHERE active = true)" +
+ "))";
+
+ List tables = getTablesFromSQL(sql);
+
+ // Should find: target table + tables from nested subqueries
+ assertContainsAllTables(tables, "orders", "customer_access", "regions");
+ }
+
+ @Test
+ public void testTablesFinderWithJoinsInSubquery() throws JSQLParserException {
+ String sql = "CREATE POLICY join_policy ON orders " +
+ "USING (EXISTS (" +
+ " SELECT 1 FROM customers c " +
+ " JOIN customer_access ca ON c.id = ca.customer_id " +
+ " WHERE c.id = orders.customer_id" +
+ "))";
+
+ List tables = getTablesFromSQL(sql);
+
+ // Should find: target table + tables from JOIN in subquery
+ assertContainsAllTables(tables, "orders", "customers", "customer_access");
+ }
+
+ // =========================================================================
+ // Advanced SQL Features - CTEs, Schema Qualification, Functions
+ // =========================================================================
+
+ @Test
+ public void testTablesFinderWithCTE() throws JSQLParserException {
+ String sql = "CREATE POLICY cte_policy ON documents " +
+ "USING (tenant_id IN (" +
+ " WITH active_tenants AS (SELECT id FROM tenants WHERE active = true) " +
+ " SELECT id FROM active_tenants" +
+ "))";
+
+ List tables = getTablesFromSQL(sql);
+
+ // Should find: target table + table referenced in CTE
+ assertContainsAllTables(tables, "documents", "tenants");
+ }
+
+ @Test
+ public void testTablesFinderWithSchemaQualifiedTables() throws JSQLParserException {
+ String sql = "CREATE POLICY schema_policy ON myschema.documents " +
+ "USING (tenant_id IN (SELECT id FROM otherschema.tenants))";
+
+ List tables = getTablesFromSQL(sql);
+
+ // Should find both schema-qualified tables
+ assertEquals(2, tables.size(),
+ "Should find both schema-qualified tables. Found: " + tables);
+
+ // Check if tables are found (with or without schema prefix depending on TablesNamesFinder
+ // behavior)
+ boolean foundDocuments = tables.stream()
+ .anyMatch(t -> t.contains("documents"));
+ boolean foundTenants = tables.stream()
+ .anyMatch(t -> t.contains("tenants"));
+
+ assertTrue(foundDocuments, "Should find documents table. Found: " + tables);
+ assertTrue(foundTenants, "Should find tenants table. Found: " + tables);
+ }
+
+ @Test
+ public void testTablesFinderWithTableFunctions() throws JSQLParserException {
+ // PostgreSQL table-valued functions can be used in FROM clauses
+ String sql = "CREATE POLICY function_policy ON documents " +
+ "USING (tenant_id IN (" +
+ " SELECT tenant_id FROM get_accessible_tenants(current_user_id())" +
+ "))";
+
+ List tables = getTablesFromSQL(sql);
+
+ // Should at least find the target table
+ // Note: Table-valued functions might not be reported as "tables" depending on
+ // implementation
+ assertTrue(tables.contains("documents"),
+ "Should at least find the target table. Found: " + tables);
+ }
+
+ // =========================================================================
+ // Edge Cases - EXISTS, UNION, Empty Policies
+ // =========================================================================
+
+ @Test
+ public void testTablesFinderWithExistsClause() throws JSQLParserException {
+ String sql = "CREATE POLICY exists_policy ON documents " +
+ "USING (EXISTS (" +
+ " SELECT 1 FROM tenant_access " +
+ " WHERE tenant_id = documents.tenant_id AND active = true" +
+ "))";
+
+ List tables = getTablesFromSQL(sql);
+
+ // Should find: target table + table in EXISTS subquery
+ assertContainsAllTables(tables, "documents", "tenant_access");
+ }
+
+ @Test
+ public void testTablesFinderWithUnionInSubquery() throws JSQLParserException {
+ String sql = "CREATE POLICY union_policy ON documents " +
+ "USING (tenant_id IN (" +
+ " SELECT tenant_id FROM primary_tenants " +
+ " UNION " +
+ " SELECT tenant_id FROM secondary_tenants" +
+ "))";
+
+ List tables = getTablesFromSQL(sql);
+
+ // Should find: target table + both tables in UNION
+ assertContainsAllTables(tables, "documents", "primary_tenants", "secondary_tenants");
+ }
+
+ @Test
+ public void testTablesFinderEmptyPolicy() throws JSQLParserException {
+ // Policy with no USING or WITH CHECK clauses
+ String sql = "CREATE POLICY simple_policy ON documents";
+
+ List tables = getTablesFromSQL(sql);
+
+ // Should only find the target table
+ assertContainsAllTables(tables, "documents");
+ }
+}
diff --git a/src/test/java/net/sf/jsqlparser/statement/create/CreatePolicyTest.java b/src/test/java/net/sf/jsqlparser/statement/create/CreatePolicyTest.java
new file mode 100644
index 000000000..829efd2c7
--- /dev/null
+++ b/src/test/java/net/sf/jsqlparser/statement/create/CreatePolicyTest.java
@@ -0,0 +1,158 @@
+/*-
+ * #%L
+ * JSQLParser library
+ * %%
+ * Copyright (C) 2004 - 2025 JSQLParser
+ * %%
+ * Dual licensed under GNU LGPL 2.1 or Apache License 2.0
+ * #L%
+ */
+package net.sf.jsqlparser.statement.create;
+
+import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.statement.Statement;
+import net.sf.jsqlparser.statement.create.policy.CreatePolicy;
+import org.junit.jupiter.api.Test;
+
+import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests for PostgreSQL CREATE POLICY statement (Row Level Security)
+ */
+public class CreatePolicyTest {
+
+ @Test
+ public void testCreatePolicyBasic() throws JSQLParserException {
+ String sql = "CREATE POLICY policy_name ON table_name";
+ assertSqlCanBeParsedAndDeparsed(sql, true);
+
+ Statement stmt = CCJSqlParserUtil.parse(sql);
+ assertInstanceOf(CreatePolicy.class, stmt);
+ CreatePolicy policy = (CreatePolicy) stmt;
+ assertEquals("policy_name", policy.getPolicyName());
+ assertEquals("table_name", policy.getTable().getName());
+ }
+
+ @Test
+ public void testCreatePolicyWithSchema() throws JSQLParserException {
+ String sql =
+ "CREATE POLICY single_tenant_access_policy ON customer_custom_data.phone_opt_out";
+ assertSqlCanBeParsedAndDeparsed(sql, true);
+
+ Statement stmt = CCJSqlParserUtil.parse(sql);
+ CreatePolicy policy = (CreatePolicy) stmt;
+ assertEquals("single_tenant_access_policy", policy.getPolicyName());
+ assertEquals("customer_custom_data.phone_opt_out",
+ policy.getTable().getFullyQualifiedName());
+ }
+
+ @Test
+ public void testCreatePolicyWithForClause() throws JSQLParserException {
+ String sql = "CREATE POLICY policy1 ON table1 FOR SELECT";
+ assertSqlCanBeParsedAndDeparsed(sql, true);
+
+ CreatePolicy policy = (CreatePolicy) CCJSqlParserUtil.parse(sql);
+ assertEquals("SELECT", policy.getCommand());
+ }
+
+ @Test
+ public void testCreatePolicyWithAllCommands() throws JSQLParserException {
+ String[] commands = {"ALL", "SELECT", "INSERT", "UPDATE", "DELETE"};
+ for (String cmd : commands) {
+ String sql = "CREATE POLICY p ON t FOR " + cmd;
+ assertSqlCanBeParsedAndDeparsed(sql, true);
+ CreatePolicy policy = (CreatePolicy) CCJSqlParserUtil.parse(sql);
+ assertEquals(cmd, policy.getCommand());
+ }
+ }
+
+ @Test
+ public void testCreatePolicyWithSingleRole() throws JSQLParserException {
+ String sql = "CREATE POLICY policy1 ON table1 TO role1";
+ assertSqlCanBeParsedAndDeparsed(sql, true);
+
+ CreatePolicy policy = (CreatePolicy) CCJSqlParserUtil.parse(sql);
+ assertEquals(1, policy.getRoles().size());
+ assertEquals("role1", policy.getRoles().get(0));
+ }
+
+ @Test
+ public void testCreatePolicyWithMultipleRoles() throws JSQLParserException {
+ String sql = "CREATE POLICY policy1 ON table1 TO role1, role2, role3";
+ assertSqlCanBeParsedAndDeparsed(sql, true);
+
+ CreatePolicy policy = (CreatePolicy) CCJSqlParserUtil.parse(sql);
+ assertEquals(3, policy.getRoles().size());
+ assertEquals("role1", policy.getRoles().get(0));
+ assertEquals("role2", policy.getRoles().get(1));
+ assertEquals("role3", policy.getRoles().get(2));
+ }
+
+ @Test
+ public void testCreatePolicyWithUsing() throws JSQLParserException {
+ String sql = "CREATE POLICY policy1 ON table1 USING (user_id = current_user_id())";
+ assertSqlCanBeParsedAndDeparsed(sql, true);
+
+ CreatePolicy policy = (CreatePolicy) CCJSqlParserUtil.parse(sql);
+ assertNotNull(policy.getUsingExpression());
+ }
+
+ @Test
+ public void testCreatePolicyWithWithCheck() throws JSQLParserException {
+ String sql = "CREATE POLICY policy1 ON table1 WITH CHECK (status = 'active')";
+ assertSqlCanBeParsedAndDeparsed(sql, true);
+
+ CreatePolicy policy = (CreatePolicy) CCJSqlParserUtil.parse(sql);
+ assertNotNull(policy.getWithCheckExpression());
+ }
+
+ @Test
+ public void testCreatePolicyComplete() throws JSQLParserException {
+ String sql =
+ "CREATE POLICY single_tenant_access_policy ON customer_custom_data.phone_opt_out " +
+ "FOR SELECT " +
+ "TO gong_app_single_tenant_ro_role, gong_app_single_tenant_rw_role " +
+ "USING (company_id = current_setting('gong.tenant.company_id')::bigint)";
+ assertSqlCanBeParsedAndDeparsed(sql, true);
+
+ CreatePolicy policy = (CreatePolicy) CCJSqlParserUtil.parse(sql);
+ assertEquals("single_tenant_access_policy", policy.getPolicyName());
+ assertEquals("customer_custom_data.phone_opt_out",
+ policy.getTable().getFullyQualifiedName());
+ assertEquals("SELECT", policy.getCommand());
+ assertEquals(2, policy.getRoles().size());
+ assertNotNull(policy.getUsingExpression());
+ }
+
+ @Test
+ public void testCreatePolicyWithBothUsingAndWithCheck() throws JSQLParserException {
+ String sql = "CREATE POLICY policy1 ON table1 " +
+ "USING (department_id = current_user_department()) " +
+ "WITH CHECK (status IN ('draft', 'published'))";
+ assertSqlCanBeParsedAndDeparsed(sql, true);
+
+ CreatePolicy policy = (CreatePolicy) CCJSqlParserUtil.parse(sql);
+ assertNotNull(policy.getUsingExpression());
+ assertNotNull(policy.getWithCheckExpression());
+ }
+
+ @Test
+ public void testCreatePolicyCompleteWithAllClauses() throws JSQLParserException {
+ String sql = "CREATE POLICY admin_policy ON documents " +
+ "FOR UPDATE " +
+ "TO admin_role, superuser " +
+ "USING (author_id = current_user_id()) " +
+ "WITH CHECK (updated_at >= CURRENT_TIMESTAMP)";
+ assertSqlCanBeParsedAndDeparsed(sql, true);
+
+ CreatePolicy policy = (CreatePolicy) CCJSqlParserUtil.parse(sql);
+ assertEquals("admin_policy", policy.getPolicyName());
+ assertEquals("documents", policy.getTable().getName());
+ assertEquals("UPDATE", policy.getCommand());
+ assertEquals(2, policy.getRoles().size());
+ assertNotNull(policy.getUsingExpression());
+ assertNotNull(policy.getWithCheckExpression());
+ }
+}