UNPKG

patio

Version:
660 lines (616 loc) 27.2 kB
var comb = require("comb"), argsToArray = comb.argsToArray, merge = comb.merge, isFunction = comb.isFunction, isDefined = comb.isDefined, isHash = comb.isHash, isString = comb.isString, toArray = comb.array.toArray, methodMissing = require("../proxy").methodMissing, define = comb.define; var Generator = define(null, { instance:{ /**@lends patio.SchemaGenerator.prototype*/ __primaryKey:null, /** * An internal class that the user is not expected to instantiate directly. * Instances are created by {@link patio.Database#createTable}. * It is used to specify table creation parameters. It takes a Database * object and a block of column/index/constraint specifications, and * gives the Database a table description, which the database uses to * create a table. * * {@link patio.SchemaGenerator} has some methods but also includes method_missing, * allowing users to specify column type as a method instead of using * the column method, which makes for a cleaner code. * @constructs * @example *comb.executeInOrder(DB, function(DB){ * DB.createTable("airplane_type", function () { * this.primaryKey("id"); * this.name(String, {allowNull:false}); * this.max_seats(Number, {size:3, allowNull:false}); * this.company(String, {allowNull:false}); * }); * DB.createTable("airplane", function () { * this.primaryKey("id"); * this.total_no_of_seats(Number, {size:3, allowNull:false}); * this.foreignKey("typeId", "airplane_type", {key:"id"}); * }); * DB.createTable("flight_leg", function () { * this.primaryKey("id"); * this.scheduled_departure_time("time"); * this.scheduled_arrival_time("time"); * this.foreignKey("departure_code", "airport", {key:"airport_code", type : String, size : 4}); * this.foreignKey("arrival_code", "airport", {key:"airport_code", type : String, size : 4}); * this.foreignKey("flight_id", "flight", {key:"id"}); * }); * DB.createTable("leg_instance", function () { * this.primaryKey("id"); * this.date("date"); * this.arr_time("datetime"); * this.dep_time("datetime"); * this.foreignKey("airplane_id", "airplane", {key:"id"}); * this.foreignKey("flight_leg_id", "flight_leg", {key:"id"}); * }); *}); * @param {patio.Database} the database this generator is for */ constructor:function (db) { this.db = db; this.columns = []; this.indexes = []; this.constraints = []; this.__primaryKey = null; }, /** * Add an unnamed constraint to the DDL, specified by the given block * or args: * * @example * db.createTable("test", function(){ * this.check({num : {between : [1,5]}}) * //=> CHECK num >= 1 AND num <= 5 * this.check(function(){return this.num.gt(5);}); * //=> CHECK num > 5 * }); **/ check:function () { return this.constraint.apply(this, [null].concat(argsToArray(arguments))); }, /** * Add a column with the given name, type, and opts to the DDL. * * <pre class="code"> * DB.createTable("test", function(){ * this.column("num", "integer"); * //=> num INTEGER * this.column("name", String, {allowNull : false, "default" : "a"); * //=> name varchar(255) NOT NULL DEFAULT 'a' * this.column("ip", "inet"); * //=> ip inet * }); * </pre> * * You can also create columns via method missing, so the following are * equivalent: * <pre class="code"> * DB.createTable("test", function(){ * this.column("number", "integer"); * this.number("integer"); * }); * </pre> * * @param {String|patio.sql.Identifier} name the name of the column * @param type the datatype of the column. * @param {Object} [opts] additional options * * @param [opts.default] The default value for the column. * @param [opts.deferrable] This ensure Referential Integrity will work even if * reference table will use for its foreign key a value that does not * exists(yet) on referenced table. Basically it adds * DEFERRABLE INITIALLY DEFERRED on key creation. * @param {Boolean} [opts.index] Create an index on this column. * @param {String|patio.sql.Identifier} [key] For foreign key columns, the column in the associated table * that this column references. Unnecessary if this column references the primary key of the * associated table. * @param {Boolean} [opts.allowNull] Mark the column as allowing NULL values (if true), * or not allowing NULL values (if false). If unspecified, will default * to whatever the database default is. * @param {String} [opts.onDelete] Specify the behavior of this column when being deleted * ("restrict", "cascade", "setNull", "setDefault", "noAction"). * @param {String} [opts.onUpdate] Specify the behavior of this column when being updated * Valid options ("restrict", "cascade", "setNull", "setDefault", "noAction"). * @param {Boolean} [opts.primaryKey] Make the column as a single primary key column. This should only * be used if you have a single, non-autoincrementing primary key column. * @param {Number} [opts.size] The size of the column, generally used with string * columns to specify the maximum number of characters the column will hold. * An array of two integers can be provided to set the size and the * precision, respectively, of decimal columns. * @param {String} [opts.unique] Mark the column as unique, generally has the same effect as * creating a unique index on the column. * @param {String} [opts.unsigned] Make the column type unsigned, only useful for integer * columns. * @param {Array} [opts.elements] Available items used for set and enum columns. * **/ column:function (name, type, opts) { opts = opts || {}; this.columns.push(merge({name:name, type:type}, opts)); if (opts.index) { this.index(name); } }, /** * Adds a named constraint (or unnamed if name is nil) to the DDL, * with the given block or args. * @example * DB.createTable("test", function(){ * this.constraint("blah", {num : {between : [1,5]}}) * //=> CONSTRAINT blah CHECK num >= 1 AND num <= 5 * this.check("foo", function(){ * return this.num.gt(5); * }); # CONSTRAINT foo CHECK num > 5 * }); * @param {String|patio.sql.Identifier} name the name of the constraint * @param {...} args variable number of arguments to create the constraint filter. * See {@link patio.Dataset#filter} for valid filter arguments. * */ constraint:function (name, args) { args = argsToArray(arguments).slice(1); var block = isFunction(args[args.length - 1]) ? args.pop : null; this.constraints.push({name:name, type:"check", check:block || args}); }, /** * Add a foreign key in the table that references another table to the DDL. See {@link patio.SchemaGenerator#column{ * for options. * * @example * DB.createTable("flight_leg", function () { * this.primaryKey("id"); * this.scheduled_departure_time("time"); * this.scheduled_arrival_time("time"); * this.foreignKey("departure_code", "airport", {key:"airport_code", type : String, size : 4}); * this.foreignKey("arrival_code", "airport", {key:"airport_code", type : String, size : 4}); * this.foreignKey("flight_id", "flight", {key:"id"}); * }); **/ foreignKey:function (name, table, opts) { opts = opts || {}; opts = isHash(table) ? merge({}, table, opts) : isString(table) ? merge({table:table}, opts) : opts; if (Array.isArray(name)) { return this.__compositeForeignKey(name, opts); } else { return this.column(name, "integer", opts); } }, /** *Add a full text index on the given columns to the DDL. * * @example * DB.createTable("posts", function () { * this.title("text"); * this.body("text"); * this.fullTextIndex("title"); * this.fullTextIndex(["title", "body"]); * }); */ fullTextIndex:function (columns, opts) { opts = opts || {}; return this.index(columns, merge({type:"fullText"}, opts)); }, /** * * Check if the DDL includes the creation of a column with the given name. * @return {Boolean} true if the DDL includes the creation of a column with the given name. */ hasColumn:function (name) { return this.columns.some(function (c) { return c.name === name; }); }, /** * Add an index on the given column(s) with the given options to the DDL. * The available options are: * @example * DB.createTable("test", function(table) { * table.primaryKey("id", "integer", {null : false}); * table.column("name", "text"); * table.index("name", {unique : true}); * }); * * @param columns the column/n to create the index from. * @param {Object} [opts] Additional options * @param {String} [opts.type] The type of index to use (only supported by some databases) * @param {Boolean} [opts.unique] :: Make the index unique, so duplicate values are not allowed. * @param [opts.where] :: Create a partial index (only supported by some databases) **/ index:function (columns, opts) { this.indexes.push(merge({columns:toArray(columns)}, opts || {})); }, /** * Adds an auto-incrementing primary key column or a primary key constraint to the DDL. * To create a constraint, the first argument should be an array of columns * specifying the primary key columns. To create an auto-incrementing primary key * column, a single column can be used. In both cases, an options hash can be used * as the second argument. * * If you want to create a primary key column that is not auto-incrementing, you * should not use this method. Instead, you should use the regular {@link patio.SchemaGenerator#column} * method with a {primaryKey : true} option. * * @example * db.createTable("airplane_type", function () { * this.primaryKey("id"); * //=> id integer NOT NULL PRIMARY KEY AUTOINCREMENT * this.name(String, {allowNull:false}); * this.max_seats(Number, {size:3, allowNull:false}); * this.company(String, {allowNull:false}); * }); * */ primaryKey:function (name) { if (Array.isArray(name)) { return this.__compositePrimaryKey.apply(this, arguments); } else { var args = argsToArray(arguments, 1), type; var opts = args.pop(); this.__primaryKey = merge({}, this.db.serialPrimaryKeyOptions, {name:name}, opts); if (isDefined((type = args.pop()))) { merge(opts, {type:type}); } merge(this.__primaryKey, opts); return this.__primaryKey; } }, /** * Add a spatial index on the given columns to the DDL. */ spatialIndex:function (columns, opts) { opts = opts || {}; return this.index(columns, merge({type:"spatial"}, opts)); }, /** * Add a unique constraint on the given columns to the DDL. See {@link patio.SchemaGenerator#constraint} * for argument types. * @example * DB.createTable("test", function(){ * this.unique("name"); * //=> UNIQUE (name) * }); * */ unique:function (columns, opts) { opts = opts || {}; this.constraints.push(merge({type:"unique", columns:toArray(columns)}, opts)); }, /** * @private * Add a composite primary key constraint */ __compositePrimaryKey:function (columns) { var args = argsToArray(arguments, 1); var opts = args.pop() || {}; this.constraints.push(merge({type:"primaryKey", columns:columns}, opts)); }, /** * @private * Add a composite foreign key constraint */ __compositeForeignKey:function (columns, opts) { this.constraints.push(merge({type:"foreignKey", columns:columns}, opts)); }, /**@ignore*/ getters:{ // The name of the primary key for this generator, if it has a primary key. primaryKeyName:function () { return this.__primaryKey ? this.__primaryKey.name : null; } } } }); exports.SchemaGenerator = function (db, block) { var gen = new Generator(db); var prox = methodMissing(gen, function (name) { return function (type, opts) { name = name || null; opts = opts || {}; if (name) { return this.column(name, type, opts); } else { throw new TypeError("name required got " + name); } }; }); block.apply(prox, [prox]); gen.columns = prox.columns; if (gen.__primaryKey && !gen.hasColumn(gen.primaryKeyName)) { gen.columns.unshift(gen.__primaryKey); } return gen; }; var AlterTableGenerator = define(null, { instance:{ /**@lends patio.AlterTableGenerator.prototype*/ /** * An internal class that the user is not expected to instantiate directly. * Instances are created by {@link patio.Database#alterTable}. * It is used to specify table alteration parameters. It takes a Database * object and a function which is called in the scope of the {@link patio.AlterTableGenerator} * to perform on the table, and gives the Database an array of table altering operations, * which the database uses to alter a table's description. * * @example * DB.alterTable("xyz", function() { * this.addColumn("aaa", "text", {null : false, unique : true}); * this.dropColumn("bbb"); * this.renameColumn("ccc", "ddd"); * this.setColumnType("eee", "integer"); * this.setColumnDefault("hhh", 'abcd'); * this.addIndex("fff", {unique : true}); * this.dropIndex("ggg"); * }); * * //or using the passed in generator * DB.alterTable("xyz", function(table) { * table.addColumn("aaa", "text", {null : false, unique : true}); * table.dropColumn("bbb"); * table.renameColumn("ccc", "ddd"); * table.setColumnType("eee", "integer"); * table.setColumnDefault("hhh", 'abcd'); * table.addIndex("fff", {unique : true}); * table.dropIndex("ggg"); * }); * @constructs * * @param {patio.Database} db the database object which is performing the alter table operation. * @param {Function} block a block which performs the operations. The block is called in the scope * of the {@link patio.AlterTableGenerator} and is passed an instance of {@link patio.AlterTableGenerator} * as the first argument. */ constructor:function (db, block) { this.db = db; this.operations = []; block.apply(this, [this]); }, /** * Add a column with the given name, type, and opts to the DDL for the table. * See {@link patio.SchemaGenerator#column} for the available options. * * @example * DB.alterTable("test", function(){ * this.addColumn("name", String); * //=> ADD COLUMN name varchar(255) * }); **/ addColumn:function (name, type, opts) { opts = opts || {}; this.operations.push(merge({op:"addColumn", name:name, type:type}, opts)); }, /** * Add a constraint with the given name and args to the DDL for the table. * See {@link patio.SchemaGenerator#constraint}. * * @example * var sql = patio.sql; * DB.alterTable("test", function(){ * this.addConstraint("valid_name", sql.name.like('A%')); * //=>ADD CONSTRAINT valid_name CHECK (name LIKE 'A%') * }); * */ addConstraint:function (name) { var args = argsToArray(arguments).slice(1); var block = isFunction(args[args.length - 1]) ? args[args.length - 1]() : null; this.operations.push({op:"addConstraint", name:name, type:"check", check:block || args}); }, /** * Add a unique constraint to the given column(s). * See {@link patio.SchemaGenerator#constraint}. * @example * DB.alterTable("test", function(){ * this.addUniqueConstraint("name"); * //=> ADD UNIQUE (name) * this.addUniqueConstraint("name", {name : "uniqueName}); * //=> ADD CONSTRAINT uniqueName UNIQUE (name) * }); **/ addUniqueConstraint:function (columns, opts) { opts = opts || {}; this.operations.push(merge({op:"addConstraint", type:"unique", columns:toArray(columns)}, opts)); }, /** * Add a foreign key with the given name and referencing the given table * to the DDL for the table. See {@link patio.SchemaGenerator#column} * for the available options. * * You can also pass an array of column names for creating composite foreign * keys. In this case, it will assume the columns exists and will only add * the constraint. * * NOTE: If you need to add a foreign key constraint to a single existing column * use the composite key syntax even if it is only one column. * @example * DB.alterTable("albums", function(){ * this.addForeignKey("artist_id", "table"); * //=>ADD COLUMN artist_id integer REFERENCES table * this.addForeignKey(["name"], "table") * //=>ADD FOREIGN KEY (name) REFERENCES table * }); */ addForeignKey:function (name, table, opts) { opts = opts; if (Array.isArray(name)) { return this.__addCompositeForeignKey(name, table, opts); } else { return this.addColumn(name, this.db.defaultPrimaryKeyType, merge({table:table}, opts)); } }, /** * Add a full text index on the given columns to the DDL for the table. * See @{link patio.SchemaGenerator#index} for available options. */ addFullTextIndex:function (columns, opts) { opts = opts || {}; return this.addIndex(columns, merge({type:"fullText"}, opts)); }, /** * Add an index on the given columns to the DDL for the table. See * {@link patio.SchemaGenerator#index} for available options. * @example * DB.alterTable("table", function(){ * this.addIndex("artist_id"); * //=> CREATE INDEX table_artist_id_index ON table (artist_id) * }); */ addIndex:function (columns, opts) { opts = opts || {}; this.operations.push(merge({op:"addIndex", columns:toArray(columns)}, opts)); }, /** * Add a primary key to the DDL for the table. See {@link patio.SchemaGenerator#column} * for the available options. Like {@link patio.ALterTableGenerator#addForeignKey}, if you specify * the column name as an array, it just creates a constraint: * * @example * DB.alterTable("albums", function(){ * this.addPrimaryKey("id"); * //=> ADD COLUMN id serial PRIMARY KEY * this.addPrimaryKey(["artist_id", "name"]) * //=>ADD PRIMARY KEY (artist_id, name) * }); */ addPrimaryKey:function (name, opts) { opts = opts || {}; if (Array.isArray(name)) { return this.__addCompositePrimaryKey(name, opts); } else { opts = merge({}, this.db.serialPrimaryKeyOptions, opts); delete opts.type; return this.addColumn(name, "integer", opts); } }, /** * Add a spatial index on the given columns to the DDL for the table. * See {@link patio.SchemaGenerator#index} for available options. * */ addSpatialIndex:function (columns, opts) { opts = opts || {}; this.addIndex(columns, merge({}, {type:"spatial"}, opts)); }, /** * Remove a column from the DDL for the table. * * @example * DB.alterTable("albums", function(){ * this.dropColumn("artist_id"); * //=>DROP COLUMN artist_id * }); * * @param {String|patio.sql.Identifier} name the name of the column to drop. */ dropColumn:function (name) { this.operations.push({op:"dropColumn", name:name}); }, /** * Remove a constraint from the DDL for the table. * @example * DB.alterTable("test", function(){ * this.dropConstraint("constraint_name"); * //=>DROP CONSTRAINT constraint_name * }); * @param {String|patio.sql.Identifier} name the name of the constraint to drop. */ dropConstraint:function (name) { this.operations.push({op:"dropConstraint", name:name}); }, /** * Remove a child table's inheritance from a parent table. * @example * DB.alterTable("test", function () { * this.noInherit("parentTable"); * //=>ALTER TABLE test NO INHERIT parent_table * }); * @param {String|patio.sql.Identifier} name the name of the table to remove inheritance from. */ noInherit: function (name) { this.operations.push({op:"noInherit", name: name}); }, /** * Remove an index from the DDL for the table. * * @example * DB.alterTable("albums", function(){ * this.dropIndex("artist_id") * //=>DROP INDEX table_artist_id_index * this.dropIndex(["a", "b"]) * //=>DROP INDEX table_a_b_index * this.dropIndex(["a", "b"], {name : "foo"}) * //=>DROP INDEX foo * }); */ dropIndex:function (columns, opts) { opts = opts || {}; this.operations.push(merge({op:"dropIndex", columns:toArray(columns)}, opts)); }, /** * Modify a column's name in the DDL for the table. * * @example * DB.alterTable("artist", function(){ * this.renameColumn("name", "artistName"); * //=> RENAME COLUMN name TO artist_name * }); */ renameColumn:function (name, newName, opts) { opts = opts || {}; this.operations.push(merge({op:"renameColumn", name:name, newName:newName}, opts)); }, /** * Modify a column's default value in the DDL for the table. * @example * DB.alterTable("artist", function(){ * //=>this.setColumnDefault("artist_name", "a"); * //=> ALTER COLUMN artist_name SET DEFAULT 'a' * }); * */ setColumnDefault:function (name, def) { this.operations.push({op:"setColumnDefault", name:name, "default":def}); }, /** * Modify a column's type in the DDL for the table. * * @example * DB.alterTable("artist", function(){ * this.setColumnType("artist_name", 'char(10)'); * //=> ALTER COLUMN artist_name TYPE char(10) * }); */ setColumnType:function (name, type, opts) { opts = opts || {}; this.operations.push(merge({op:"setColumnType", name:name, type:type}, opts)); }, /** * Modify a column's NOT NULL constraint. * @example * DB.alterTable("artist", function(){ * this.setColumnAllowNull("artist_name", false); * //=> ALTER COLUMN artist_name SET NOT NULL * }); **/ setAllowNull:function (name, allowNull) { this.operations.push({op:"setColumnNull", name:name, "null":allowNull}); }, /** * @private * Add a composite primary key constraint **/ __addCompositePrimaryKey:function (columns, opts) { this.operations.push(merge({op:"addConstraint", type:"primaryKey", columns:columns}, opts)); }, /** * @private * Add a composite foreign key constraint * */ __addCompositeForeignKey:function (columns, table, opts) { this.operations.push(merge({op:"addConstraint", type:"foreignKey", columns:columns, table:table}, opts)); } } }).as(exports, "AlterTableGenerator");