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