github.com/nokia/migrate/v4@v4.16.0/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  	"strconv"
    10  	"strings"
    11  	"time"
    12  
    13  	"go.uber.org/atomic"
    14  
    15  	"github.com/hashicorp/go-multierror"
    16  	"github.com/nokia/migrate/v4"
    17  	"github.com/nokia/migrate/v4/database"
    18  	"github.com/nokia/migrate/v4/database/multistmt"
    19  	"github.com/nokia/migrate/v4/source"
    20  )
    21  
    22  var (
    23  	multiStmtDelimiter = []byte(";")
    24  
    25  	DefaultMigrationsTable       = "schema_migrations"
    26  	DefaultMigrationsTableEngine = "TinyLog"
    27  	DefaultMultiStatementMaxSize = 10 * 1 << 20 // 10 MB
    28  
    29  	ErrNilConfig = fmt.Errorf("no config")
    30  )
    31  
    32  type Config struct {
    33  	DatabaseName          string
    34  	ClusterName           string
    35  	MigrationsTable       string
    36  	MigrationsTableEngine string
    37  	MultiStatementEnabled bool
    38  	MultiStatementMaxSize int
    39  }
    40  
    41  func init() {
    42  	database.Register("clickhouse", &ClickHouse{})
    43  }
    44  
    45  func WithInstance(conn *sql.DB, config *Config) (database.Driver, error) {
    46  	if config == nil {
    47  		return nil, ErrNilConfig
    48  	}
    49  
    50  	if err := conn.Ping(); err != nil {
    51  		return nil, err
    52  	}
    53  
    54  	ch := &ClickHouse{
    55  		conn:   conn,
    56  		config: config,
    57  	}
    58  
    59  	if err := ch.init(); err != nil {
    60  		return nil, err
    61  	}
    62  
    63  	return ch, nil
    64  }
    65  
    66  type ClickHouse struct {
    67  	conn     *sql.DB
    68  	config   *Config
    69  	isLocked atomic.Bool
    70  }
    71  
    72  func (ch *ClickHouse) Open(dsn string) (database.Driver, error) {
    73  	purl, err := url.Parse(dsn)
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  	q := migrate.FilterCustomQuery(purl)
    78  	q.Scheme = "tcp"
    79  	conn, err := sql.Open("clickhouse", q.String())
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  
    84  	multiStatementMaxSize := DefaultMultiStatementMaxSize
    85  	if s := purl.Query().Get("x-multi-statement-max-size"); len(s) > 0 {
    86  		multiStatementMaxSize, err = strconv.Atoi(s)
    87  		if err != nil {
    88  			return nil, err
    89  		}
    90  	}
    91  
    92  	migrationsTableEngine := DefaultMigrationsTableEngine
    93  	if s := purl.Query().Get("x-migrations-table-engine"); len(s) > 0 {
    94  		migrationsTableEngine = s
    95  	}
    96  
    97  	ch = &ClickHouse{
    98  		conn: conn,
    99  		config: &Config{
   100  			MigrationsTable:       purl.Query().Get("x-migrations-table"),
   101  			MigrationsTableEngine: migrationsTableEngine,
   102  			DatabaseName:          purl.Query().Get("database"),
   103  			ClusterName:           purl.Query().Get("x-cluster-name"),
   104  			MultiStatementEnabled: purl.Query().Get("x-multi-statement") == "true",
   105  			MultiStatementMaxSize: multiStatementMaxSize,
   106  		},
   107  	}
   108  
   109  	if err := ch.init(); err != nil {
   110  		return nil, err
   111  	}
   112  
   113  	return ch, nil
   114  }
   115  
   116  func (ch *ClickHouse) init() error {
   117  	if len(ch.config.DatabaseName) == 0 {
   118  		if err := ch.conn.QueryRow("SELECT currentDatabase()").Scan(&ch.config.DatabaseName); err != nil {
   119  			return err
   120  		}
   121  	}
   122  
   123  	if len(ch.config.MigrationsTable) == 0 {
   124  		ch.config.MigrationsTable = DefaultMigrationsTable
   125  	}
   126  
   127  	if ch.config.MultiStatementMaxSize <= 0 {
   128  		ch.config.MultiStatementMaxSize = DefaultMultiStatementMaxSize
   129  	}
   130  
   131  	if len(ch.config.MigrationsTableEngine) == 0 {
   132  		ch.config.MigrationsTableEngine = DefaultMigrationsTableEngine
   133  	}
   134  
   135  	return ch.ensureVersionTable()
   136  }
   137  
   138  func (ch *ClickHouse) Run(r io.Reader) error {
   139  	if ch.config.MultiStatementEnabled {
   140  		var err error
   141  		if e := multistmt.Parse(r, multiStmtDelimiter, ch.config.MultiStatementMaxSize, func(m []byte) bool {
   142  			tq := strings.TrimSpace(string(m))
   143  			if tq == "" {
   144  				return true
   145  			}
   146  			if _, e := ch.conn.Exec(string(m)); e != nil {
   147  				err = database.Error{OrigErr: e, Err: "migration failed", Query: m}
   148  				return false
   149  			}
   150  			return true
   151  		}); e != nil {
   152  			return e
   153  		}
   154  		return err
   155  	}
   156  
   157  	migration, err := ioutil.ReadAll(r)
   158  	if err != nil {
   159  		return err
   160  	}
   161  
   162  	if _, err := ch.conn.Exec(string(migration)); err != nil {
   163  		return database.Error{OrigErr: err, Err: "migration failed", Query: migration}
   164  	}
   165  
   166  	return nil
   167  }
   168  
   169  func (ch *ClickHouse) RunFunctionMigration(fn source.MigrationFunc) error {
   170  	return database.ErrNotImpl
   171  }
   172  
   173  func (ch *ClickHouse) Version() (int, bool, error) {
   174  	var (
   175  		version int
   176  		dirty   uint8
   177  		query   = "SELECT version, dirty FROM `" + ch.config.MigrationsTable + "` ORDER BY sequence DESC LIMIT 1"
   178  	)
   179  	if err := ch.conn.QueryRow(query).Scan(&version, &dirty); err != nil {
   180  		if err == sql.ErrNoRows {
   181  			return database.NilVersion, false, nil
   182  		}
   183  		return 0, false, &database.Error{OrigErr: err, Query: []byte(query)}
   184  	}
   185  	return version, dirty == 1, nil
   186  }
   187  
   188  func (ch *ClickHouse) SetVersion(version int, dirty bool) error {
   189  	var (
   190  		bool = func(v bool) uint8 {
   191  			if v {
   192  				return 1
   193  			}
   194  			return 0
   195  		}
   196  		tx, err = ch.conn.Begin()
   197  	)
   198  	if err != nil {
   199  		return err
   200  	}
   201  
   202  	query := "INSERT INTO " + ch.config.MigrationsTable + " (version, dirty, sequence) VALUES (?, ?, ?)"
   203  	if _, err := tx.Exec(query, version, bool(dirty), time.Now().UnixNano()); err != nil {
   204  		return &database.Error{OrigErr: err, Query: []byte(query)}
   205  	}
   206  
   207  	return tx.Commit()
   208  }
   209  
   210  // ensureVersionTable checks if versions table exists and, if not, creates it.
   211  // Note that this function locks the database, which deviates from the usual
   212  // convention of "caller locks" in the ClickHouse type.
   213  func (ch *ClickHouse) ensureVersionTable() (err error) {
   214  	if err = ch.Lock(); err != nil {
   215  		return err
   216  	}
   217  
   218  	defer func() {
   219  		if e := ch.Unlock(); e != nil {
   220  			if err == nil {
   221  				err = e
   222  			} else {
   223  				err = multierror.Append(err, e)
   224  			}
   225  		}
   226  	}()
   227  
   228  	var (
   229  		table string
   230  		query = "SHOW TABLES FROM " + ch.config.DatabaseName + " LIKE '" + ch.config.MigrationsTable + "'"
   231  	)
   232  	// check if migration table exists
   233  	if err := ch.conn.QueryRow(query).Scan(&table); err != nil {
   234  		if err != sql.ErrNoRows {
   235  			return &database.Error{OrigErr: err, Query: []byte(query)}
   236  		}
   237  	} else {
   238  		return nil
   239  	}
   240  
   241  	// if not, create the empty migration table
   242  	if len(ch.config.ClusterName) > 0 {
   243  		query = fmt.Sprintf(`
   244  			CREATE TABLE %s ON CLUSTER %s (
   245  				version    Int64,
   246  				dirty      UInt8,
   247  				sequence   UInt64
   248  			) Engine=%s`, ch.config.MigrationsTable, ch.config.ClusterName, ch.config.MigrationsTableEngine)
   249  	} else {
   250  		query = fmt.Sprintf(`
   251  			CREATE TABLE %s (
   252  				version    Int64,
   253  				dirty      UInt8,
   254  				sequence   UInt64
   255  			) Engine=%s`, ch.config.MigrationsTable, ch.config.MigrationsTableEngine)
   256  	}
   257  
   258  	if strings.HasSuffix(ch.config.MigrationsTableEngine, "Tree") {
   259  		query = fmt.Sprintf(`%s ORDER BY sequence`, query)
   260  	}
   261  
   262  	if _, err := ch.conn.Exec(query); err != nil {
   263  		return &database.Error{OrigErr: err, Query: []byte(query)}
   264  	}
   265  	return nil
   266  }
   267  
   268  func (ch *ClickHouse) Drop() (err error) {
   269  	query := "SHOW TABLES FROM " + ch.config.DatabaseName
   270  	tables, err := ch.conn.Query(query)
   271  	if err != nil {
   272  		return &database.Error{OrigErr: err, Query: []byte(query)}
   273  	}
   274  	defer func() {
   275  		if errClose := tables.Close(); errClose != nil {
   276  			err = multierror.Append(err, errClose)
   277  		}
   278  	}()
   279  
   280  	for tables.Next() {
   281  		var table string
   282  		if err := tables.Scan(&table); err != nil {
   283  			return err
   284  		}
   285  
   286  		query = "DROP TABLE IF EXISTS " + ch.config.DatabaseName + "." + table
   287  
   288  		if _, err := ch.conn.Exec(query); err != nil {
   289  			return &database.Error{OrigErr: err, Query: []byte(query)}
   290  		}
   291  	}
   292  	if err := tables.Err(); err != nil {
   293  		return &database.Error{OrigErr: err, Query: []byte(query)}
   294  	}
   295  
   296  	return nil
   297  }
   298  
   299  func (ch *ClickHouse) Lock() error {
   300  	if !ch.isLocked.CAS(false, true) {
   301  		return database.ErrLocked
   302  	}
   303  
   304  	return nil
   305  }
   306  
   307  func (ch *ClickHouse) Unlock() error {
   308  	if !ch.isLocked.CAS(true, false) {
   309  		return database.ErrNotLocked
   310  	}
   311  
   312  	return nil
   313  }
   314  func (ch *ClickHouse) Close() error { return ch.conn.Close() }