From cbdda720552afe9b72ba5fc716e2d6b5a73f56e6 Mon Sep 17 00:00:00 2001 From: Vladimir Mihailenco Date: Thu, 3 Nov 2022 11:58:19 +0200 Subject: [PATCH] feat: add queries to create/drop views --- ch/chschema/sqlfmt.go | 4 + ch/db.go | 8 + ch/query_base.go | 34 ++++ ch/query_select.go | 34 ---- ch/query_table_create.go | 2 +- ch/query_test.go | 20 +++ ch/query_view_create.go | 271 +++++++++++++++++++++++++++++ ch/query_view_drop.go | 108 ++++++++++++ ch/testdata/snapshots/TestQuery-11 | 1 + ch/testdata/snapshots/TestQuery-12 | 1 + ch/testdata/snapshots/TestQuery-13 | 1 + 11 files changed, 449 insertions(+), 35 deletions(-) create mode 100644 ch/query_view_create.go create mode 100644 ch/query_view_drop.go create mode 100644 ch/testdata/snapshots/TestQuery-11 create mode 100644 ch/testdata/snapshots/TestQuery-12 create mode 100644 ch/testdata/snapshots/TestQuery-13 diff --git a/ch/chschema/sqlfmt.go b/ch/chschema/sqlfmt.go index b763305..467ef40 100644 --- a/ch/chschema/sqlfmt.go +++ b/ch/chschema/sqlfmt.go @@ -134,6 +134,10 @@ func (q QueryWithArgs) IsZero() bool { return q.Query == "" && q.Args == nil } +func (q QueryWithArgs) IsEmpty() bool { + return q.Query == "" +} + func (q QueryWithArgs) AppendQuery(fmter Formatter, b []byte) ([]byte, error) { if q.Args == nil { return fmter.AppendIdent(b, q.Query), nil diff --git a/ch/db.go b/ch/db.go index 97d4838..8d0df94 100644 --- a/ch/db.go +++ b/ch/db.go @@ -452,6 +452,14 @@ func (db *DB) NewTruncateTable() *TruncateTableQuery { return NewTruncateTableQuery(db) } +func (db *DB) NewCreateView() *CreateViewQuery { + return NewCreateViewQuery(db) +} + +func (db *DB) NewDropView() *DropViewQuery { + return NewDropViewQuery(db) +} + func (db *DB) ResetModel(ctx context.Context, models ...any) error { for _, model := range models { if _, err := db.NewDropTable().Model(model).IfExists().Exec(ctx); err != nil { diff --git a/ch/query_base.go b/ch/query_base.go index f8918cb..16bdaf6 100644 --- a/ch/query_base.go +++ b/ch/query_base.go @@ -356,6 +356,40 @@ func (q *baseQuery) appendSettings(fmter chschema.Formatter, b []byte) (_ []byte return b, nil } +func (q *baseQuery) appendColumns(fmter chschema.Formatter, b []byte) (_ []byte, err error) { + switch { + case q.columns != nil: + for i, f := range q.columns { + if i > 0 { + b = append(b, ", "...) + } + b, err = f.AppendQuery(fmter, b) + if err != nil { + return nil, err + } + } + case q.table != nil: + b = appendTableColumns(b, q.table.CHAlias, q.table.Fields) + default: + b = append(b, '*') + } + return b, nil +} + +func appendTableColumns(b []byte, table chschema.Safe, fields []*chschema.Field) []byte { + for i, f := range fields { + if i > 0 { + b = append(b, ", "...) + } + if len(table) > 0 { + b = append(b, table...) + b = append(b, '.') + } + b = append(b, f.Column...) + } + return b +} + //------------------------------------------------------------------------------ type whereBaseQuery struct { diff --git a/ch/query_select.go b/ch/query_select.go index 8c3dc89..3a8251b 100644 --- a/ch/query_select.go +++ b/ch/query_select.go @@ -442,40 +442,6 @@ func (q *SelectQuery) appendWith(fmter chschema.Formatter, b []byte) (_ []byte, return b, nil } -func (q *SelectQuery) appendColumns(fmter chschema.Formatter, b []byte) (_ []byte, err error) { - switch { - case q.columns != nil: - for i, f := range q.columns { - if i > 0 { - b = append(b, ", "...) - } - b, err = f.AppendQuery(fmter, b) - if err != nil { - return nil, err - } - } - case q.table != nil: - b = appendTableColumns(b, q.table.CHAlias, q.table.Fields) - default: - b = append(b, '*') - } - return b, nil -} - -func appendTableColumns(b []byte, table chschema.Safe, fields []*chschema.Field) []byte { - for i, f := range fields { - if i > 0 { - b = append(b, ", "...) - } - if len(table) > 0 { - b = append(b, table...) - b = append(b, '.') - } - b = append(b, f.Column...) - } - return b -} - func (q *SelectQuery) Scan(ctx context.Context, values ...any) error { return q.scan(ctx, false, values...) } diff --git a/ch/query_table_create.go b/ch/query_table_create.go index 03afeda..919fd30 100644 --- a/ch/query_table_create.go +++ b/ch/query_table_create.go @@ -37,7 +37,7 @@ func (q *CreateTableQuery) WithQuery(fn func(*CreateTableQuery) *CreateTableQuer return fn(q) } -// ------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ func (q *CreateTableQuery) Table(tables ...string) *CreateTableQuery { for _, table := range tables { diff --git a/ch/query_test.go b/ch/query_test.go index cffbfe3..993f42c 100644 --- a/ch/query_test.go +++ b/ch/query_test.go @@ -76,6 +76,26 @@ func TestQuery(t *testing.T) { Order("id"). Setting("ttl_only_drop_parts = 1") }, + func(db *ch.DB) chschema.QueryAppender { + return db.NewDropView().View("view_name") + }, + func(db *ch.DB) chschema.QueryAppender { + return db.NewDropView().IfExists().ViewExpr("view_name") + }, + func(db *ch.DB) chschema.QueryAppender { + return db.NewCreateView(). + Materialized(). + IfNotExists(). + View("view_name"). + To("dest_table"). + Column("col1"). + ColumnExpr("col1 AS alias"). + TableExpr("src_table AS alias"). + Where("foo = bar"). + Group("group1"). + GroupExpr("group2, group3"). + OrderExpr("order2, order3") + }, } db := chDB() diff --git a/ch/query_view_create.go b/ch/query_view_create.go new file mode 100644 index 0000000..492b9df --- /dev/null +++ b/ch/query_view_create.go @@ -0,0 +1,271 @@ +package ch + +import ( + "context" + "database/sql" + + "github.com/uptrace/go-clickhouse/ch/chschema" + "github.com/uptrace/go-clickhouse/ch/internal" +) + +type CreateViewQuery struct { + whereBaseQuery + + materialized bool + ifNotExists bool + view chschema.QueryWithArgs + cluster chschema.QueryWithArgs + to chschema.QueryWithArgs + group []chschema.QueryWithArgs + order chschema.QueryWithArgs +} + +var _ Query = (*CreateViewQuery)(nil) + +func NewCreateViewQuery(db *DB) *CreateViewQuery { + return &CreateViewQuery{ + whereBaseQuery: whereBaseQuery{ + baseQuery: baseQuery{ + db: db, + }, + }, + } +} + +func (q *CreateViewQuery) Model(model any) *CreateViewQuery { + q.setTableModel(model) + return q +} + +func (q *CreateViewQuery) WithQuery(fn func(*CreateViewQuery) *CreateViewQuery) *CreateViewQuery { + return fn(q) +} + +//------------------------------------------------------------------------------ + +func (q *CreateViewQuery) View(view string) *CreateViewQuery { + q.view = chschema.UnsafeIdent(view) + return q +} + +func (q *CreateViewQuery) ViewExpr(query string, args ...any) *CreateViewQuery { + q.view = chschema.SafeQuery(query, args) + return q +} + +func (q *CreateViewQuery) OnCluster(cluster string) *CreateViewQuery { + q.cluster = chschema.UnsafeIdent(cluster) + return q +} + +func (q *CreateViewQuery) OnClusterExpr(query string, args ...any) *CreateViewQuery { + q.cluster = chschema.SafeQuery(query, args) + return q +} + +func (q *CreateViewQuery) To(to string) *CreateViewQuery { + q.to = chschema.UnsafeIdent(to) + return q +} + +func (q *CreateViewQuery) ToExpr(query string, args ...any) *CreateViewQuery { + q.to = chschema.SafeQuery(query, args) + return q +} + +func (q *CreateViewQuery) Table(tables ...string) *CreateViewQuery { + for _, table := range tables { + q.addTable(chschema.UnsafeIdent(table)) + } + return q +} + +func (q *CreateViewQuery) TableExpr(query string, args ...any) *CreateViewQuery { + q.addTable(chschema.SafeQuery(query, args)) + return q +} + +func (q *CreateViewQuery) ModelTableExpr(query string, args ...any) *CreateViewQuery { + q.modelTableName = chschema.SafeQuery(query, args) + return q +} + +//------------------------------------------------------------------------------ + +func (q *CreateViewQuery) Column(columns ...string) *CreateViewQuery { + for _, column := range columns { + q.addColumn(chschema.UnsafeIdent(column)) + } + return q +} + +func (q *CreateViewQuery) ColumnExpr(query string, args ...any) *CreateViewQuery { + q.addColumn(chschema.SafeQuery(query, args)) + return q +} + +func (q *CreateViewQuery) ExcludeColumn(columns ...string) *CreateViewQuery { + q.excludeColumn(columns) + return q +} + +//------------------------------------------------------------------------------ + +func (q *CreateViewQuery) Materialized() *CreateViewQuery { + q.materialized = true + return q +} + +func (q *CreateViewQuery) IfNotExists() *CreateViewQuery { + q.ifNotExists = true + return q +} + +//------------------------------------------------------------------------------ + +func (q *CreateViewQuery) Where(query string, args ...any) *CreateViewQuery { + q.addWhere(chschema.SafeQueryWithSep(query, args, " AND ")) + return q +} + +func (q *CreateViewQuery) WhereOr(query string, args ...any) *CreateViewQuery { + q.addWhere(chschema.SafeQueryWithSep(query, args, " OR ")) + return q +} + +func (q *CreateViewQuery) WhereGroup(sep string, fn func(*CreateViewQuery) *CreateViewQuery) *CreateViewQuery { + saved := q.where + q.where = nil + + q = fn(q) + + where := q.where + q.where = saved + + q.addWhereGroup(sep, where) + + return q +} + +func (q *CreateViewQuery) Group(columns ...string) *CreateViewQuery { + for _, column := range columns { + q.group = append(q.group, chschema.UnsafeIdent(column)) + } + return q +} + +func (q *CreateViewQuery) GroupExpr(group string, args ...any) *CreateViewQuery { + q.group = append(q.group, chschema.SafeQuery(group, args)) + return q +} + +func (q *CreateViewQuery) OrderExpr(query string, args ...any) *CreateViewQuery { + q.order = chschema.SafeQuery(query, args) + return q +} + +func (q *CreateViewQuery) Setting(query string, args ...any) *CreateViewQuery { + q.settings = append(q.settings, chschema.SafeQuery(query, args)) + return q +} + +//------------------------------------------------------------------------------ + +func (q *CreateViewQuery) Operation() string { + return "CREATE VIEW" +} + +var _ chschema.QueryAppender = (*CreateViewQuery)(nil) + +func (q *CreateViewQuery) AppendQuery(fmter chschema.Formatter, b []byte) (_ []byte, err error) { + if q.err != nil { + return nil, q.err + } + + b = append(b, "CREATE "...) + if q.materialized { + b = append(b, "MATERIALIZED "...) + } + b = append(b, "VIEW "...) + if q.ifNotExists { + b = append(b, "IF NOT EXISTS "...) + } + + b, err = q.view.AppendQuery(fmter, b) + if err != nil { + return nil, err + } + + if !q.cluster.IsEmpty() { + b = append(b, " ON CLUSTER "...) + b, err = q.cluster.AppendQuery(fmter, b) + if err != nil { + return nil, err + } + } + + b = append(b, " TO "...) + b, err = q.to.AppendQuery(fmter, b) + if err != nil { + return nil, err + } + b = append(b, " AS "...) + + b = append(b, "SELECT "...) + + b, err = q.appendColumns(fmter, b) + if err != nil { + return nil, err + } + + b = append(b, " FROM "...) + b, err = q.appendTablesWithAlias(fmter, b) + if err != nil { + return nil, err + } + + b, err = q.appendWhere(fmter, b) + if err != nil { + return nil, err + } + + if len(q.group) > 0 { + b = append(b, " GROUP BY "...) + for i, f := range q.group { + if i > 0 { + b = append(b, ", "...) + } + b, err = f.AppendQuery(fmter, b) + if err != nil { + return nil, err + } + } + } + + if !q.order.IsZero() { + b = append(b, " ORDER BY "...) + b, err = q.order.AppendQuery(fmter, b) + if err != nil { + return nil, err + } + } + + b, err = q.appendSettings(fmter, b) + if err != nil { + return nil, err + } + + return b, nil +} + +//------------------------------------------------------------------------------ + +func (q *CreateViewQuery) Exec(ctx context.Context, dest ...any) (sql.Result, error) { + queryBytes, err := q.AppendQuery(q.db.fmter, q.db.makeQueryBytes()) + if err != nil { + return nil, err + } + query := internal.String(queryBytes) + + return q.exec(ctx, q, query) +} diff --git a/ch/query_view_drop.go b/ch/query_view_drop.go new file mode 100644 index 0000000..00a6883 --- /dev/null +++ b/ch/query_view_drop.go @@ -0,0 +1,108 @@ +package ch + +import ( + "context" + "database/sql" + + "github.com/uptrace/go-clickhouse/ch/chschema" + "github.com/uptrace/go-clickhouse/ch/internal" +) + +type DropViewQuery struct { + baseQuery + + ifExists bool + view chschema.QueryWithArgs + cluster chschema.QueryWithArgs +} + +var _ Query = (*DropViewQuery)(nil) + +func NewDropViewQuery(db *DB) *DropViewQuery { + q := &DropViewQuery{ + baseQuery: baseQuery{ + db: db, + }, + } + return q +} + +func (q *DropViewQuery) Model(model any) *DropViewQuery { + q.setTableModel(model) + return q +} + +func (q *DropViewQuery) WithQuery(fn func(*DropViewQuery) *DropViewQuery) *DropViewQuery { + return fn(q) +} + +//------------------------------------------------------------------------------ + +func (q *DropViewQuery) IfExists() *DropViewQuery { + q.ifExists = true + return q +} + +func (q *DropViewQuery) View(view string) *DropViewQuery { + q.view = chschema.UnsafeIdent(view) + return q +} + +func (q *DropViewQuery) ViewExpr(query string, args ...any) *DropViewQuery { + q.view = chschema.SafeQuery(query, args) + return q +} + +func (q *DropViewQuery) OnCluster(cluster string) *DropViewQuery { + q.cluster = chschema.UnsafeIdent(cluster) + return q +} + +func (q *DropViewQuery) OnClusterExpr(query string, args ...any) *DropViewQuery { + q.cluster = chschema.SafeQuery(query, args) + return q +} + +//------------------------------------------------------------------------------ + +func (q *DropViewQuery) Operation() string { + return "DROP TABLE" +} + +func (q *DropViewQuery) AppendQuery(fmter chschema.Formatter, b []byte) (_ []byte, err error) { + if q.err != nil { + return nil, q.err + } + + b = append(b, "DROP VIEW "...) + if q.ifExists { + b = append(b, "IF EXISTS "...) + } + + b, err = q.view.AppendQuery(fmter, b) + if err != nil { + return nil, err + } + + if !q.cluster.IsEmpty() { + b = append(b, " ON CLUSTER "...) + b, err = q.cluster.AppendQuery(fmter, b) + if err != nil { + return nil, err + } + } + + return b, nil +} + +//------------------------------------------------------------------------------ + +func (q *DropViewQuery) Exec(ctx context.Context, dest ...any) (sql.Result, error) { + queryBytes, err := q.AppendQuery(q.db.fmter, q.db.makeQueryBytes()) + if err != nil { + return nil, err + } + query := internal.String(queryBytes) + + return q.exec(ctx, q, query) +} diff --git a/ch/testdata/snapshots/TestQuery-11 b/ch/testdata/snapshots/TestQuery-11 new file mode 100644 index 0000000..af828e9 --- /dev/null +++ b/ch/testdata/snapshots/TestQuery-11 @@ -0,0 +1 @@ +DROP VIEW "hello_world" diff --git a/ch/testdata/snapshots/TestQuery-12 b/ch/testdata/snapshots/TestQuery-12 new file mode 100644 index 0000000..94d0790 --- /dev/null +++ b/ch/testdata/snapshots/TestQuery-12 @@ -0,0 +1 @@ +DROP VIEW IF EXISTS hello_world diff --git a/ch/testdata/snapshots/TestQuery-13 b/ch/testdata/snapshots/TestQuery-13 new file mode 100644 index 0000000..5c032c1 --- /dev/null +++ b/ch/testdata/snapshots/TestQuery-13 @@ -0,0 +1 @@ +CREATE VIEW "view_name"TO "dest_table" AS SELECT "col1", col1 AS alias FROM src_table AS alias