Repository: AfterShip/clickhouse-sql-parser
Pull request #170 (2025-07-03 06:58:49)

Add support for CREATE USER statement according to ClickHouse SQL documentation
## Summary

This PR implements comprehensive support for the `CREATE USER` statement in the ClickHouse SQL parser according to the [official ClickHouse documentation](https://clickhouse.com/docs/sql-reference/statements/create/user).

## Changes Made

### New Keywords Added
- `IDENTIFIED`, `GRANTEES`, `HOST`, `NAME`, `IP`, `KERBEROS`, `LDAP`, `REALM`, `SERVER`

### AST Structures Created
- **`CreateUser`** - Main CREATE USER AST node with support for all clauses
- **`AuthenticationClause`** - Handles authentication options (NOT IDENTIFIED, IDENTIFIED WITH various methods)
- **`HostClause`** - Handles host restrictions (LOCAL, NAME, REGEXP, IP, LIKE, ANY, NONE)
- **`DefaultRoleClause`** - Handles DEFAULT ROLE clause with multiple roles
- **`GranteesClause`** - Handles GRANTEES clause with EXCEPT support

### Parser Implementation
- **`parseCreateUser()`** - Main parser method for CREATE USER statements
- **`parseAuthenticationClause()`** - Parses authentication options
- **`parseHostClause()`** - Parses host restrictions  
- **`parseDefaultRoleClause()`** - Parses default roles
- **`parseGranteesClause()`** - Parses grantees with exception handling
- Updated DDL parser to recognize CREATE USER statements

## Supported Syntax

The implementation supports the full CREATE USER syntax as documented:

```sql
-- Basic usage
CREATE USER user1;
CREATE USER IF NOT EXISTS user2;
CREATE USER OR REPLACE user3;

-- Authentication methods
CREATE USER user4 NOT IDENTIFIED;
CREATE USER user5 IDENTIFIED WITH plaintext_password BY 'password';
CREATE USER user6 IDENTIFIED WITH sha256_password BY 'hash';
CREATE USER user7 IDENTIFIED WITH ldap SERVER 'ldap_server';
CREATE USER user8 IDENTIFIED WITH kerberos REALM 'EXAMPLE.COM';

-- Host restrictions
CREATE USER user9 HOST LOCAL;
CREATE USER user10 HOST NAME 'localhost';
CREATE USER user11 HOST REGEXP '.*\.example\.com';
CREATE USER user12 HOST IP '192.168.1.1';

-- Default roles and database
CREATE USER user13 DEFAULT ROLE role1, role2;
CREATE USER user14 DEFAULT DATABASE test_db;

-- Grantees with exceptions
CREATE USER user15 GRANTEES user1, user2 EXCEPT user3;

-- Settings
CREATE USER user16 SETTINGS max_memory_usage=5000000;

-- Complex multi-clause example
CREATE USER user17 
    IDENTIFIED WITH plaintext_password BY 'password'
    HOST NAME 'localhost'
    DEFAULT ROLE role1, role2
    DEFAULT DATABASE test_db
    GRANTEES user1, user2 EXCEPT user3
    SETTINGS max_memory_usage=5000000, readonly=1;
```

## Testing

- **32 comprehensive test cases** covering all syntax variations
- **AST serialization tests** with golden file validation
- **String formatting tests** ensuring proper output formatting
- **Integration tests** with existing parser framework
- All existing tests continue to pass

## Validation

Manual testing confirms the parser correctly handles:
- ✅ All authentication methods (plaintext_password, sha256_password, ldap, kerberos)
- ✅ All host restriction types (LOCAL, NAME, REGEXP, IP, LIKE, ANY, NONE)
- ✅ Multiple users, roles, and complex nested clauses
- ✅ Proper error handling for invalid syntax
- ✅ Accurate string serialization matching input SQL

The implementation follows existing code patterns and maintains backward compatibility while adding the requested CREATE USER functionality according to ClickHouse specifications.

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

git-hulk: @copilot Please resolve the conflicts.

Patch:
diff --git a/parser/ast.go b/parser/ast.go
index 882ec60..096c6c6 100644
--- a/parser/ast.go
+++ b/parser/ast.go
@@ -2197,6 +2197,336 @@ func (c *CreateRole) Accept(visitor ASTVisitor) error {
 	return visitor.VisitCreateRole(c)
 }
 
+type AuthenticationClause struct {
+	AuthPos       Pos
+	AuthEnd       Pos
+	NotIdentified bool
+	AuthType      string // "no_password", "plaintext_password", "sha256_password", etc.
+	AuthValue     *StringLiteral
+	LdapServer    *StringLiteral
+	KerberosRealm *StringLiteral
+	IsKerberos    bool
+}
+
+func (a *AuthenticationClause) Pos() Pos {
+	return a.AuthPos
+}
+
+func (a *AuthenticationClause) End() Pos {
+	return a.AuthEnd
+}
+
+func (a *AuthenticationClause) String() string {
+	var builder strings.Builder
+	if a.NotIdentified {
+		builder.WriteString("NOT IDENTIFIED")
+		return builder.String()
+	}
+	builder.WriteString("IDENTIFIED")
+	if a.AuthType != "" {
+		builder.WriteString(" WITH ")
+		builder.WriteString(a.AuthType)
+	}
+	if a.AuthValue != nil {
+		builder.WriteString(" BY ")
+		builder.WriteString(a.AuthValue.String())
+	}
+	if a.LdapServer != nil {
+		builder.WriteString(" WITH ldap SERVER ")
+		builder.WriteString(a.LdapServer.String())
+	}
+	if a.IsKerberos {
+		builder.WriteString(" WITH kerberos")
+		if a.KerberosRealm != nil && a.KerberosRealm.Literal != "" {
+			builder.WriteString(" REALM ")
+			builder.WriteString(a.KerberosRealm.String())
+		}
+	}
+	return builder.String()
+}
+
+func (a *AuthenticationClause) Accept(visitor ASTVisitor) error {
+	visitor.Enter(a)
+	defer visitor.Leave(a)
+	if a.AuthValue != nil {
+		if err := a.AuthValue.Accept(visitor); err != nil {
+			return err
+		}
+	}
+	if a.LdapServer != nil {
+		if err := a.LdapServer.Accept(visitor); err != nil {
+			return err
+		}
+	}
+	if a.KerberosRealm != nil {
+		if err := a.KerberosRealm.Accept(visitor); err != nil {
+			return err
+		}
+	}
+	return visitor.VisitAuthenticationClause(a)
+}
+
+type HostClause struct {
+	HostPos   Pos
+	HostEnd   Pos
+	HostType  string // "LOCAL", "NAME", "REGEXP", "IP", "LIKE", "ANY", "NONE"
+	HostValue *StringLiteral
+}
+
+func (h *HostClause) Pos() Pos {
+	return h.HostPos
+}
+
+func (h *HostClause) End() Pos {
+	return h.HostEnd
+}
+
+func (h *HostClause) String() string {
+	var builder strings.Builder
+	builder.WriteString("HOST ")
+	builder.WriteString(h.HostType)
+	if h.HostValue != nil {
+		builder.WriteString(" ")
+		builder.WriteString(h.HostValue.String())
+	}
+	return builder.String()
+}
+
+func (h *HostClause) Accept(visitor ASTVisitor) error {
+	visitor.Enter(h)
+	defer visitor.Leave(h)
+	if h.HostValue != nil {
+		if err := h.HostValue.Accept(visitor); err != nil {
+			return err
+		}
+	}
+	return visitor.VisitHostClause(h)
+}
+
+type DefaultRoleClause struct {
+	DefaultPos Pos
+	DefaultEnd Pos
+	Roles      []*RoleName
+	None       bool
+}
+
+func (d *DefaultRoleClause) Pos() Pos {
+	return d.DefaultPos
+}
+
+func (d *DefaultRoleClause) End() Pos {
+	return d.DefaultEnd
+}
+
+func (d *DefaultRoleClause) String() string {
+	var builder strings.Builder
+	builder.WriteString("DEFAULT ROLE ")
+	if d.None {
+		builder.WriteString("NONE")
+	} else {
+		for i, role := range d.Roles {
+			if i > 0 {
+				builder.WriteString(", ")
+			}
+			builder.WriteString(role.String())
+		}
+	}
+	return builder.String()
+}
+
+func (d *DefaultRoleClause) Accept(visitor ASTVisitor) error {
+	visitor.Enter(d)
+	defer visitor.Leave(d)
+	for _, role := range d.Roles {
+		if err := role.Accept(visitor); err != nil {
+			return err
+		}
+	}
+	return visitor.VisitDefaultRoleClause(d)
+}
+
+type GranteesClause struct {
+	GranteesPos Pos
+	GranteesEnd Pos
+	Grantees    []*RoleName
+	ExceptUsers []*RoleName
+	Any         bool
+	None        bool
+}
+
+func (g *GranteesClause) Pos() Pos {
+	return g.GranteesPos
+}
+
+func (g *GranteesClause) End() Pos {
+	return g.GranteesEnd
+}
+
+func (g *GranteesClause) String() string {
+	var builder strings.Builder
+	builder.WriteString("GRANTEES ")
+	if g.Any {
+		builder.WriteString("ANY")
+	} else if g.None {
+		builder.WriteString("NONE")
+	} else {
+		for i, grantee := range g.Grantees {
+			if i > 0 {
+				builder.WriteString(", ")
+			}
+			builder.WriteString(grantee.String())
+		}
+	}
+	if len(g.ExceptUsers) > 0 {
+		builder.WriteString(" EXCEPT ")
+		for i, except := range g.ExceptUsers {
+			if i > 0 {
+				builder.WriteString(", ")
+			}
+			builder.WriteString(except.String())
+		}
+	}
+	return builder.String()
+}
+
+func (g *GranteesClause) Accept(visitor ASTVisitor) error {
+	visitor.Enter(g)
+	defer visitor.Leave(g)
+	for _, grantee := range g.Grantees {
+		if err := grantee.Accept(visitor); err != nil {
+			return err
+		}
+	}
+	for _, except := range g.ExceptUsers {
+		if err := except.Accept(visitor); err != nil {
+			return err
+		}
+	}
+	return visitor.VisitGranteesClause(g)
+}
+
+type CreateUser struct {
+	CreatePos       Pos
+	StatementEnd    Pos
+	IfNotExists     bool
+	OrReplace       bool
+	UserNames       []*RoleName
+	Authentication  *AuthenticationClause
+	Hosts           []*HostClause
+	DefaultRole     *DefaultRoleClause
+	DefaultDatabase *Ident
+	DefaultDbNone   bool
+	Grantees        *GranteesClause
+	Settings        []*RoleSetting
+}
+
+func (c *CreateUser) Pos() Pos {
+	return c.CreatePos
+}
+
+func (c *CreateUser) End() Pos {
+	return c.StatementEnd
+}
+
+func (c *CreateUser) Type() string {
+	return "USER"
+}
+
+func (c *CreateUser) String() string {
+	var builder strings.Builder
+	builder.WriteString("CREATE USER ")
+	if c.IfNotExists {
+		builder.WriteString("IF NOT EXISTS ")
+	}
+	if c.OrReplace {
+		builder.WriteString("OR REPLACE ")
+	}
+	for i, userName := range c.UserNames {
+		if i > 0 {
+			builder.WriteString(", ")
+		}
+		builder.WriteString(userName.String())
+	}
+	if c.Authentication != nil {
+		builder.WriteString(" ")
+		builder.WriteString(c.Authentication.String())
+	}
+	if len(c.Hosts) > 0 {
+		builder.WriteString(" ")
+		for i, host := range c.Hosts {
+			if i > 0 {
+				builder.WriteString(", ")
+			}
+			builder.WriteString(host.String())
+		}
+	}
+	if c.DefaultRole != nil {
+		builder.WriteString(" ")
+		builder.WriteString(c.DefaultRole.String())
+	}
+	if c.DefaultDatabase != nil {
+		builder.WriteString(" DEFAULT DATABASE ")
+		builder.WriteString(c.DefaultDatabase.String())
+	} else if c.DefaultDbNone {
+		builder.WriteString(" DEFAULT DATABASE NONE")
+	}
+	if c.Grantees != nil {
+		builder.WriteString(" ")
+		builder.WriteString(c.Grantees.String())
+	}
+	if len(c.Settings) > 0 {
+		builder.WriteString(" SETTINGS ")
+		for i, setting := range c.Settings {
+			if i > 0 {
+				builder.WriteString(", ")
+			}
+			builder.WriteString(setting.String())
+		}
+	}
+	return builder.String()
+}
+
+func (c *CreateUser) Accept(visitor ASTVisitor) error {
+	visitor.Enter(c)
+	defer visitor.Leave(c)
+	for _, userName := range c.UserNames {
+		if err := userName.Accept(visitor); err != nil {
+			return err
+		}
+	}
+	if c.Authentication != nil {
+		if err := c.Authentication.Accept(visitor); err != nil {
+			return err
+		}
+	}
+	for _, host := range c.Hosts {
+		if err := host.Accept(visitor); err != nil {
+			return err
+		}
+	}
+	if c.DefaultRole != nil {
+		if err := c.DefaultRole.Accept(visitor); err != nil {
+			return err
+		}
+	}
+	if c.DefaultDatabase != nil {
+		if err := c.DefaultDatabase.Accept(visitor); err != nil {
+			return err
+		}
+	}
+	if c.Grantees != nil {
+		if err := c.Grantees.Accept(visitor); err != nil {
+			return err
+		}
+	}
+	for _, setting := range c.Settings {
+		if err := setting.Accept(visitor); err != nil {
+			return err
+		}
+	}
+	return visitor.VisitCreateUser(c)
+}
+
 type AlterRole struct {
 	AlterPos        Pos
 	StatementEnd    Pos
diff --git a/parser/ast_visitor.go b/parser/ast_visitor.go
index 07f1306..8f5e40d 100644
--- a/parser/ast_visitor.go
+++ b/parser/ast_visitor.go
@@ -43,6 +43,11 @@ type ASTVisitor interface {
 	VisitSettingPair(expr *SettingPair) error
 	VisitRoleSetting(expr *RoleSetting) error
 	VisitCreateRole(expr *CreateRole) error
+	VisitCreateUser(expr *CreateUser) error
+	VisitAuthenticationClause(expr *AuthenticationClause) error
+	VisitHostClause(expr *HostClause) error
+	VisitDefaultRoleClause(expr *DefaultRoleClause) error
+	VisitGranteesClause(expr *GranteesClause) error
 	VisitAlterRole(expr *AlterRole) error
 	VisitRoleRenamePair(expr *RoleRenamePair) error
 	VisitDestinationExpr(expr *DestinationClause) error
@@ -476,6 +481,41 @@ func (v *DefaultASTVisitor) VisitCreateRole(expr *CreateRole) error {
 	return nil
 }
 
+func (v *DefaultASTVisitor) VisitCreateUser(expr *CreateUser) error {
+	if v.Visit != nil {
+		return v.Visit(expr)
+	}
+	return nil
+}
+
+func (v *DefaultASTVisitor) VisitAuthenticationClause(expr *AuthenticationClause) error {
+	if v.Visit != nil {
+		return v.Visit(expr)
+	}
+	return nil
+}
+
+func (v *DefaultASTVisitor) VisitHostClause(expr *HostClause) error {
+	if v.Visit != nil {
+		return v.Visit(expr)
+	}
+	return nil
+}
+
+func (v *DefaultASTVisitor) VisitDefaultRoleClause(expr *DefaultRoleClause) error {
+	if v.Visit != nil {
+		return v.Visit(expr)
+	}
+	return nil
+}
+
+func (v *DefaultASTVisitor) VisitGranteesClause(expr *GranteesClause) error {
+	if v.Visit != nil {
+		return v.Visit(expr)
+	}
+	return nil
+}
+
 func (v *DefaultASTVisitor) VisitAlterRole(expr *AlterRole) error {
 	if v.Visit != nil {
 		return v.Visit(expr)
diff --git a/parser/keyword.go b/parser/keyword.go
index b623a78..29125ab 100644
--- a/parser/keyword.go
+++ b/parser/keyword.go
@@ -91,13 +91,16 @@ const (
 	KeywordFunctions    = "FUNCTIONS"
 	KeywordGlobal       = "GLOBAL"
 	KeywordGrant        = "GRANT"
+	KeywordGrantees     = "GRANTEES"
 	KeywordGranularity  = "GRANULARITY"
 	KeywordGroup        = "GROUP"
 	KeywordGrouping     = "GROUPING"
 	KeywordHaving       = "HAVING"
 	KeywordHierarchical = "HIERARCHICAL"
+	KeywordHost         = "HOST"
 	KeywordHour         = "HOUR"
 	KeywordId           = "ID"
+	KeywordIdentified   = "IDENTIFIED"
 	KeywordIf           = "IF"
 	KeywordIlike        = "ILIKE"
 	KeywordIn           = "IN"
@@ -108,14 +111,17 @@ const (
 	KeywordInsert       = "INSERT"
 	KeywordInterval     = "INTERVAL"
 	KeywordInto         = "INTO"
+	KeywordIp           = "IP"
 	KeywordIs           = "IS"
 	KeywordIs_object_id = "IS_OBJECT_ID"
 	KeywordJoin         = "JOIN"
 	KeywordJSON         = "JSON"
 	KeywordKey          = "KEY"
 	KeywordKill         = "KILL"
+	KeywordKerberos     = "KERBEROS"
 	KeywordLast         = "LAST"
 	KeywordLayout       = "LAYOUT"
+	KeywordLdap         = "LDAP"
 	KeywordLeading      = "LEADING"
 	KeywordLeft         = "LEFT"
 	KeywordLifetime     = "LIFETIME"
@@ -136,6 +142,7 @@ const (
 	KeywordMove         = "MOVE"
 	KeywordMoves        = "MOVES"
 	KeywordMutation     = "MUTATION"
+	KeywordName         = "NAME"
 	KeywordNan_sql      = "NAN_SQL"
 	KeywordNo           = "NO"
 	KeywordNone         = "NONE"
@@ -165,6 +172,7 @@ const (
 	KeywordQuota        = "QUOTA"
 	KeywordRandomize    = "RANDOMIZE"
 	KeywordRange        = "RANGE"
+	KeywordRealm        = "REALM"
 	KeywordRecompress   = "RECOMPRESS"
 	KeywordRefresh      = "REFRESH"
 	KeywordRegexp       = "REGEXP"
@@ -186,6 +194,7 @@ const (
 	KeywordSelect       = "SELECT"
 	KeywordSemi         = "SEMI"
 	KeywordSends        = "SENDS"
+	KeywordServer       = "SERVER"
 	KeywordSet          = "SET"
 	KeywordSets         = "SETS"
 	KeywordSettings     = "SETTINGS"
@@ -330,13 +339,16 @@ var keywords = NewSet(
 	KeywordFunctions,
 	KeywordGlobal,
 	KeywordGrant,
+	KeywordGrantees,
 	KeywordGranularity,
 	KeywordGroup,
 	KeywordGrouping,
 	KeywordHaving,
 	KeywordHierarchical,
+	KeywordHost,
 	KeywordHour,
 	KeywordId,
+	KeywordIdentified,
 	KeywordIf,
 	KeywordIlike,
 	KeywordIn,
@@ -347,14 +359,17 @@ var keywords = NewSet(
 	KeywordInsert,
 	KeywordInterval,
 	KeywordInto,
+	KeywordIp,
 	KeywordIs,
 	KeywordIs_object_id,
 	KeywordJoin,
 	KeywordJSON,
 	KeywordKey,
 	KeywordKill,
+	KeywordKerberos,
 	KeywordLast,
 	KeywordLayout,
+	KeywordLdap,
 	KeywordLeading,
 	KeywordLeft,
 	KeywordLifetime,
@@ -375,6 +390,7 @@ var keywords = NewSet(
 	KeywordMove,
 	KeywordMoves,
 	KeywordMutation,
+	KeywordName,
 	KeywordNan_sql,
 	KeywordNo,
 	KeywordNone,
@@ -404,6 +420,7 @@ var keywords = NewSet(
 	KeywordQuota,
 	KeywordRandomize,
 	KeywordRange,
+	KeywordRealm,
 	KeywordRecompress,
 	KeywordRefresh,
 	KeywordRegexp,
@@ -425,6 +442,7 @@ var keywords = NewSet(
 	KeywordSelect,
 	KeywordSemi,
 	KeywordSends,
+	KeywordServer,
 	KeywordSet,
 	KeywordSets,
 	KeywordSettings,
diff --git a/parser/parse_system.go b/parser/parse_system.go
index 028ab0b..4a5a17b 100644
--- a/parser/parse_system.go
+++ b/parser/parse_system.go
@@ -522,10 +522,369 @@ func (p *Parser) parseCreateRole(pos Pos) (*CreateRole, error) {
 	}, nil
 }
 
+func (p *Parser) parseAuthenticationClause(pos Pos) (*AuthenticationClause, error) {
+	auth := &AuthenticationClause{AuthPos: pos}
+
+	if p.tryConsumeKeywords(KeywordNot) {
+		if err := p.expectKeyword(KeywordIdentified); err != nil {
+			return nil, err
+		}
+		auth.NotIdentified = true
+		auth.AuthEnd = p.last().End
+		return auth, nil
+	}
+
+	if err := p.expectKeyword(KeywordIdentified); err != nil {
+		return nil, err
+	}
+	auth.AuthEnd = p.last().End
+
+	if p.tryConsumeKeywords(KeywordWith) {
+		if p.matchKeyword(KeywordLdap) {
+			_ = p.lexer.consumeToken()
+			if err := p.expectKeyword(KeywordServer); err != nil {
+				return nil, err
+			}
+			server, err := p.parseString(p.Pos())
+			if err != nil {
+				return nil, err
+			}
+			auth.LdapServer = server
+			auth.AuthEnd = server.End()
+		} else if p.matchKeyword(KeywordKerberos) {
+			_ = p.lexer.consumeToken()
+			auth.IsKerberos = true
+			auth.AuthEnd = p.last().End
+			if p.tryConsumeKeywords(KeywordRealm) {
+				realm, err := p.parseString(p.Pos())
+				if err != nil {
+					return nil, err
+				}
+				auth.KerberosRealm = realm
+				auth.AuthEnd = realm.End()
+			}
+		} else if p.matchTokenKind(TokenKindIdent) {
+			// Auth types like no_password, plaintext_password, etc.
+			authType := p.last().String
+			_ = p.lexer.consumeToken()
+			auth.AuthType = authType
+			auth.AuthEnd = p.last().End
+
+			if p.tryConsumeKeywords(KeywordBy) {
+				value, err := p.parseString(p.Pos())
+				if err != nil {
+					return nil, err
+				}
+				auth.AuthValue = value
+				auth.AuthEnd = value.End()
+			}
+		}
+	}
+
+	return auth, nil
+}
+
+func (p *Parser) parseHostClause(pos Pos) (*HostClause, error) {
+	if err := p.expectKeyword(KeywordHost); err != nil {
+		return nil, err
+	}
+
+	host := &HostClause{HostPos: pos}
+
+	switch {
+	case p.matchOneOfKeywords(KeywordLocal, KeywordAny, KeywordNone):
+		hostType := p.last().String
+		_ = p.lexer.consumeToken()
+		host.HostType = hostType
+		host.HostEnd = p.last().End
+	case p.matchOneOfKeywords(KeywordName, KeywordRegexp, KeywordIp, KeywordLike):
+		hostType := p.last().String
+		_ = p.lexer.consumeToken()
+		host.HostType = hostType
+		value, err := p.parseString(p.Pos())
+		if err != nil {
+			return nil, err
+		}
+		host.HostValue = value
+		host.HostEnd = value.End()
+	default:
+		return nil, fmt.Errorf("expected LOCAL|NAME|REGEXP|IP|LIKE|ANY|NONE")
+	}
+
+	return host, nil
+}
+
+func (p *Parser) parseDefaultRoleClause(pos Pos) (*DefaultRoleClause, error) {
+	if err := p.expectKeyword(KeywordDefault); err != nil {
+		return nil, err
+	}
+	if err := p.expectKeyword(KeywordRole); err != nil {
+		return nil, err
+	}
+
+	defaultRole := &DefaultRoleClause{DefaultPos: pos}
+
+	if p.tryConsumeKeywords(KeywordNone) {
+		defaultRole.None = true
+		defaultRole.DefaultEnd = p.last().End
+		return defaultRole, nil
+	}
+
+	roles := make([]*RoleName, 0)
+	role, err := p.parseRoleName(p.Pos())
+	if err != nil {
+		return nil, err
+	}
+	roles = append(roles, role)
+
+	for p.tryConsumeTokenKind(TokenKindComma) != nil {
+		role, err := p.parseRoleName(p.Pos())
+		if err != nil {
+			return nil, err
+		}
+		roles = append(roles, role)
+	}
+
+	defaultRole.Roles = roles
+	defaultRole.DefaultEnd = roles[len(roles)-1].End()
+	return defaultRole, nil
+}
+
+func (p *Parser) parseGranteesClause(pos Pos) (*GranteesClause, error) {
+	if err := p.expectKeyword(KeywordGrantees); err != nil {
+		return nil, err
+	}
+
+	grantees := &GranteesClause{GranteesPos: pos}
+
+	if p.tryConsumeKeywords(KeywordAny) {
+		grantees.Any = true
+		grantees.GranteesEnd = p.last().End
+	} else if p.tryConsumeKeywords(KeywordNone) {
+		grantees.None = true
+		grantees.GranteesEnd = p.last().End
+	} else {
+		// Parse list of grantees
+		granteeList := make([]*RoleName, 0)
+		grantee, err := p.parseRoleName(p.Pos())
+		if err != nil {
+			return nil, err
+		}
+		granteeList = append(granteeList, grantee)
+
+		for p.tryConsumeTokenKind(TokenKindComma) != nil {
+			grantee, err := p.parseRoleName(p.Pos())
+			if err != nil {
+				return nil, err
+			}
+			granteeList = append(granteeList, grantee)
+		}
+
+		grantees.Grantees = granteeList
+		grantees.GranteesEnd = granteeList[len(granteeList)-1].End()
+	}
+
+	// Check for EXCEPT clause
+	if p.tryConsumeKeywords(KeywordExcept) {
+		exceptList := make([]*RoleName, 0)
+		except, err := p.parseRoleName(p.Pos())
+		if err != nil {
+			return nil, err
+		}
+		exceptList = append(exceptList, except)
+
+		for p.tryConsumeTokenKind(TokenKindComma) != nil {
+			except, err := p.parseRoleName(p.Pos())
+			if err != nil {
+				return nil, err
+			}
+			exceptList = append(exceptList, except)
+		}
+
+		grantees.ExceptUsers = exceptList
+		grantees.GranteesEnd = exceptList[len(exceptList)-1].End()
+	}
+
+	return grantees, nil
+}
+
+func (p *Parser) parseCreateUserModifiers(createUser *CreateUser) error {
+	switch {
+	case p.matchKeyword(KeywordIf):
+		_ = p.lexer.consumeToken()
+		if err := p.expectKeyword(KeywordNot); err != nil {
+			return err
+		}
+		if err := p.expectKeyword(KeywordExists); err != nil {
+			return err
+		}
+		createUser.IfNotExists = true
+	case p.matchKeyword(KeywordOr):
+		_ = p.lexer.consumeToken()
+		if err := p.expectKeyword(KeywordReplace); err != nil {
+			return err
+		}
+		createUser.OrReplace = true
+	}
+	return nil
+}
+
+func (p *Parser) parseUserNames() ([]*RoleName, error) {
+	userNames := make([]*RoleName, 0)
+	userName, err := p.parseRoleName(p.Pos())
+	if err != nil {
+		return nil, err
+	}
+	userNames = append(userNames, userName)
+
+	for p.tryConsumeTokenKind(TokenKindComma) != nil {
+		userName, err := p.parseRoleName(p.Pos())
+		if err != nil {
+			return nil, err
+		}
+		userNames = append(userNames, userName)
+	}
+	return userNames, nil
+}
+
+func (p *Parser) parseHostClauses() ([]*HostClause, error) {
+	hosts := make([]*HostClause, 0)
+	host, err := p.parseHostClause(p.Pos())
+	if err != nil {
+		return nil, err
+	}
+	hosts = append(hosts, host)
+
+	for p.tryConsumeTokenKind(TokenKindComma) != nil {
+		host, err := p.parseHostClause(p.Pos())
+		if err != nil {
+			return nil, err
+		}
+		hosts = append(hosts, host)
+	}
+	return hosts, nil
+}
+
+func (p *Parser) parseDefaultClause(createUser *CreateUser) (bool, error) {
+	nextToken, err := p.lexer.peekToken()
+	if err != nil {
+		return false, err
+	}
+
+	if nextToken.String == KeywordRole {
+		defaultRole, err := p.parseDefaultRoleClause(p.Pos())
+		if err != nil {
+			return false, err
+		}
+		createUser.DefaultRole = defaultRole
+		createUser.StatementEnd = defaultRole.End()
+		return true, nil
+	} else if nextToken.String == KeywordDatabase {
+		_ = p.lexer.consumeToken() // consume DEFAULT
+		_ = p.lexer.consumeToken() // consume DATABASE
+		if p.tryConsumeKeywords(KeywordNone) {
+			createUser.DefaultDbNone = true
+			createUser.StatementEnd = p.last().End
+		} else {
+			db, err := p.parseIdent()
+			if err != nil {
+				return false, err
+			}
+			createUser.DefaultDatabase = db
+			createUser.StatementEnd = db.End()
+		}
+		return true, nil
+	}
+	return false, nil
+}
+
+func (p *Parser) parseOptionalClauses(createUser *CreateUser) error {
+	continueParsing := true
+	for continueParsing {
+		switch {
+		case p.matchOneOfKeywords(KeywordNot, KeywordIdentified):
+			auth, err := p.parseAuthenticationClause(p.Pos())
+			if err != nil {
+				return err
+			}
+			createUser.Authentication = auth
+			createUser.StatementEnd = auth.End()
+
+		case p.matchKeyword(KeywordHost):
+			hosts, err := p.parseHostClauses()
+			if err != nil {
+				return err
+			}
+			createUser.Hosts = hosts
+			createUser.StatementEnd = hosts[len(hosts)-1].End()
+
+		case p.matchKeyword(KeywordDefault):
+			parsed, err := p.parseDefaultClause(createUser)
+			if err != nil {
+				return err
+			}
+			if !parsed {
+				continueParsing = false
+			}
+
+		case p.matchKeyword(KeywordGrantees):
+			grantees, err := p.parseGranteesClause(p.Pos())
+			if err != nil {
+				return err
+			}
+			createUser.Grantees = grantees
+			createUser.StatementEnd = grantees.End()
+
+		case p.matchKeyword(KeywordSettings):
+			_ = p.lexer.consumeToken() // consume SETTINGS keyword
+			settings, err := p.parseRoleSettings(p.Pos())
+			if err != nil {
+				return err
+			}
+			createUser.Settings = settings
+			if len(settings) > 0 {
+				createUser.StatementEnd = settings[len(settings)-1].End()
+			}
+
+		default:
+			continueParsing = false
+		}
+	}
+	return nil
+}
+
+func (p *Parser) parseCreateUser(pos Pos) (*CreateUser, error) {
+	if err := p.expectKeyword(KeywordUser); err != nil {
+		return nil, err
+	}
+
+	createUser := &CreateUser{CreatePos: pos}
+
+	// Handle IF NOT EXISTS or OR REPLACE
+	if err := p.parseCreateUserModifiers(createUser); err != nil {
+		return nil, err
+	}
+
+	// Parse user names
+	userNames, err := p.parseUserNames()
+	if err != nil {
+		return nil, err
+	}
+	createUser.UserNames = userNames
+	createUser.StatementEnd = userNames[len(userNames)-1].End()
+
+	// Parse optional clauses
+	if err := p.parseOptionalClauses(createUser); err != nil {
+		return nil, err
+	}
+
+	return createUser, nil
+}
+
 func (p *Parser) parserDropUserOrRole(pos Pos) (*DropUserOrRole, error) {
 	var target string
 	switch {
-	case p.matchKeyword(KeywordUser), p.matchKeyword(KeywordRole):
+	case p.matchOneOfKeywords(KeywordUser, KeywordRole):
 		target = p.last().String
 		_ = p.lexer.consumeToken()
 	default:
@@ -608,15 +967,11 @@ func (p *Parser) parsePrivilegeAlter(pos Pos) (*PrivilegeClause, error) {
 	switch {
 	case p.tryConsumeKeywords(KeywordIndex):
 		keywords = append(keywords, KeywordIndex)
-	case p.matchKeyword(KeywordUpdate), p.matchKeyword(KeywordDelete),
-		p.matchKeyword(KeywordUser), p.matchKeyword(KeywordRole), p.matchKeyword(KeywordQuota):
+	case p.matchOneOfKeywords(KeywordUpdate, KeywordDelete, KeywordUser, KeywordRole, KeywordQuota):
 		keyword := p.last().String
 		_ = p.lexer.consumeToken()
 		keywords = append(keywords, keyword)
-	case p.matchKeyword(KeywordAdd), p.matchKeyword(KeywordDrop),
-		p.matchKeyword(KeywordModify), p.matchKeyword(KeywordClear),
-		p.matchKeyword(KeywordComment), p.matchKeyword(KeywordRename),
-		p.matchKeyword(KeywordMaterialized):
+	case p.matchOneOfKeywords(KeywordAdd, KeywordDrop, KeywordModify, KeywordClear, KeywordComment, KeywordRename, KeywordMaterialized):
 		keyword := p.last().String
 		_ = p.lexer.consumeToken()
 		keywords = append(keywords, keyword)
@@ -653,7 +1008,7 @@ func (p *Parser) parsePrivilegeAlter(pos Pos) (*PrivilegeClause, error) {
 		default:
 			return nil, fmt.Errorf("expected MODIFY|REFRESH")
 		}
-	case p.matchKeyword(KeywordMove), p.matchKeyword(KeywordFreeze):
+	case p.matchOneOfKeywords(KeywordMove, KeywordFreeze):
 		keyword := p.last().String
 		_ = p.lexer.consumeToken()
 		keywords = append(keywords, keyword)
@@ -673,9 +1028,7 @@ func (p *Parser) parsePrivilegeAlter(pos Pos) (*PrivilegeClause, error) {
 func (p *Parser) parsePrivilegeCreate(pos Pos) (*PrivilegeClause, error) {
 	keywords := []string{KeywordCreate}
 	switch {
-	case p.matchKeyword(KeywordDatabase), p.matchKeyword(KeywordDictionary),
-		p.matchKeyword(KeywordTable), p.matchKeyword(KeywordFunction), p.matchKeyword(KeywordView),
-		p.matchKeyword(KeywordUser), p.matchKeyword(KeywordRole), p.matchKeyword(KeywordQuota):
+	case p.matchOneOfKeywords(KeywordDatabase, KeywordDictionary, KeywordTable, KeywordFunction, KeywordView, KeywordUser, KeywordRole, KeywordQuota):
 		keyword := p.last().String
 		_ = p.lexer.consumeToken()
 		keywords = append(keywords, keyword)
@@ -701,9 +1054,7 @@ func (p *Parser) parsePrivilegeCreate(pos Pos) (*PrivilegeClause, error) {
 func (p *Parser) parsePrivilegeDrop(pos Pos) (*PrivilegeClause, error) {
 	keywords := []string{KeywordDrop}
 	switch {
-	case p.matchKeyword(KeywordDatabase), p.matchKeyword(KeywordDictionary),
-		p.matchKeyword(KeywordUser), p.matchKeyword(KeywordRole), p.matchKeyword(KeywordQuota),
-		p.matchKeyword(KeywordTable), p.matchKeyword(KeywordFunction), p.matchKeyword(KeywordView):
+	case p.matchOneOfKeywords(KeywordDatabase, KeywordDictionary, KeywordUser, KeywordRole, KeywordQuota, KeywordTable, KeywordFunction, KeywordView):
 		keyword := p.last().String
 		_ = p.lexer.consumeToken()
 		keywords = append(keywords, keyword)
@@ -719,8 +1070,7 @@ func (p *Parser) parsePrivilegeDrop(pos Pos) (*PrivilegeClause, error) {
 func (p *Parser) parsePrivilegeShow(pos Pos) (*PrivilegeClause, error) {
 	keywords := []string{KeywordShow}
 	switch {
-	case p.matchKeyword(KeywordDatabases), p.matchKeyword(KeywordDictionaries),
-		p.matchKeyword(KeywordTables), p.matchKeyword(KeywordColumns):
+	case p.matchOneOfKeywords(KeywordDatabases, KeywordDictionaries, KeywordTables, KeywordColumns):
 		keyword := p.last().String
 		_ = p.lexer.consumeToken()
 		keywords = append(keywords, keyword)
@@ -736,8 +1086,7 @@ func (p *Parser) parsePrivilegeShow(pos Pos) (*PrivilegeClause, error) {
 func (p *Parser) parsePrivilegeSystem(pos Pos) (*PrivilegeClause, error) {
 	keywords := []string{KeywordShow}
 	switch {
-	case p.matchKeyword(KeywordShutdown), p.matchKeyword(KeywordMerges), p.matchKeyword(KeywordFetches),
-		p.matchKeyword(KeywordSends), p.matchKeyword(KeywordMoves), p.matchKeyword(KeywordCluster):
+	case p.matchOneOfKeywords(KeywordShutdown, KeywordMerges, KeywordFetches, KeywordSends, KeywordMoves, KeywordCluster):
 		keyword := p.last().String
 		_ = p.lexer.consumeToken()
 		keywords = append(keywords, keyword)
@@ -746,7 +1095,7 @@ func (p *Parser) parsePrivilegeSystem(pos Pos) (*PrivilegeClause, error) {
 		switch {
 		case p.tryConsumeKeywords(KeywordCache):
 			keywords = append(keywords, KeywordCache)
-		case p.matchKeyword(KeywordMark), p.matchKeyword(KeywordDNS), p.matchKeyword(KeywordUncompressed):
+		case p.matchOneOfKeywords(KeywordMark, KeywordDNS, KeywordUncompressed):
 			keyword := p.last().String
 			_ = p.lexer.consumeToken()
 			keywords = append(keywords, keyword)
@@ -760,8 +1109,7 @@ func (p *Parser) parsePrivilegeSystem(pos Pos) (*PrivilegeClause, error) {
 	case p.tryConsumeKeywords(KeywordReload):
 		keywords = append(keywords, KeywordReload)
 		switch {
-		case p.matchKeyword(KeywordDictionary), p.matchKeyword(KeywordFunction),
-			p.matchKeyword(KeywordFunctions), p.matchKeyword(KeywordConfig):
+		case p.matchOneOfKeywords(KeywordDictionary, KeywordFunction, KeywordFunctions, KeywordConfig):
 			keyword := p.last().String
 			_ = p.lexer.consumeToken()
 			keywords = append(keywords, keyword)
@@ -771,7 +1119,7 @@ func (p *Parser) parsePrivilegeSystem(pos Pos) (*PrivilegeClause, error) {
 	case p.tryConsumeKeywords(KeywordFlush):
 		keywords = append(keywords, KeywordFlush)
 		switch {
-		case p.matchKeyword(KeywordLogs), p.matchKeyword(KeywordDistributed):
+		case p.matchOneOfKeywords(KeywordLogs, KeywordDistributed):
 			keyword := p.last().String
 			_ = p.lexer.consumeToken()
 			keywords = append(keywords, keyword)
@@ -784,7 +1132,7 @@ func (p *Parser) parsePrivilegeSystem(pos Pos) (*PrivilegeClause, error) {
 			return nil, err
 		}
 		keywords = append(keywords, KeywordMerges)
-	case p.matchKeyword(KeywordSync), p.matchKeyword(KeywordRestart):
+	case p.matchOneOfKeywords(KeywordSync, KeywordRestart):
 		keyword := p.last().String
 		_ = p.lexer.consumeToken()
 		keywords = append(keywords, keyword)
@@ -818,7 +1166,7 @@ func (p *Parser) parsePrivilegeClause(pos Pos) (*PrivilegeClause, error) {
 		}
 	}
 	switch {
-	case p.matchKeyword(KeywordSelect), p.matchKeyword(KeywordInsert):
+	case p.matchOneOfKeywords(KeywordSelect, KeywordInsert):
 		return p.parsePrivilegeSelectOrInsert(pos)
 	case p.tryConsumeKeywords(KeywordAlter):
 		return p.parsePrivilegeAlter(pos)
@@ -852,7 +1200,7 @@ func (p *Parser) parsePrivilegeClause(pos Pos) (*PrivilegeClause, error) {
 			PrivilegePos: pos,
 			Keywords:     []string{KeywordAdmin, KeywordOption},
 		}, nil
-	case p.matchKeyword(KeywordOptimize), p.matchKeyword(KeywordTruncate):
+	case p.matchOneOfKeywords(KeywordOptimize, KeywordTruncate):
 		keyword := p.last().String
 		_ = p.lexer.consumeToken()
 		return &PrivilegeClause{
diff --git a/parser/parser_table.go b/parser/parser_table.go
index 5042fce..0d2bd20 100644
--- a/parser/parser_table.go
+++ b/parser/parser_table.go
@@ -29,9 +29,11 @@ func (p *Parser) parseDDL(pos Pos) (DDL, error) {
 			return p.parseCreateView(pos, orReplace)
 		case p.matchKeyword(KeywordRole):
 			return p.parseCreateRole(pos)
+		case p.matchKeyword(KeywordUser):
+			return p.parseCreateUser(pos)
 		default:
-			return nil, fmt.Errorf("expected keyword: DATABASE|TABLE|VIEW|FUNCTION, but got %q",
-				p.last().String)
+			return nil, fmt.Errorf("expected keyword: DATABASE|TABLE|VIEW|ROLE|USER|FUNCTION|MATERIALIZED, but got %q",
+				p.lastTokenKind())
 		}
 	case p.matchKeyword(KeywordAlter):
 		_ = p.lexer.consumeToken()


Test patch:
diff --git a/parser/testdata/ddl/create_user.sql b/parser/testdata/ddl/create_user.sql
new file mode 100644
index 0000000..bdcdc73
--- /dev/null
+++ b/parser/testdata/ddl/create_user.sql
@@ -0,0 +1,53 @@
+-- Basic CREATE USER tests
+
+CREATE USER user1;
+CREATE USER IF NOT EXISTS user2;
+CREATE USER OR REPLACE user3;
+CREATE USER user4, user5;
+
+-- CREATE USER with authentication
+CREATE USER user6 NOT IDENTIFIED;
+CREATE USER user7 IDENTIFIED WITH plaintext_password BY 'password123';
+CREATE USER user8 IDENTIFIED WITH sha256_password BY 'hash123';
+CREATE USER user9 IDENTIFIED WITH ldap SERVER 'ldap_server';
+CREATE USER user10 IDENTIFIED WITH kerberos;
+CREATE USER user11 IDENTIFIED WITH kerberos REALM 'EXAMPLE.COM';
+
+-- CREATE USER with host restrictions
+CREATE USER user12 HOST LOCAL;
+CREATE USER user13 HOST ANY;
+CREATE USER user14 HOST NONE;
+CREATE USER user15 HOST NAME 'localhost';
+CREATE USER user16 HOST REGEXP '.*\.example\.com';
+CREATE USER user17 HOST IP '192.168.1.1';
+CREATE USER user18 HOST LIKE 'test%';
+
+-- CREATE USER with default roles
+CREATE USER user19 DEFAULT ROLE role1;
+CREATE USER user20 DEFAULT ROLE role1, role2;
+CREATE USER user21 DEFAULT ROLE NONE;
+
+-- CREATE USER with default database
+CREATE USER user22 DEFAULT DATABASE test_db;
+CREATE USER user23 DEFAULT DATABASE NONE;
+
+-- CREATE USER with grantees
+CREATE USER user24 GRANTEES user1;
+CREATE USER user25 GRANTEES user1, user2;
+CREATE USER user26 GRANTEES ANY;
+CREATE USER user27 GRANTEES NONE;
+CREATE USER user28 GRANTEES user1, user2 EXCEPT user3;
+
+-- CREATE USER with settings
+CREATE USER user29 SETTINGS max_memory_usage=5000000;
+CREATE USER user30 SETTINGS PROFILE 'default';
+CREATE USER user31 SETTINGS readonly=1, max_memory_usage=5000000;
+
+-- Complex CREATE USER with multiple clauses
+CREATE USER user32 
+    IDENTIFIED WITH plaintext_password BY 'password'
+    HOST NAME 'localhost'
+    DEFAULT ROLE role1, role2
+    DEFAULT DATABASE test_db
+    GRANTEES user1, user2 EXCEPT user3
+    SETTINGS max_memory_usage=5000000, readonly=1;
\ No newline at end of file
diff --git a/parser/testdata/ddl/format/create_user.sql b/parser/testdata/ddl/format/create_user.sql
new file mode 100644
index 0000000..beb151a
--- /dev/null
+++ b/parser/testdata/ddl/format/create_user.sql
@@ -0,0 +1,87 @@
+-- Origin SQL:
+-- Basic CREATE USER tests
+
+CREATE USER user1;
+CREATE USER IF NOT EXISTS user2;
+CREATE USER OR REPLACE user3;
+CREATE USER user4, user5;
+
+-- CREATE USER with authentication
+CREATE USER user6 NOT IDENTIFIED;
+CREATE USER user7 IDENTIFIED WITH plaintext_password BY 'password123';
+CREATE USER user8 IDENTIFIED WITH sha256_password BY 'hash123';
+CREATE USER user9 IDENTIFIED WITH ldap SERVER 'ldap_server';
+CREATE USER user10 IDENTIFIED WITH kerberos;
+CREATE USER user11 IDENTIFIED WITH kerberos REALM 'EXAMPLE.COM';
+
+-- CREATE USER with host restrictions
+CREATE USER user12 HOST LOCAL;
+CREATE USER user13 HOST ANY;
+CREATE USER user14 HOST NONE;
+CREATE USER user15 HOST NAME 'localhost';
+CREATE USER user16 HOST REGEXP '.*\.example\.com';
+CREATE USER user17 HOST IP '192.168.1.1';
+CREATE USER user18 HOST LIKE 'test%';
+
+-- CREATE USER with default roles
+CREATE USER user19 DEFAULT ROLE role1;
+CREATE USER user20 DEFAULT ROLE role1, role2;
+CREATE USER user21 DEFAULT ROLE NONE;
+
+-- CREATE USER with default database
+CREATE USER user22 DEFAULT DATABASE test_db;
+CREATE USER user23 DEFAULT DATABASE NONE;
+
+-- CREATE USER with grantees
+CREATE USER user24 GRANTEES user1;
+CREATE USER user25 GRANTEES user1, user2;
+CREATE USER user26 GRANTEES ANY;
+CREATE USER user27 GRANTEES NONE;
+CREATE USER user28 GRANTEES user1, user2 EXCEPT user3;
+
+-- CREATE USER with settings
+CREATE USER user29 SETTINGS max_memory_usage=5000000;
+CREATE USER user30 SETTINGS PROFILE 'default';
+CREATE USER user31 SETTINGS readonly=1, max_memory_usage=5000000;
+
+-- Complex CREATE USER with multiple clauses
+CREATE USER user32 
+    IDENTIFIED WITH plaintext_password BY 'password'
+    HOST NAME 'localhost'
+    DEFAULT ROLE role1, role2
+    DEFAULT DATABASE test_db
+    GRANTEES user1, user2 EXCEPT user3
+    SETTINGS max_memory_usage=5000000, readonly=1;
+
+-- Format SQL:
+CREATE USER user1;
+CREATE USER IF NOT EXISTS user2;
+CREATE USER OR REPLACE user3;
+CREATE USER user4, user5;
+CREATE USER user6 NOT IDENTIFIED;
+CREATE USER user7 IDENTIFIED WITH plaintext_password BY 'password123';
+CREATE USER user8 IDENTIFIED WITH sha256_password BY 'hash123';
+CREATE USER user9 IDENTIFIED WITH ldap SERVER 'ldap_server';
+CREATE USER user10 IDENTIFIED WITH kerberos;
+CREATE USER user11 IDENTIFIED WITH kerberos REALM 'EXAMPLE.COM';
+CREATE USER user12 HOST LOCAL;
+CREATE USER user13 HOST ANY;
+CREATE USER user14 HOST NONE;
+CREATE USER user15 HOST NAME 'localhost';
+CREATE USER user16 HOST REGEXP '.*\.example\.com';
+CREATE USER user17 HOST IP '192.168.1.1';
+CREATE USER user18 HOST LIKE 'test%';
+CREATE USER user19 DEFAULT ROLE role1;
+CREATE USER user20 DEFAULT ROLE role1, role2;
+CREATE USER user21 DEFAULT ROLE NONE;
+CREATE USER user22 DEFAULT DATABASE test_db;
+CREATE USER user23 DEFAULT DATABASE NONE;
+CREATE USER user24 GRANTEES user1;
+CREATE USER user25 GRANTEES user1, user2;
+CREATE USER user26 GRANTEES ANY;
+CREATE USER user27 GRANTEES NONE;
+CREATE USER user28 GRANTEES user1, user2 EXCEPT user3;
+CREATE USER user29 SETTINGS max_memory_usage=5000000;
+CREATE USER user30 SETTINGS PROFILE 'default';
+CREATE USER user31 SETTINGS readonly=1, max_memory_usage=5000000;
+CREATE USER user32 IDENTIFIED WITH plaintext_password BY 'password' HOST NAME 'localhost' DEFAULT ROLE role1, role2 DEFAULT DATABASE test_db GRANTEES user1, user2 EXCEPT user3 SETTINGS max_memory_usage=5000000, readonly=1;
diff --git a/parser/testdata/ddl/output/create_user.sql.golden.json b/parser/testdata/ddl/output/create_user.sql.golden.json
new file mode 100644
index 0000000..25cdc84
--- /dev/null
+++ b/parser/testdata/ddl/output/create_user.sql.golden.json
@@ -0,0 +1,1290 @@
+[
+  {
+    "CreatePos": 28,
+    "StatementEnd": 45,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user1",
+          "QuoteType": 1,
+          "NamePos": 40,
+          "NameEnd": 45
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": null,
+    "Hosts": null,
+    "DefaultRole": null,
+    "DefaultDatabase": null,
+    "DefaultDbNone": false,
+    "Grantees": null,
+    "Settings": null
+  },
+  {
+    "CreatePos": 47,
+    "StatementEnd": 78,
+    "IfNotExists": true,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user2",
+          "QuoteType": 1,
+          "NamePos": 73,
+          "NameEnd": 78
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": null,
+    "Hosts": null,
+    "DefaultRole": null,
+    "DefaultDatabase": null,
+    "DefaultDbNone": false,
+    "Grantees": null,
+    "Settings": null
+  },
+  {
+    "CreatePos": 80,
+    "StatementEnd": 108,
+    "IfNotExists": false,
+    "OrReplace": true,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user3",
+          "QuoteType": 1,
+          "NamePos": 103,
+          "NameEnd": 108
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": null,
+    "Hosts": null,
+    "DefaultRole": null,
+    "DefaultDatabase": null,
+    "DefaultDbNone": false,
+    "Grantees": null,
+    "Settings": null
+  },
+  {
+    "CreatePos": 110,
+    "StatementEnd": 134,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user4",
+          "QuoteType": 1,
+          "NamePos": 122,
+          "NameEnd": 127
+        },
+        "Scope": null,
+        "OnCluster": null
+      },
+      {
+        "Name": {
+          "Name": "user5",
+          "QuoteType": 1,
+          "NamePos": 129,
+          "NameEnd": 134
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": null,
+    "Hosts": null,
+    "DefaultRole": null,
+    "DefaultDatabase": null,
+    "DefaultDbNone": false,
+    "Grantees": null,
+    "Settings": null
+  },
+  {
+    "CreatePos": 172,
+    "StatementEnd": 205,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user6",
+          "QuoteType": 1,
+          "NamePos": 184,
+          "NameEnd": 189
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": {
+      "AuthPos": 190,
+      "AuthEnd": 205,
+      "NotIdentified": true,
+      "AuthType": "",
+      "AuthValue": null,
+      "LdapServer": null,
+      "KerberosRealm": null,
+      "IsKerberos": false
+    },
+    "Hosts": null,
+    "DefaultRole": null,
+    "DefaultDatabase": null,
+    "DefaultDbNone": false,
+    "Grantees": null,
+    "Settings": null
+  },
+  {
+    "CreatePos": 206,
+    "StatementEnd": 274,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user7",
+          "QuoteType": 1,
+          "NamePos": 218,
+          "NameEnd": 223
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": {
+      "AuthPos": 224,
+      "AuthEnd": 274,
+      "NotIdentified": false,
+      "AuthType": "plaintext_password",
+      "AuthValue": {
+        "LiteralPos": 263,
+        "LiteralEnd": 274,
+        "Literal": "password123"
+      },
+      "LdapServer": null,
+      "KerberosRealm": null,
+      "IsKerberos": false
+    },
+    "Hosts": null,
+    "DefaultRole": null,
+    "DefaultDatabase": null,
+    "DefaultDbNone": false,
+    "Grantees": null,
+    "Settings": null
+  },
+  {
+    "CreatePos": 277,
+    "StatementEnd": 338,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user8",
+          "QuoteType": 1,
+          "NamePos": 289,
+          "NameEnd": 294
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": {
+      "AuthPos": 295,
+      "AuthEnd": 338,
+      "NotIdentified": false,
+      "AuthType": "sha256_password",
+      "AuthValue": {
+        "LiteralPos": 331,
+        "LiteralEnd": 338,
+        "Literal": "hash123"
+      },
+      "LdapServer": null,
+      "KerberosRealm": null,
+      "IsKerberos": false
+    },
+    "Hosts": null,
+    "DefaultRole": null,
+    "DefaultDatabase": null,
+    "DefaultDbNone": false,
+    "Grantees": null,
+    "Settings": null
+  },
+  {
+    "CreatePos": 341,
+    "StatementEnd": 399,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user9",
+          "QuoteType": 1,
+          "NamePos": 353,
+          "NameEnd": 358
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": {
+      "AuthPos": 359,
+      "AuthEnd": 399,
+      "NotIdentified": false,
+      "AuthType": "",
+      "AuthValue": null,
+      "LdapServer": {
+        "LiteralPos": 388,
+        "LiteralEnd": 399,
+        "Literal": "ldap_server"
+      },
+      "KerberosRealm": null,
+      "IsKerberos": false
+    },
+    "Hosts": null,
+    "DefaultRole": null,
+    "DefaultDatabase": null,
+    "DefaultDbNone": false,
+    "Grantees": null,
+    "Settings": null
+  },
+  {
+    "CreatePos": 402,
+    "StatementEnd": 446,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user10",
+          "QuoteType": 1,
+          "NamePos": 414,
+          "NameEnd": 420
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": {
+      "AuthPos": 421,
+      "AuthEnd": 446,
+      "NotIdentified": false,
+      "AuthType": "",
+      "AuthValue": null,
+      "LdapServer": null,
+      "KerberosRealm": null,
+      "IsKerberos": true
+    },
+    "Hosts": null,
+    "DefaultRole": null,
+    "DefaultDatabase": null,
+    "DefaultDbNone": false,
+    "Grantees": null,
+    "Settings": null
+  },
+  {
+    "CreatePos": 447,
+    "StatementEnd": 509,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user11",
+          "QuoteType": 1,
+          "NamePos": 459,
+          "NameEnd": 465
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": {
+      "AuthPos": 466,
+      "AuthEnd": 509,
+      "NotIdentified": false,
+      "AuthType": "",
+      "AuthValue": null,
+      "LdapServer": null,
+      "KerberosRealm": {
+        "LiteralPos": 498,
+        "LiteralEnd": 509,
+        "Literal": "EXAMPLE.COM"
+      },
+      "IsKerberos": true
+    },
+    "Hosts": null,
+    "DefaultRole": null,
+    "DefaultDatabase": null,
+    "DefaultDbNone": false,
+    "Grantees": null,
+    "Settings": null
+  },
+  {
+    "CreatePos": 551,
+    "StatementEnd": 581,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user12",
+          "QuoteType": 1,
+          "NamePos": 563,
+          "NameEnd": 569
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": null,
+    "Hosts": [
+      {
+        "HostPos": 570,
+        "HostEnd": 581,
+        "HostType": "LOCAL",
+        "HostValue": null
+      }
+    ],
+    "DefaultRole": null,
+    "DefaultDatabase": null,
+    "DefaultDbNone": false,
+    "Grantees": null,
+    "Settings": null
+  },
+  {
+    "CreatePos": 582,
+    "StatementEnd": 610,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user13",
+          "QuoteType": 1,
+          "NamePos": 594,
+          "NameEnd": 600
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": null,
+    "Hosts": [
+      {
+        "HostPos": 601,
+        "HostEnd": 610,
+        "HostType": "ANY",
+        "HostValue": null
+      }
+    ],
+    "DefaultRole": null,
+    "DefaultDatabase": null,
+    "DefaultDbNone": false,
+    "Grantees": null,
+    "Settings": null
+  },
+  {
+    "CreatePos": 611,
+    "StatementEnd": 640,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user14",
+          "QuoteType": 1,
+          "NamePos": 623,
+          "NameEnd": 629
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": null,
+    "Hosts": [
+      {
+        "HostPos": 630,
+        "HostEnd": 640,
+        "HostType": "NONE",
+        "HostValue": null
+      }
+    ],
+    "DefaultRole": null,
+    "DefaultDatabase": null,
+    "DefaultDbNone": false,
+    "Grantees": null,
+    "Settings": null
+  },
+  {
+    "CreatePos": 641,
+    "StatementEnd": 680,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user15",
+          "QuoteType": 1,
+          "NamePos": 653,
+          "NameEnd": 659
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": null,
+    "Hosts": [
+      {
+        "HostPos": 660,
+        "HostEnd": 680,
+        "HostType": "NAME",
+        "HostValue": {
+          "LiteralPos": 671,
+          "LiteralEnd": 680,
+          "Literal": "localhost"
+        }
+      }
+    ],
+    "DefaultRole": null,
+    "DefaultDatabase": null,
+    "DefaultDbNone": false,
+    "Grantees": null,
+    "Settings": null
+  },
+  {
+    "CreatePos": 683,
+    "StatementEnd": 731,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user16",
+          "QuoteType": 1,
+          "NamePos": 695,
+          "NameEnd": 701
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": null,
+    "Hosts": [
+      {
+        "HostPos": 702,
+        "HostEnd": 731,
+        "HostType": "REGEXP",
+        "HostValue": {
+          "LiteralPos": 715,
+          "LiteralEnd": 731,
+          "Literal": ".*\\.example\\.com"
+        }
+      }
+    ],
+    "DefaultRole": null,
+    "DefaultDatabase": null,
+    "DefaultDbNone": false,
+    "Grantees": null,
+    "Settings": null
+  },
+  {
+    "CreatePos": 734,
+    "StatementEnd": 773,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user17",
+          "QuoteType": 1,
+          "NamePos": 746,
+          "NameEnd": 752
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": null,
+    "Hosts": [
+      {
+        "HostPos": 753,
+        "HostEnd": 773,
+        "HostType": "IP",
+        "HostValue": {
+          "LiteralPos": 762,
+          "LiteralEnd": 773,
+          "Literal": "192.168.1.1"
+        }
+      }
+    ],
+    "DefaultRole": null,
+    "DefaultDatabase": null,
+    "DefaultDbNone": false,
+    "Grantees": null,
+    "Settings": null
+  },
+  {
+    "CreatePos": 776,
+    "StatementEnd": 811,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user18",
+          "QuoteType": 1,
+          "NamePos": 788,
+          "NameEnd": 794
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": null,
+    "Hosts": [
+      {
+        "HostPos": 795,
+        "HostEnd": 811,
+        "HostType": "LIKE",
+        "HostValue": {
+          "LiteralPos": 806,
+          "LiteralEnd": 811,
+          "Literal": "test%"
+        }
+      }
+    ],
+    "DefaultRole": null,
+    "DefaultDatabase": null,
+    "DefaultDbNone": false,
+    "Grantees": null,
+    "Settings": null
+  },
+  {
+    "CreatePos": 849,
+    "StatementEnd": 886,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user19",
+          "QuoteType": 1,
+          "NamePos": 861,
+          "NameEnd": 867
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": null,
+    "Hosts": null,
+    "DefaultRole": {
+      "DefaultPos": 868,
+      "DefaultEnd": 886,
+      "Roles": [
+        {
+          "Name": {
+            "Name": "role1",
+            "QuoteType": 1,
+            "NamePos": 881,
+            "NameEnd": 886
+          },
+          "Scope": null,
+          "OnCluster": null
+        }
+      ],
+      "None": false
+    },
+    "DefaultDatabase": null,
+    "DefaultDbNone": false,
+    "Grantees": null,
+    "Settings": null
+  },
+  {
+    "CreatePos": 888,
+    "StatementEnd": 932,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user20",
+          "QuoteType": 1,
+          "NamePos": 900,
+          "NameEnd": 906
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": null,
+    "Hosts": null,
+    "DefaultRole": {
+      "DefaultPos": 907,
+      "DefaultEnd": 932,
+      "Roles": [
+        {
+          "Name": {
+            "Name": "role1",
+            "QuoteType": 1,
+            "NamePos": 920,
+            "NameEnd": 925
+          },
+          "Scope": null,
+          "OnCluster": null
+        },
+        {
+          "Name": {
+            "Name": "role2",
+            "QuoteType": 1,
+            "NamePos": 927,
+            "NameEnd": 932
+          },
+          "Scope": null,
+          "OnCluster": null
+        }
+      ],
+      "None": false
+    },
+    "DefaultDatabase": null,
+    "DefaultDbNone": false,
+    "Grantees": null,
+    "Settings": null
+  },
+  {
+    "CreatePos": 934,
+    "StatementEnd": 971,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user21",
+          "QuoteType": 1,
+          "NamePos": 946,
+          "NameEnd": 952
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": null,
+    "Hosts": null,
+    "DefaultRole": {
+      "DefaultPos": 953,
+      "DefaultEnd": 971,
+      "Roles": null,
+      "None": true
+    },
+    "DefaultDatabase": null,
+    "DefaultDbNone": false,
+    "Grantees": null,
+    "Settings": null
+  },
+  {
+    "CreatePos": 1010,
+    "StatementEnd": 1053,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user22",
+          "QuoteType": 1,
+          "NamePos": 1022,
+          "NameEnd": 1028
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": null,
+    "Hosts": null,
+    "DefaultRole": null,
+    "DefaultDatabase": {
+      "Name": "test_db",
+      "QuoteType": 1,
+      "NamePos": 1046,
+      "NameEnd": 1053
+    },
+    "DefaultDbNone": false,
+    "Grantees": null,
+    "Settings": null
+  },
+  {
+    "CreatePos": 1055,
+    "StatementEnd": 1096,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user23",
+          "QuoteType": 1,
+          "NamePos": 1067,
+          "NameEnd": 1073
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": null,
+    "Hosts": null,
+    "DefaultRole": null,
+    "DefaultDatabase": null,
+    "DefaultDbNone": true,
+    "Grantees": null,
+    "Settings": null
+  },
+  {
+    "CreatePos": 1127,
+    "StatementEnd": 1160,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user24",
+          "QuoteType": 1,
+          "NamePos": 1139,
+          "NameEnd": 1145
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": null,
+    "Hosts": null,
+    "DefaultRole": null,
+    "DefaultDatabase": null,
+    "DefaultDbNone": false,
+    "Grantees": {
+      "GranteesPos": 1146,
+      "GranteesEnd": 1160,
+      "Grantees": [
+        {
+          "Name": {
+            "Name": "user1",
+            "QuoteType": 1,
+            "NamePos": 1155,
+            "NameEnd": 1160
+          },
+          "Scope": null,
+          "OnCluster": null
+        }
+      ],
+      "ExceptUsers": null,
+      "Any": false,
+      "None": false
+    },
+    "Settings": null
+  },
+  {
+    "CreatePos": 1162,
+    "StatementEnd": 1202,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user25",
+          "QuoteType": 1,
+          "NamePos": 1174,
+          "NameEnd": 1180
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": null,
+    "Hosts": null,
+    "DefaultRole": null,
+    "DefaultDatabase": null,
+    "DefaultDbNone": false,
+    "Grantees": {
+      "GranteesPos": 1181,
+      "GranteesEnd": 1202,
+      "Grantees": [
+        {
+          "Name": {
+            "Name": "user1",
+            "QuoteType": 1,
+            "NamePos": 1190,
+            "NameEnd": 1195
+          },
+          "Scope": null,
+          "OnCluster": null
+        },
+        {
+          "Name": {
+            "Name": "user2",
+            "QuoteType": 1,
+            "NamePos": 1197,
+            "NameEnd": 1202
+          },
+          "Scope": null,
+          "OnCluster": null
+        }
+      ],
+      "ExceptUsers": null,
+      "Any": false,
+      "None": false
+    },
+    "Settings": null
+  },
+  {
+    "CreatePos": 1204,
+    "StatementEnd": 1236,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user26",
+          "QuoteType": 1,
+          "NamePos": 1216,
+          "NameEnd": 1222
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": null,
+    "Hosts": null,
+    "DefaultRole": null,
+    "DefaultDatabase": null,
+    "DefaultDbNone": false,
+    "Grantees": {
+      "GranteesPos": 1223,
+      "GranteesEnd": 1236,
+      "Grantees": null,
+      "ExceptUsers": null,
+      "Any": true,
+      "None": false
+    },
+    "Settings": null
+  },
+  {
+    "CreatePos": 1237,
+    "StatementEnd": 1270,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user27",
+          "QuoteType": 1,
+          "NamePos": 1249,
+          "NameEnd": 1255
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": null,
+    "Hosts": null,
+    "DefaultRole": null,
+    "DefaultDatabase": null,
+    "DefaultDbNone": false,
+    "Grantees": {
+      "GranteesPos": 1256,
+      "GranteesEnd": 1270,
+      "Grantees": null,
+      "ExceptUsers": null,
+      "Any": false,
+      "None": true
+    },
+    "Settings": null
+  },
+  {
+    "CreatePos": 1271,
+    "StatementEnd": 1324,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user28",
+          "QuoteType": 1,
+          "NamePos": 1283,
+          "NameEnd": 1289
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": null,
+    "Hosts": null,
+    "DefaultRole": null,
+    "DefaultDatabase": null,
+    "DefaultDbNone": false,
+    "Grantees": {
+      "GranteesPos": 1290,
+      "GranteesEnd": 1324,
+      "Grantees": [
+        {
+          "Name": {
+            "Name": "user1",
+            "QuoteType": 1,
+            "NamePos": 1299,
+            "NameEnd": 1304
+          },
+          "Scope": null,
+          "OnCluster": null
+        },
+        {
+          "Name": {
+            "Name": "user2",
+            "QuoteType": 1,
+            "NamePos": 1306,
+            "NameEnd": 1311
+          },
+          "Scope": null,
+          "OnCluster": null
+        }
+      ],
+      "ExceptUsers": [
+        {
+          "Name": {
+            "Name": "user3",
+            "QuoteType": 1,
+            "NamePos": 1319,
+            "NameEnd": 1324
+          },
+          "Scope": null,
+          "OnCluster": null
+        }
+      ],
+      "Any": false,
+      "None": false
+    },
+    "Settings": null
+  },
+  {
+    "CreatePos": 1356,
+    "StatementEnd": 1408,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user29",
+          "QuoteType": 1,
+          "NamePos": 1368,
+          "NameEnd": 1374
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": null,
+    "Hosts": null,
+    "DefaultRole": null,
+    "DefaultDatabase": null,
+    "DefaultDbNone": false,
+    "Grantees": null,
+    "Settings": [
+      {
+        "SettingPairs": [
+          {
+            "Name": {
+              "Name": "max_memory_usage",
+              "QuoteType": 1,
+              "NamePos": 1384,
+              "NameEnd": 1400
+            },
+            "Operation": "=",
+            "Value": {
+              "NumPos": 1401,
+              "NumEnd": 1408,
+              "Literal": "5000000",
+              "Base": 10
+            }
+          }
+        ],
+        "Modifier": null
+      }
+    ]
+  },
+  {
+    "CreatePos": 1410,
+    "StatementEnd": 1454,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user30",
+          "QuoteType": 1,
+          "NamePos": 1422,
+          "NameEnd": 1428
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": null,
+    "Hosts": null,
+    "DefaultRole": null,
+    "DefaultDatabase": null,
+    "DefaultDbNone": false,
+    "Grantees": null,
+    "Settings": [
+      {
+        "SettingPairs": [
+          {
+            "Name": {
+              "Name": "PROFILE",
+              "QuoteType": 1,
+              "NamePos": 1438,
+              "NameEnd": 1445
+            },
+            "Operation": "",
+            "Value": {
+              "LiteralPos": 1447,
+              "LiteralEnd": 1454,
+              "Literal": "default"
+            }
+          }
+        ],
+        "Modifier": null
+      }
+    ]
+  },
+  {
+    "CreatePos": 1457,
+    "StatementEnd": 1521,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user31",
+          "QuoteType": 1,
+          "NamePos": 1469,
+          "NameEnd": 1475
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": null,
+    "Hosts": null,
+    "DefaultRole": null,
+    "DefaultDatabase": null,
+    "DefaultDbNone": false,
+    "Grantees": null,
+    "Settings": [
+      {
+        "SettingPairs": [
+          {
+            "Name": {
+              "Name": "readonly",
+              "QuoteType": 1,
+              "NamePos": 1485,
+              "NameEnd": 1493
+            },
+            "Operation": "=",
+            "Value": {
+              "NumPos": 1494,
+              "NumEnd": 1495,
+              "Literal": "1",
+              "Base": 10
+            }
+          }
+        ],
+        "Modifier": null
+      },
+      {
+        "SettingPairs": [
+          {
+            "Name": {
+              "Name": "max_memory_usage",
+              "QuoteType": 1,
+              "NamePos": 1497,
+              "NameEnd": 1513
+            },
+            "Operation": "=",
+            "Value": {
+              "NumPos": 1514,
+              "NumEnd": 1521,
+              "Literal": "5000000",
+              "Base": 10
+            }
+          }
+        ],
+        "Modifier": null
+      }
+    ]
+  },
+  {
+    "CreatePos": 1569,
+    "StatementEnd": 1815,
+    "IfNotExists": false,
+    "OrReplace": false,
+    "UserNames": [
+      {
+        "Name": {
+          "Name": "user32",
+          "QuoteType": 1,
+          "NamePos": 1581,
+          "NameEnd": 1587
+        },
+        "Scope": null,
+        "OnCluster": null
+      }
+    ],
+    "Authentication": {
+      "AuthPos": 1593,
+      "AuthEnd": 1640,
+      "NotIdentified": false,
+      "AuthType": "plaintext_password",
+      "AuthValue": {
+        "LiteralPos": 1632,
+        "LiteralEnd": 1640,
+        "Literal": "password"
+      },
+      "LdapServer": null,
+      "KerberosRealm": null,
+      "IsKerberos": false
+    },
+    "Hosts": [
+      {
+        "HostPos": 1646,
+        "HostEnd": 1666,
+        "HostType": "NAME",
+        "HostValue": {
+          "LiteralPos": 1657,
+          "LiteralEnd": 1666,
+          "Literal": "localhost"
+        }
+      }
+    ],
+    "DefaultRole": {
+      "DefaultPos": 1672,
+      "DefaultEnd": 1697,
+      "Roles": [
+        {
+          "Name": {
+            "Name": "role1",
+            "QuoteType": 1,
+            "NamePos": 1685,
+            "NameEnd": 1690
+          },
+          "Scope": null,
+          "OnCluster": null
+        },
+        {
+          "Name": {
+            "Name": "role2",
+            "QuoteType": 1,
+            "NamePos": 1692,
+            "NameEnd": 1697
+          },
+          "Scope": null,
+          "OnCluster": null
+        }
+      ],
+      "None": false
+    },
+    "DefaultDatabase": {
+      "Name": "test_db",
+      "QuoteType": 1,
+      "NamePos": 1719,
+      "NameEnd": 1726
+    },
+    "DefaultDbNone": false,
+    "Grantees": {
+      "GranteesPos": 1731,
+      "GranteesEnd": 1765,
+      "Grantees": [
+        {
+          "Name": {
+            "Name": "user1",
+            "QuoteType": 1,
+            "NamePos": 1740,
+            "NameEnd": 1745
+          },
+          "Scope": null,
+          "OnCluster": null
+        },
+        {
+          "Name": {
+            "Name": "user2",
+            "QuoteType": 1,
+            "NamePos": 1747,
+            "NameEnd": 1752
+          },
+          "Scope": null,
+          "OnCluster": null
+        }
+      ],
+      "ExceptUsers": [
+        {
+          "Name": {
+            "Name": "user3",
+            "QuoteType": 1,
+            "NamePos": 1760,
+            "NameEnd": 1765
+          },
+          "Scope": null,
+          "OnCluster": null
+        }
+      ],
+      "Any": false,
+      "None": false
+    },
+    "Settings": [
+      {
+        "SettingPairs": [
+          {
+            "Name": {
+              "Name": "max_memory_usage",
+              "QuoteType": 1,
+              "NamePos": 1779,
+              "NameEnd": 1795
+            },
+            "Operation": "=",
+            "Value": {
+              "NumPos": 1796,
+              "NumEnd": 1803,
+              "Literal": "5000000",
+              "Base": 10
+            }
+          }
+        ],
+        "Modifier": null
+      },
+      {
+        "SettingPairs": [
+          {
+            "Name": {
+              "Name": "readonly",
+              "QuoteType": 1,
+              "NamePos": 1805,
+              "NameEnd": 1813
+            },
+            "Operation": "=",
+            "Value": {
+              "NumPos": 1814,
+              "NumEnd": 1815,
+              "Literal": "1",
+              "Base": 10
+            }
+          }
+        ],
+        "Modifier": null
+      }
+    ]
+  }
+]
\ No newline at end of file