Skip to content

Commit d434f0d

Browse files
authored
track db_names for mysql and singlestore; breaking change for singlestore (#63)
* track db_names for mysql and singlestore; breaking change for singlestore * lint
1 parent 3d48dae commit d434f0d

File tree

4 files changed

+112
-32
lines changed

4 files changed

+112
-32
lines changed

lsmysql/mysql.go

Lines changed: 77 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
// * uses /* -- and # for comments
2323
// * supports advisory locks
2424
// * has quoting modes (ANSI_QUOTES)
25+
// * can use ALTER TABLE to modify the primary key of a table
2526
//
2627
// Because mysql DDL commands cause transactions to autocommit, tracking the schema changes in
2728
// a secondary table (like libschema does) is inherently unsafe. The MySQL driver will
@@ -43,9 +44,9 @@ type MySQL struct {
4344
lockTx *sql.Tx
4445
lockStr string
4546
db *sql.DB
46-
databaseName string // used in skip.go only
47+
databaseName string
4748
lock sync.Mutex
48-
trackingSchemaTable func(*libschema.Database) (string, string, error)
49+
trackingSchemaTable func(*libschema.Database) (string, string, string, error)
4950
skipDatabase bool
5051
}
5152

@@ -219,7 +220,7 @@ func (p *MySQL) DoOneMigration(ctx context.Context, log *internal.Log, d *libsch
219220
// called internally which means that is safe to override
220221
// in types that embed MySQL.
221222
func (p *MySQL) CreateSchemaTableIfNotExists(ctx context.Context, _ *internal.Log, d *libschema.Database) error {
222-
schema, tableName, err := p.trackingSchemaTable(d)
223+
schema, tableName, _, err := p.trackingSchemaTable(d)
223224
if err != nil {
224225
return err
225226
}
@@ -248,7 +249,10 @@ func (p *MySQL) CreateSchemaTableIfNotExists(ctx context.Context, _ *internal.Lo
248249

249250
var simpleIdentifierRE = regexp.MustCompile(`\A[A-Za-z][A-Za-z0-9_]*\z`)
250251

251-
func WithTrackingTableQuoter(f func(*libschema.Database) (schemaName string, tableName string, err error)) MySQLOpt {
252+
// WithTrackingTableQuoter is a somewhat internal function -- used by lssinglestore.
253+
// It replaces the private function that takes apart the name of the tracking
254+
// table and provides the components.
255+
func WithTrackingTableQuoter(f func(*libschema.Database) (schemaName, tableName, simpleTableName string, err error)) MySQLOpt {
252256
return func(p *MySQL) {
253257
p.trackingSchemaTable = f
254258
}
@@ -259,34 +263,34 @@ func WithTrackingTableQuoter(f func(*libschema.Database) (schemaName string, tab
259263
// mode, you could have a table called `table` (eg: `CREATE TABLE "table"`) but
260264
// if you're not in ANSI_QUOTES mode then you cannot. We're going to assume
261265
// that we're not in ANSI_QUOTES mode because we cannot assume that we are.
262-
func trackingSchemaTable(d *libschema.Database) (string, string, error) {
266+
func trackingSchemaTable(d *libschema.Database) (string, string, string, error) {
263267
tableName := d.Options.TrackingTable
264268
s := strings.Split(tableName, ".")
265269
switch len(s) {
266270
case 2:
267271
schema := s[0]
268272
if !simpleIdentifierRE.MatchString(schema) {
269-
return "", "", errors.Errorf("Tracking table schema name must be a simple identifier, not '%s'", schema)
273+
return "", "", "", errors.Errorf("Tracking table schema name must be a simple identifier, not '%s'", schema)
270274
}
271275
table := s[1]
272276
if !simpleIdentifierRE.MatchString(table) {
273-
return "", "", errors.Errorf("Tracking table table name must be a simple identifier, not '%s'", table)
277+
return "", "", "", errors.Errorf("Tracking table table name must be a simple identifier, not '%s'", table)
274278
}
275-
return schema, schema + "." + table, nil
279+
return schema, schema + "." + table, table, nil
276280
case 1:
277281
if !simpleIdentifierRE.MatchString(tableName) {
278-
return "", "", errors.Errorf("Tracking table table name must be a simple identifier, not '%s'", tableName)
282+
return "", "", "", errors.Errorf("Tracking table table name must be a simple identifier, not '%s'", tableName)
279283
}
280-
return "", tableName, nil
284+
return "", tableName, tableName, nil
281285
default:
282-
return "", "", errors.Errorf("Tracking table '%s' is not valid", tableName)
286+
return "", "", "", errors.Errorf("Tracking table '%s' is not valid", tableName)
283287
}
284288
}
285289

286290
// trackingTable returns the schema+table reference for the migration tracking table.
287291
// The name is already quoted properly for use as a save mysql identifier.
288292
func (p *MySQL) trackingTable(d *libschema.Database) string {
289-
_, table, _ := p.trackingSchemaTable(d)
293+
_, table, _, _ := p.trackingSchemaTable(d)
290294
return table
291295
}
292296

@@ -301,9 +305,9 @@ func (p *MySQL) saveStatus(log *internal.Log, tx *sql.Tx, d *libschema.Database,
301305
"error": migrationError,
302306
})
303307
q := fmt.Sprintf(`
304-
REPLACE INTO %s (library, migration, done, error, updated_at)
305-
VALUES (?, ?, ?, ?, now())`, p.trackingTable(d))
306-
_, err := tx.Exec(q, m.Base().Name.Library, m.Base().Name.Name, done, estr)
308+
REPLACE INTO %s (db_name, library, migration, done, error, updated_at)
309+
VALUES (?, ?, ?, ?, ?, now())`, p.trackingTable(d))
310+
_, err := tx.Exec(q, p.databaseName, m.Base().Name.Library, m.Base().Name.Name, done, estr)
307311
if err != nil {
308312
return errors.Wrapf(err, "Save status for %s", m.Base().Name)
309313
}
@@ -321,14 +325,15 @@ func (p *MySQL) saveStatus(log *internal.Log, tx *sql.Tx, d *libschema.Database,
321325
// does not release the lock. We'll use a transaction just to make sure that
322326
// we're using the same connection. If LockMigrationsTable succeeds, be sure to
323327
// call UnlockMigrationsTable.
324-
func (p *MySQL) LockMigrationsTable(ctx context.Context, _ *internal.Log, d *libschema.Database) error {
325-
// LockMigrationsTable is overridden for SingleStore
326-
p.lock.Lock()
327-
defer p.lock.Unlock()
328-
_, tableName, err := p.trackingSchemaTable(d)
328+
func (p *MySQL) LockMigrationsTable(ctx context.Context, _ *internal.Log, d *libschema.Database) (finalErr error) {
329+
schema, tableName, simpleTableName, err := p.trackingSchemaTable(d)
329330
if err != nil {
330331
return err
331332
}
333+
334+
// LockMigrationsTable is overridden for SingleStore
335+
p.lock.Lock()
336+
defer p.lock.Unlock()
332337
if p.lockTx != nil {
333338
return errors.Errorf("libschema migrations table, '%s' already locked", tableName)
334339
}
@@ -343,6 +348,56 @@ func (p *MySQL) LockMigrationsTable(ctx context.Context, _ *internal.Log, d *lib
343348
return errors.Wrapf(err, "Could not get lock for libschema migrations")
344349
}
345350
p.lockTx = tx
351+
352+
// This moment, after getting an exclusive lock on the migrations table, is
353+
// the right moment to do any schema upgrades of the migrations table.
354+
355+
defer func() {
356+
if finalErr != nil {
357+
_, _ = tx.Exec(`SELECT RELEASE_LOCK(?)`, p.lockStr)
358+
_ = tx.Rollback()
359+
p.lockTx = nil
360+
}
361+
}()
362+
363+
currentDatabaseValue := p.databaseName
364+
defer func() {
365+
p.databaseName = currentDatabaseValue
366+
}()
367+
p.databaseName = schema
368+
369+
ok, err := p.DoesColumnExist(simpleTableName, "db_name")
370+
if err != nil {
371+
return errors.Wrapf(err, "could not check if %s has a db_name column", tableName)
372+
}
373+
if !ok {
374+
_, err = d.DB().ExecContext(ctx, fmt.Sprintf(`
375+
ALTER TABLE %s
376+
ADD COLUMN db_name varchar(255)`, tableName))
377+
if err != nil {
378+
return errors.Wrapf(err, "could not add db_name column to %s", tableName)
379+
}
380+
}
381+
ok, err = p.ColumnIsInPrimaryKey(simpleTableName, "db_name")
382+
if err != nil {
383+
return errors.Wrapf(err, "could not check if %s.db_name column is in the primary key", tableName)
384+
}
385+
if ok {
386+
return nil
387+
}
388+
_, err = d.DB().ExecContext(ctx, fmt.Sprintf(`
389+
UPDATE %s
390+
SET db_name = ?
391+
WHERE db_name IS NULL`, tableName), p.databaseName)
392+
if err != nil {
393+
return errors.Wrapf(err, "could not set %s.db_name column", tableName)
394+
}
395+
_, err = d.DB().ExecContext(ctx, fmt.Sprintf(`
396+
ALTER TABLE %s
397+
DROP PRIMARY KEY, ADD PRIMARY KEY (db_name, library, migration)`, tableName))
398+
if err != nil {
399+
return errors.Wrapf(err, "could change primary key for %s", tableName)
400+
}
346401
return nil
347402
}
348403

@@ -379,7 +434,8 @@ func (p *MySQL) LoadStatus(ctx context.Context, _ *internal.Log, d *libschema.Da
379434
tableName := p.trackingTable(d)
380435
rows, err := d.DB().QueryContext(ctx, fmt.Sprintf(`
381436
SELECT library, migration, done
382-
FROM %s`, tableName))
437+
FROM %s
438+
WHERE db_name = ?`, tableName), p.databaseName)
383439
if err != nil {
384440
return nil, errors.Wrap(err, "Cannot query migration status")
385441
}

lsmysql/skip.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,25 @@ func (p *MySQL) HasPrimaryKey(table string) (bool, error) {
4343
return count != 0, errors.Wrapf(err, "has primary key %s.%s", database, table)
4444
}
4545

46+
// ColumnIsInPrimaryKey returns true if the column part of the prmary key.
47+
// The table is assumed to be in the current database unless m.UseDatabase() has been called.
48+
func (p *MySQL) ColumnIsInPrimaryKey(table string, column string) (bool, error) {
49+
database, err := p.DatabaseName()
50+
if err != nil {
51+
return false, err
52+
}
53+
var count int
54+
err = p.db.QueryRow(`
55+
SELECT COUNT(*)
56+
FROM information_schema.columns
57+
WHERE table_schema = ?
58+
AND table_name = ?
59+
AND column_name = ?
60+
AND column_key = 'PRI'`,
61+
database, table, column).Scan(&count)
62+
return count != 0, errors.Wrapf(err, "column is in primary key %s.%s.%s", database, table, column)
63+
}
64+
4665
// TableHasIndex returns true if there is an index matching the
4766
// name given.
4867
// The table is assumed to be in the current database unless m.UseDatabase() has been called.

lssinglestore/singlestore.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ func (p *SingleStore) LockMigrationsTable(ctx context.Context, _ *internal.Log,
9595
if p.lockTx != nil {
9696
return errors.Errorf("migrations already locked")
9797
}
98-
_, tableName, err := trackingSchemaTable(d)
98+
_, tableName, _, err := trackingSchemaTable(d)
9999
if err != nil {
100100
return err
101101
}
@@ -152,34 +152,34 @@ func makeID(raw string) (string, error) {
152152
}
153153
}
154154

155-
func trackingSchemaTable(d *libschema.Database) (string, string, error) {
155+
func trackingSchemaTable(d *libschema.Database) (string, string, string, error) {
156156
tableName := d.Options.TrackingTable
157157
s := strings.Split(tableName, ".")
158158
switch len(s) {
159159
case 2:
160160
schema, err := makeID(s[0])
161161
if err != nil {
162-
return "", "", errors.Wrap(err, "cannot make tracking table schema name")
162+
return "", "", "", errors.Wrap(err, "cannot make tracking table schema name")
163163
}
164164
table, err := makeID(s[1])
165165
if err != nil {
166-
return "", "", errors.Wrap(err, "cannot make tracking table table name")
166+
return "", "", "", errors.Wrap(err, "cannot make tracking table table name")
167167
}
168-
return schema, schema + "." + table, nil
168+
return schema, schema + "." + table, table, nil
169169
case 1:
170170
table, err := makeID(tableName)
171171
if err != nil {
172-
return "", "", errors.Wrap(err, "cannot make tracking table table name")
172+
return "", "", "", errors.Wrap(err, "cannot make tracking table table name")
173173
}
174-
return "", table, nil
174+
return "", table, table, nil
175175
default:
176-
return "", "", errors.Errorf("tracking table '%s' is not valid", tableName)
176+
return "", "", "", errors.Errorf("tracking table '%s' is not valid", tableName)
177177
}
178178
}
179179

180180
// CreateSchemaTableIfNotExists creates the migration tracking table for libschema.
181181
func (p *SingleStore) CreateSchemaTableIfNotExists(ctx context.Context, _ *internal.Log, d *libschema.Database) error {
182-
schema, tableName, err := trackingSchemaTable(d)
182+
schema, tableName, _, err := trackingSchemaTable(d)
183183
if err != nil {
184184
return err
185185
}
@@ -193,14 +193,15 @@ func (p *SingleStore) CreateSchemaTableIfNotExists(ctx context.Context, _ *inter
193193
}
194194
_, err = d.DB().ExecContext(ctx, fmt.Sprintf(`
195195
CREATE TABLE IF NOT EXISTS %s (
196+
db_name varchar(255) NOT NULL,
196197
library varchar(255) NOT NULL,
197198
migration varchar(255) NOT NULL,
198199
done boolean NOT NULL,
199200
error text NOT NULL,
200201
updated_at timestamp DEFAULT now(),
201202
SORT KEY (library, migration),
202203
SHARD KEY (library, migration),
203-
PRIMARY KEY (library, migration)
204+
PRIMARY KEY (db_name, library, migration)
204205
)`, tableName))
205206
if err != nil {
206207
return errors.Wrapf(err, "Could not create libschema migrations table '%s'", tableName)

lssinglestore/unit_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ func TestTrackingSchemaTable(t *testing.T) {
1717
tt string
1818
err bool
1919
schema string
20+
simple string
2021
table string
2122
}{
2223
{
2324
tt: "`foo`.xk-z",
2425
schema: "`foo`",
2526
table: "`foo`.`xk-z`",
27+
simple: "`xk-z`",
2628
},
2729
{
2830
tt: "`foo.xk-z",
@@ -32,6 +34,7 @@ func TestTrackingSchemaTable(t *testing.T) {
3234
tt: "foo",
3335
schema: "",
3436
table: "foo",
37+
simple: "foo",
3538
},
3639
{
3740
tt: "x.y.z",
@@ -50,13 +53,14 @@ func TestTrackingSchemaTable(t *testing.T) {
5053
TrackingTable: tc.tt,
5154
},
5255
}
53-
schema, table, err := trackingSchemaTable(d)
56+
schema, table, simple, err := trackingSchemaTable(d)
5457
if tc.err {
5558
assert.Error(t, err)
5659
} else {
5760
if assert.NoError(t, err) {
5861
assert.Equal(t, tc.schema, schema, "schema")
5962
assert.Equal(t, tc.table, table, "table")
63+
assert.Equal(t, tc.simple, simple, "simpleTable")
6064
}
6165
}
6266
})

0 commit comments

Comments
 (0)