github.com/fr-nvriep/migrate/v4@v4.3.2/database/ql/ql.go (about) 1 package ql 2 3 import ( 4 "database/sql" 5 "fmt" 6 "github.com/hashicorp/go-multierror" 7 "io" 8 "io/ioutil" 9 "strings" 10 11 nurl "net/url" 12 13 _ "github.com/cznic/ql/driver" 14 "github.com/fr-nvriep/migrate/v4" 15 "github.com/fr-nvriep/migrate/v4/database" 16 ) 17 18 func init() { 19 database.Register("ql", &Ql{}) 20 } 21 22 var DefaultMigrationsTable = "schema_migrations" 23 var ( 24 ErrDatabaseDirty = fmt.Errorf("database is dirty") 25 ErrNilConfig = fmt.Errorf("no config") 26 ErrNoDatabaseName = fmt.Errorf("no database name") 27 ErrAppendPEM = fmt.Errorf("failed to append PEM") 28 ) 29 30 type Config struct { 31 MigrationsTable string 32 DatabaseName string 33 } 34 35 type Ql struct { 36 db *sql.DB 37 isLocked bool 38 39 config *Config 40 } 41 42 func WithInstance(instance *sql.DB, config *Config) (database.Driver, error) { 43 if config == nil { 44 return nil, ErrNilConfig 45 } 46 47 if err := instance.Ping(); err != nil { 48 return nil, err 49 } 50 51 if len(config.MigrationsTable) == 0 { 52 config.MigrationsTable = DefaultMigrationsTable 53 } 54 55 mx := &Ql{ 56 db: instance, 57 config: config, 58 } 59 if err := mx.ensureVersionTable(); err != nil { 60 return nil, err 61 } 62 return mx, nil 63 } 64 65 // ensureVersionTable checks if versions table exists and, if not, creates it. 66 // Note that this function locks the database, which deviates from the usual 67 // convention of "caller locks" in the Ql type. 68 func (m *Ql) ensureVersionTable() (err error) { 69 if err = m.Lock(); err != nil { 70 return err 71 } 72 73 defer func() { 74 if e := m.Unlock(); e != nil { 75 if err == nil { 76 err = e 77 } else { 78 err = multierror.Append(err, e) 79 } 80 } 81 }() 82 83 tx, err := m.db.Begin() 84 if err != nil { 85 return err 86 } 87 if _, err := tx.Exec(fmt.Sprintf(` 88 CREATE TABLE IF NOT EXISTS %s (version uint64,dirty bool); 89 CREATE UNIQUE INDEX IF NOT EXISTS version_unique ON %s (version); 90 `, m.config.MigrationsTable, m.config.MigrationsTable)); err != nil { 91 if err := tx.Rollback(); err != nil { 92 return err 93 } 94 return err 95 } 96 if err := tx.Commit(); err != nil { 97 return err 98 } 99 return nil 100 } 101 102 func (m *Ql) Open(url string) (database.Driver, error) { 103 purl, err := nurl.Parse(url) 104 if err != nil { 105 return nil, err 106 } 107 dbfile := strings.Replace(migrate.FilterCustomQuery(purl).String(), "ql://", "", 1) 108 db, err := sql.Open("ql", dbfile) 109 if err != nil { 110 return nil, err 111 } 112 migrationsTable := purl.Query().Get("x-migrations-table") 113 if len(migrationsTable) == 0 { 114 migrationsTable = DefaultMigrationsTable 115 } 116 mx, err := WithInstance(db, &Config{ 117 DatabaseName: purl.Path, 118 MigrationsTable: migrationsTable, 119 }) 120 if err != nil { 121 return nil, err 122 } 123 return mx, nil 124 } 125 func (m *Ql) Close() error { 126 return m.db.Close() 127 } 128 func (m *Ql) Drop() (err error) { 129 query := `SELECT Name FROM __Table` 130 tables, err := m.db.Query(query) 131 if err != nil { 132 return &database.Error{OrigErr: err, Query: []byte(query)} 133 } 134 defer func() { 135 if errClose := tables.Close(); errClose != nil { 136 err = multierror.Append(err, errClose) 137 } 138 }() 139 tableNames := make([]string, 0) 140 for tables.Next() { 141 var tableName string 142 if err := tables.Scan(&tableName); err != nil { 143 return err 144 } 145 if len(tableName) > 0 { 146 if !strings.HasPrefix(tableName, "__") { 147 tableNames = append(tableNames, tableName) 148 } 149 } 150 } 151 if len(tableNames) > 0 { 152 for _, t := range tableNames { 153 query := "DROP TABLE " + t 154 err = m.executeQuery(query) 155 if err != nil { 156 return &database.Error{OrigErr: err, Query: []byte(query)} 157 } 158 } 159 } 160 161 return nil 162 } 163 func (m *Ql) Lock() error { 164 if m.isLocked { 165 return database.ErrLocked 166 } 167 m.isLocked = true 168 return nil 169 } 170 func (m *Ql) Unlock() error { 171 if !m.isLocked { 172 return nil 173 } 174 m.isLocked = false 175 return nil 176 } 177 func (m *Ql) Run(migration io.Reader) error { 178 migr, err := ioutil.ReadAll(migration) 179 if err != nil { 180 return err 181 } 182 query := string(migr[:]) 183 184 return m.executeQuery(query) 185 } 186 func (m *Ql) executeQuery(query string) error { 187 tx, err := m.db.Begin() 188 if err != nil { 189 return &database.Error{OrigErr: err, Err: "transaction start failed"} 190 } 191 if _, err := tx.Exec(query); err != nil { 192 if errRollback := tx.Rollback(); errRollback != nil { 193 err = multierror.Append(err, errRollback) 194 } 195 return &database.Error{OrigErr: err, Query: []byte(query)} 196 } 197 if err := tx.Commit(); err != nil { 198 return &database.Error{OrigErr: err, Err: "transaction commit failed"} 199 } 200 return nil 201 } 202 func (m *Ql) SetVersion(version int, dirty bool) error { 203 tx, err := m.db.Begin() 204 if err != nil { 205 return &database.Error{OrigErr: err, Err: "transaction start failed"} 206 } 207 208 query := "TRUNCATE TABLE " + m.config.MigrationsTable 209 if _, err := tx.Exec(query); err != nil { 210 return &database.Error{OrigErr: err, Query: []byte(query)} 211 } 212 213 if version >= 0 { 214 query := fmt.Sprintf(`INSERT INTO %s (version, dirty) VALUES (%d, %t)`, m.config.MigrationsTable, version, dirty) 215 if _, err := tx.Exec(query); err != nil { 216 if errRollback := tx.Rollback(); errRollback != nil { 217 err = multierror.Append(err, errRollback) 218 } 219 return &database.Error{OrigErr: err, Query: []byte(query)} 220 } 221 } 222 223 if err := tx.Commit(); err != nil { 224 return &database.Error{OrigErr: err, Err: "transaction commit failed"} 225 } 226 227 return nil 228 } 229 230 func (m *Ql) Version() (version int, dirty bool, err error) { 231 query := "SELECT version, dirty FROM " + m.config.MigrationsTable + " LIMIT 1" 232 err = m.db.QueryRow(query).Scan(&version, &dirty) 233 if err != nil { 234 return database.NilVersion, false, nil 235 } 236 return version, dirty, nil 237 }