github.com/fr-nvriep/migrate/v4@v4.3.2/database/clickhouse/clickhouse.go (about)

     1  package clickhouse
     2  
     3  import (
     4  	"database/sql"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"net/url"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/fr-nvriep/migrate/v4"
    13  	"github.com/fr-nvriep/migrate/v4/database"
    14  	"github.com/hashicorp/go-multierror"
    15  )
    16  
    17  var DefaultMigrationsTable = "schema_migrations"
    18  
    19  var ErrNilConfig = fmt.Errorf("no config")
    20  
    21  type Config struct {
    22  	DatabaseName          string
    23  	MigrationsTable       string
    24  	MultiStatementEnabled bool
    25  }
    26  
    27  func init() {
    28  	database.Register("clickhouse", &ClickHouse{})
    29  }
    30  
    31  func WithInstance(conn *sql.DB, config *Config) (database.Driver, error) {
    32  	if config == nil {
    33  		return nil, ErrNilConfig
    34  	}
    35  
    36  	if err := conn.Ping(); err != nil {
    37  		return nil, err
    38  	}
    39  
    40  	ch := &ClickHouse{
    41  		conn:   conn,
    42  		config: config,
    43  	}
    44  
    45  	if err := ch.init(); err != nil {
    46  		return nil, err
    47  	}
    48  
    49  	return ch, nil
    50  }
    51  
    52  type ClickHouse struct {
    53  	conn   *sql.DB
    54  	config *Config
    55  }
    56  
    57  func (ch *ClickHouse) Open(dsn string) (database.Driver, error) {
    58  	purl, err := url.Parse(dsn)
    59  	if err != nil {
    60  		return nil, err
    61  	}
    62  	q := migrate.FilterCustomQuery(purl)
    63  	q.Scheme = "tcp"
    64  	conn, err := sql.Open("clickhouse", q.String())
    65  	if err != nil {
    66  		return nil, err
    67  	}
    68  
    69  	ch = &ClickHouse{
    70  		conn: conn,
    71  		config: &Config{
    72  			MigrationsTable:       purl.Query().Get("x-migrations-table"),
    73  			DatabaseName:          purl.Query().Get("database"),
    74  			MultiStatementEnabled: purl.Query().Get("x-multi-statement") == "true",
    75  		},
    76  	}
    77  
    78  	if err := ch.init(); err != nil {
    79  		return nil, err
    80  	}
    81  
    82  	return ch, nil
    83  }
    84  
    85  func (ch *ClickHouse) init() error {
    86  	if len(ch.config.DatabaseName) == 0 {
    87  		if err := ch.conn.QueryRow("SELECT currentDatabase()").Scan(&ch.config.DatabaseName); err != nil {
    88  			return err
    89  		}
    90  	}
    91  
    92  	if len(ch.config.MigrationsTable) == 0 {
    93  		ch.config.MigrationsTable = DefaultMigrationsTable
    94  	}
    95  
    96  	return ch.ensureVersionTable()
    97  }
    98  
    99  func (ch *ClickHouse) Run(r io.Reader) error {
   100  	migration, err := ioutil.ReadAll(r)
   101  	if err != nil {
   102  		return err
   103  	}
   104  
   105  	if ch.config.MultiStatementEnabled {
   106  		// split query by semi-colon
   107  		queries := strings.Split(string(migration), ";")
   108  		for _, q := range queries {
   109  			tq := strings.TrimSpace(q)
   110  			if tq == "" {
   111  				continue
   112  			}
   113  			if _, err := ch.conn.Exec(q); err != nil {
   114  				return database.Error{OrigErr: err, Err: "migration failed", Query: []byte(q)}
   115  			}
   116  		}
   117  		return nil
   118  	}
   119  
   120  	if _, err := ch.conn.Exec(string(migration)); err != nil {
   121  		return database.Error{OrigErr: err, Err: "migration failed", Query: migration}
   122  	}
   123  
   124  	return nil
   125  }
   126  func (ch *ClickHouse) Version() (int, bool, error) {
   127  	var (
   128  		version int
   129  		dirty   uint8
   130  		query   = "SELECT version, dirty FROM `" + ch.config.MigrationsTable + "` ORDER BY sequence DESC LIMIT 1"
   131  	)
   132  	if err := ch.conn.QueryRow(query).Scan(&version, &dirty); err != nil {
   133  		if err == sql.ErrNoRows {
   134  			return database.NilVersion, false, nil
   135  		}
   136  		return 0, false, &database.Error{OrigErr: err, Query: []byte(query)}
   137  	}
   138  	return version, dirty == 1, nil
   139  }
   140  
   141  func (ch *ClickHouse) SetVersion(version int, dirty bool) error {
   142  	var (
   143  		bool = func(v bool) uint8 {
   144  			if v {
   145  				return 1
   146  			}
   147  			return 0
   148  		}
   149  		tx, err = ch.conn.Begin()
   150  	)
   151  	if err != nil {
   152  		return err
   153  	}
   154  
   155  	query := "INSERT INTO " + ch.config.MigrationsTable + " (version, dirty, sequence) VALUES (?, ?, ?)"
   156  	if _, err := tx.Exec(query, version, bool(dirty), time.Now().UnixNano()); err != nil {
   157  		return &database.Error{OrigErr: err, Query: []byte(query)}
   158  	}
   159  
   160  	return tx.Commit()
   161  }
   162  
   163  // ensureVersionTable checks if versions table exists and, if not, creates it.
   164  // Note that this function locks the database, which deviates from the usual
   165  // convention of "caller locks" in the ClickHouse type.
   166  func (ch *ClickHouse) ensureVersionTable() (err error) {
   167  	if err = ch.Lock(); err != nil {
   168  		return err
   169  	}
   170  
   171  	defer func() {
   172  		if e := ch.Unlock(); e != nil {
   173  			if err == nil {
   174  				err = e
   175  			} else {
   176  				err = multierror.Append(err, e)
   177  			}
   178  		}
   179  	}()
   180  
   181  	var (
   182  		table string
   183  		query = "SHOW TABLES FROM " + ch.config.DatabaseName + " LIKE '" + ch.config.MigrationsTable + "'"
   184  	)
   185  	// check if migration table exists
   186  	if err := ch.conn.QueryRow(query).Scan(&table); err != nil {
   187  		if err != sql.ErrNoRows {
   188  			return &database.Error{OrigErr: err, Query: []byte(query)}
   189  		}
   190  	} else {
   191  		return nil
   192  	}
   193  	// if not, create the empty migration table
   194  	query = `
   195  		CREATE TABLE ` + ch.config.MigrationsTable + ` (
   196  			version    Int64, 
   197  			dirty      UInt8,
   198  			sequence   UInt64
   199  		) Engine=TinyLog
   200  	`
   201  	if _, err := ch.conn.Exec(query); err != nil {
   202  		return &database.Error{OrigErr: err, Query: []byte(query)}
   203  	}
   204  	return nil
   205  }
   206  
   207  func (ch *ClickHouse) Drop() (err error) {
   208  	query := "SHOW TABLES FROM " + ch.config.DatabaseName
   209  	tables, err := ch.conn.Query(query)
   210  
   211  	if err != nil {
   212  		return &database.Error{OrigErr: err, Query: []byte(query)}
   213  	}
   214  	defer func() {
   215  		if errClose := tables.Close(); errClose != nil {
   216  			err = multierror.Append(err, errClose)
   217  		}
   218  	}()
   219  	for tables.Next() {
   220  		var table string
   221  		if err := tables.Scan(&table); err != nil {
   222  			return err
   223  		}
   224  
   225  		query = "DROP TABLE IF EXISTS " + ch.config.DatabaseName + "." + table
   226  
   227  		if _, err := ch.conn.Exec(query); err != nil {
   228  			return &database.Error{OrigErr: err, Query: []byte(query)}
   229  		}
   230  	}
   231  	return nil
   232  }
   233  
   234  func (ch *ClickHouse) Lock() error   { return nil }
   235  func (ch *ClickHouse) Unlock() error { return nil }
   236  func (ch *ClickHouse) Close() error  { return ch.conn.Close() }