diff --git a/drivers/mysql/metadata.go b/drivers/mysql/metadata.go index b5356877..f195a188 100644 --- a/drivers/mysql/metadata.go +++ b/drivers/mysql/metadata.go @@ -377,7 +377,7 @@ ORDER BY c.TABLE_NAME ASC, c.ORDINAL_POSITION ASC` tblMetas = append(tblMetas, curTblMeta) - rowCountTbl, rowCount, i := curTblName.String, &curTblMeta.RowCount, len(tblMetas) + rowCountTbl, rowCount, i := curTblName.String, &curTblMeta.RowCount, len(tblMetas)-1 gRowCount.Go(func() error { err := db.QueryRowContext(gctx, "SELECT COUNT(*) FROM `"+rowCountTbl+"`").Scan(rowCount) if err != nil { @@ -394,7 +394,6 @@ ORDER BY c.TABLE_NAME ASC, c.ORDINAL_POSITION ASC` } return nil }) - } col := &source.ColMetadata{ diff --git a/drivers/mysql/mysql.go b/drivers/mysql/mysql.go index 0bd7a23f..05fdd63d 100644 --- a/drivers/mysql/mysql.go +++ b/drivers/mysql/mysql.go @@ -102,6 +102,18 @@ func (d *driveri) CreateTable(ctx context.Context, db sqlz.DB, tblDef *sqlmodel. return errz.Err(err) } +// AlterTableAddColumn implements driver.SQLDriver. +func (d *driveri) AlterTableAddColumn(ctx context.Context, db *sql.DB, tbl string, col string, kind sqlz.Kind) error { + q := fmt.Sprintf("ALTER TABLE %q ADD COLUMN %q ", tbl, col) + dbTypeNameFromKind(kind) + + _, err := db.ExecContext(ctx, q) + if err != nil { + return errz.Wrapf(err, "alter table: failed to add column %q to table %q", col, tbl) + } + + return nil +} + // PrepareInsertStmt implements driver.SQLDriver. func (d *driveri) PrepareInsertStmt(ctx context.Context, db sqlz.DB, destTbl string, destColNames []string, numRows int) (*driver.StmtExecer, error) { destColsMeta, err := d.getTableRecordMeta(ctx, db, destTbl, destColNames) @@ -316,11 +328,6 @@ func (d *driveri) Truncate(ctx context.Context, src *source.Source, tbl string, return beforeCount, errz.Err(tx.Commit()) } -// AlterTableAddColumn implements driver.Driver. -func (d *driveri) AlterTableAddColumn(ctx context.Context, db sqlz.DB, tbl string, col string, kind sqlz.Kind, ordinal int) error { - return errz.New("not implemented") -} - // database implements driver.Database. type database struct { log lg.Log diff --git a/drivers/postgres/postgres.go b/drivers/postgres/postgres.go index c27cca32..5d08ad28 100644 --- a/drivers/postgres/postgres.go +++ b/drivers/postgres/postgres.go @@ -157,6 +157,18 @@ func (d *driveri) CreateTable(ctx context.Context, db sqlz.DB, tblDef *sqlmodel. return errz.Err(err) } +// AlterTableAddColumn implements driver.SQLDriver. +func (d *driveri) AlterTableAddColumn(ctx context.Context, db *sql.DB, tbl string, col string, kind sqlz.Kind) error { + q := fmt.Sprintf("ALTER TABLE %q ADD COLUMN %q ", tbl, col) + dbTypeNameFromKind(kind) + + _, err := db.ExecContext(ctx, q) + if err != nil { + return errz.Wrapf(err, "alter table: failed to add column %q to table %q", col, tbl) + } + + return nil +} + // PrepareInsertStmt implements driver.SQLDriver. func (d *driveri) PrepareInsertStmt(ctx context.Context, db sqlz.DB, destTbl string, destColNames []string, numRows int) (*driver.StmtExecer, error) { // Note that the pgx driver doesn't support res.LastInsertId. @@ -396,11 +408,6 @@ func (d *driveri) RecordMeta(colTypes []*sql.ColumnType) (sqlz.RecordMeta, drive return recMeta, mungeFn, nil } -// AlterTableAddColumn implements driver.Driver. -func (d *driveri) AlterTableAddColumn(ctx context.Context, db sqlz.DB, tbl string, col string, kind sqlz.Kind, ordinal int) error { - return errz.New("not implemented") -} - // database is the postgres implementation of driver.Database. type database struct { log lg.Log diff --git a/drivers/sqlite3/sqlite3.go b/drivers/sqlite3/sqlite3.go index a5e7a150..467aaf43 100644 --- a/drivers/sqlite3/sqlite3.go +++ b/drivers/sqlite3/sqlite3.go @@ -249,6 +249,18 @@ func (d *driveri) CreateTable(ctx context.Context, db sqlz.DB, tblDef *sqlmodel. return errz.Err(stmt.Close()) } +// AlterTableAddColumn implements driver.SQLDriver. +func (d *driveri) AlterTableAddColumn(ctx context.Context, db *sql.DB, tbl string, col string, kind sqlz.Kind) error { + q := fmt.Sprintf("ALTER TABLE %q ADD COLUMN %q ", tbl, col) + DBTypeForKind(kind) + + _, err := db.ExecContext(ctx, q) + if err != nil { + return errz.Wrapf(err, "alter table: failed to add column %q to table %q", col, tbl) + } + + return nil +} + // PrepareInsertStmt implements driver.SQLDriver. func (d *driveri) PrepareInsertStmt(ctx context.Context, db sqlz.DB, destTbl string, destColNames []string, numRows int) (*driver.StmtExecer, error) { destColsMeta, err := d.getTableRecordMeta(ctx, db, destTbl, destColNames) @@ -356,11 +368,6 @@ func (d *driveri) TableColumnTypes(ctx context.Context, db sqlz.DB, tblName stri return colTypes, nil } -// AlterTableAddColumn implements driver.Driver. -func (d *driveri) AlterTableAddColumn(ctx context.Context, db sqlz.DB, tbl string, col string, kind sqlz.Kind, ordinal int) error { - return errz.New("not implemented") -} - func (d *driveri) getTableRecordMeta(ctx context.Context, db sqlz.DB, tblName string, colNames []string) (sqlz.RecordMeta, error) { colTypes, err := d.TableColumnTypes(ctx, db, tblName, colNames) if err != nil { diff --git a/drivers/sqlite3/testdata/sakila.db b/drivers/sqlite3/testdata/sakila.db index d8c0d954..9e4cf655 100644 Binary files a/drivers/sqlite3/testdata/sakila.db and b/drivers/sqlite3/testdata/sakila.db differ diff --git a/drivers/sqlserver/sqlserver.go b/drivers/sqlserver/sqlserver.go index f23f1213..d0509cf5 100644 --- a/drivers/sqlserver/sqlserver.go +++ b/drivers/sqlserver/sqlserver.go @@ -248,6 +248,18 @@ func (d *driveri) CreateTable(ctx context.Context, db sqlz.DB, tblDef *sqlmodel. return errz.Err(err) } +// AlterTableAddColumn implements driver.SQLDriver. +func (d *driveri) AlterTableAddColumn(ctx context.Context, db *sql.DB, tbl string, col string, kind sqlz.Kind) error { + q := fmt.Sprintf("ALTER TABLE %q ADD %q ", tbl, col) + dbTypeNameFromKind(kind) + + _, err := db.ExecContext(ctx, q) + if err != nil { + return errz.Wrapf(err, "alter table: failed to add column %q to table %q", col, tbl) + } + + return nil +} + // CopyTable implements driver.SQLDriver. func (d *driveri) CopyTable(ctx context.Context, db sqlz.DB, fromTable, toTable string, copyData bool) (int64, error) { var stmt string @@ -317,11 +329,6 @@ func (d *driveri) PrepareUpdateStmt(ctx context.Context, db sqlz.DB, destTbl str return execer, nil } -// AlterTableAddColumn implements driver.Driver. -func (d *driveri) AlterTableAddColumn(ctx context.Context, db sqlz.DB, tbl string, col string, kind sqlz.Kind, ordinal int) error { - return errz.New("not implemented") -} - func (d *driveri) getTableColsMeta(ctx context.Context, db sqlz.DB, tblName string, colNames []string) (sqlz.RecordMeta, error) { // SQLServer has this unusual incantation for its LIMIT equivalent: // diff --git a/libsq/driver/driver.go b/libsq/driver/driver.go index bf6de115..63dae19a 100644 --- a/libsq/driver/driver.go +++ b/libsq/driver/driver.go @@ -143,8 +143,10 @@ type SQLDriver interface { // or equivalent clause is added, if supported. DropTable(ctx context.Context, db sqlz.DB, tbl string, ifExists bool) error - // AlterTableAddColumn adds column col to tbl at position ordinal. - AlterTableAddColumn(ctx context.Context, db sqlz.DB, tbl string, col string, kind sqlz.Kind, ordinal int) error + // AlterTableAddColumn adds column col to tbl. The column is appended + // to the list of columns (that is, the column position cannot be + // specified). + AlterTableAddColumn(ctx context.Context, db *sql.DB, tbl string, col string, kind sqlz.Kind) error } // Database models a database handle. It is conceptually equivalent to diff --git a/libsq/driver/driver_test.go b/libsq/driver/driver_test.go index 467c0299..ae977db3 100644 --- a/libsq/driver/driver_test.go +++ b/libsq/driver/driver_test.go @@ -9,6 +9,7 @@ import ( "github.com/neilotoole/sq/drivers/sqlite3" "github.com/neilotoole/sq/drivers/sqlserver" "github.com/neilotoole/sq/drivers/xlsx" + "github.com/neilotoole/sq/libsq/sqlz" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -437,3 +438,34 @@ func TestDatabase_SourceMetadata(t *testing.T) { }) } } + +func TestSQLDriver_AlterTableAddColumn(t *testing.T) { + testCases := []string{sakila.SL3, sakila.Pg, sakila.MS} + + for _, handle := range testCases { + handle := handle + + t.Run(handle, func(t *testing.T) { + th, src, dbase, drvr := testh.NewWith(t, handle) + + // Make a copy of the table to play with + tbl := th.CopyTable(true, src, sakila.TblActor, "", true) + + const wantCol, wantKind = "col_int", sqlz.KindInt + wantCols := append(sakila.TblActorCols(), wantCol) + wantKinds := append(sakila.TblActorColKinds(), wantKind) + + err := drvr.AlterTableAddColumn(th.Context, dbase.DB(), tbl, wantCol, wantKind) + require.NoError(t, err) + + sink, err := th.QuerySQL(src, "SELECT * FROM "+tbl) + require.NoError(t, err) + + gotCols := sink.RecMeta.Names() + require.Equal(t, wantCols, gotCols) + + gotKinds := sink.RecMeta.Kinds() + require.Equal(t, wantKinds, gotKinds) + }) + } +}