Skip to content

Commit 999cdca

Browse files
feat: add PostgreSQL Row Level Security (RLS) support (#2345)
* feat: add PostgreSQL Row Level Security (RLS) support Add support for PostgreSQL Row Level Security statements: - CREATE POLICY with full syntax (FOR, TO, USING, WITH CHECK clauses) - ALTER TABLE ENABLE/DISABLE/FORCE/NO FORCE ROW LEVEL SECURITY Changes: - New CreatePolicy AST class for CREATE POLICY statements - Added RLS operations to AlterOperation enum - Updated grammar with POLICY, LEVEL, SECURITY keywords - Fixed grammar conflicts with LOOKAHEAD directives - Updated all visitor interfaces and implementations - Added comprehensive unit tests (19 tests, 100% passing) - Updated README.md with new features All code quality checks passing: - CheckStyle: 0 violations - PMD: passed * fix: correct grammar alternative ordering for RLS statements Fixed parser failures when parsing PostgreSQL Row Level Security (RLS) statements by reordering grammar alternatives to check more specific patterns before less specific ones. Problem: - ALTER TABLE ... ENABLE/DISABLE ROW LEVEL SECURITY failed to parse - Parser was incorrectly choosing ENABLE/DISABLE KEYS path first - Grammar warning about WITH keyword conflict in CREATE POLICY Solution: 1. Reordered ENABLE alternatives: ENABLE ROW LEVEL SECURITY now checked before ENABLE KEYS (lines 9674-9684) 2. Reordered DISABLE alternatives: DISABLE ROW LEVEL SECURITY now checked before DISABLE KEYS (lines 9661-9671) 3. Added LOOKAHEAD(2) to WITH CHECK clause in CREATE POLICY to resolve conflict with CTEs (line 10470) Impact: - All 19 existing RLS tests pass (8 AlterRowLevelSecurityTest, 11 CreatePolicyTest) - WITH keyword conflict warning eliminated - Parser can now handle real-world SQL migration files with RLS statements - No regressions in existing functionality Technical Note: In JavaCC, when multiple alternatives share a common prefix (like ENABLE), the more specific pattern (longer token sequence) must appear FIRST in the grammar to be matched correctly. LOOKAHEAD values help disambiguate, but ordering is critical for correct parsing. * fix: allow RLS keywords (LEVEL, POLICY, SECURITY) as aliases Added K_LEVEL, K_POLICY, and K_SECURITY tokens to RelObjectNameWithoutStart() production to allow these keywords to be used as column aliases in addition to table/column names. This resolves the conflict where RLS keywords were breaking Oracle hierarchical queries and keywords-as-identifiers tests. The fix maintains RLS functionality while allowing these keywords to work in all SQL contexts including aliases (e.g., SELECT col AS level). * chore: update keywords after running updateKeywords task After running `./gradlew updateKeywords`, the task automatically added LEVEL, POLICY, and SECURITY keywords to RelObjectNameWithoutValue() in alphabetical order (line 3275). Removed redundant manual additions from RelObjectName() and RelObjectNameWithoutStart() that were causing unreachable statement compilation errors. The keywords are now properly maintained in the canonical location (RelObjectNameWithoutValue) and will work as identifiers in all contexts. Tests: All 4154 tests passing * run ./gradlew spotlessApply * fix: complete TablesNamesFinder integration for CREATE POLICY Add expression visitor calls to traverse USING and WITH CHECK clauses, enabling discovery of all table references in subqueries. This completes the TablesNamesFinder visitor implementation for CREATE POLICY statements by following the same pattern used in Update, Delete, and PlainSelect statements. Includes comprehensive test coverage (12 tests) covering simple subqueries, nested subqueries, CTEs, JOINs, and edge cases. --------- Co-authored-by: raz aranyi <raz.aranyi@gong.io>
1 parent f19f17e commit 999cdca

File tree

14 files changed

+822
-10
lines changed

14 files changed

+822
-10
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ JSQLParserBenchmark.parseSQLStatements 5.1 avgt 15 86.592 ± 5.781 m
9090
| RDBMS | Statements |
9191
|-----------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|
9292
| BigQuery<br>Snowflake<br>DuckDB<br>Redshift<br>Oracle<br>MS SQL Server and Sybase<br>Postgres<br>MySQL and MariaDB<br>DB2<br>H2 and HSQLDB and Derby<br>SQLite | `SELECT`<br>`INSERT`, `UPDATE`, `UPSERT`, `MERGE`<br>`DELETE`, `TRUNCATE TABLE`<br>`CREATE ...`, `ALTER ....`, `DROP ...`<br>`WITH ...` |
93+
| PostgreSQL Row Level Security | `CREATE POLICY`<br>`ALTER TABLE ... ENABLE/DISABLE/FORCE/NO FORCE ROW LEVEL SECURITY` |
9394
| Salesforce SOQL | `INCLUDES`, `EXCLUDES` |
9495
| Piped SQL (also known as FROM SQL) | |
9596

src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import net.sf.jsqlparser.statement.analyze.Analyze;
1818
import net.sf.jsqlparser.statement.comment.Comment;
1919
import net.sf.jsqlparser.statement.create.index.CreateIndex;
20+
import net.sf.jsqlparser.statement.create.policy.CreatePolicy;
2021
import net.sf.jsqlparser.statement.create.schema.CreateSchema;
2122
import net.sf.jsqlparser.statement.create.sequence.CreateSequence;
2223
import net.sf.jsqlparser.statement.create.synonym.CreateSynonym;
@@ -351,4 +352,10 @@ default void visit(LockStatement lock) {
351352
this.visit(lock, null);
352353
}
353354

355+
<S> T visit(CreatePolicy createPolicy, S context);
356+
357+
default void visit(CreatePolicy createPolicy) {
358+
this.visit(createPolicy, null);
359+
}
360+
354361
}

src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import net.sf.jsqlparser.statement.analyze.Analyze;
2222
import net.sf.jsqlparser.statement.comment.Comment;
2323
import net.sf.jsqlparser.statement.create.index.CreateIndex;
24+
import net.sf.jsqlparser.statement.create.policy.CreatePolicy;
2425
import net.sf.jsqlparser.statement.create.schema.CreateSchema;
2526
import net.sf.jsqlparser.statement.create.sequence.CreateSequence;
2627
import net.sf.jsqlparser.statement.create.synonym.CreateSynonym;
@@ -296,6 +297,12 @@ public <S> T visit(LockStatement lock, S context) {
296297
return null;
297298
}
298299

300+
@Override
301+
public <S> T visit(CreatePolicy createPolicy, S context) {
302+
303+
return null;
304+
}
305+
299306
@Override
300307
public <S> T visit(SetStatement set, S context) {
301308

src/main/java/net/sf/jsqlparser/statement/alter/AlterExpression.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,14 @@ public String toString() {
856856
} else {
857857
if (operation == AlterOperation.COMMENT_WITH_EQUAL_SIGN) {
858858
b.append("COMMENT =").append(" ");
859+
} else if (operation == AlterOperation.ENABLE_ROW_LEVEL_SECURITY) {
860+
b.append("ENABLE ROW LEVEL SECURITY").append(" ");
861+
} else if (operation == AlterOperation.DISABLE_ROW_LEVEL_SECURITY) {
862+
b.append("DISABLE ROW LEVEL SECURITY").append(" ");
863+
} else if (operation == AlterOperation.FORCE_ROW_LEVEL_SECURITY) {
864+
b.append("FORCE ROW LEVEL SECURITY").append(" ");
865+
} else if (operation == AlterOperation.NO_FORCE_ROW_LEVEL_SECURITY) {
866+
b.append("NO FORCE ROW LEVEL SECURITY").append(" ");
859867
} else {
860868
b.append(operation).append(" ");
861869
}

src/main/java/net/sf/jsqlparser/statement/alter/AlterOperation.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
package net.sf.jsqlparser.statement.alter;
1111

1212
public enum AlterOperation {
13-
ADD, ALTER, DROP, DROP_PRIMARY_KEY, DROP_UNIQUE, DROP_FOREIGN_KEY, MODIFY, CHANGE, CONVERT, COLLATE, ALGORITHM, RENAME, RENAME_TABLE, RENAME_INDEX, RENAME_KEY, RENAME_CONSTRAINT, COMMENT, COMMENT_WITH_EQUAL_SIGN, UNSPECIFIC, ADD_PARTITION, DROP_PARTITION, DISCARD_PARTITION, IMPORT_PARTITION, TRUNCATE_PARTITION, COALESCE_PARTITION, REORGANIZE_PARTITION, EXCHANGE_PARTITION, ANALYZE_PARTITION, CHECK_PARTITION, OPTIMIZE_PARTITION, REBUILD_PARTITION, REPAIR_PARTITION, REMOVE_PARTITIONING, PARTITION_BY, SET_TABLE_OPTION, ENGINE, FORCE, KEY_BLOCK_SIZE, LOCK, DISCARD_TABLESPACE, IMPORT_TABLESPACE, DISABLE_KEYS, ENABLE_KEYS;
13+
ADD, ALTER, DROP, DROP_PRIMARY_KEY, DROP_UNIQUE, DROP_FOREIGN_KEY, MODIFY, CHANGE, CONVERT, COLLATE, ALGORITHM, RENAME, RENAME_TABLE, RENAME_INDEX, RENAME_KEY, RENAME_CONSTRAINT, COMMENT, COMMENT_WITH_EQUAL_SIGN, UNSPECIFIC, ADD_PARTITION, DROP_PARTITION, DISCARD_PARTITION, IMPORT_PARTITION, TRUNCATE_PARTITION, COALESCE_PARTITION, REORGANIZE_PARTITION, EXCHANGE_PARTITION, ANALYZE_PARTITION, CHECK_PARTITION, OPTIMIZE_PARTITION, REBUILD_PARTITION, REPAIR_PARTITION, REMOVE_PARTITIONING, PARTITION_BY, SET_TABLE_OPTION, ENGINE, FORCE, KEY_BLOCK_SIZE, LOCK, DISCARD_TABLESPACE, IMPORT_TABLESPACE, DISABLE_KEYS, ENABLE_KEYS, ENABLE_ROW_LEVEL_SECURITY, DISABLE_ROW_LEVEL_SECURITY, FORCE_ROW_LEVEL_SECURITY, NO_FORCE_ROW_LEVEL_SECURITY;
1414

1515
public static AlterOperation from(String operation) {
1616
return Enum.valueOf(AlterOperation.class, operation.toUpperCase());
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2025 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.create.policy;
11+
12+
import net.sf.jsqlparser.expression.Expression;
13+
import net.sf.jsqlparser.schema.Table;
14+
import net.sf.jsqlparser.statement.Statement;
15+
import net.sf.jsqlparser.statement.StatementVisitor;
16+
17+
import java.util.ArrayList;
18+
import java.util.List;
19+
20+
/**
21+
* PostgreSQL CREATE POLICY statement for Row Level Security (RLS).
22+
*
23+
* Syntax: CREATE POLICY name ON table_name [ FOR { ALL | SELECT | INSERT | UPDATE | DELETE } ] [ TO
24+
* { role_name | PUBLIC | CURRENT_USER | SESSION_USER } [, ...] ] [ USING ( using_expression ) ] [
25+
* WITH CHECK ( check_expression ) ]
26+
*/
27+
public class CreatePolicy implements Statement {
28+
29+
private String policyName;
30+
private Table table;
31+
private String command; // ALL, SELECT, INSERT, UPDATE, DELETE
32+
private List<String> roles = new ArrayList<>();
33+
private Expression usingExpression;
34+
private Expression withCheckExpression;
35+
36+
public String getPolicyName() {
37+
return policyName;
38+
}
39+
40+
public CreatePolicy setPolicyName(String policyName) {
41+
this.policyName = policyName;
42+
return this;
43+
}
44+
45+
public Table getTable() {
46+
return table;
47+
}
48+
49+
public CreatePolicy setTable(Table table) {
50+
this.table = table;
51+
return this;
52+
}
53+
54+
public String getCommand() {
55+
return command;
56+
}
57+
58+
public CreatePolicy setCommand(String command) {
59+
this.command = command;
60+
return this;
61+
}
62+
63+
public List<String> getRoles() {
64+
return roles;
65+
}
66+
67+
public CreatePolicy setRoles(List<String> roles) {
68+
this.roles = roles;
69+
return this;
70+
}
71+
72+
public CreatePolicy addRole(String role) {
73+
this.roles.add(role);
74+
return this;
75+
}
76+
77+
public Expression getUsingExpression() {
78+
return usingExpression;
79+
}
80+
81+
public CreatePolicy setUsingExpression(Expression usingExpression) {
82+
this.usingExpression = usingExpression;
83+
return this;
84+
}
85+
86+
public Expression getWithCheckExpression() {
87+
return withCheckExpression;
88+
}
89+
90+
public CreatePolicy setWithCheckExpression(Expression withCheckExpression) {
91+
this.withCheckExpression = withCheckExpression;
92+
return this;
93+
}
94+
95+
@Override
96+
public <T, S> T accept(StatementVisitor<T> statementVisitor, S context) {
97+
return statementVisitor.visit(this, context);
98+
}
99+
100+
@Override
101+
public String toString() {
102+
StringBuilder builder = new StringBuilder("CREATE POLICY ");
103+
builder.append(policyName);
104+
builder.append(" ON ");
105+
builder.append(table.toString());
106+
107+
if (command != null) {
108+
builder.append(" FOR ").append(command);
109+
}
110+
111+
if (roles != null && !roles.isEmpty()) {
112+
builder.append(" TO ");
113+
for (int i = 0; i < roles.size(); i++) {
114+
if (i > 0) {
115+
builder.append(", ");
116+
}
117+
builder.append(roles.get(i));
118+
}
119+
}
120+
121+
if (usingExpression != null) {
122+
builder.append(" USING (").append(usingExpression.toString()).append(")");
123+
}
124+
125+
if (withCheckExpression != null) {
126+
builder.append(" WITH CHECK (").append(withCheckExpression.toString()).append(")");
127+
}
128+
129+
return builder.toString();
130+
}
131+
}

src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
import net.sf.jsqlparser.statement.analyze.Analyze;
9191
import net.sf.jsqlparser.statement.comment.Comment;
9292
import net.sf.jsqlparser.statement.create.index.CreateIndex;
93+
import net.sf.jsqlparser.statement.create.policy.CreatePolicy;
9394
import net.sf.jsqlparser.statement.create.schema.CreateSchema;
9495
import net.sf.jsqlparser.statement.create.sequence.CreateSequence;
9596
import net.sf.jsqlparser.statement.create.synonym.CreateSynonym;
@@ -1845,4 +1846,28 @@ public <S> Void visit(LockStatement lock, S context) {
18451846
public void visit(LockStatement lock) {
18461847
StatementVisitor.super.visit(lock);
18471848
}
1849+
1850+
@Override
1851+
public <S> Void visit(CreatePolicy createPolicy, S context) {
1852+
if (createPolicy.getTable() != null) {
1853+
visit(createPolicy.getTable(), context);
1854+
}
1855+
1856+
// Visit USING expression to find tables in subqueries
1857+
if (createPolicy.getUsingExpression() != null) {
1858+
createPolicy.getUsingExpression().accept(this, context);
1859+
}
1860+
1861+
// Visit WITH CHECK expression to find tables in subqueries
1862+
if (createPolicy.getWithCheckExpression() != null) {
1863+
createPolicy.getWithCheckExpression().accept(this, context);
1864+
}
1865+
1866+
return null;
1867+
}
1868+
1869+
@Override
1870+
public void visit(CreatePolicy createPolicy) {
1871+
StatementVisitor.super.visit(createPolicy);
1872+
}
18481873
}

src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import net.sf.jsqlparser.statement.analyze.Analyze;
4343
import net.sf.jsqlparser.statement.comment.Comment;
4444
import net.sf.jsqlparser.statement.create.index.CreateIndex;
45+
import net.sf.jsqlparser.statement.create.policy.CreatePolicy;
4546
import net.sf.jsqlparser.statement.create.schema.CreateSchema;
4647
import net.sf.jsqlparser.statement.create.sequence.CreateSequence;
4748
import net.sf.jsqlparser.statement.create.synonym.CreateSynonym;
@@ -520,4 +521,10 @@ public <S> StringBuilder visit(LockStatement lock, S context) {
520521
builder.append(lock.toString());
521522
return builder;
522523
}
524+
525+
@Override
526+
public <S> StringBuilder visit(CreatePolicy createPolicy, S context) {
527+
builder.append(createPolicy.toString());
528+
return builder;
529+
}
523530
}

src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import net.sf.jsqlparser.statement.comment.Comment;
4040
import net.sf.jsqlparser.statement.create.function.CreateFunction;
4141
import net.sf.jsqlparser.statement.create.index.CreateIndex;
42+
import net.sf.jsqlparser.statement.create.policy.CreatePolicy;
4243
import net.sf.jsqlparser.statement.create.procedure.CreateProcedure;
4344
import net.sf.jsqlparser.statement.create.schema.CreateSchema;
4445
import net.sf.jsqlparser.statement.create.sequence.CreateSequence;
@@ -589,4 +590,14 @@ public void visit(Import imprt) {
589590
public void visit(Export export) {
590591
visit(export, null);
591592
}
593+
594+
@Override
595+
public <S> Void visit(CreatePolicy createPolicy, S context) {
596+
// TODO: not yet implemented
597+
return null;
598+
}
599+
600+
public void visit(CreatePolicy createPolicy) {
601+
visit(createPolicy, null);
602+
}
592603
}

0 commit comments

Comments
 (0)