From 553598b6e96120001bdd9119d32a1a6c5448fbd3 Mon Sep 17 00:00:00 2001 From: Whitney Young Date: Fri, 3 Apr 2015 15:13:01 -0700 Subject: [PATCH] Alter table queries. --- docs/source/guides/migrations.md | 21 ++++ lib/grammar/phrasing_schema.js | 54 ++++++++++ lib/schema/schema.js | 19 ++++ lib/schema/table/alter.js | 76 ++++++++++++++ lib/schema/table/column_alterer.js | 53 ++++++++++ lib/schema/table/column_creator.js | 19 +++- lib/schema/table/create.js | 11 ++- test/schema/alter_table_tests.js | 154 +++++++++++++++++++++++++++++ test/schema/create_table_tests.js | 2 +- 9 files changed, 398 insertions(+), 11 deletions(-) create mode 100644 lib/schema/table/alter.js create mode 100644 test/schema/alter_table_tests.js diff --git a/docs/source/guides/migrations.md b/docs/source/guides/migrations.md index ace6458..3d62c8a 100644 --- a/docs/source/guides/migrations.md +++ b/docs/source/guides/migrations.md @@ -111,6 +111,27 @@ issue or pull request to see this happen sooner. +#### `#alterTable` + +Alter existing tables. Pass the name of the table you want to alter and a callback +that will receive a table object with which you will be able to create columns +of different [field types](#field-types) as well as drop existing columns. + +```js +schema.alterTable('articles', function(table) { + table.string('title'); // add a title column + table.drop('body'); // drop the body column +}); +``` + +- `drop` Drops a table column (**not yet supported in SQLite3**) + +```js +schema.alterTable('articles', function(table) { + table.drop('title'); // drop the title column +}); +``` + #### `#dropTable` diff --git a/lib/grammar/phrasing_schema.js b/lib/grammar/phrasing_schema.js index 86b4db5..de770be 100644 --- a/lib/grammar/phrasing_schema.js +++ b/lib/grammar/phrasing_schema.js @@ -110,6 +110,60 @@ module.exports = Mixin.create(/** @lends Phrasing# */ { return this._grammar.join(fragments); }, + /** + * Alter table statement. + * + * Subclasses should override to customize this query. + * + * @method + * @public + * @param {Object} data + * @param {String} data.name + * @param {Array.} data.added + * @param {Array.} data.dropped + * @return {Statement} The statement. + */ + alterTable: function(data) { + var quoteField = this._grammar.field.bind(this._grammar); + var added = data.added.map(this.addColumn, this); + var dropped = data.dropped.map(this.dropColumn, this); + var delimited = this._grammar.delimit([].concat(dropped, added)); + var joined = this._grammar.join(delimited); + var fragments = ['ALTER TABLE ']; + fragments.push(quoteField(data.name), ' '); + fragments = fragments.concat(joined); + return Statement.create(this._grammar.join(fragments)); + }, + + /** + * Add column fragment. + * + * Subclasses should override to customize this fragment. + * + * @method + * @public + * @param {Object} column + * @return {Fragment} The fragment. + * @see {@link Phrasing#createColumn} + */ + addColumn: function(column) { + return this._grammar.join(['ADD COLUMN ', this.createColumn(column)]); + }, + + /** + * Rename column fragment. + * + * Subclasses should override to customize this fragment. + * + * @method + * @public + * @param {String} column + * @return {Fragment} The fragment. + */ + dropColumn: function(column) { + return this._grammar.join(['DROP COLUMN ', this._grammar.field(column)]); + }, + /** * Drop table statement. * diff --git a/lib/schema/schema.js b/lib/schema/schema.js index a5650b2..bdd2181 100644 --- a/lib/schema/schema.js +++ b/lib/schema/schema.js @@ -3,6 +3,7 @@ var _ = require('lodash'); var BaseQuery = require('../query/base'); var CreateTableQuery = require('./table/create'); +var AlterTableQuery = require('./table/alter'); var DropTableQuery = require('./table/drop'); /** @@ -36,6 +37,24 @@ var Schema = BaseQuery.extend(/** @lends Schema# */ { */ createTable: function() { return this._spawn(CreateTableQuery, arguments); }, + /** + * @function Schema~AlterTableCallback + * @param {TableAlterer} table The table object on which you can alter a + * table, for instance creating and dropping columns. + */ + + /** + * Alter a table. + * + * @method + * @public + * @param {String} name The name of the table to alter. + * @param {Schema~AlterTableCallback} cb A callback that will allow you to + * create and drop table columns. + * @return {CreateTableQuery} A query to execute to alter the table. + */ + alterTable: function() { return this._spawn(AlterTableQuery, arguments); }, + /** * Drop a table. * diff --git a/lib/schema/table/alter.js b/lib/schema/table/alter.js new file mode 100644 index 0000000..b916531 --- /dev/null +++ b/lib/schema/table/alter.js @@ -0,0 +1,76 @@ +'use strict'; + +var _ = require('lodash'); +var BaseQuery = require('../../query/base'); +var ColumnAlterer = require('./column_alterer'); + +/** + * A query that allows altering tables. + * + * You will not create this query object directly. Instead, you will + * receive it via {@link Schema#alterTable}. + * + * @protected + * @constructor AlterTableQuery + * @extends BaseQuery + */ +var AlterTableQuery = BaseQuery.extend(/** @lends AlterTableQuery# */ { + init: function() { throw new Error('AlterTableQuery must be spawned.'); }, + + /** + * Override of {@link BaseQuery#_create}. + * + * @method + * @private + * @see {@link BaseQuery#_create} + * @see {@link Schema#alterTable} for parameter details. + */ + _create: function(name, cb) { + if (!cb) { throw new Error('Missing callback to create columns.'); } + this._super(); + this._name = name; + this._alterer = ColumnAlterer.create(); + + cb(this._alterer); + + var pkCount = _(this._alterer.added) + .map('options').filter('primaryKey').size(); + if (pkCount > 1) { + throw new Error('Table may only have one primary key column.'); + } + }, + + /** + * Duplication implementation. + * + * @method + * @protected + * @see {@link BaseQuery#_take} + */ + _take: function(orig) { + this._super(orig); + this._name = orig._name; + this._alterer = orig._alterer; + }, + + /** + * Override of {@link BaseQuery#sql}. + * + * @method + * @protected + * @see {@link BaseQuery#sql} + */ + sql: function() { + return this._adapter.phrasing.alterTable({ + name: this._name, + added: this._alterer.added, + dropped: this._alterer.dropped, + renamed: this._alterer.renamed + }); + } + +}); + +module.exports = AlterTableQuery.reopenClass({ + __name__: 'AlterTableQuery' +}); diff --git a/lib/schema/table/column_alterer.js b/lib/schema/table/column_alterer.js index e69de29b..41b8e4c 100644 --- a/lib/schema/table/column_alterer.js +++ b/lib/schema/table/column_alterer.js @@ -0,0 +1,53 @@ +var ColumnCreator = require('./column_creator'); +var property = require('../../util/property').fn; + +/** + * A class that can create, drop & alter columns for tables. + * + * @protected + * @constructor ColumnAlterer + * @param {Array} columns An empty array that will be mutated to contain + * {@link ColumnAlterer~Column} objects as they are created. + */ +var ColumnAlterer = ColumnCreator.extend({ + init: function() { + this._super.apply(this, arguments); + this._dropped = []; + }, + + /** + * Alias for {@link ColumnCreator#columns}. + * + * @private + * @scope internal + * @type {Array.} + * @readonly + */ + added: property({ property: 'columns' }), + + /** + * Names of dropped columns. + * + * @private + * @scope internal + * @type {Array.} + * @readonly + */ + dropped: property(), + + /** + * Drop a column. + * + * @method + * @public + * @param {String} column The column name. + */ + drop: function(column) { + this._dropped.push(column); + } + +}); + +module.exports = ColumnAlterer.reopenClass({ + __name__: 'Table~ColumnAlterer' +}); diff --git a/lib/schema/table/column_creator.js b/lib/schema/table/column_creator.js index 4c5ed8b..48a5e64 100644 --- a/lib/schema/table/column_creator.js +++ b/lib/schema/table/column_creator.js @@ -2,6 +2,7 @@ var Class = require('../../util/class'); var Column = require('./column'); +var property = require('../../util/property').fn; /** * Helper method for defining methods that can create attributable columns that @@ -31,15 +32,23 @@ var col = function(type) { * * @protected * @constructor ColumnCreator - * @param {Array} columns An empty array that will be mutated to contain - * {@link Column} objects as they are created. */ var ColumnCreator = Class.extend({ - init: function(columns) { - this._super(); - this._columns = columns; + init: function() { + this._super.apply(this, arguments); + this._columns = []; }, + /** + * Created columns. + * + * @private + * @scope internal + * @type {Array.} + * @readonly + */ + columns: property(), + /** * Alias for {@link ColumnCreator#serial}. * diff --git a/lib/schema/table/create.js b/lib/schema/table/create.js index a0ad3ac..ded6224 100644 --- a/lib/schema/table/create.js +++ b/lib/schema/table/create.js @@ -29,14 +29,15 @@ var CreateTableQuery = BaseQuery.extend(/** @lends CreateTableQuery# */ { if (!cb) { throw new Error('Missing callback to create columns.'); } this._super(); this._name = name; - this._columns = []; + this._creator = ColumnCreator.create(); this._options = { ifNotExists: false }; - cb(ColumnCreator.create(this._columns)); + cb(this._creator); - var pkCount = _(this._columns).map('options').filter('primaryKey').size(); + var pkCount = _(this._creator.columns) + .map('options').filter('primaryKey').size(); if (pkCount > 1) { throw new Error('Table may only have one primary key column.'); } @@ -52,7 +53,7 @@ var CreateTableQuery = BaseQuery.extend(/** @lends CreateTableQuery# */ { _take: function(orig) { this._super(orig); this._name = orig._name; - this._columns = orig._columns; + this._creator = orig._creator; this._options = _.clone(orig._options); }, @@ -66,7 +67,7 @@ var CreateTableQuery = BaseQuery.extend(/** @lends CreateTableQuery# */ { sql: function() { return this._adapter.phrasing.createTable({ name: this._name, - columns: this._columns, + columns: this._creator.columns, options: this._options }); }, diff --git a/test/schema/alter_table_tests.js b/test/schema/alter_table_tests.js new file mode 100644 index 0000000..4d5f454 --- /dev/null +++ b/test/schema/alter_table_tests.js @@ -0,0 +1,154 @@ +'use strict'; + +var chai = require('chai'); +var expect = chai.expect; + +var Database = require('../../lib/database'); +var AlterTableQuery = require('../../lib/schema/table/alter'); +var FakeAdapter = require('../fakes/adapter'); +var Statement = require('../../lib/grammar/statement'); + +var db, adapter; + +describe('AlterTableQuery', function() { + before(function() { + adapter = FakeAdapter.create({}); + db = Database.create({ adapter: adapter }); + }); + + it('cannot be created directly', function() { + expect(function() { + AlterTableQuery.create(); + }).to.throw(/AlterTableQuery must be spawned/i); + }); + + it('must provide a callback', function() { + expect(function() { + db.schema.alterTable('users'); + }).to.throw(/missing callback/i); + }); + + it('generates primary key columns via `pk`', function() { + var query = db.schema.alterTable('users', function(table) { + table.integer('id').pk(); + }); + expect(query.sql()).to.eql(Statement.create( + 'ALTER TABLE "users" ADD COLUMN "id" integer PRIMARY KEY', [] + )); + }); + + it('can add multiple columns', function() { + var query = db.schema.alterTable('users', function(table) { + table.integer('id').pk(); + table.integer('age'); + }); + expect(query.sql()).to.eql(Statement.create( + 'ALTER TABLE "users" ADD COLUMN "id" integer PRIMARY KEY, ' + + 'ADD COLUMN "age" integer', [] + )); + }); + + it('generates primary key columns via `primarykey`', function() { + var query = db.schema.alterTable('users', function(table) { + table.integer('id').primaryKey(); + }); + expect(query.sql()).to.eql(Statement.create( + 'ALTER TABLE "users" ADD COLUMN "id" integer PRIMARY KEY', [] + )); + }); + + it('does not allow more than one primary key', function() { + expect(function() { + db.schema.alterTable('users', function(table) { + table.integer('id').pk(); + table.integer('id2').pk(); + }); + }).to.throw(/only.*one primary key/); + }); + + it('generates not null columns', function() { + var query = db.schema.alterTable('users', function(table) { + table.integer('id').notNull(); + }); + expect(query.sql()).to.eql(Statement.create( + 'ALTER TABLE "users" ADD COLUMN "id" integer NOT NULL', [] + )); + }); + + it('generates unique columns', function() { + var query = db.schema.alterTable('users', function(table) { + table.integer('id').unique(); + }); + expect(query.sql()).to.eql(Statement.create( + 'ALTER TABLE "users" ADD COLUMN "id" integer UNIQUE', [] + )); + }); + + it('generates columns with defaults', function() { + var query = db.schema.alterTable('users', function(table) { + table.integer('id').default(0); + }); + expect(query.sql()).to.eql(Statement.create( + 'ALTER TABLE "users" ADD COLUMN "id" integer DEFAULT 0', [] + )); + }); + + it('generates columns using foreign keys', function() { + var query = db.schema.alterTable('users', function(table) { + table.integer('profile_id').references('profiles.id'); + }); + expect(query.sql()).to.eql(Statement.create( + 'ALTER TABLE "users" ADD COLUMN "profile_id" integer REFERENCES "profiles" ("id")', [] + )); + }); + + it('generates columns using foreign key on self', function() { + var query = db.schema.alterTable('users', function(table) { + table.integer('boss_id').references('id'); + }); + expect(query.sql()).to.eql(Statement.create( + 'ALTER TABLE "users" ADD COLUMN "boss_id" integer REFERENCES "id"', [] + )); + }); + + it('gives error when foreign key is invalid', function() { + expect(function() { + db.schema.alterTable('users', function(table) { + table.integer('foreign_id').references('bad.foreign.key'); + }) + .sql(); + }).to.throw(/invalid.*"bad\.foreign\.key"/i); + }); + + it('can drop columns', function() { + var query = db.schema.alterTable('users', function(table) { + table.drop('name'); + }); + expect(query.sql()).to.eql(Statement.create( + 'ALTER TABLE "users" DROP COLUMN "name"', [] + )); + }); + + it('can be cloned', function() { + var query = db.schema.alterTable('users', function(table) { + table.drop('id'); + }).clone(); + expect(query.sql()).to.eql(Statement.create( + 'ALTER TABLE "users" DROP COLUMN "id"', [] + )); + }); + + it('can combine options', function() { + var query = db.schema.alterTable('users', function(table) { + table.integer('profile_id').references('profiles.id').notNull().unique(); + table.drop('age'); + }); + + expect(query.sql()).to.eql(Statement.create( + 'ALTER TABLE "users" ' + + 'DROP COLUMN "age", ' + + 'ADD COLUMN "profile_id" integer NOT NULL UNIQUE ' + + 'REFERENCES "profiles" ("id")', [] + )); + }); +}); diff --git a/test/schema/create_table_tests.js b/test/schema/create_table_tests.js index 4e2d63f..e491346 100644 --- a/test/schema/create_table_tests.js +++ b/test/schema/create_table_tests.js @@ -39,7 +39,7 @@ describe('CreateTableQuery', function() { it('generates primary key columns via `primarykey`', function() { var query = db.schema.createTable('users', function(table) { - table.integer('id').pk(); + table.integer('id').primaryKey(); }); expect(query.sql()).to.eql(Statement.create( 'CREATE TABLE "users" ("id" integer PRIMARY KEY)', []