Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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