Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ whereClause, found := clickhouse.Find(stmt, func(node clickhouse.Expr) bool {
})
```

## Row policies

`CREATE`, `ALTER`, and `DROP [ROW] POLICY` have dedicated AST nodes. Policy inspection supports `SHOW CREATE [ROW] POLICY` and `SHOW [ROW] POLICIES`.

See [row-policy syntax and AST fields](docs/row-policies.md) for target bindings, principal sets, and the distinction between an omitted ALTER clause and an explicit `NONE`.

## Update test assets

For the files inside `output` and `format` dir are generated by the test cases,
Expand Down
69 changes: 69 additions & 0 deletions docs/row-policies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Row policies

The parser supports `CREATE`, `ALTER`, and `DROP [ROW] POLICY`, plus `SHOW CREATE [ROW] POLICY` and `SHOW [ROW] POLICIES`. These produce typed AST nodes rather than opaque SQL strings. Parsing does not execute policies, resolve users or roles, or check whether a table exists.

```sql
CREATE ROW POLICY example_policy
ON db.example_table, db.example_table_local
AS PERMISSIVE
FOR SELECT USING id > 0
TO example_role;

ALTER ROW POLICY example_policy ON db.example_table
USING NONE TO NONE;

DROP ROW POLICY IF EXISTS example_policy
ON db.example_table, db.example_table_local;

SHOW CREATE ROW POLICY example_policy ON db.example_table;
SHOW ROW POLICIES ON db.*;
```

## Target bindings

`Targets` contains explicit `RowPolicyTarget` nodes. Each binds one `PolicyName` to a `TableIdentifier`.

The following forms are supported:

```sql
CREATE POLICY p1, p2 ON db.t USING 1 TO reader;
CREATE POLICY p ON db.t, other.t USING 1 TO reader;
CREATE POLICY p1 ON db.t, p2 ON other.t USING 1 TO reader;
```

A multi-name list cannot also have a multi-table target list. `p1, p2 ON t1, t2` is not a Cartesian product and is rejected.

Database policies use `db.*`, or `*` for the current database. A quoted table named `*` remains distinct from a wildcard. Policy declarations reject prefix wildcards and wildcard database names rather than silently narrowing them. `SHOW [CREATE] ROW POLICIES ON *.*` can inspect all policies.

`ON CLUSTER` is a statement-level clause. CREATE/ALTER can select an access storage with `IN`; DROP uses `FROM`.

## ALTER presence semantics

`CreateRowPolicy` and `AlterRowPolicy` embed `RowPolicyOptions`. The options retain omitted fields because omission on ALTER means keeping the existing value.

- `Mode == ""` means omitted. `RowPolicyPermissive` and `RowPolicyRestrictive` are explicit modes.
- No `Filters` means no filter update. Each `RowPolicyFilter` has optional `Using` and `WithCheck` conditions. A condition with `None == true` clears that condition; an expression containing zero is still an expression.
- `To == nil` means omitted. A non-nil `RowPolicySubjects` with no principals and `All == false` represents `TO NONE`.
- `RenameTo` is only available on ALTER and requires exactly one target.

CREATE's `OR REPLACE` is different from ALTER. Consumers must not interpret omitted CREATE fields as instructions to preserve the previous policy.

Repeated filter clauses remain ordered in `Filters`. They are not collapsed into a single expression.

## Principal sets

`RowPolicySubjects` represents `TO`, including named users/roles, `ALL`, `NONE`, `EXCEPT`, and `CURRENT_USER`. Dynamic current-user aliases normalize to `CURRENT_USER`; quoted principals with the same spelling remain names.

A `RowPolicyPrincipal` contains either `Name` and an optional `Host`, or `CurrentUser == true`. A complete single-quoted principal name can contain `@`; a separate host suffix follows an identifier, as in `user1@'localhost'` or a backtick-quoted identifier followed by `@'localhost'`.

## Filtering scope and traversal

ClickHouse currently implements SELECT row filtering. `FOR ALL` normalizes to SELECT. `FOR INSERT`, `FOR UPDATE`, and `FOR DELETE` are rejected.

The parser retains `WITH CHECK` syntax as a separate condition when present. That does not imply that ClickHouse enforces write-row checks. Consumers should validate the enforcement capabilities of their target server.

Filter expressions are visible through both `Accept`/`ASTVisitor` and `Walk`. These APIs inspect syntax; consumers must not evaluate request-dependent functions such as `getSetting` or `currentUser` while rewriting policy DDL.

Custom `ASTVisitor` implementations need the new policy visit methods. Embedding `DefaultASTVisitor` supplies the default methods.

Internal `ATTACH ROW POLICY` access-storage serialization is not part of the supported SQL statement grammar.
290 changes: 290 additions & 0 deletions parser/ast_row_policy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
package parser

// RowPolicyMode controls how a policy combines with other policies.
// The empty value means omitted, not explicitly PERMISSIVE.
type RowPolicyMode string

const rowPolicyType = "ROW POLICY"

const (
RowPolicyPermissive RowPolicyMode = "PERMISSIVE"
RowPolicyRestrictive RowPolicyMode = "RESTRICTIVE"
)

// RowPolicyTarget binds a policy name to exactly one table or database wildcard.
// An unquoted Table.Table named * is a wildcard; a quoted * is a literal name.
type RowPolicyTarget struct {
PolicyName *Ident
Table *TableIdentifier
}

func (r *RowPolicyTarget) Pos() Pos { return r.PolicyName.Pos() }
func (r *RowPolicyTarget) End() Pos { return rowPolicyTableEnd(r.Table) }
func (r *RowPolicyTarget) Accept(visitor ASTVisitor) error {
visitor.Enter(r)
defer visitor.Leave(r)
if err := r.PolicyName.Accept(visitor); err != nil {
return err
}
if err := r.Table.Accept(visitor); err != nil {
return err
}
return visitor.VisitRowPolicyTarget(r)
}

// RowPolicyCondition distinguishes an explicit NONE from an expression.
// A nil condition on a filter means that clause was omitted.
type RowPolicyCondition struct {
ConditionPos Pos
ConditionEnd Pos
None bool
Expr Expr
}

func (r *RowPolicyCondition) Pos() Pos { return r.ConditionPos }
func (r *RowPolicyCondition) End() Pos { return r.ConditionEnd }
func (r *RowPolicyCondition) Accept(visitor ASTVisitor) error {
visitor.Enter(r)
defer visitor.Leave(r)
if r.Expr != nil {
if err := r.Expr.Accept(visitor); err != nil {
return err
}
}
return visitor.VisitRowPolicyCondition(r)
}

// RowPolicyFilter holds a SELECT filter clause. FOR ALL normalizes to SELECT,
// the only row-policy command implemented by ClickHouse.
type RowPolicyFilter struct {
FilterPos Pos
FilterEnd Pos
Using *RowPolicyCondition
WithCheck *RowPolicyCondition
}

func (r *RowPolicyFilter) Pos() Pos { return r.FilterPos }
func (r *RowPolicyFilter) End() Pos { return r.FilterEnd }
func (r *RowPolicyFilter) Accept(visitor ASTVisitor) error {
visitor.Enter(r)
defer visitor.Leave(r)
if r.Using != nil {
if err := r.Using.Accept(visitor); err != nil {
return err
}
}
if r.WithCheck != nil {
if err := r.WithCheck.Accept(visitor); err != nil {
return err
}
}
return visitor.VisitRowPolicyFilter(r)
}

// RowPolicyPrincipal is a named user/role or the dynamic CURRENT_USER tag.
type RowPolicyPrincipal struct {
PrincipalPos Pos
PrincipalEnd Pos
Name *Ident
Host *Ident
CurrentUser bool
}

func (r *RowPolicyPrincipal) Pos() Pos { return r.PrincipalPos }
func (r *RowPolicyPrincipal) End() Pos { return r.PrincipalEnd }
func (r *RowPolicyPrincipal) Accept(visitor ASTVisitor) error {
visitor.Enter(r)
defer visitor.Leave(r)
if r.Name != nil {
if err := r.Name.Accept(visitor); err != nil {
return err
}
}
if r.Host != nil {
if err := r.Host.Accept(visitor); err != nil {
return err
}
}
return visitor.VisitRowPolicyPrincipal(r)
}

// RowPolicySubjects represents the TO clause, including inclusions/exclusions.
// A non-nil empty set is TO NONE; nil on an ALTER means keep the existing set.
type RowPolicySubjects struct {
ToPos Pos
ToEnd Pos
All bool
Principals []*RowPolicyPrincipal
Except []*RowPolicyPrincipal
}

func (r *RowPolicySubjects) Pos() Pos { return r.ToPos }
func (r *RowPolicySubjects) End() Pos { return r.ToEnd }
func (r *RowPolicySubjects) Accept(visitor ASTVisitor) error {
visitor.Enter(r)
defer visitor.Leave(r)
for _, principal := range r.Principals {
if err := principal.Accept(visitor); err != nil {
return err
}
}
for _, principal := range r.Except {
if err := principal.Accept(visitor); err != nil {
return err
}
}
return visitor.VisitRowPolicySubjects(r)
}

// RowPolicyOptions is the shared desired definition for CREATE and ALTER.
// Omitted fields stay omitted so ALTER does not reset unrelated attributes.
type RowPolicyOptions struct {
Targets []*RowPolicyTarget
OnCluster *ClusterClause
AccessStorageType *Ident
Mode RowPolicyMode
Filters []*RowPolicyFilter
To *RowPolicySubjects
}

func (r *RowPolicyOptions) accept(visitor ASTVisitor, rename *Ident) error {
for _, target := range r.Targets {
if err := target.Accept(visitor); err != nil {
return err
}
}
if r.AccessStorageType != nil {
if err := r.AccessStorageType.Accept(visitor); err != nil {
return err
}
}
if r.OnCluster != nil {
if err := r.OnCluster.Accept(visitor); err != nil {
return err
}
}
if rename != nil {
if err := rename.Accept(visitor); err != nil {
return err
}
}
for _, filter := range r.Filters {
if err := filter.Accept(visitor); err != nil {
return err
}
}
if r.To != nil {
return r.To.Accept(visitor)
}
return nil
}

// CreateRowPolicy creates one or more explicitly bound policies.
type CreateRowPolicy struct {
CreatePos Pos
StatementEnd Pos
IfNotExists bool
OrReplace bool
RowPolicyOptions
}

func (r *CreateRowPolicy) Pos() Pos { return r.CreatePos }
func (r *CreateRowPolicy) End() Pos { return r.StatementEnd }
func (r *CreateRowPolicy) Type() string { return rowPolicyType }
func (r *CreateRowPolicy) Accept(visitor ASTVisitor) error {
visitor.Enter(r)
defer visitor.Leave(r)
if err := r.RowPolicyOptions.accept(visitor, nil); err != nil {
return err
}
return visitor.VisitCreateRowPolicy(r)
}

// AlterRowPolicy changes only the specified policy attributes.
type AlterRowPolicy struct {
AlterPos Pos
StatementEnd Pos
IfExists bool
RenameTo *Ident
RowPolicyOptions
}

func (r *AlterRowPolicy) Pos() Pos { return r.AlterPos }
func (r *AlterRowPolicy) End() Pos { return r.StatementEnd }
func (r *AlterRowPolicy) Type() string { return rowPolicyType }
func (r *AlterRowPolicy) Accept(visitor ASTVisitor) error {
visitor.Enter(r)
defer visitor.Leave(r)
if err := r.RowPolicyOptions.accept(visitor, r.RenameTo); err != nil {
return err
}
return visitor.VisitAlterRowPolicy(r)
}

// DropRowPolicy drops exactly the listed policy/table identities.
type DropRowPolicy struct {
DropPos Pos
StatementEnd Pos
IfExists bool
Targets []*RowPolicyTarget
OnCluster *ClusterClause
AccessStorageType *Ident
}

func (r *DropRowPolicy) Pos() Pos { return r.DropPos }
func (r *DropRowPolicy) End() Pos { return r.StatementEnd }
func (r *DropRowPolicy) Type() string { return rowPolicyType }
func (r *DropRowPolicy) Accept(visitor ASTVisitor) error {
visitor.Enter(r)
defer visitor.Leave(r)
for _, target := range r.Targets {
if err := target.Accept(visitor); err != nil {
return err
}
}
if r.AccessStorageType != nil {
if err := r.AccessStorageType.Accept(visitor); err != nil {
return err
}
}
if r.OnCluster != nil {
if err := r.OnCluster.Accept(visitor); err != nil {
return err
}
}
return visitor.VisitDropRowPolicy(r)
}

// ShowRowPolicies lists policies or their CREATE definitions. A nil selector
// lists all policies; Name, Table and Targets are mutually exclusive selectors.
type ShowRowPolicies struct {
ShowPos Pos
StatementEnd Pos
Create bool
Name *Ident
Table *TableIdentifier
Targets []*RowPolicyTarget
}

func (r *ShowRowPolicies) Pos() Pos { return r.ShowPos }
func (r *ShowRowPolicies) End() Pos { return r.StatementEnd }
func (r *ShowRowPolicies) Accept(visitor ASTVisitor) error {
visitor.Enter(r)
defer visitor.Leave(r)
if r.Name != nil {
if err := r.Name.Accept(visitor); err != nil {
return err
}
}
if r.Table != nil {
if err := r.Table.Accept(visitor); err != nil {
return err
}
}
for _, target := range r.Targets {
if err := target.Accept(visitor); err != nil {
return err
}
}
return visitor.VisitShowRowPolicies(r)
}
Loading
Loading