Skip to content

Commit

Permalink
ColumnDefinition: Add .oldName() method
Browse files Browse the repository at this point in the history
This will allow users to specify a column's old name so that the column's
name can be changed without losing the data in the column.
  • Loading branch information
nwoltman committed Dec 5, 2016
1 parent 4dec938 commit 1e78383
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 25 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -670,7 +670,9 @@ Only allows newly-defined tables to be created. Existing tables are never altere

Specifies that newly-defined tables will be created, existing tables that are no longer defined will be dropped, and existing tables that have a different definition from what is found in the database will be migrated with minimal data-loss.

**Note:** The migration cannot always retain data. Currently, the only known example is that renaming a column will not preserve its data (and it's possible that there are others). For this reason, and the fact that there is no reasonable way to test how well the migration will work on a live production database, using the `alter` strategy in production is discouraged.
**When renaming table columns**, you must specify the column's old name in the [column definition](#columndefinition) with the `.oldName('name')` method. If you don't, the column will be dropped and you will lose all of the data that was in that column.

**Note:** It is up to you to understand how changes to an existing table might affect the data. For example, changing a BIGINT column to a MEDIUMINT will cause integers greater than what can fit into a MEDIUMINT to wrap around and essentially become a different value (and you won't be able to recover the original value). Furthermore, some changes to tables cannot be done and will cause an error. An example of this would be adding a column with the `NOT NULL` attribute to a non-empty table without specifying a default value.

#### drop

Expand Down Expand Up @@ -886,6 +888,7 @@ This class is what is used to define the column's attributes. These attributes c
+ `primaryKey()` - Declares the column to be the table's primary key
+ `unique()` - Declares the column as a unique index
+ `index()` - Declares the column as an index
+ `oldName(name: string)` - The previous/current column name. If a column with this name exists, it will be renamed to the column name associated with the column defintion so that the data in that column will not be lost.

All `ColumnDefinition` methods return the `ColumnDefinition`, so they are chainable.

Expand Down
5 changes: 4 additions & 1 deletion jsdoc2md/README.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ Only allows newly-defined tables to be created. Existing tables are never altere

Specifies that newly-defined tables will be created, existing tables that are no longer defined will be dropped, and existing tables that have a different definition from what is found in the database will be migrated with minimal data-loss.

**Note:** The migration cannot always retain data. Currently, the only known example is that renaming a column will not preserve its data (and it's possible that there are others). For this reason, and the fact that there is no reasonable way to test how well the migration will work on a live production database, using the `alter` strategy in production is discouraged.
**When renaming table columns**, you must specify the column's old name in the [column definition](#columndefinition) with the `.oldName('name')` method. If you don't, the column will be dropped and you will lose all of the data that was in that column.

**Note:** It is up to you to understand how changes to an existing table might affect the data. For example, changing a BIGINT column to a MEDIUMINT will cause integers greater than what can fit into a MEDIUMINT to wrap around and essentially become a different value (and you won't be able to recover the original value). Furthermore, some changes to tables cannot be done and will cause an error. An example of this would be adding a column with the `NOT NULL` attribute to a non-empty table without specifying a default value.

#### drop

Expand Down Expand Up @@ -342,6 +344,7 @@ This class is what is used to define the column's attributes. These attributes c
+ `primaryKey()` - Declares the column to be the table's primary key
+ `unique()` - Declares the column as a unique index
+ `index()` - Declares the column as an index
+ `oldName(name: string)` - The previous/current column name. If a column with this name exists, it will be renamed to the column name associated with the column defintion so that the data in that column will not be lost.

All `ColumnDefinition` methods return the `ColumnDefinition`, so they are chainable.

Expand Down
7 changes: 7 additions & 0 deletions lib/ColumnDefinitions/ColumnDefinition.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class ColumnDefinition {
this.$primaryKey = false;
this.$unique = false;
this.$index = false;
this.$oldName = null;
this._notNull = false;
this._default = undefined;
}
Expand Down Expand Up @@ -71,6 +72,11 @@ class ColumnDefinition {
return this;
}

oldName(oldName) {
this.$oldName = oldName;
return this;
}

$equals(columnDefinition) {
var thisType = this.$type;
var otherType = columnDefinition.$type;
Expand All @@ -96,6 +102,7 @@ class ColumnDefinition {
return (this._default === 'NULL' || this._default === undefined) &&
(columnDefinition._default === 'NULL' || columnDefinition._default === undefined);
// Don't need to worry about keys since those are handled on the schema body
// The old column name also does not contribute to the equality of the column definitions
}

$toSQL() {
Expand Down
7 changes: 4 additions & 3 deletions lib/Operation.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ const Operation = {
DROP_COLUMN: 5,
MODIFY_TABLE_OPTIONS: 6,
MODIFY_COLUMN: 7,
ADD_COLUMN: 8,
ADD_KEY: 9,
ADD_FOREIGN_KEY: 10,
CHANGE_COLUMN: 8,
ADD_COLUMN: 9,
ADD_KEY: 10,
ADD_FOREIGN_KEY: 11,
},

create(type, sql) {
Expand Down
43 changes: 28 additions & 15 deletions lib/TableDefinition.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,41 +101,54 @@ class TableDefinition {
}

_getMigrateColumnsOperations(oldSchema) {
const operations = [];
const pool = this._pool;
const escapedName = this._escapedName;
const newSchema = this._schema;
const operations = [];
const renamedColumns = []; // List of old column names
var columnName;

// Drop old columns
for (columnName in oldSchema.columns) {
if (newSchema.columns[columnName]) {
continue;
}
operations.push(Operation.create(
Operation.Types.DROP_COLUMN,
'ALTER TABLE ' + this._escapedName + ' DROP COLUMN ' + this._pool.escapeId(columnName)
));
}

// Add new columns and modify existing columns
for (columnName in newSchema.columns) {
const newColumnDefinition = newSchema.columns[columnName];
const oldColumnDefinition = oldSchema.columns[columnName];
const oldColumnName = newColumnDefinition.$oldName && oldSchema.columns[newColumnDefinition.$oldName]
? newColumnDefinition.$oldName
: columnName;
const oldColumnDefinition = oldSchema.columns[oldColumnName];

if (!oldColumnDefinition) {
operations.push(Operation.create(
Operation.Types.ADD_COLUMN,
'ALTER TABLE ' + this._escapedName + ' ADD COLUMN ' + this._pool.escapeId(columnName) +
'ALTER TABLE ' + escapedName + ' ADD COLUMN ' + pool.escapeId(columnName) +
' ' + newColumnDefinition.$toSQL()
));
} else if (columnName !== oldColumnName) {
operations.push(Operation.create(
Operation.Types.CHANGE_COLUMN,
`ALTER TABLE ${escapedName} CHANGE COLUMN ${pool.escapeId(oldColumnName)} ${pool.escapeId(columnName)} ` +
newColumnDefinition.$toSQL()
));
renamedColumns.push(oldColumnName);
} else if (!newColumnDefinition.$equals(oldColumnDefinition, oldSchema)) {
operations.push(Operation.create(
Operation.Types.MODIFY_COLUMN,
'ALTER TABLE ' + this._escapedName + ' MODIFY COLUMN ' + this._pool.escapeId(columnName) +
'ALTER TABLE ' + escapedName + ' MODIFY COLUMN ' + pool.escapeId(columnName) +
' ' + newColumnDefinition.$toSQL()
));
}
}

// Drop old columns (unless the column is being changed)
for (columnName in oldSchema.columns) {
if (newSchema.columns[columnName] || renamedColumns.indexOf(columnName) >= 0) {
continue;
}
operations.push(Operation.create(
Operation.Types.DROP_COLUMN,
'ALTER TABLE ' + escapedName + ' DROP COLUMN ' + pool.escapeId(columnName)
));
}

return operations;
}

Expand Down
51 changes: 46 additions & 5 deletions test/integration/MySQLPlus.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,42 +125,58 @@ describe('MySQLPlus', function() {
const columnsTableName = 'columns_table';
const columnsTableSchema = {
columns: {
id: ColTypes.int().unsigned().notNull().primaryKey().autoIncrement(),
id: ColTypes.int().unsigned().notNull().primaryKey(),
uuid: ColTypes.char(44).unique(),
email: ColTypes.char(255),
fp: ColTypes.float(7, 4),
dropme: ColTypes.blob(),
renameme: ColTypes.tinyint(),
changeme: ColTypes.tinyint(),
neverchange: ColTypes.tinyint().oldName('fake_column'),
norename: ColTypes.tinyint().oldName('fake_column'),
},
indexes: ['email'],
};
const columnsTableExpectedSQL =
'CREATE TABLE `columns_table` (\n' +
' `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n' +
' `id` int(10) unsigned NOT NULL,\n' +
' `uuid` char(44) DEFAULT NULL,\n' +
' `email` char(255) DEFAULT NULL,\n' +
' `fp` float(7,4) DEFAULT NULL,\n' +
' `dropme` blob,\n' +
' `renameme` tinyint(4) DEFAULT NULL,\n' +
' `changeme` tinyint(4) DEFAULT NULL,\n' +
' `neverchange` tinyint(4) DEFAULT NULL,\n' +
' `norename` tinyint(4) DEFAULT NULL,\n' +
' PRIMARY KEY (`id`),\n' +
' UNIQUE KEY `unique_columns_table_uuid` (`uuid`),\n' +
' KEY `index_columns_table_email` (`email`)\n' +
') ENGINE=InnoDB DEFAULT CHARSET=utf8';

const columnsTableMigratedSchema = {
columns: {
id: ColTypes.bigint(5).unsigned().notNull().primaryKey().autoIncrement(),
id: ColTypes.bigint(5).unsigned().notNull().primaryKey(),
uuid: ColTypes.char(44).unique(),
email: ColTypes.varchar(255).notNull(),
fp: ColTypes.float(8, 3),
renamed: ColTypes.tinyint().oldName('renameme'),
changed: ColTypes.smallint().oldName('changeme'),
neverchange: ColTypes.tinyint().oldName('fake_column'),
norename: ColTypes.smallint().oldName('fake_column'),
added: ColTypes.text(),
},
uniqueKeys: ['email'],
};
const columnsTableMigratedExpectedSQL =
'CREATE TABLE `columns_table` (\n' +
' `id` bigint(5) unsigned NOT NULL AUTO_INCREMENT,\n' +
' `id` bigint(5) unsigned NOT NULL,\n' +
' `uuid` char(44) DEFAULT NULL,\n' +
' `email` varchar(255) NOT NULL,\n' +
' `fp` float(8,3) DEFAULT NULL,\n' +
' `renamed` tinyint(4) DEFAULT NULL,\n' +
' `changed` smallint(6) DEFAULT NULL,\n' +
' `neverchange` tinyint(4) DEFAULT NULL,\n' +
' `norename` smallint(6) DEFAULT NULL,\n' +
' `added` text,\n' +
' PRIMARY KEY (`id`),\n' +
' UNIQUE KEY `unique_columns_table_email` (`email`),\n' +
Expand Down Expand Up @@ -727,7 +743,16 @@ describe('MySQLPlus', function() {
pool.defineTable(indexesTableName, indexesTableMigragedSchema);
pool.defineTable(foreignKeysTableName, foreignKeysTableMigratedSchema);
pool.defineTable(optionsTableName, optionsTableMigratedSchema);
pool.sync(done);

// Insert data into the columns table before syncing the table changes so
// we can check if the data is still there after some columns get renamed
pool.query(
`INSERT INTO ${columnsTableName} (id, email, renameme, changeme) VALUES (1, 'a', 1, 2), (2, 'b', 3, 4)`,
err => {
if (err) throw err;
pool.sync(done);
}
);
});

after(done => {
Expand Down Expand Up @@ -817,6 +842,22 @@ describe('MySQLPlus', function() {
});
});

it('should not lose data when renaming columns', done => {
pool.query(`SELECT renamed, changed FROM ${columnsTableName}`, (err, rows) => {
if (err) throw err;

rows.length.should.equal(2);

rows[0].renamed.should.equal(1);
rows[0].changed.should.equal(2);

rows[1].renamed.should.equal(3);
rows[1].changed.should.equal(4);

done();
});
});

});

});
20 changes: 20 additions & 0 deletions test/unit/ColumnDefinitions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ describe('ColumnDefinitions', () => {
b = ColumnDefinitions.int();
a.$equals(b).should.be.true();
b.$equals(a).should.be.true();

// Setting an old name doesn't affect equality
a = ColumnDefinitions.int().oldName('old_a');
b = ColumnDefinitions.int().oldName('old_b');
a.$equals(b).should.be.true();
b.$equals(a).should.be.true();
});


Expand Down Expand Up @@ -447,6 +453,20 @@ describe('ColumnDefinitions', () => {
cd.$toSQL().should.equal('blob NOT NULL');
});

it('should allow columns to have their old column name specified but not change the SQL', () => {
ColumnDefinitions.int().oldName('old').$toSQL()
.should.equal(ColumnDefinitions.int().$toSQL());

ColumnDefinitions.char().oldName('old').$toSQL()
.should.equal(ColumnDefinitions.char().$toSQL());

ColumnDefinitions.blob().oldName('old').$toSQL()
.should.equal(ColumnDefinitions.blob().$toSQL());

ColumnDefinitions.timestamp().oldName('old').$toSQL()
.should.equal(ColumnDefinitions.timestamp().$toSQL());
});

});


Expand Down

0 comments on commit 1e78383

Please sign in to comment.