diff --git a/README.md b/README.md index b0c09ae6..5b7a4af7 100644 --- a/README.md +++ b/README.md @@ -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, diff --git a/docs/row-policies.md b/docs/row-policies.md new file mode 100644 index 00000000..e549abb4 --- /dev/null +++ b/docs/row-policies.md @@ -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` preserves the explicit `FOR` command list in `Commands`; an empty list means the clause was omitted. `Using` and `WithCheck` conditions are optional. 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. The parser preserves an omitted `FOR` clause, explicit `FOR SELECT`, and explicit `FOR ALL` rather than adding or replacing the clause during formatting. `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. diff --git a/parser/ast_row_policy.go b/parser/ast_row_policy.go new file mode 100644 index 00000000..995c2099 --- /dev/null +++ b/parser/ast_row_policy.go @@ -0,0 +1,463 @@ +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) +} + +func (r *RowPolicyTarget) FormatSQL(f *Formatter) { + f.WriteExpr(r.PolicyName) + f.WriteString(" ON ") + f.WriteExpr(r.Table) +} + +// 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) +} + +func (r *RowPolicyCondition) FormatSQL(f *Formatter) { + if r.None { + f.WriteString("NONE") + return + } + f.WriteExpr(r.Expr) +} + +// RowPolicyFilter holds a row filter and its optional FOR clause. +// Empty Commands means FOR was omitted; explicit SELECT and ALL are preserved. +type RowPolicyFilter struct { + FilterPos Pos + FilterEnd Pos + Commands []string + 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) +} + +func (r *RowPolicyFilter) FormatSQL(f *Formatter) { + hasFor := len(r.Commands) > 0 + if hasFor { + f.WriteString("FOR ") + for i, command := range r.Commands { + if i > 0 { + f.WriteString(", ") + } + f.WriteString(command) + } + } + if r.Using != nil { + if hasFor { + f.WriteByte(whitespace) + } + f.WriteString("USING ") + f.WriteExpr(r.Using) + } + if r.WithCheck != nil { + if hasFor || r.Using != nil { + f.WriteByte(whitespace) + } + f.WriteString("WITH CHECK ") + f.WriteExpr(r.WithCheck) + } +} + +// 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) +} + +func (r *RowPolicyPrincipal) FormatSQL(f *Formatter) { + if r.CurrentUser { + f.WriteString("CURRENT_USER") + return + } + f.WriteExpr(r.Name) + if r.Host != nil { + f.WriteByte('@') + f.WriteExpr(r.Host) + } +} + +// 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) +} + +func (r *RowPolicySubjects) FormatSQL(f *Formatter) { + f.WriteString("TO ") + if r.All { + f.WriteString("ALL") + } else if len(r.Principals) == 0 { + f.WriteString("NONE") + } else { + formatRowPolicyPrincipals(f, r.Principals) + } + if len(r.Except) > 0 { + f.WriteString(" EXCEPT ") + formatRowPolicyPrincipals(f, r.Except) + } +} + +func formatRowPolicyPrincipals(f *Formatter, principals []*RowPolicyPrincipal) { + for i, principal := range principals { + if i > 0 { + f.WriteString(", ") + } + f.WriteExpr(principal) + } +} + +func formatRowPolicyTargets(f *Formatter, targets []*RowPolicyTarget) { + for i, target := range targets { + if i > 0 { + f.WriteString(", ") + } + f.WriteExpr(target) + } +} + +// 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) format(f *Formatter, rename *Ident) { + formatRowPolicyTargets(f, r.Targets) + if r.AccessStorageType != nil { + f.WriteString(" IN ") + f.WriteExpr(r.AccessStorageType) + } + if r.OnCluster != nil { + f.WriteByte(whitespace) + f.WriteExpr(r.OnCluster) + } + if rename != nil { + f.WriteString(" RENAME TO ") + f.WriteExpr(rename) + } + if r.Mode != "" { + f.WriteString(" AS ") + f.WriteString(string(r.Mode)) + } + for i, filter := range r.Filters { + if i > 0 { + f.WriteByte(',') + } + f.WriteByte(whitespace) + f.WriteExpr(filter) + } + if r.To != nil { + f.WriteByte(whitespace) + f.WriteExpr(r.To) + } +} + +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) +} + +func (r *CreateRowPolicy) FormatSQL(f *Formatter) { + f.WriteString("CREATE ROW POLICY ") + if r.IfNotExists { + f.WriteString("IF NOT EXISTS ") + } else if r.OrReplace { + f.WriteString("OR REPLACE ") + } + r.RowPolicyOptions.format(f, nil) +} + +// 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) +} + +func (r *AlterRowPolicy) FormatSQL(f *Formatter) { + f.WriteString("ALTER ROW POLICY ") + if r.IfExists { + f.WriteString("IF EXISTS ") + } + r.RowPolicyOptions.format(f, r.RenameTo) +} + +// 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) +} + +func (r *DropRowPolicy) FormatSQL(f *Formatter) { + f.WriteString("DROP ROW POLICY ") + if r.IfExists { + f.WriteString("IF EXISTS ") + } + formatRowPolicyTargets(f, r.Targets) + if r.AccessStorageType != nil { + f.WriteString(" FROM ") + f.WriteExpr(r.AccessStorageType) + } + if r.OnCluster != nil { + f.WriteByte(whitespace) + f.WriteExpr(r.OnCluster) + } +} + +// 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) +} + +func (r *ShowRowPolicies) FormatSQL(f *Formatter) { + f.WriteString("SHOW ") + if r.Create { + f.WriteString("CREATE ") + } + if len(r.Targets) > 0 { + f.WriteString("ROW POLICY ") + formatRowPolicyTargets(f, r.Targets) + return + } + f.WriteString("ROW POLICIES") + if r.Name != nil { + f.WriteByte(whitespace) + f.WriteExpr(r.Name) + } + if r.Table != nil { + f.WriteString(" ON ") + f.WriteExpr(r.Table) + } +} diff --git a/parser/ast_visitor.go b/parser/ast_visitor.go index 7729faf8..ec0f812f 100644 --- a/parser/ast_visitor.go +++ b/parser/ast_visitor.go @@ -48,6 +48,15 @@ type ASTVisitor interface { VisitRoleName(expr *RoleName) error VisitSettingPair(expr *SettingPair) error VisitRoleSetting(expr *RoleSetting) error + VisitRowPolicyTarget(expr *RowPolicyTarget) error + VisitRowPolicyCondition(expr *RowPolicyCondition) error + VisitRowPolicyFilter(expr *RowPolicyFilter) error + VisitRowPolicyPrincipal(expr *RowPolicyPrincipal) error + VisitRowPolicySubjects(expr *RowPolicySubjects) error + VisitCreateRowPolicy(expr *CreateRowPolicy) error + VisitAlterRowPolicy(expr *AlterRowPolicy) error + VisitDropRowPolicy(expr *DropRowPolicy) error + VisitShowRowPolicies(expr *ShowRowPolicies) error VisitCreateRole(expr *CreateRole) error VisitCreateUser(expr *CreateUser) error VisitAuthenticationClause(expr *AuthenticationClause) error @@ -545,6 +554,69 @@ func (v *DefaultASTVisitor) VisitRoleSetting(expr *RoleSetting) error { return nil } +func (v *DefaultASTVisitor) VisitRowPolicyTarget(expr *RowPolicyTarget) error { + if v.Visit != nil { + return v.Visit(expr) + } + return nil +} + +func (v *DefaultASTVisitor) VisitRowPolicyCondition(expr *RowPolicyCondition) error { + if v.Visit != nil { + return v.Visit(expr) + } + return nil +} + +func (v *DefaultASTVisitor) VisitRowPolicyFilter(expr *RowPolicyFilter) error { + if v.Visit != nil { + return v.Visit(expr) + } + return nil +} + +func (v *DefaultASTVisitor) VisitRowPolicyPrincipal(expr *RowPolicyPrincipal) error { + if v.Visit != nil { + return v.Visit(expr) + } + return nil +} + +func (v *DefaultASTVisitor) VisitRowPolicySubjects(expr *RowPolicySubjects) error { + if v.Visit != nil { + return v.Visit(expr) + } + return nil +} + +func (v *DefaultASTVisitor) VisitCreateRowPolicy(expr *CreateRowPolicy) error { + if v.Visit != nil { + return v.Visit(expr) + } + return nil +} + +func (v *DefaultASTVisitor) VisitAlterRowPolicy(expr *AlterRowPolicy) error { + if v.Visit != nil { + return v.Visit(expr) + } + return nil +} + +func (v *DefaultASTVisitor) VisitDropRowPolicy(expr *DropRowPolicy) error { + if v.Visit != nil { + return v.Visit(expr) + } + return nil +} + +func (v *DefaultASTVisitor) VisitShowRowPolicies(expr *ShowRowPolicies) error { + if v.Visit != nil { + return v.Visit(expr) + } + return nil +} + func (v *DefaultASTVisitor) VisitCreateRole(expr *CreateRole) error { if v.Visit != nil { return v.Visit(expr) diff --git a/parser/format.go b/parser/format.go index 78fe2563..67dbec02 100644 --- a/parser/format.go +++ b/parser/format.go @@ -1672,6 +1672,9 @@ func (i *Ident) FormatSQL(formatter *Formatter) { func (i *IndexOperation) FormatSQL(formatter *Formatter) { formatter.WriteExpr(i.Object) + if _, numeric := i.Object.(*NumberLiteral); numeric && i.Operation == TokenKindDot { + formatter.WriteByte(whitespace) + } formatter.WriteString(string(i.Operation)) formatter.WriteExpr(i.Index) } diff --git a/parser/keyword.go b/parser/keyword.go index e3457df6..340e248a 100644 --- a/parser/keyword.go +++ b/parser/keyword.go @@ -42,6 +42,7 @@ const ( KeywordCross = "CROSS" KeywordCube = "CUBE" KeywordCurrent = "CURRENT" + KeywordCurrentUser = "CURRENT_USER" KeywordDatabase = "DATABASE" KeywordDatabases = "DATABASES" KeywordDate = "DATE" @@ -170,6 +171,9 @@ const ( KeywordPlacing = "PLACING" KeywordPipeline = "PIPELINE" KeywordPolicy = "POLICY" + KeywordPolicies = "POLICIES" + KeywordPermissive = "PERMISSIVE" + KeywordRestrictive = "RESTRICTIVE" KeywordPopulate = "POPULATE" KeywordPreceding = "PRECEDING" KeywordPrewhere = "PREWHERE" @@ -263,6 +267,9 @@ const ( KeywordSecurity = "SECURITY" ) +// KeywordCurrentUserAlt is ClickHouse's CURRENTUSER spelling of CURRENT_USER. +const KeywordCurrentUserAlt = "CURRENTUSER" + // reservedKeywords are the structural keywords — statement starters, clause // starters and expression operators — that cannot be used as bare identifiers: // letting them match identifier positions makes a missing name silently swallow @@ -370,6 +377,8 @@ var keywords = NewSet( KeywordCross, KeywordCube, KeywordCurrent, + KeywordCurrentUser, + KeywordCurrentUserAlt, KeywordDatabase, KeywordDatabases, KeywordDate, @@ -498,6 +507,9 @@ var keywords = NewSet( KeywordPipeline, KeywordPlacing, KeywordPolicy, + KeywordPolicies, + KeywordPermissive, + KeywordRestrictive, KeywordPopulate, KeywordPreceding, KeywordPrewhere, diff --git a/parser/numeric_tuple_field_test.go b/parser/numeric_tuple_field_test.go new file mode 100644 index 00000000..ebe80165 --- /dev/null +++ b/parser/numeric_tuple_field_test.go @@ -0,0 +1,27 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestFormatter_NumericTupleFieldBoundary(t *testing.T) { + for _, query := range []string{ + "SELECT 0 .A", + "SELECT -1 .A", + "ALTER POLICY p ON db.t USING 0 .A", + "ALTER POLICY A ON A USING 0 .A", + } { + t.Run(query, func(t *testing.T) { + statements, err := NewParser(query).ParseStmts() + require.NoError(t, err) + require.Len(t, statements, 1) + formatted := Format(statements[0]) + again, err := NewParser(formatted).ParseStmts() + require.NoError(t, err) + require.Len(t, again, 1) + require.Equal(t, formatted, Format(again[0])) + }) + } +} diff --git a/parser/parser_row_policy.go b/parser/parser_row_policy.go new file mode 100644 index 00000000..e3be4223 --- /dev/null +++ b/parser/parser_row_policy.go @@ -0,0 +1,638 @@ +package parser + +import "fmt" + +func (p *Parser) consumeRowPolicyKeyword(keyword string) error { + if !p.matchKeyword(keyword) { + return &ParseError{Pos: p.Pos(), Got: p.current(), Keyword: keyword} + } + return p.lexer.consumeToken() +} + +func (p *Parser) consumeRowPolicyPrefix() error { + if p.matchKeyword(KeywordRow) { + if err := p.consumeRowPolicyKeyword(KeywordRow); err != nil { + return err + } + } + return p.consumeRowPolicyKeyword(KeywordPolicy) +} + +func (p *Parser) rowPolicyOnCluster() bool { + state := p.lexer.saveState() + defer p.lexer.restoreState(state) + return p.tryConsumeKeywords(KeywordOn, KeywordCluster) +} + +func rowPolicyIdentEnd(ident *Ident) Pos { + if ident.QuoteType == BackTicks || ident.QuoteType == DoubleQuote { + return ident.End() + 1 + } + return ident.End() +} + +func rowPolicyExprEnd(expr Expr) Pos { + end := expr.End() + Walk(expr, func(node Expr) bool { + if ident, ok := node.(*Ident); ok && rowPolicyIdentEnd(ident) > end { + end = rowPolicyIdentEnd(ident) + } + return true + }) + return end +} + +func rowPolicyTableEnd(table *TableIdentifier) Pos { + return rowPolicyIdentEnd(table.Table) +} + +func rowPolicyWildcard(ident *Ident) bool { + return ident.Name == "*" && (ident.QuoteType == 0 || ident.QuoteType == Unquoted) +} + +func (p *Parser) parseRowPolicyTable(allowGlobal bool) (*TableIdentifier, error) { + first, err := p.parseIdentOrStar() + if err != nil { + return nil, err + } + if p.matchTokenKind(TokenKindMul) { + return nil, fmt.Errorf("prefix wildcards are not supported in row policy targets") + } + if !p.matchTokenKind(TokenKindDot) { + return &TableIdentifier{Table: first}, nil + } + if rowPolicyWildcard(first) && !allowGlobal { + return nil, fmt.Errorf("row policy targets cannot use a wildcard database") + } + if _, err := p.tryConsumeTokenKind(TokenKindDot); err != nil { + return nil, err + } + var table *Ident + if p.matchTokenKind(TokenKindMul) { + table, err = p.parseIdentOrStar() + } else { + table, err = p.parseAnyKeyword() + } + if err != nil { + return nil, err + } + if p.matchTokenKind(TokenKindMul) { + return nil, fmt.Errorf("prefix wildcards are not supported in row policy targets") + } + if rowPolicyWildcard(first) && !rowPolicyWildcard(table) { + return nil, fmt.Errorf("global policy inspection requires *.*") + } + return &TableIdentifier{Database: first, Table: table}, nil +} + +func (p *Parser) parseRowPolicyNames(multiple bool) ([]*Ident, error) { + name, err := p.parseIdentOrString() + if err != nil { + return nil, err + } + names := []*Ident{name} + for multiple && p.matchTokenKind(TokenKindComma) { + if _, err := p.tryConsumeTokenKind(TokenKindComma); err != nil { + return nil, err + } + name, err = p.parseIdentOrString() + if err != nil { + return nil, err + } + names = append(names, name) + } + return names, nil +} + +func (p *Parser) parseRowPolicyTables(multiple bool) ([]*TableIdentifier, error) { + table, err := p.parseRowPolicyTable(false) + if err != nil { + return nil, err + } + tables := []*TableIdentifier{table} + for multiple && p.matchTokenKind(TokenKindComma) { + state := p.lexer.saveState() + if _, err := p.tryConsumeTokenKind(TokenKindComma); err != nil { + return nil, err + } + candidate, err := p.parseRowPolicyTable(false) + if err != nil || (candidate.Database == nil && p.matchKeyword(KeywordOn) && !p.rowPolicyOnCluster()) { + p.lexer.restoreState(state) + break + } + tables = append(tables, candidate) + } + return tables, nil +} + +func bindRowPolicyTargets(names []*Ident, tables []*TableIdentifier) []*RowPolicyTarget { + var targets []*RowPolicyTarget + for _, name := range names { + for _, table := range tables { + policyCopy := *name + tableCopy := *table + columnCopy := *table.Table + tableCopy.Table = &columnCopy + if table.Database != nil { + dbCopy := *table.Database + tableCopy.Database = &dbCopy + } + targets = append(targets, &RowPolicyTarget{PolicyName: &policyCopy, Table: &tableCopy}) + } + } + return targets +} + +func (p *Parser) parseRowPolicyTargetGroup( + multiple, allowCluster bool, +) ([]*RowPolicyTarget, *ClusterClause, Pos, error) { + names, err := p.parseRowPolicyNames(multiple) + if err != nil { + return nil, nil, 0, err + } + var cluster *ClusterClause + if allowCluster && p.rowPolicyOnCluster() { + cluster, _, err = p.parseRowPolicyCluster(nil) + if err != nil { + return nil, nil, 0, err + } + } + if err := p.consumeRowPolicyKeyword(KeywordOn); err != nil { + return nil, nil, 0, err + } + tables, err := p.parseRowPolicyTables(multiple && len(names) == 1) + if err != nil { + return nil, nil, 0, err + } + end := rowPolicyTableEnd(tables[len(tables)-1]) + if allowCluster && cluster == nil && p.rowPolicyOnCluster() { + cluster, end, err = p.parseRowPolicyCluster(nil) + if err != nil { + return nil, nil, 0, err + } + } + return bindRowPolicyTargets(names, tables), cluster, end, nil +} + +func (p *Parser) parseRowPolicyTargets(allowCluster bool) ([]*RowPolicyTarget, *ClusterClause, Pos, error) { + targets, cluster, end, err := p.parseRowPolicyTargetGroup(true, allowCluster) + if err != nil { + return nil, nil, 0, err + } + canAppendPair := len(targets) == 1 + for p.matchTokenKind(TokenKindComma) { + if !canAppendPair || cluster != nil { + return nil, nil, 0, fmt.Errorf("cannot combine multiple policy names/tables or clusters with another target group") + } + if _, err := p.tryConsumeTokenKind(TokenKindComma); err != nil { + return nil, nil, 0, err + } + var pair []*RowPolicyTarget + pair, cluster, end, err = p.parseRowPolicyTargetGroup(false, allowCluster) + if err != nil { + return nil, nil, 0, err + } + targets = append(targets, pair...) + } + return targets, cluster, end, nil +} + +func (p *Parser) parseRowPolicyCondition() (*RowPolicyCondition, error) { + condition := &RowPolicyCondition{ConditionPos: p.Pos()} + if p.matchKeyword(KeywordNone) { + condition.None = true + condition.ConditionEnd = p.current().End + if err := p.consumeRowPolicyKeyword(KeywordNone); err != nil { + return nil, err + } + return condition, nil + } + expr, err := p.parseExpr(p.Pos()) + if err != nil { + return nil, err + } + condition.Expr = expr + condition.ConditionEnd = rowPolicyExprEnd(expr) + return condition, nil +} + +func (p *Parser) parseRowPolicyFilter() (*RowPolicyFilter, error) { + filter := &RowPolicyFilter{FilterPos: p.Pos()} + if p.matchKeyword(KeywordFor) { + if err := p.consumeRowPolicyKeyword(KeywordFor); err != nil { + return nil, err + } + for { + if !p.matchOneOfKeywords(KeywordSelect, KeywordAll) { + return nil, fmt.Errorf("row policy supports FOR SELECT or ALL, got %q", p.currentTokenString()) + } + command := KeywordSelect + if p.matchKeyword(KeywordAll) { + command = KeywordAll + } + filter.Commands = append(filter.Commands, command) + if err := p.lexer.consumeToken(); err != nil { + return nil, err + } + if !p.matchTokenKind(TokenKindComma) { + break + } + if _, err := p.tryConsumeTokenKind(TokenKindComma); err != nil { + return nil, err + } + } + } + var err error + if p.matchKeyword(KeywordUsing) { + if err = p.consumeRowPolicyKeyword(KeywordUsing); err != nil { + return nil, err + } + filter.Using, err = p.parseRowPolicyCondition() + if err != nil { + return nil, err + } + filter.FilterEnd = filter.Using.End() + } + if p.matchKeyword(KeywordWith) { + if err = p.consumeRowPolicyKeyword(KeywordWith); err != nil { + return nil, err + } + if err = p.consumeRowPolicyKeyword(KeywordCheck); err != nil { + return nil, err + } + filter.WithCheck, err = p.parseRowPolicyCondition() + if err != nil { + return nil, err + } + filter.FilterEnd = filter.WithCheck.End() + } + if filter.Using == nil && filter.WithCheck == nil { + return nil, fmt.Errorf("row policy filter requires USING or WITH CHECK") + } + return filter, nil +} + +func (p *Parser) parseRowPolicyPrincipal() (*RowPolicyPrincipal, error) { + principal := &RowPolicyPrincipal{PrincipalPos: p.Pos()} + if p.matchOneOfKeywords(KeywordCurrentUser, KeywordCurrentUserAlt) { + principal.CurrentUser = true + principal.PrincipalEnd = p.current().End + if err := p.lexer.consumeToken(); err != nil { + return nil, err + } + if p.matchTokenKind(TokenKindLParen) { + if _, err := p.tryConsumeTokenKind(TokenKindLParen); err != nil { + return nil, err + } + if !p.matchTokenKind(TokenKindRParen) { + return nil, fmt.Errorf("CURRENT_USER takes no arguments") + } + principal.PrincipalEnd = p.current().End + if _, err := p.tryConsumeTokenKind(TokenKindRParen); err != nil { + return nil, err + } + } + return principal, nil + } + var err error + principal.Name, err = p.parseIdentOrString() + if err != nil { + return nil, err + } + principal.PrincipalEnd = rowPolicyIdentEnd(principal.Name) + if principal.Name.QuoteType != SingleQuote && p.matchTokenKind(TokenKindAtSign) { + if _, err := p.tryConsumeTokenKind(TokenKindAtSign); err != nil { + return nil, err + } + principal.Host, err = p.parseIdentOrString() + if err != nil { + return nil, err + } + principal.PrincipalEnd = rowPolicyIdentEnd(principal.Host) + } + return principal, nil +} + +func (p *Parser) parseRowPolicyPrincipalList(allowAll bool) ([]*RowPolicyPrincipal, bool, Pos, error) { + var principals []*RowPolicyPrincipal + var all bool + var end Pos + for { + switch { + case p.matchKeyword(KeywordNone): + end = p.current().End + if err := p.consumeRowPolicyKeyword(KeywordNone); err != nil { + return nil, false, 0, err + } + case allowAll && p.matchKeyword(KeywordAll): + all = true + end = p.current().End + if err := p.consumeRowPolicyKeyword(KeywordAll); err != nil { + return nil, false, 0, err + } + default: + principal, err := p.parseRowPolicyPrincipal() + if err != nil { + return nil, false, 0, err + } + principals = append(principals, principal) + end = principal.End() + } + if !p.matchTokenKind(TokenKindComma) { + break + } + if _, err := p.tryConsumeTokenKind(TokenKindComma); err != nil { + return nil, false, 0, err + } + } + if all { + principals = nil + } + return principals, all, end, nil +} + +func (p *Parser) parseRowPolicySubjects() (*RowPolicySubjects, error) { + subjects := &RowPolicySubjects{ToPos: p.Pos()} + if err := p.consumeRowPolicyKeyword(KeywordTo); err != nil { + return nil, err + } + var err error + subjects.Principals, subjects.All, subjects.ToEnd, err = p.parseRowPolicyPrincipalList(true) + if err != nil { + return nil, err + } + if p.matchKeyword(KeywordExcept) { + if err := p.consumeRowPolicyKeyword(KeywordExcept); err != nil { + return nil, err + } + subjects.Except, _, subjects.ToEnd, err = p.parseRowPolicyPrincipalList(false) + if err != nil { + return nil, err + } + } + return subjects, nil +} + +func (p *Parser) parseRowPolicyRename(allowed bool) (*Ident, Pos, error) { + if !allowed { + return nil, 0, fmt.Errorf("RENAME TO requires a single ALTER ROW POLICY target") + } + if err := p.consumeRowPolicyKeyword(KeywordRename); err != nil { + return nil, 0, err + } + if err := p.consumeRowPolicyKeyword(KeywordTo); err != nil { + return nil, 0, err + } + name, err := p.parseIdentOrString() + if err != nil { + return nil, 0, err + } + return name, rowPolicyIdentEnd(name), nil +} + +func (p *Parser) parseRowPolicyMode(existing RowPolicyMode) (RowPolicyMode, Pos, error) { + if existing != "" { + return "", 0, fmt.Errorf("duplicate row policy mode") + } + if err := p.consumeRowPolicyKeyword(KeywordAs); err != nil { + return "", 0, err + } + var mode RowPolicyMode + switch { + case p.matchKeyword(KeywordPermissive): + mode = RowPolicyPermissive + case p.matchKeyword(KeywordRestrictive): + mode = RowPolicyRestrictive + default: + return "", 0, fmt.Errorf("expected PERMISSIVE or RESTRICTIVE, got %q", p.currentTokenString()) + } + end := p.current().End + if err := p.lexer.consumeToken(); err != nil { + return "", 0, err + } + return mode, end, nil +} + +func (p *Parser) parseRowPolicyStorage(keyword string, existing *Ident) (*Ident, Pos, error) { + if existing != nil { + return nil, 0, fmt.Errorf("duplicate row policy access storage") + } + if err := p.consumeRowPolicyKeyword(keyword); err != nil { + return nil, 0, err + } + storage, err := p.parseIdentOrString() + if err != nil { + return nil, 0, err + } + return storage, rowPolicyIdentEnd(storage), nil +} + +func (p *Parser) parseRowPolicyCluster(existing *ClusterClause) (*ClusterClause, Pos, error) { + if existing != nil { + return nil, 0, fmt.Errorf("duplicate ON CLUSTER in row policy") + } + cluster, err := p.tryParseClusterClause(p.Pos()) + if err != nil { + return nil, 0, err + } + if cluster == nil { + return nil, 0, fmt.Errorf("expected ON CLUSTER") + } + return cluster, rowPolicyExprEnd(cluster), nil +} + +func (p *Parser) parseRowPolicyTail(options *RowPolicyOptions, end Pos) (Pos, error) { + if p.matchKeyword(KeywordTo) { + subjects, err := p.parseRowPolicySubjects() + if err != nil { + return 0, err + } + options.To, end = subjects, subjects.End() + } + if p.rowPolicyOnCluster() { + var err error + options.OnCluster, end, err = p.parseRowPolicyCluster(options.OnCluster) + if err != nil { + return 0, err + } + } + return end, nil +} + +func (p *Parser) parseRowPolicyOptions(options *RowPolicyOptions, alter bool, end Pos) (*Ident, Pos, error) { + var rename *Ident + for { + var err error + switch { + case p.matchKeyword(KeywordRename): + rename, end, err = p.parseRowPolicyRename(alter && len(options.Targets) == 1 && rename == nil) + case p.matchKeyword(KeywordAs): + options.Mode, end, err = p.parseRowPolicyMode(options.Mode) + case p.matchOneOfKeywords(KeywordFor, KeywordUsing, KeywordWith): + var filter *RowPolicyFilter + filter, err = p.parseRowPolicyFilter() + if err == nil { + options.Filters = append(options.Filters, filter) + end = filter.End() + if p.matchTokenKind(TokenKindComma) { + _, err = p.tryConsumeTokenKind(TokenKindComma) + if err == nil && !p.matchOneOfKeywords(KeywordFor, KeywordUsing, KeywordWith) { + err = fmt.Errorf("expected another row policy filter after comma") + } + } + } + case p.matchKeyword(KeywordIn): + options.AccessStorageType, end, err = p.parseRowPolicyStorage(KeywordIn, options.AccessStorageType) + case p.rowPolicyOnCluster(): + options.OnCluster, end, err = p.parseRowPolicyCluster(options.OnCluster) + default: + end, err = p.parseRowPolicyTail(options, end) + return rename, end, err + } + if err != nil { + return nil, 0, err + } + } +} + +func (p *Parser) parseCreateRowPolicy(pos Pos) (*CreateRowPolicy, error) { + if err := p.consumeRowPolicyPrefix(); err != nil { + return nil, err + } + statement := &CreateRowPolicy{CreatePos: pos} + var err error + statement.IfNotExists, err = p.tryParseIfNotExists() + if err != nil { + return nil, err + } + if !statement.IfNotExists && p.matchKeyword(KeywordOr) { + if err := p.consumeRowPolicyKeyword(KeywordOr); err != nil { + return nil, err + } + if err := p.consumeRowPolicyKeyword(KeywordReplace); err != nil { + return nil, err + } + statement.OrReplace = true + } + statement.Targets, statement.OnCluster, statement.StatementEnd, err = p.parseRowPolicyTargets(true) + if err != nil { + return nil, err + } + _, statement.StatementEnd, err = p.parseRowPolicyOptions(&statement.RowPolicyOptions, false, statement.StatementEnd) + return statement, err +} + +func (p *Parser) parseAlterRowPolicy(pos Pos) (*AlterRowPolicy, error) { + if err := p.consumeRowPolicyPrefix(); err != nil { + return nil, err + } + statement := &AlterRowPolicy{AlterPos: pos} + var err error + statement.IfExists, err = p.tryParseIfExists() + if err != nil { + return nil, err + } + statement.Targets, statement.OnCluster, statement.StatementEnd, err = p.parseRowPolicyTargets(true) + if err != nil { + return nil, err + } + statement.RenameTo, statement.StatementEnd, err = p.parseRowPolicyOptions( + &statement.RowPolicyOptions, true, statement.StatementEnd, + ) + return statement, err +} + +func (p *Parser) parseDropRowPolicy(pos Pos) (*DropRowPolicy, error) { + if err := p.consumeRowPolicyPrefix(); err != nil { + return nil, err + } + statement := &DropRowPolicy{DropPos: pos} + var err error + statement.IfExists, err = p.tryParseIfExists() + if err != nil { + return nil, err + } + statement.Targets, statement.OnCluster, statement.StatementEnd, err = p.parseRowPolicyTargets(true) + if err != nil { + return nil, err + } + if p.matchKeyword(KeywordFrom) { + statement.AccessStorageType, statement.StatementEnd, err = p.parseRowPolicyStorage(KeywordFrom, nil) + if err != nil { + return nil, err + } + } + if p.rowPolicyOnCluster() { + statement.OnCluster, statement.StatementEnd, err = p.parseRowPolicyCluster(statement.OnCluster) + if err != nil { + return nil, err + } + } + return statement, nil +} + +func (p *Parser) parseShowWithPolicies(pos Pos) (Expr, error) { + state := p.lexer.saveState() + if err := p.consumeRowPolicyKeyword(KeywordShow); err != nil { + return nil, err + } + create := p.matchKeyword(KeywordCreate) + if create { + if err := p.consumeRowPolicyKeyword(KeywordCreate); err != nil { + return nil, err + } + } + if p.matchOneOfKeywords(KeywordRow, KeywordPolicy, KeywordPolicies) { + return p.parseShowRowPolicies(pos, create) + } + p.lexer.restoreState(state) + return p.parseShowStmt(pos) +} + +func (p *Parser) parseShowRowPolicies(pos Pos, create bool) (*ShowRowPolicies, error) { + statement := &ShowRowPolicies{ShowPos: pos, Create: create} + if p.matchKeyword(KeywordRow) { + if err := p.consumeRowPolicyKeyword(KeywordRow); err != nil { + return nil, err + } + } + plural := p.matchKeyword(KeywordPolicies) + if !plural && (!create || !p.matchKeyword(KeywordPolicy)) { + return nil, fmt.Errorf("expected ROW POLICIES or CREATE ROW POLICY") + } + statement.StatementEnd = p.current().End + if err := p.lexer.consumeToken(); err != nil { + return nil, err + } + if p.matchKeyword(KeywordOn) { + if err := p.consumeRowPolicyKeyword(KeywordOn); err != nil { + return nil, err + } + var err error + statement.Table, err = p.parseRowPolicyTable(true) + if err != nil { + return nil, err + } + statement.StatementEnd = rowPolicyTableEnd(statement.Table) + return statement, nil + } + if create && !p.matchTokenKind(TokenKindEOF, ";") && p.current() != nil { + state := p.lexer.saveState() + targets, _, end, err := p.parseRowPolicyTargets(false) + if err == nil { + statement.Targets, statement.StatementEnd = targets, end + return statement, nil + } + p.lexer.restoreState(state) + statement.Name, err = p.parseIdentOrString() + if err != nil { + return nil, err + } + statement.StatementEnd = rowPolicyIdentEnd(statement.Name) + return statement, nil + } + if !plural { + return nil, fmt.Errorf("SHOW CREATE ROW POLICY requires a policy or table selector") + } + return statement, nil +} diff --git a/parser/parser_table.go b/parser/parser_table.go index 0894b4ba..ac2e2f70 100644 --- a/parser/parser_table.go +++ b/parser/parser_table.go @@ -25,6 +25,11 @@ func (p *Parser) parseDDL(pos Pos) (DDL, error) { } } switch { + case p.matchKeyword(KeywordRow), p.matchKeyword(KeywordPolicy): + if isAttach { + return nil, fmt.Errorf("ATTACH ROW POLICY is an internal access-storage format") + } + return p.parseCreateRowPolicy(pos) case p.matchKeyword(KeywordNamed): return p.parseCreateNamedCollection(pos) case p.matchKeyword(KeywordDatabase): @@ -53,6 +58,8 @@ func (p *Parser) parseDDL(pos Pos) (DDL, error) { case p.matchKeyword(KeywordAlter): _ = p.lexer.consumeToken() switch { + case p.matchKeyword(KeywordRow), p.matchKeyword(KeywordPolicy): + return p.parseAlterRowPolicy(pos) case p.matchKeyword(KeywordRole): return p.parseAlterRole(pos) case p.matchKeyword(KeywordTable): @@ -62,8 +69,14 @@ func (p *Parser) parseDDL(pos Pos) (DDL, error) { } case p.matchKeyword(KeywordDrop), p.matchKeyword(KeywordDetach): + isDrop := p.matchKeyword(KeywordDrop) _ = p.lexer.consumeToken() switch { + case p.matchKeyword(KeywordRow), p.matchKeyword(KeywordPolicy): + if !isDrop { + return nil, fmt.Errorf("DETACH ROW POLICY is not supported") + } + return p.parseDropRowPolicy(pos) case p.matchKeyword(KeywordDatabase): return p.parseDropDatabase(pos) case p.matchKeyword(KeywordTemporary), @@ -1692,7 +1705,7 @@ func (p *Parser) parseStmt(pos Pos) (Expr, error) { case p.matchKeyword(KeywordGrant): expr, err = p.parseGrantPrivilegeStmt(pos) case p.matchKeyword(KeywordShow): - expr, err = p.parseShowStmt(pos) + expr, err = p.parseShowWithPolicies(pos) case p.matchKeyword(KeywordDesc), p.matchKeyword(KeywordDescribe): expr, err = p.parseDescribeStmt(pos) default: diff --git a/parser/row_policy_ast_test.go b/parser/row_policy_ast_test.go new file mode 100644 index 00000000..977fce75 --- /dev/null +++ b/parser/row_policy_ast_test.go @@ -0,0 +1,314 @@ +package parser + +import ( + "errors" + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRowPolicyTargetBindings(t *testing.T) { + cases := []struct { + query string + want [][3]string + }{ + {"CREATE POLICY p1, p2 ON db.t", [][3]string{{"p1", "db", "t"}, {"p2", "db", "t"}}}, + {"CREATE POLICY p ON db.t, other.t2", [][3]string{{"p", "db", "t"}, {"p", "other", "t2"}}}, + {"CREATE POLICY p1 ON db.t, p2 ON other.t2", [][3]string{{"p1", "db", "t"}, {"p2", "other", "t2"}}}, + {"CREATE POLICY p ON t, t2", [][3]string{{"p", "", "t"}, {"p", "", "t2"}}}, + } + for _, tc := range cases { + t.Run(tc.query, func(t *testing.T) { + statements, err := NewParser(tc.query).ParseStmts() + require.NoError(t, err) + create, ok := statements[0].(*CreateRowPolicy) + require.True(t, ok, "got %T", statements[0]) + var got [][3]string + for _, target := range create.Targets { + database := "" + if target.Table.Database != nil { + database = target.Table.Database.Name + } + got = append(got, [3]string{target.PolicyName.Name, database, target.Table.Table.Name}) + } + require.Equal(t, tc.want, got) + create.Targets[0].PolicyName.Name = "changed" + require.Equal(t, tc.want[1][0], create.Targets[1].PolicyName.Name) + create.Targets[0].Table.Table.Name = "changed" + require.Equal(t, tc.want[1][2], create.Targets[1].Table.Table.Name) + }) + } +} + +func TestAlterRowPolicyPresence(t *testing.T) { + parse := func(query string) *AlterRowPolicy { + t.Helper() + statements, err := NewParser(query).ParseStmts() + require.NoError(t, err) + alter, ok := statements[0].(*AlterRowPolicy) + require.True(t, ok, "got %T", statements[0]) + return alter + } + // Omitted ALTER clauses must not reset the existing mode, filter or recipients. + omitted := parse("ALTER ROW POLICY p ON db.t") + require.Empty(t, omitted.Mode) + require.Nil(t, omitted.To) + require.Empty(t, omitted.Filters) + + // Explicit NONE must remain distinct from omission and a false expression. + cleared := parse("ALTER ROW POLICY p ON db.t AS PERMISSIVE USING NONE TO NONE") + require.Equal(t, RowPolicyPermissive, cleared.Mode) + require.NotNil(t, cleared.To) + require.False(t, cleared.To.All) + require.Empty(t, cleared.To.Principals) + require.Len(t, cleared.Filters, 1) + require.True(t, cleared.Filters[0].Using.None) + require.Nil(t, cleared.Filters[0].Using.Expr) + require.Nil(t, cleared.Filters[0].WithCheck) + + zero := parse("ALTER ROW POLICY p ON db.t USING 0") + require.False(t, zero.Filters[0].Using.None) + number, ok := zero.Filters[0].Using.Expr.(*NumberLiteral) + require.True(t, ok, "got %T", zero.Filters[0].Using.Expr) + require.Equal(t, "0", number.Literal) + + checkOnly := parse("ALTER ROW POLICY p ON db.t WITH CHECK id = 42") + require.Nil(t, checkOnly.Filters[0].Using) + require.NotNil(t, checkOnly.Filters[0].WithCheck.Expr) + require.Contains(t, Format(checkOnly), "WITH CHECK id = 42") + + repeated := parse("ALTER ROW POLICY p ON db.t USING 1, USING 0") + require.Len(t, repeated.Filters, 2) + require.Equal(t, "1", Format(repeated.Filters[0].Using)) + require.Equal(t, "0", Format(repeated.Filters[1].Using)) +} + +func TestRowPolicyPrincipalSets(t *testing.T) { + cases := []struct { + clause string + all bool + names []string + excluded []string + }{ + {"TO NONE", false, nil, nil}, + {"TO ALL", true, nil, nil}, + {"TO ALL, ignored EXCEPT r1, CURRENT_USER()", true, nil, []string{"r1", "CURRENT_USER"}}, + {"TO r1, r2 EXCEPT r2", false, []string{"r1", "r2"}, []string{"r2"}}, + {"TO `ALL`, `CURRENT_USER`, currentUser()", false, []string{"`ALL`", "`CURRENT_USER`", "CURRENT_USER"}, nil}, + {"TO 'user`name@host', john@localhost", false, []string{"'user`name@host'", "john@localhost"}, nil}, + } + for _, tc := range cases { + t.Run(tc.clause, func(t *testing.T) { + statements, err := NewParser("CREATE ROW POLICY p ON db.t USING 1 " + tc.clause).ParseStmts() + require.NoError(t, err) + create, ok := statements[0].(*CreateRowPolicy) + require.True(t, ok, "got %T", statements[0]) + set := create.To + require.Equal(t, tc.all, set.All) + var names, excluded []string + for _, p := range set.Principals { + names = append(names, Format(p)) + } + for _, p := range set.Except { + excluded = append(excluded, Format(p)) + } + require.Equal(t, tc.names, names) + require.Equal(t, tc.excluded, excluded) + formatted := Format(statements[0]) + again, err := NewParser(formatted).ParseStmts() + require.NoError(t, err) + require.Equal(t, formatted, Format(again[0])) + }) + } +} + +func TestRowPolicyWildcardIdentity(t *testing.T) { + statements, err := NewParser("CREATE POLICY p ON db.*, db.`*`, *").ParseStmts() + require.NoError(t, err) + create, ok := statements[0].(*CreateRowPolicy) + require.True(t, ok, "got %T", statements[0]) + targets := create.Targets + require.Len(t, targets, 3) + require.Equal(t, "*", targets[0].Table.Table.Name) + require.NotEqual(t, BackTicks, targets[0].Table.Table.QuoteType) + require.Equal(t, BackTicks, targets[1].Table.Table.QuoteType) + require.Nil(t, targets[2].Table.Database, "unqualified * must retain the implicit current database") + require.Contains(t, Format(statements[0]), "db.`*`") +} + +func TestRowPolicyGlobalInspectionScope(t *testing.T) { + for _, prefix := range []string{"SHOW ROW POLICIES", "SHOW CREATE ROW POLICIES"} { + statements, err := NewParser(prefix + " ON *.*").ParseStmts() + require.NoError(t, err) + show, ok := statements[0].(*ShowRowPolicies) + require.True(t, ok, "got %T", statements[0]) + require.Equal(t, "*", show.Table.Database.Name) + require.Equal(t, "*", show.Table.Table.Name) + } + for _, command := range []string{"CREATE", "ALTER", "DROP"} { + _, err := NewParser(command + " ROW POLICY p ON *.*").ParseStmts() + require.Error(t, err, command) + } + _, err := NewParser("SHOW ROW POLICIES ON *.events").ParseStmts() + require.Error(t, err) +} + +func TestRowPolicyTraversal(t *testing.T) { + queries := []string{ + "CREATE POLICY p ON db.t IN local_directory ON CLUSTER c AS RESTRICTIVE USING id = 42 WITH CHECK id > 0 TO reader, CURRENT_USER EXCEPT user1@'host'", + "ALTER POLICY p ON db.t RENAME TO renamed USING NONE TO NONE", + "DROP POLICY p ON db.t FROM local_directory ON CLUSTER c", + "SHOW CREATE POLICY p1 ON db.t, p2 ON other.t", + "SHOW CREATE POLICY p", + "SHOW POLICIES ON db.*", + } + for _, query := range queries { + t.Run(query, func(t *testing.T) { + statements, err := NewParser(query).ParseStmts() + require.NoError(t, err) + var walked []string + Walk(statements[0], func(expr Expr) bool { + walked = append(walked, fmt.Sprintf("%T", expr)) + return true + }) + visitor := &rowPolicyTestVisitor{} + require.NoError(t, statements[0].Accept(visitor)) + require.Equal(t, walked, visitor.entered) + require.Zero(t, visitor.depth) + }) + } + statements, err := NewParser(queries[0]).ParseStmts() + require.NoError(t, err) + stop := errors.New("stop on condition") + visitor := &rowPolicyTestVisitor{DefaultASTVisitor: DefaultASTVisitor{Visit: func(expr Expr) error { + if _, ok := expr.(*RowPolicyCondition); ok { + return stop + } + return nil + }}} + require.ErrorIs(t, statements[0].Accept(visitor), stop) + require.Zero(t, visitor.depth) + conditions := 0 + completed := Walk(statements[0], func(expr Expr) bool { + if _, ok := expr.(*RowPolicyFilter); ok { + return false + } + if _, ok := expr.(*RowPolicyCondition); ok { + conditions++ + } + return true + }) + require.False(t, completed) + require.Zero(t, conditions, "stopping at a filter must prevent visits to its conditions") +} + +func TestRowPolicyStatementPositions(t *testing.T) { + queries := []string{ + "CREATE POLICY p ON db.`table`", + "CREATE POLICY p ON db.t AS PERMISSIVE", + "CREATE POLICY p ON db.t USING t.`column`", + "CREATE POLICY p ON db.t TO user1@'host'", + "ALTER POLICY p ON db.t RENAME TO `new-name`", + "DROP POLICY p ON db.t ON CLUSTER `my-cluster`", + "SHOW CREATE POLICY p ON db.`table`", + "CREATE POLICY `读策略` ON `数据库`.`表` TO `读者`", + } + for _, query := range queries { + t.Run(query, func(t *testing.T) { + statements, err := NewParser(query + "; -- trailing comment").ParseStmts() + require.NoError(t, err) + require.Equal(t, Pos(0), statements[0].Pos()) + require.Equal(t, Pos(len(query)), statements[0].End()) + }) + } +} + +func TestParser_RowPolicyTruncatedInput(t *testing.T) { + query := "CREATE ROW POLICY p ON db.t IN local_directory AS RESTRICTIVE USING has([1,2], id) TO ALL EXCEPT r" + for i := range query { + require.NotPanics(t, func() { _, _ = NewParser(query[:i]).ParseStmts() }) + } + for _, suffix := range []string{" /*", " TO 'unterminated", " AS", " USING", " TO", " IN", " ON CLUSTER"} { + _, err := NewParser("CREATE ROW POLICY p ON db.t" + suffix).ParseStmts() + require.Error(t, err, suffix) + } + for _, query := range []string{ + "CREATE POLICY p ON db.t AS PERMISSIVE /*", + "CREATE POLICY p ON db.t TO NONE /*", + "CREATE POLICY p ON db.t USING NONE /*", + "CREATE POLICY p ON db.t TO CURRENT_USER /*", + "CREATE POLICY p ON db.t ON CLUSTER 'c' /*", + "CREATE POLICY p ON db.t AS PERMISSIVE AS RESTRICTIVE", + "CREATE POLICY p ON db.t IN a IN b", + "CREATE POLICY p ON db.t TO 'user'@'host'", + "CREATE POLICY p ON db.t ON CLUSTER a ON CLUSTER b", + } { + _, err := NewParser(query).ParseStmts() + require.Error(t, err, query) + } +} + +func TestParser_RowPolicyContextualKeywords(t *testing.T) { + for _, word := range []string{"permissive", "restrictive", "policies", "current_user", "currentuser"} { + for _, query := range []string{ + "SELECT " + word + " FROM db.t", + "SELECT x AS " + word + " FROM db.t", + "SELECT x " + word + " FROM db.t", + "SELECT " + word + "() FROM db.t", + "CREATE TABLE db." + word + " (" + word + " UInt64) ENGINE = MergeTree ORDER BY " + word, + "CREATE ROLE " + word, + } { + statements, err := NewParser(query).ParseStmts() + require.NoError(t, err, query) + require.Len(t, statements, 1) + again, err := NewParser(Format(statements[0])).ParseStmts() + require.NoError(t, err, query) + require.Len(t, again, 1) + require.Equal(t, Format(statements[0]), Format(again[0])) + } + } +} + +func FuzzParser_RowPolicyRoundTrip(f *testing.F) { + for _, seed := range []string{ + "CREATE POLICY p ON db.t USING x = 1 TO ALL EXCEPT r", + "ALTER POLICY p ON db.t USING NONE TO NONE", + "DROP POLICY p ON db.t", + "SHOW CREATE POLICY p ON db.t", + "SHOW ROW POLICIES ON *.*", + "CREATE POLICY", + } { + f.Add(seed) + } + f.Fuzz(func(t *testing.T, query string) { + if len(query) > 4096 || !strings.Contains(strings.ToUpper(query), "POLIC") { + t.Skip() + } + statements, err := NewParser(query).ParseStmts() + if err != nil { + return + } + for _, statement := range statements { + formatted := Format(statement) + again, err := NewParser(formatted).ParseStmts() + require.NoError(t, err) + require.Len(t, again, 1) + require.Equal(t, formatted, Format(again[0])) + } + }) +} + +type rowPolicyTestVisitor struct { + DefaultASTVisitor + entered []string + depth int +} + +func (v *rowPolicyTestVisitor) Enter(expr Expr) { + v.entered = append(v.entered, fmt.Sprintf("%T", expr)) + v.depth++ +} + +func (v *rowPolicyTestVisitor) Leave(Expr) { v.depth-- } diff --git a/parser/row_policy_corpus_test.go b/parser/row_policy_corpus_test.go new file mode 100644 index 00000000..7ae4c729 --- /dev/null +++ b/parser/row_policy_corpus_test.go @@ -0,0 +1,111 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParser_RowPolicyNativeSyntax(t *testing.T) { + t.Parallel() + cases := []struct { + name string + query string + valid bool + }{ + {name: "create_basic", query: "CREATE ROW POLICY p ON db.t FOR SELECT USING id = 42 TO r", valid: true}, + {name: "create_alias", query: "CREATE POLICY p ON db.t USING id = 42 TO r", valid: true}, + {name: "create_if_missing", query: "CREATE ROW POLICY IF NOT EXISTS p ON db.t USING 1 TO r", valid: true}, + {name: "create_replace", query: "CREATE ROW POLICY OR REPLACE p ON db.t USING 1 TO r", valid: true}, + {name: "no_filter_or_roles", query: "CREATE ROW POLICY p ON db.t", valid: true}, + {name: "filter_none", query: "CREATE ROW POLICY p ON db.t USING NONE", valid: true}, + {name: "filter_false", query: "CREATE ROW POLICY p ON db.t USING 0", valid: true}, + {name: "restrictive_before_filter", query: "CREATE ROW POLICY p ON db.t AS RESTRICTIVE FOR SELECT USING x = 1 TO r", valid: true}, + {name: "restrictive_after_filter", query: "CREATE ROW POLICY p ON db.t USING x = 1 AS RESTRICTIVE TO r", valid: true}, + {name: "permissive", query: "CREATE ROW POLICY p ON db.t AS PERMISSIVE USING 1 TO r", valid: true}, + {name: "for_all", query: "CREATE ROW POLICY p ON db.t FOR ALL USING x = 1 TO r", valid: true}, + {name: "two_names_one_table", query: "CREATE ROW POLICY p1, p2 ON db.t USING 1 TO r", valid: true}, + {name: "one_name_two_tables", query: "CREATE ROW POLICY p ON db.t, db.t_local USING 1 TO r", valid: true}, + {name: "paired_names_tables", query: "CREATE ROW POLICY p1 ON db.t, p2 ON other.t USING 1 TO r", valid: true}, + {name: "no_database", query: "CREATE ROW POLICY p ON t USING 1 TO r", valid: true}, + {name: "database_wildcard", query: "CREATE ROW POLICY p ON db.* USING 1 TO r", valid: true}, + {name: "current_database_wildcard", query: "CREATE ROW POLICY p ON * USING 1 TO r", valid: true}, + {name: "quoted_literal_star", query: "CREATE ROW POLICY p ON db.`*` USING 1 TO r", valid: true}, + {name: "quoted_names", query: "CREATE ROW POLICY 'policy name' ON `my-db`.`my-table` USING `context.id` = 42 TO `role-name`, 'user-name'", valid: true}, + {name: "storage", query: "CREATE ROW POLICY p ON db.t IN local_directory USING 1 TO r", valid: true}, + {name: "cluster_before_target", query: "CREATE ROW POLICY p ON CLUSTER 'cluster-name' ON db.t USING 1 TO r", valid: true}, + {name: "cluster_after_target", query: "CREATE ROW POLICY p ON db.t ON CLUSTER 'cluster-name' USING 1 TO r", valid: true}, + {name: "cluster_at_end", query: "CREATE ROW POLICY p ON db.t USING 1 TO r ON CLUSTER 'cluster-name'", valid: true}, + {name: "to_none", query: "CREATE ROW POLICY p ON db.t USING 1 TO NONE", valid: true}, + {name: "to_all", query: "CREATE ROW POLICY p ON db.t USING 1 TO ALL", valid: true}, + {name: "to_all_except", query: "CREATE ROW POLICY p ON db.t USING 1 TO ALL EXCEPT r1, r2", valid: true}, + {name: "to_named_except", query: "CREATE ROW POLICY p ON db.t USING 1 TO r1, r2 EXCEPT r2", valid: true}, + {name: "to_current_user", query: "CREATE ROW POLICY p ON db.t USING 1 TO CURRENT_USER", valid: true}, + {name: "to_current_user_parens", query: "CREATE ROW POLICY p ON db.t USING 1 TO CURRENT_USER()", valid: true}, + {name: "to_currentuser_function", query: "CREATE ROW POLICY p ON db.t USING 1 TO currentUser()", valid: true}, + {name: "to_user_host", query: "CREATE ROW POLICY p ON db.t USING 1 TO john@'localhost'", valid: true}, + {name: "predicate_function_commas", query: "CREATE ROW POLICY p ON db.t USING has([1, 2, 3], id) AND id = toUInt64(getSetting('filter_id')) TO r", valid: true}, + {name: "predicate_subquery", query: "CREATE ROW POLICY p ON db.t USING id IN (SELECT id FROM db.lookup_table) TO r", valid: true}, + {name: "alter_filter", query: "ALTER ROW POLICY p ON db.t USING id = 99", valid: true}, + {name: "alter_if_exists", query: "ALTER POLICY IF EXISTS p ON db.t USING 0", valid: true}, + {name: "alter_filter_none", query: "ALTER ROW POLICY p ON db.t USING NONE", valid: true}, + {name: "alter_roles_none", query: "ALTER ROW POLICY p ON db.t TO NONE", valid: true}, + {name: "alter_mode", query: "ALTER ROW POLICY p ON db.t AS PERMISSIVE", valid: true}, + {name: "alter_rename", query: "ALTER ROW POLICY p ON db.t RENAME TO 'new-policy'", valid: true}, + {name: "alter_multiple", query: "ALTER ROW POLICY p1, p2 ON db.t TO ALL", valid: true}, + {name: "drop_basic", query: "DROP ROW POLICY p ON db.t", valid: true}, + {name: "drop_if_exists_alias", query: "DROP POLICY IF EXISTS p ON db.t", valid: true}, + {name: "drop_pairs", query: "DROP ROW POLICY p1 ON db.t, p2 ON other.t", valid: true}, + {name: "drop_storage_cluster", query: "DROP ROW POLICY p ON db.t FROM local_directory ON CLUSTER 'c'", valid: true}, + {name: "attach_id_roles", query: "ATTACH ROW POLICY p ON db.t USING 1 TO ID('11111111-1111-1111-1111-111111111111')", valid: false}, + {name: "attach_no_roles", query: "ATTACH ROW POLICY p ON db.t USING 1", valid: false}, + {name: "show_exact", query: "SHOW CREATE ROW POLICY p ON db.t", valid: true}, + {name: "show_alias", query: "SHOW CREATE POLICY p ON db.t", valid: true}, + {name: "show_multiple", query: "SHOW CREATE ROW POLICY p1, p2 ON db.t", valid: true}, + {name: "show_short_name", query: "SHOW CREATE ROW POLICY p", valid: true}, + {name: "show_all", query: "SHOW CREATE ROW POLICIES", valid: true}, + {name: "show_table", query: "SHOW CREATE ROW POLICIES ON db.t", valid: true}, + {name: "show_policies", query: "SHOW ROW POLICIES ON db.*", valid: true}, + {name: "legacy_global_wildcard", query: "CREATE ROW POLICY p ON *.* USING 1 TO r", valid: false}, + {name: "legacy_prefix_wildcard", query: "CREATE ROW POLICY p ON db.prefix* USING 1 TO r", valid: false}, + {name: "legacy_prefix_database", query: "CREATE ROW POLICY p ON db*.* USING 1 TO r", valid: false}, + {name: "with_check_only", query: "CREATE ROW POLICY p ON db.t WITH CHECK x = 1 TO r", valid: true}, + {name: "with_using_and_check", query: "CREATE ROW POLICY p ON db.t USING x = 1 WITH CHECK y = 2 TO r", valid: true}, + {name: "repeated_filter", query: "ALTER ROW POLICY p ON db.t USING x = 1, USING x = 2", valid: true}, + {name: "invalid_cartesian", query: "CREATE ROW POLICY p1, p2 ON db.t, db.t2 USING 1 TO r", valid: false}, + {name: "invalid_multi_name_then_pair", query: "CREATE ROW POLICY p1, p2 ON db.t, p3 ON db.t3 USING 1 TO r", valid: false}, + {name: "invalid_clusters_per_pair", query: "CREATE ROW POLICY p1 ON CLUSTER 'c1' ON db.t, p2 ON CLUSTER 'c2' ON db.t2 USING 1 TO r", valid: false}, + {name: "invalid_insert", query: "CREATE ROW POLICY p ON db.t FOR INSERT USING 1 TO r", valid: false}, + {name: "invalid_update", query: "CREATE ROW POLICY p ON db.t FOR UPDATE USING 1 TO r", valid: false}, + {name: "invalid_delete", query: "CREATE ROW POLICY p ON db.t FOR DELETE USING 1 TO r", valid: false}, + {name: "invalid_multi_rename", query: "ALTER ROW POLICY p1, p2 ON db.t RENAME TO p3", valid: false}, + {name: "invalid_drop_sync", query: "DROP ROW POLICY p ON db.t SYNC", valid: false}, + {name: "invalid_comment", query: "CREATE ROW POLICY p ON db.t USING 1 TO r COMMENT 'owner'", valid: false}, + {name: "invalid_attach_named_role", query: "ATTACH ROW POLICY p ON db.t USING 1 TO r", valid: false}, + {name: "invalid_create_or_replace_order", query: "CREATE OR REPLACE ROW POLICY p ON db.t USING 1 TO r", valid: false}, + {name: "invalid_top_alias", query: "CREATE ROW POLICY p ON db.t USING x AS y TO r", valid: false}, + {name: "invalid_missing_expression", query: "CREATE ROW POLICY p ON db.t USING TO r", valid: false}, + {name: "invalid_trailing_roles_comma", query: "CREATE ROW POLICY p ON db.t USING 1 TO r,", valid: false}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + statements, err := NewParser(tc.query).ParseStmts() + if !tc.valid { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Len(t, statements, 1) + compact := Format(statements[0]) + again, err := NewParser(compact).ParseStmts() + require.NoError(t, err) + require.Len(t, again, 1) + require.Equal(t, compact, Format(again[0])) + beautifier := NewFormatter().WithBeautify() + beautifier.WriteExpr(statements[0]) + pretty, err := NewParser(beautifier.String()).ParseStmts() + require.NoError(t, err) + require.Equal(t, compact, Format(pretty[0])) + }) + } +} diff --git a/parser/row_policy_test.go b/parser/row_policy_test.go new file mode 100644 index 00000000..8481b140 --- /dev/null +++ b/parser/row_policy_test.go @@ -0,0 +1,79 @@ +package parser + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParser_RowPolicyStatements(t *testing.T) { + t.Parallel() + queries := []string{ + "CREATE ROW POLICY p ON db.t FOR SELECT USING id = 42 TO r", + "CREATE POLICY IF NOT EXISTS p ON db.t, db.t_local USING id = toUInt64(getSetting('filter_id')) TO r", + "ALTER ROW POLICY IF EXISTS p ON db.t USING NONE TO NONE", + "DROP POLICY IF EXISTS p ON db.t FROM local_directory ON CLUSTER 'c'", + "SHOW CREATE ROW POLICY p ON db.t", + "SHOW ROW POLICIES ON db.*", + "SHOW CREATE ROW POLICIES ON *.*", + "SHOW ROW POLICIES ON *.*", + } + for _, query := range queries { + t.Run(query, func(t *testing.T) { + statements, err := NewParser(query).ParseStmts() + require.NoError(t, err) + require.Len(t, statements, 1) + formatted := Format(statements[0]) + again, err := NewParser(formatted).ParseStmts() + require.NoError(t, err) + require.Len(t, again, 1) + require.Equal(t, formatted, Format(again[0])) + }) + } +} + +func TestParser_RowPolicyPreservesForClause(t *testing.T) { + for _, prefix := range []string{ + "CREATE ROW POLICY p ON db.t ", + "ALTER ROW POLICY p ON db.t ", + } { + for _, clause := range []string{ + "USING id = 42 TO r", + "WITH CHECK id > 0 TO r", + "USING id = 42 WITH CHECK id > 0 TO r", + "FOR SELECT USING id = 42 TO r", + "FOR ALL USING id = 42 TO r", + "FOR SELECT, ALL USING id = 42 TO r", + "FOR ALL USING id = 42, USING id = 43 TO r", + } { + query := prefix + clause + t.Run(query, func(t *testing.T) { + statements, err := NewParser(query).ParseStmts() + require.NoError(t, err) + require.Len(t, statements, 1) + require.Equal(t, query, Format(statements[0])) + beautifier := NewFormatter().WithBeautify() + beautifier.WriteExpr(statements[0]) + for _, sql := range []string{Format(statements[0]), beautifier.String()} { + again, err := NewParser(sql).ParseStmts() + require.NoError(t, err) + require.Len(t, again, 1) + require.Equal(t, query, Format(again[0])) + } + }) + } + } +} + +func TestParser_RowPolicyMixedStatements(t *testing.T) { + query := "CREATE TABLE db.t (id UInt64) ENGINE = MergeTree ORDER BY id; CREATE ROW POLICY p ON db.t USING id = 42 TO r; SELECT id FROM db.t" + statements, err := NewParser(query).ParseStmts() + require.NoError(t, err) + require.Len(t, statements, 3) + require.True(t, strings.HasPrefix(Format(statements[1]), "CREATE ROW POLICY")) + + statements, err = NewParser("SELECT 1; CREATE ROW POLICY p ON *.*").ParseStmts() + require.Error(t, err) + require.Empty(t, statements, "a failed policy must not return a successfully parsed prefix") +} diff --git a/parser/testdata/ddl/format/beautify/row_policy.sql b/parser/testdata/ddl/format/beautify/row_policy.sql new file mode 100644 index 00000000..b53c2e25 --- /dev/null +++ b/parser/testdata/ddl/format/beautify/row_policy.sql @@ -0,0 +1,28 @@ +-- Origin SQL: +CREATE ROW POLICY example_policy ON db.example_table USING id = 42 TO example_role; +CREATE POLICY IF NOT EXISTS example_policy ON db.example_table, db.example_table_local USING id = toUInt64(getSetting('filter_id')) TO example_role; +CREATE ROW POLICY OR REPLACE p1, p2 ON db.example_table IN local_directory ON CLUSTER 'example-cluster' AS RESTRICTIVE USING has([1, 2], id) TO ALL EXCEPT admin, CURRENT_USER; +CREATE POLICY all_tables ON db.*, literal_table ON db.`*` USING 1 TO reader; +ALTER ROW POLICY IF EXISTS example_policy ON db.example_table USING id = 99 WITH CHECK id > 0 TO reader, `service-user`@'localhost'; +ALTER POLICY example_policy ON db.example_table RENAME TO renamed AS PERMISSIVE USING NONE TO NONE; +DROP ROW POLICY IF EXISTS p1, p2 ON db.example_table FROM local_directory ON CLUSTER 'example-cluster'; +SHOW CREATE ROW POLICY example_policy ON db.example_table, example_policy ON db.example_table_local; +SHOW CREATE ROW POLICIES ON db.*; +SHOW ROW POLICIES; +CREATE ROW POLICY selected_rows ON db.example_table FOR SELECT USING id > 0 TO example_role; +CREATE ROW POLICY all_commands ON db.example_table FOR ALL USING id > 0 TO example_role; + + +-- Beautify SQL: +CREATE ROW POLICY example_policy ON db.example_table USING id = 42 TO example_role; +CREATE ROW POLICY IF NOT EXISTS example_policy ON db.example_table, example_policy ON db.example_table_local USING id = toUInt64(getSetting('filter_id')) TO example_role; +CREATE ROW POLICY OR REPLACE p1 ON db.example_table, p2 ON db.example_table IN local_directory ON CLUSTER 'example-cluster' AS RESTRICTIVE USING has([1, 2], id) TO ALL EXCEPT admin, CURRENT_USER; +CREATE ROW POLICY all_tables ON db.*, literal_table ON db.`*` USING 1 TO reader; +ALTER ROW POLICY IF EXISTS example_policy ON db.example_table USING id = 99 WITH CHECK id > 0 TO reader, `service-user`@'localhost'; +ALTER ROW POLICY example_policy ON db.example_table RENAME TO renamed AS PERMISSIVE USING NONE TO NONE; +DROP ROW POLICY IF EXISTS p1 ON db.example_table, p2 ON db.example_table FROM local_directory ON CLUSTER 'example-cluster'; +SHOW CREATE ROW POLICY example_policy ON db.example_table, example_policy ON db.example_table_local; +SHOW CREATE ROW POLICIES ON db.*; +SHOW ROW POLICIES; +CREATE ROW POLICY selected_rows ON db.example_table FOR SELECT USING id > 0 TO example_role; +CREATE ROW POLICY all_commands ON db.example_table FOR ALL USING id > 0 TO example_role; diff --git a/parser/testdata/ddl/format/row_policy.sql b/parser/testdata/ddl/format/row_policy.sql new file mode 100644 index 00000000..3f341c4b --- /dev/null +++ b/parser/testdata/ddl/format/row_policy.sql @@ -0,0 +1,28 @@ +-- Origin SQL: +CREATE ROW POLICY example_policy ON db.example_table USING id = 42 TO example_role; +CREATE POLICY IF NOT EXISTS example_policy ON db.example_table, db.example_table_local USING id = toUInt64(getSetting('filter_id')) TO example_role; +CREATE ROW POLICY OR REPLACE p1, p2 ON db.example_table IN local_directory ON CLUSTER 'example-cluster' AS RESTRICTIVE USING has([1, 2], id) TO ALL EXCEPT admin, CURRENT_USER; +CREATE POLICY all_tables ON db.*, literal_table ON db.`*` USING 1 TO reader; +ALTER ROW POLICY IF EXISTS example_policy ON db.example_table USING id = 99 WITH CHECK id > 0 TO reader, `service-user`@'localhost'; +ALTER POLICY example_policy ON db.example_table RENAME TO renamed AS PERMISSIVE USING NONE TO NONE; +DROP ROW POLICY IF EXISTS p1, p2 ON db.example_table FROM local_directory ON CLUSTER 'example-cluster'; +SHOW CREATE ROW POLICY example_policy ON db.example_table, example_policy ON db.example_table_local; +SHOW CREATE ROW POLICIES ON db.*; +SHOW ROW POLICIES; +CREATE ROW POLICY selected_rows ON db.example_table FOR SELECT USING id > 0 TO example_role; +CREATE ROW POLICY all_commands ON db.example_table FOR ALL USING id > 0 TO example_role; + + +-- Format SQL: +CREATE ROW POLICY example_policy ON db.example_table USING id = 42 TO example_role; +CREATE ROW POLICY IF NOT EXISTS example_policy ON db.example_table, example_policy ON db.example_table_local USING id = toUInt64(getSetting('filter_id')) TO example_role; +CREATE ROW POLICY OR REPLACE p1 ON db.example_table, p2 ON db.example_table IN local_directory ON CLUSTER 'example-cluster' AS RESTRICTIVE USING has([1, 2], id) TO ALL EXCEPT admin, CURRENT_USER; +CREATE ROW POLICY all_tables ON db.*, literal_table ON db.`*` USING 1 TO reader; +ALTER ROW POLICY IF EXISTS example_policy ON db.example_table USING id = 99 WITH CHECK id > 0 TO reader, `service-user`@'localhost'; +ALTER ROW POLICY example_policy ON db.example_table RENAME TO renamed AS PERMISSIVE USING NONE TO NONE; +DROP ROW POLICY IF EXISTS p1 ON db.example_table, p2 ON db.example_table FROM local_directory ON CLUSTER 'example-cluster'; +SHOW CREATE ROW POLICY example_policy ON db.example_table, example_policy ON db.example_table_local; +SHOW CREATE ROW POLICIES ON db.*; +SHOW ROW POLICIES; +CREATE ROW POLICY selected_rows ON db.example_table FOR SELECT USING id > 0 TO example_role; +CREATE ROW POLICY all_commands ON db.example_table FOR ALL USING id > 0 TO example_role; diff --git a/parser/testdata/ddl/output/row_policy.sql.golden.json b/parser/testdata/ddl/output/row_policy.sql.golden.json new file mode 100644 index 00000000..68261b4c --- /dev/null +++ b/parser/testdata/ddl/output/row_policy.sql.golden.json @@ -0,0 +1,994 @@ +[ + { + "CreatePos": 0, + "StatementEnd": 82, + "IfNotExists": false, + "OrReplace": false, + "Targets": [ + { + "PolicyName": { + "Name": "example_policy", + "QuoteType": 1, + "NamePos": 18, + "NameEnd": 32 + }, + "Table": { + "Database": { + "Name": "db", + "QuoteType": 1, + "NamePos": 36, + "NameEnd": 38 + }, + "Table": { + "Name": "example_table", + "QuoteType": 1, + "NamePos": 39, + "NameEnd": 52 + } + } + } + ], + "OnCluster": null, + "AccessStorageType": null, + "Mode": "", + "Filters": [ + { + "FilterPos": 53, + "FilterEnd": 66, + "Commands": null, + "Using": { + "ConditionPos": 59, + "ConditionEnd": 66, + "None": false, + "Expr": { + "LeftExpr": { + "Name": "id", + "QuoteType": 1, + "NamePos": 59, + "NameEnd": 61 + }, + "Operation": "=", + "RightExpr": { + "NumPos": 64, + "NumEnd": 66, + "Literal": "42", + "Base": 10 + }, + "HasGlobal": false, + "HasNot": false + } + }, + "WithCheck": null + } + ], + "To": { + "ToPos": 67, + "ToEnd": 82, + "All": false, + "Principals": [ + { + "PrincipalPos": 70, + "PrincipalEnd": 82, + "Name": { + "Name": "example_role", + "QuoteType": 1, + "NamePos": 70, + "NameEnd": 82 + }, + "Host": null, + "CurrentUser": false + } + ], + "Except": null + } + }, + { + "CreatePos": 84, + "StatementEnd": 231, + "IfNotExists": true, + "OrReplace": false, + "Targets": [ + { + "PolicyName": { + "Name": "example_policy", + "QuoteType": 1, + "NamePos": 112, + "NameEnd": 126 + }, + "Table": { + "Database": { + "Name": "db", + "QuoteType": 1, + "NamePos": 130, + "NameEnd": 132 + }, + "Table": { + "Name": "example_table", + "QuoteType": 1, + "NamePos": 133, + "NameEnd": 146 + } + } + }, + { + "PolicyName": { + "Name": "example_policy", + "QuoteType": 1, + "NamePos": 112, + "NameEnd": 126 + }, + "Table": { + "Database": { + "Name": "db", + "QuoteType": 1, + "NamePos": 148, + "NameEnd": 150 + }, + "Table": { + "Name": "example_table_local", + "QuoteType": 1, + "NamePos": 151, + "NameEnd": 170 + } + } + } + ], + "OnCluster": null, + "AccessStorageType": null, + "Mode": "", + "Filters": [ + { + "FilterPos": 171, + "FilterEnd": 215, + "Commands": null, + "Using": { + "ConditionPos": 177, + "ConditionEnd": 215, + "None": false, + "Expr": { + "LeftExpr": { + "Name": "id", + "QuoteType": 1, + "NamePos": 177, + "NameEnd": 179 + }, + "Operation": "=", + "RightExpr": { + "Name": { + "Name": "toUInt64", + "QuoteType": 1, + "NamePos": 182, + "NameEnd": 190 + }, + "Params": { + "LeftParenPos": 190, + "RightParenPos": 214, + "Items": { + "ListPos": 191, + "ListEnd": 214, + "HasDistinct": false, + "Items": [ + { + "Expr": { + "Name": { + "Name": "getSetting", + "QuoteType": 1, + "NamePos": 191, + "NameEnd": 201 + }, + "Params": { + "LeftParenPos": 201, + "RightParenPos": 213, + "Items": { + "ListPos": 202, + "ListEnd": 213, + "HasDistinct": false, + "Items": [ + { + "Expr": { + "LiteralPos": 202, + "LiteralEnd": 213, + "Literal": "filter_id" + }, + "Alias": null + } + ] + }, + "ColumnArgList": null + } + }, + "Alias": null + } + ] + }, + "ColumnArgList": null + } + }, + "HasGlobal": false, + "HasNot": false + } + }, + "WithCheck": null + } + ], + "To": { + "ToPos": 216, + "ToEnd": 231, + "All": false, + "Principals": [ + { + "PrincipalPos": 219, + "PrincipalEnd": 231, + "Name": { + "Name": "example_role", + "QuoteType": 1, + "NamePos": 219, + "NameEnd": 231 + }, + "Host": null, + "CurrentUser": false + } + ], + "Except": null + } + }, + { + "CreatePos": 233, + "StatementEnd": 407, + "IfNotExists": false, + "OrReplace": true, + "Targets": [ + { + "PolicyName": { + "Name": "p1", + "QuoteType": 1, + "NamePos": 262, + "NameEnd": 264 + }, + "Table": { + "Database": { + "Name": "db", + "QuoteType": 1, + "NamePos": 272, + "NameEnd": 274 + }, + "Table": { + "Name": "example_table", + "QuoteType": 1, + "NamePos": 275, + "NameEnd": 288 + } + } + }, + { + "PolicyName": { + "Name": "p2", + "QuoteType": 1, + "NamePos": 266, + "NameEnd": 268 + }, + "Table": { + "Database": { + "Name": "db", + "QuoteType": 1, + "NamePos": 272, + "NameEnd": 274 + }, + "Table": { + "Name": "example_table", + "QuoteType": 1, + "NamePos": 275, + "NameEnd": 288 + } + } + } + ], + "OnCluster": { + "OnPos": 308, + "Expr": { + "LiteralPos": 319, + "LiteralEnd": 336, + "Literal": "example-cluster" + } + }, + "AccessStorageType": { + "Name": "local_directory", + "QuoteType": 1, + "NamePos": 292, + "NameEnd": 307 + }, + "Mode": "RESTRICTIVE", + "Filters": [ + { + "FilterPos": 352, + "FilterEnd": 373, + "Commands": null, + "Using": { + "ConditionPos": 358, + "ConditionEnd": 373, + "None": false, + "Expr": { + "Name": { + "Name": "has", + "QuoteType": 1, + "NamePos": 358, + "NameEnd": 361 + }, + "Params": { + "LeftParenPos": 361, + "RightParenPos": 372, + "Items": { + "ListPos": 362, + "ListEnd": 372, + "HasDistinct": false, + "Items": [ + { + "Expr": { + "LeftBracketPos": 362, + "RightBracketPos": 367, + "Items": { + "ListPos": 363, + "ListEnd": 367, + "HasDistinct": false, + "Items": [ + { + "Expr": { + "NumPos": 363, + "NumEnd": 364, + "Literal": "1", + "Base": 10 + }, + "Alias": null + }, + { + "Expr": { + "NumPos": 366, + "NumEnd": 367, + "Literal": "2", + "Base": 10 + }, + "Alias": null + } + ] + } + }, + "Alias": null + }, + { + "Expr": { + "Name": "id", + "QuoteType": 1, + "NamePos": 370, + "NameEnd": 372 + }, + "Alias": null + } + ] + }, + "ColumnArgList": null + } + } + }, + "WithCheck": null + } + ], + "To": { + "ToPos": 374, + "ToEnd": 407, + "All": true, + "Principals": null, + "Except": [ + { + "PrincipalPos": 388, + "PrincipalEnd": 393, + "Name": { + "Name": "admin", + "QuoteType": 1, + "NamePos": 388, + "NameEnd": 393 + }, + "Host": null, + "CurrentUser": false + }, + { + "PrincipalPos": 395, + "PrincipalEnd": 407, + "Name": null, + "Host": null, + "CurrentUser": true + } + ] + } + }, + { + "CreatePos": 409, + "StatementEnd": 484, + "IfNotExists": false, + "OrReplace": false, + "Targets": [ + { + "PolicyName": { + "Name": "all_tables", + "QuoteType": 1, + "NamePos": 423, + "NameEnd": 433 + }, + "Table": { + "Database": { + "Name": "db", + "QuoteType": 1, + "NamePos": 437, + "NameEnd": 439 + }, + "Table": { + "Name": "*", + "QuoteType": 0, + "NamePos": 440, + "NameEnd": 441 + } + } + }, + { + "PolicyName": { + "Name": "literal_table", + "QuoteType": 1, + "NamePos": 443, + "NameEnd": 456 + }, + "Table": { + "Database": { + "Name": "db", + "QuoteType": 1, + "NamePos": 460, + "NameEnd": 462 + }, + "Table": { + "Name": "*", + "QuoteType": 3, + "NamePos": 464, + "NameEnd": 465 + } + } + } + ], + "OnCluster": null, + "AccessStorageType": null, + "Mode": "", + "Filters": [ + { + "FilterPos": 467, + "FilterEnd": 474, + "Commands": null, + "Using": { + "ConditionPos": 473, + "ConditionEnd": 474, + "None": false, + "Expr": { + "NumPos": 473, + "NumEnd": 474, + "Literal": "1", + "Base": 10 + } + }, + "WithCheck": null + } + ], + "To": { + "ToPos": 475, + "ToEnd": 484, + "All": false, + "Principals": [ + { + "PrincipalPos": 478, + "PrincipalEnd": 484, + "Name": { + "Name": "reader", + "QuoteType": 1, + "NamePos": 478, + "NameEnd": 484 + }, + "Host": null, + "CurrentUser": false + } + ], + "Except": null + } + }, + { + "AlterPos": 486, + "StatementEnd": 617, + "IfExists": true, + "RenameTo": null, + "Targets": [ + { + "PolicyName": { + "Name": "example_policy", + "QuoteType": 1, + "NamePos": 513, + "NameEnd": 527 + }, + "Table": { + "Database": { + "Name": "db", + "QuoteType": 1, + "NamePos": 531, + "NameEnd": 533 + }, + "Table": { + "Name": "example_table", + "QuoteType": 1, + "NamePos": 534, + "NameEnd": 547 + } + } + } + ], + "OnCluster": null, + "AccessStorageType": null, + "Mode": "", + "Filters": [ + { + "FilterPos": 548, + "FilterEnd": 579, + "Commands": null, + "Using": { + "ConditionPos": 554, + "ConditionEnd": 561, + "None": false, + "Expr": { + "LeftExpr": { + "Name": "id", + "QuoteType": 1, + "NamePos": 554, + "NameEnd": 556 + }, + "Operation": "=", + "RightExpr": { + "NumPos": 559, + "NumEnd": 561, + "Literal": "99", + "Base": 10 + }, + "HasGlobal": false, + "HasNot": false + } + }, + "WithCheck": { + "ConditionPos": 573, + "ConditionEnd": 579, + "None": false, + "Expr": { + "LeftExpr": { + "Name": "id", + "QuoteType": 1, + "NamePos": 573, + "NameEnd": 575 + }, + "Operation": "\u003e", + "RightExpr": { + "NumPos": 578, + "NumEnd": 579, + "Literal": "0", + "Base": 10 + }, + "HasGlobal": false, + "HasNot": false + } + } + } + ], + "To": { + "ToPos": 580, + "ToEnd": 617, + "All": false, + "Principals": [ + { + "PrincipalPos": 583, + "PrincipalEnd": 589, + "Name": { + "Name": "reader", + "QuoteType": 1, + "NamePos": 583, + "NameEnd": 589 + }, + "Host": null, + "CurrentUser": false + }, + { + "PrincipalPos": 592, + "PrincipalEnd": 617, + "Name": { + "Name": "service-user", + "QuoteType": 3, + "NamePos": 592, + "NameEnd": 604 + }, + "Host": { + "Name": "localhost", + "QuoteType": 4, + "NamePos": 606, + "NameEnd": 617 + }, + "CurrentUser": false + } + ], + "Except": null + } + }, + { + "AlterPos": 619, + "StatementEnd": 717, + "IfExists": false, + "RenameTo": { + "Name": "renamed", + "QuoteType": 1, + "NamePos": 677, + "NameEnd": 684 + }, + "Targets": [ + { + "PolicyName": { + "Name": "example_policy", + "QuoteType": 1, + "NamePos": 632, + "NameEnd": 646 + }, + "Table": { + "Database": { + "Name": "db", + "QuoteType": 1, + "NamePos": 650, + "NameEnd": 652 + }, + "Table": { + "Name": "example_table", + "QuoteType": 1, + "NamePos": 653, + "NameEnd": 666 + } + } + } + ], + "OnCluster": null, + "AccessStorageType": null, + "Mode": "PERMISSIVE", + "Filters": [ + { + "FilterPos": 699, + "FilterEnd": 709, + "Commands": null, + "Using": { + "ConditionPos": 705, + "ConditionEnd": 709, + "None": true, + "Expr": null + }, + "WithCheck": null + } + ], + "To": { + "ToPos": 710, + "ToEnd": 717, + "All": false, + "Principals": null, + "Except": null + } + }, + { + "DropPos": 719, + "StatementEnd": 821, + "IfExists": true, + "Targets": [ + { + "PolicyName": { + "Name": "p1", + "QuoteType": 1, + "NamePos": 745, + "NameEnd": 747 + }, + "Table": { + "Database": { + "Name": "db", + "QuoteType": 1, + "NamePos": 755, + "NameEnd": 757 + }, + "Table": { + "Name": "example_table", + "QuoteType": 1, + "NamePos": 758, + "NameEnd": 771 + } + } + }, + { + "PolicyName": { + "Name": "p2", + "QuoteType": 1, + "NamePos": 749, + "NameEnd": 751 + }, + "Table": { + "Database": { + "Name": "db", + "QuoteType": 1, + "NamePos": 755, + "NameEnd": 757 + }, + "Table": { + "Name": "example_table", + "QuoteType": 1, + "NamePos": 758, + "NameEnd": 771 + } + } + } + ], + "OnCluster": { + "OnPos": 793, + "Expr": { + "LiteralPos": 804, + "LiteralEnd": 821, + "Literal": "example-cluster" + } + }, + "AccessStorageType": { + "Name": "local_directory", + "QuoteType": 1, + "NamePos": 777, + "NameEnd": 792 + } + }, + { + "ShowPos": 823, + "StatementEnd": 922, + "Create": true, + "Name": null, + "Table": null, + "Targets": [ + { + "PolicyName": { + "Name": "example_policy", + "QuoteType": 1, + "NamePos": 846, + "NameEnd": 860 + }, + "Table": { + "Database": { + "Name": "db", + "QuoteType": 1, + "NamePos": 864, + "NameEnd": 866 + }, + "Table": { + "Name": "example_table", + "QuoteType": 1, + "NamePos": 867, + "NameEnd": 880 + } + } + }, + { + "PolicyName": { + "Name": "example_policy", + "QuoteType": 1, + "NamePos": 882, + "NameEnd": 896 + }, + "Table": { + "Database": { + "Name": "db", + "QuoteType": 1, + "NamePos": 900, + "NameEnd": 902 + }, + "Table": { + "Name": "example_table_local", + "QuoteType": 1, + "NamePos": 903, + "NameEnd": 922 + } + } + } + ] + }, + { + "ShowPos": 924, + "StatementEnd": 956, + "Create": true, + "Name": null, + "Table": { + "Database": { + "Name": "db", + "QuoteType": 1, + "NamePos": 952, + "NameEnd": 954 + }, + "Table": { + "Name": "*", + "QuoteType": 0, + "NamePos": 955, + "NameEnd": 956 + } + }, + "Targets": null + }, + { + "ShowPos": 958, + "StatementEnd": 975, + "Create": false, + "Name": null, + "Table": null, + "Targets": null + }, + { + "CreatePos": 977, + "StatementEnd": 1068, + "IfNotExists": false, + "OrReplace": false, + "Targets": [ + { + "PolicyName": { + "Name": "selected_rows", + "QuoteType": 1, + "NamePos": 995, + "NameEnd": 1008 + }, + "Table": { + "Database": { + "Name": "db", + "QuoteType": 1, + "NamePos": 1012, + "NameEnd": 1014 + }, + "Table": { + "Name": "example_table", + "QuoteType": 1, + "NamePos": 1015, + "NameEnd": 1028 + } + } + } + ], + "OnCluster": null, + "AccessStorageType": null, + "Mode": "", + "Filters": [ + { + "FilterPos": 1029, + "FilterEnd": 1052, + "Commands": [ + "SELECT" + ], + "Using": { + "ConditionPos": 1046, + "ConditionEnd": 1052, + "None": false, + "Expr": { + "LeftExpr": { + "Name": "id", + "QuoteType": 1, + "NamePos": 1046, + "NameEnd": 1048 + }, + "Operation": "\u003e", + "RightExpr": { + "NumPos": 1051, + "NumEnd": 1052, + "Literal": "0", + "Base": 10 + }, + "HasGlobal": false, + "HasNot": false + } + }, + "WithCheck": null + } + ], + "To": { + "ToPos": 1053, + "ToEnd": 1068, + "All": false, + "Principals": [ + { + "PrincipalPos": 1056, + "PrincipalEnd": 1068, + "Name": { + "Name": "example_role", + "QuoteType": 1, + "NamePos": 1056, + "NameEnd": 1068 + }, + "Host": null, + "CurrentUser": false + } + ], + "Except": null + } + }, + { + "CreatePos": 1070, + "StatementEnd": 1157, + "IfNotExists": false, + "OrReplace": false, + "Targets": [ + { + "PolicyName": { + "Name": "all_commands", + "QuoteType": 1, + "NamePos": 1088, + "NameEnd": 1100 + }, + "Table": { + "Database": { + "Name": "db", + "QuoteType": 1, + "NamePos": 1104, + "NameEnd": 1106 + }, + "Table": { + "Name": "example_table", + "QuoteType": 1, + "NamePos": 1107, + "NameEnd": 1120 + } + } + } + ], + "OnCluster": null, + "AccessStorageType": null, + "Mode": "", + "Filters": [ + { + "FilterPos": 1121, + "FilterEnd": 1141, + "Commands": [ + "ALL" + ], + "Using": { + "ConditionPos": 1135, + "ConditionEnd": 1141, + "None": false, + "Expr": { + "LeftExpr": { + "Name": "id", + "QuoteType": 1, + "NamePos": 1135, + "NameEnd": 1137 + }, + "Operation": "\u003e", + "RightExpr": { + "NumPos": 1140, + "NumEnd": 1141, + "Literal": "0", + "Base": 10 + }, + "HasGlobal": false, + "HasNot": false + } + }, + "WithCheck": null + } + ], + "To": { + "ToPos": 1142, + "ToEnd": 1157, + "All": false, + "Principals": [ + { + "PrincipalPos": 1145, + "PrincipalEnd": 1157, + "Name": { + "Name": "example_role", + "QuoteType": 1, + "NamePos": 1145, + "NameEnd": 1157 + }, + "Host": null, + "CurrentUser": false + } + ], + "Except": null + } + } +] \ No newline at end of file diff --git a/parser/testdata/ddl/row_policy.sql b/parser/testdata/ddl/row_policy.sql new file mode 100644 index 00000000..6862716c --- /dev/null +++ b/parser/testdata/ddl/row_policy.sql @@ -0,0 +1,12 @@ +CREATE ROW POLICY example_policy ON db.example_table USING id = 42 TO example_role; +CREATE POLICY IF NOT EXISTS example_policy ON db.example_table, db.example_table_local USING id = toUInt64(getSetting('filter_id')) TO example_role; +CREATE ROW POLICY OR REPLACE p1, p2 ON db.example_table IN local_directory ON CLUSTER 'example-cluster' AS RESTRICTIVE USING has([1, 2], id) TO ALL EXCEPT admin, CURRENT_USER; +CREATE POLICY all_tables ON db.*, literal_table ON db.`*` USING 1 TO reader; +ALTER ROW POLICY IF EXISTS example_policy ON db.example_table USING id = 99 WITH CHECK id > 0 TO reader, `service-user`@'localhost'; +ALTER POLICY example_policy ON db.example_table RENAME TO renamed AS PERMISSIVE USING NONE TO NONE; +DROP ROW POLICY IF EXISTS p1, p2 ON db.example_table FROM local_directory ON CLUSTER 'example-cluster'; +SHOW CREATE ROW POLICY example_policy ON db.example_table, example_policy ON db.example_table_local; +SHOW CREATE ROW POLICIES ON db.*; +SHOW ROW POLICIES; +CREATE ROW POLICY selected_rows ON db.example_table FOR SELECT USING id > 0 TO example_role; +CREATE ROW POLICY all_commands ON db.example_table FOR ALL USING id > 0 TO example_role; diff --git a/parser/walk.go b/parser/walk.go index 8f0e5999..dc58e7d3 100644 --- a/parser/walk.go +++ b/parser/walk.go @@ -785,6 +785,59 @@ func Walk(node Expr, fn WalkFunc) bool { if !Walk(n.Value, fn) { return false } + case *RowPolicyTarget: + if !Walk(n.PolicyName, fn) || !Walk(n.Table, fn) { + return false + } + case *RowPolicyCondition: + if !Walk(n.Expr, fn) { + return false + } + case *RowPolicyFilter: + if !Walk(n.Using, fn) || !Walk(n.WithCheck, fn) { + return false + } + case *RowPolicyPrincipal: + if !Walk(n.Name, fn) || !Walk(n.Host, fn) { + return false + } + case *RowPolicySubjects: + for _, principal := range n.Principals { + if !Walk(principal, fn) { + return false + } + } + for _, principal := range n.Except { + if !Walk(principal, fn) { + return false + } + } + case *CreateRowPolicy: + if !walkRowPolicyOptions(&n.RowPolicyOptions, nil, fn) { + return false + } + case *AlterRowPolicy: + if !walkRowPolicyOptions(&n.RowPolicyOptions, n.RenameTo, fn) { + return false + } + case *DropRowPolicy: + for _, target := range n.Targets { + if !Walk(target, fn) { + return false + } + } + if !Walk(n.AccessStorageType, fn) || !Walk(n.OnCluster, fn) { + return false + } + case *ShowRowPolicies: + if !Walk(n.Name, fn) || !Walk(n.Table, fn) { + return false + } + for _, target := range n.Targets { + if !Walk(target, fn) { + return false + } + } case *CreateRole: for _, name := range n.RoleNames { if !Walk(name, fn) { diff --git a/parser/walk_row_policy.go b/parser/walk_row_policy.go new file mode 100644 index 00000000..145e22fb --- /dev/null +++ b/parser/walk_row_policy.go @@ -0,0 +1,18 @@ +package parser + +func walkRowPolicyOptions(options *RowPolicyOptions, rename *Ident, fn WalkFunc) bool { + for _, target := range options.Targets { + if !Walk(target, fn) { + return false + } + } + if !Walk(options.AccessStorageType, fn) || !Walk(options.OnCluster, fn) || !Walk(rename, fn) { + return false + } + for _, filter := range options.Filters { + if !Walk(filter, fn) { + return false + } + } + return Walk(options.To, fn) +}