Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Support for foreign key constraints (#389) #595

Merged
merged 18 commits into from

9 participants

Martin Aspeli Daniel Durante Sascha Depold Jan Aagaard Meier Leroy Baeyens pward123 Adam Pflug Jochem Maas Kevin Beaty
Martin Aspeli

This implements support for creating FOREIGN KEY ... REFERENCES declarations with ON DELETE and ON UPDATE triggers when defining associations, for all supported databases.

The basic syntax is:

var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
  , User = this.sequelize.define('User', { username: Sequelize.STRING })

User.hasMany(Task, {onDelete: 'restrict'})

The same works for hasOne() and belongsTo(). Valid options are:

  • onDelete -- set to e.g. restrict or cascade
  • onUpdate -- set to e.g. restrict or cascade
  • foreignKeyConstraint -- set to true to gain a REFERENCES declaration without either onDelete or onUpdate (that is, foreignKeyConstraint is implied if onDelete or onUpdate are set). This may be a helpful performance optimisation in some cases.

Some key implementation notes:

  • Enabling constraints is opt-in: they are only created if one or more of the options above are used when defining an association.

  • MySQL (with InnoDB tables) and Postgres support foreign key references by default. SQLite does not unless PRAGMA FOREIGN_KEYS=ON is issued. We do so when opening the database connection, unless explicitly disabled with a global option, to get parity across implementations. Note that just enabling this doesn't make any difference unless there are actual constraints in place.

  • Only simple foreign keys are supported (because associations only support simple keys). This is the "80%" use case of course. Setting one of foreign key options in a situation where there is more than one primary key defined will cause the option to be ignored.

  • SQL allows two ways to define foreign keys: "inline" (someId INTEGER REFERENCES someTable(id) ON DELETE CASCADE) or "standalone" (FOREIGN KEY(someId) REFERENCES someTable(id) ON DELETE CASCADE placed as a separate clause inside a CREATE TABLE statement). Since associations in sequelize create foreign key attributes the "inline" syntax is used, and attributesToSQL is taught how to add the relevant suffix to any "raw" attribute with the relevant metadata attached. This works for Postgres and SQLite, but MySQL ignores this syntax, requiring the "standalone" approach. For MySQL, we move the declaration to the end with a bit of string manipulation. This is analogous to how PRIMARY KEY is handled and allows this to be done without major refactoring.

  • If we have foreign key constraints, the order in which tables are created matters: if foo has a foreign key to bar with a constraint, then bar has to exist before foo can be created. To make sure this happens, we use a topological sort of relationships (via the Toposort-class module, added as a dependency) to sequence calls to CREATE TABLE in sync(). This also necessitates sync() being serialised, but given it's an "on startup" operation that shouldn't be too much of an issue.

  • A similar concern happens with dropAllTables(), but here we don't have enough information to sort the list. Instead, we do one of two things: for SQLite and MySQL, we temporarily disable constraint checking. For Postgres, we use DROP TABLE ... CASCADE to drop relevant constraints when required. (MySQL and SQLite only support the former and Postgres only supports the latter). This is blunt, but OK given that the function is attempting to drop all the tables.

  • For other calls to dropTable() the caller is expected to sequence calls appropriately, or wrap the call in disableForeignKeyConstraints() and enableForeignKeyConstraints() (MySQL and SQLite; no-ops in Postgres) and/or pass {cascade: true} in options (Postgres; no-op in MySQL and SQLite).

  • There are (passing) tests of the various dialect QueryGenerators and of cascade and restrict behaviour at the integration test level.

  • I will add documentation (unless you'd prefer to do it yourself) if/once this PR is accepted, I just don't want to do it yet if there's going to be challenge.

Daniel Durante
Owner

:+1 @sdepold and @mickhansen this has my vote (looked through the code. The styling seems OK, the only concern is that we're running synchronously in a few places that were previously async, but this I actually support within the PR's context).

@optilude nice job man, this is a huge win, I'd just like to get some other opinions in :)

Martin Aspeli

@durango, glad you could look at it so quickly :)

The serialisation is a necessary evil (you can't create a table with a foreign key constraint to another table if the other table doesn't exist yet - see implementation note on the PR for more details), but it only happens during operations that are very unlikely to be on any sort of performance critical path: on sync() when we're creating new tables, and on dropAllTables().

If you like this one, you can thank me by merging #569 and then Sequelize will (hopefully) do everything I need for my project so I can go back to hacking on that one. ;)

Sascha Depold
Owner

looks awesome. will take a closer look at it this evening. thanks for your work. best PR description ever :)

lib/dialects/sqlite/connector-manager.js
@@ -5,7 +5,13 @@ var Utils = require("../../utils")
module.exports = (function() {
var ConnectorManager = function(sequelize) {
this.sequelize = sequelize
- this.database = new sqlite3.Database(sequelize.options.storage || ':memory:')
+ this.database = db = new sqlite3.Database(sequelize.options.storage || ':memory:', function(err) {
+ if(!err && sequelize.options.foreignKeys !== false) {
+ // Make it possible to define and use foreign key constraints unelss
Sascha Depold Owner
sdepold added a note

typo "unless"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Sascha Depold
Owner

rocksolid work. @mickhansen @janmeier any thoughts on this ? +1 from me

spec-jasmine/sqlite/query-generator.spec.js
((57 lines not shown))
+ },
+ ],
+
+ createTableQuery: [
+ {
+ arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)'}],
+ expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255));"
+ },
+ {
+ arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)'}],
+ expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255));"
+ },
+ {
+ arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)'}],
+ expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255));"
+ },
Jan Aagaard Meier Owner
janmeier added a note

This testcase seems to be duplicated thrice or did i miss something?

Martin Aspeli
optilude added a note

D'uh. It was a test copied from the MySQL implementation and then changed to remove MySQL-specific setting of the table engine, which of course left the three all the same. Corrected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Jan Aagaard Meier janmeier commented on the diff
spec/associations/belongs-to.spec.js
((47 lines not shown))
+ })
+ })
+ })
+
+ it("can restrict deletes", function(done) {
+ var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
+ , User = this.sequelize.define('User', { username: Sequelize.STRING })
+
+ Task.belongsTo(User, {onDelete: 'restrict'})
+
+ this.sequelize.sync({ force: true }).success(function() {
+ User.create({ username: 'foo' }).success(function(user) {
+ Task.create({ title: 'task' }).success(function(task) {
+ task.setUser(user).success(function() {
+ user.destroy().error(function() {
+ // Should fail due to FK restriction
Jan Aagaard Meier Owner
janmeier added a note

We might want to check err.code here to make 100% sure that the right error is triggered

Martin Aspeli
optilude added a note

Do we know what it's supposed to be (in a database-agnostic way)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
spec/associations/belongs-to.spec.js
((77 lines not shown))
+
+ Task.belongsTo(User, {onUpdate: 'cascade'})
+
+ this.sequelize.sync({ force: true }).success(function() {
+ User.create({ username: 'foo' }).success(function(user) {
+ Task.create({ title: 'task' }).success(function(task) {
+ task.setUser(user).success(function() {
+
+ // Changing the id of a DAO requires a little dance since
+ // the `UPDATE` query generated by `save()` uses `id` in the
+ // `WHERE` clause
+
+ var tableName = user.QueryInterface.QueryGenerator.addSchema(user.__factory)
+ user.QueryInterface.update(user, tableName, {id: 999}, user.id)
+ .success(function() {
+ // Should fail due to FK restriction
Jan Aagaard Meier Owner
janmeier added a note

Should it? Seems you are asserting here that the update succeeded ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
spec/associations/has-many.spec.js
((77 lines not shown))
+
+ User.hasMany(Task, {onUpdate: 'cascade'})
+
+ this.sequelize.sync({ force: true }).success(function() {
+ User.create({ username: 'foo' }).success(function(user) {
+ Task.create({ title: 'task' }).success(function(task) {
+ user.setTasks([task]).success(function() {
+
+ // Changing the id of a DAO requires a little dance since
+ // the `UPDATE` query generated by `save()` uses `id` in the
+ // `WHERE` clause
+
+ var tableName = user.QueryInterface.QueryGenerator.addSchema(user.__factory)
+ user.QueryInterface.update(user, tableName, {id: 999}, user.id)
+ .success(function() {
+ // Should fail due to FK restriction
Jan Aagaard Meier Owner
janmeier added a note

Seems to be another misplaced comment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Jan Aagaard Meier janmeier commented on the diff
spec/associations/has-many.spec.js
((70 lines not shown))
+ })
+ })
+ })
+
+ it("can cascade updates", function(done) {
+ var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
+ , User = this.sequelize.define('User', { username: Sequelize.STRING })
+
+ User.hasMany(Task, {onUpdate: 'cascade'})
+
+ this.sequelize.sync({ force: true }).success(function() {
+ User.create({ username: 'foo' }).success(function(user) {
+ Task.create({ title: 'task' }).success(function(task) {
+ user.setTasks([task]).success(function() {
+
+ // Changing the id of a DAO requires a little dance since
Jan Aagaard Meier Owner
janmeier added a note

I'm not quite sure what this dance is about - could you give an example of the dance-/no-dance-queries?

Martin Aspeli
optilude added a note

So basically, my first attempt was:

user.id = 9999
user.save().success(....)

The problem is that save() generates an UPDATE query like this:

UPDATE Users SET id id = 999 WHERE id = 999

because id is the primary key. So, I need to use the lower level interface.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Jan Aagaard Meier
Owner

Truly solid work! My comments are only very, very minor

I don't know what the others think, but given how well-written the description for this PR is, I would have no problem at all letting you write the docs as well! :)

Martin Aspeli

@janmeier I'm happy to write some docs, but I don't understand how they're maintained/how to generate a test site. I need some meta-docs. ;)

Sascha Depold
Owner

@optilude https://github.com/sequelize/sequelize-doc - its a mix of erb and markdown.

Sascha Depold sdepold merged commit 256db5c into from
Kevin Beaty

This introduces a global db var.

Suggest:

var db = this.database =

nice catch ... alternatively one could just not create the var at all and do this instead:

    this.database.run('PRAGMA FOREIGN_KEYS=ON')
Leroy Baeyens

I would love to have some documentation about how exactly to use this extra "feature". Because I believe, but I don't know for sure that I can use this. I have sequelize 2.0.0-alpha2 installed, but I really don't know how to use this.

Any help or further documentation would be great!
Edit: I have got it working right now, but there is a slight problem

I have the next code:

var User = sequelize.define("User", { /* */ });
var Group = sequelize.define("Group", { /* */ });
var Apikey = sequelize.define("Apikey", { /* */ });

Group.hasMany(User);
User.hasMany(Apikey);

Somehow when trying to sync({force:true}) it want to try to drop the tables in this order:
1. Group
2. User
3. Apikey

This turns into the next error:

Cannot drop table Users because it is referenced by Apikeys

Sounds logical, because the apikeys table has a reference to User

The order of dropping should be:
1. Group
2. Apikey
3. User

Because the reference from the Apikeys table to the Users table must be removed first!

Also somehow Many-to-Many relations doesn't create Foreign Keys in the database. Al One-to-Many are executing perfectly!

Sascha Depold
Owner
pward123

It would be nice if we could do something like { onDelete: 'restrict', allowNull: false } to make the foreign key field non-nullable

Adam Pflug

It seems like there is a fair amount of work going on to try to add the foreign key constraints at the same time that the tables are created. This means we need to try to determine what order to add the tables in, and then insert them synchronously in the right order. Wouldn't it be much easier to just create all the tables/fields, then alter them to add the constraints?

This also would fix a problem I'm having, which is that you cannot create unary relationship constraints the way we're currently doing it (even though it's perfectly valid SQL). For example:

User.hasMany(User, {as: 'Children', foreignKey: 'parentId', useJunctionTable: false, onDelete: 'cascade'})
Error: Cyclic dependency found. 'user' is dependent of itself.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on May 5, 2013
  1. Martin Aspeli
  2. Martin Aspeli
  3. Martin Aspeli
  4. Martin Aspeli
  5. Martin Aspeli
  6. Martin Aspeli
  7. Martin Aspeli
  8. Martin Aspeli

    Skeletal test

    optilude authored
  9. Martin Aspeli

    Fixes after rebase

    optilude authored
  10. Martin Aspeli

    Don't pretend like composite primary keys work: they need to be handl…

    optilude authored
    …ed at table level, not attribute level
  11. Martin Aspeli
Commits on May 6, 2013
  1. Martin Aspeli

    Make cascade optional

    optilude authored
  2. Martin Aspeli
  3. Martin Aspeli
  4. Martin Aspeli
  5. Martin Aspeli

    Tests for delete cascade

    optilude authored
Commits on May 7, 2013
  1. Martin Aspeli
Commits on May 8, 2013
  1. Martin Aspeli
This page is out of date. Refresh to see the latest.
2  lib/associations/belongs-to.js
View
@@ -1,5 +1,6 @@
var Utils = require("./../utils")
, DataTypes = require('./../data-types')
+ , Helpers = require('./helpers')
module.exports = (function() {
var BelongsTo = function(srcDAO, targetDAO, options) {
@@ -24,6 +25,7 @@ module.exports = (function() {
this.identifier = this.options.foreignKey || Utils._.underscoredIf(Utils.singularize(this.target.tableName) + "Id", this.source.options.underscored)
newAttributes[this.identifier] = { type: DataTypes.INTEGER }
+ Helpers.addForeignKeyConstraints(newAttributes[this.identifier], this.target, this.source, this.options)
Utils._.defaults(this.source.rawAttributes, newAttributes)
// Sync attributes to DAO proto each time a new assoc is added
2  lib/associations/has-many.js
View
@@ -1,5 +1,6 @@
var Utils = require("./../utils")
, DataTypes = require('./../data-types')
+ , Helpers = require('./helpers')
var HasManySingleLinked = require("./has-many-single-linked")
, HasManyMultiLinked = require("./has-many-double-linked")
@@ -65,6 +66,7 @@ module.exports = (function() {
} else {
var newAttributes = {}
newAttributes[this.identifier] = { type: DataTypes.INTEGER }
+ Helpers.addForeignKeyConstraints(newAttributes[this.identifier], this.source, this.target, this.options)
Utils._.defaults(this.target.rawAttributes, newAttributes)
}
2  lib/associations/has-one.js
View
@@ -1,5 +1,6 @@
var Utils = require("./../utils")
, DataTypes = require('./../data-types')
+ , Helpers = require("./helpers")
module.exports = (function() {
var HasOne = function(srcDAO, targetDAO, options) {
@@ -29,6 +30,7 @@ module.exports = (function() {
this.identifier = this.options.foreignKey || Utils._.underscoredIf(Utils.singularize(this.source.tableName) + "Id", this.options.underscored)
newAttributes[this.identifier] = { type: DataTypes.INTEGER }
+ Helpers.addForeignKeyConstraints(newAttributes[this.identifier], this.source, this.target, this.options)
Utils._.defaults(this.target.rawAttributes, newAttributes)
// Sync attributes to DAO proto each time a new assoc is added
25 lib/associations/helpers.js
View
@@ -0,0 +1,25 @@
+var Utils = require("./../utils")
+
+module.exports = {
+
+ addForeignKeyConstraints: function(newAttribute, source, target, options) {
+ // FK constraints are opt-in: users must either rset `foreignKeyConstraints`
+ // on the association, or request an `onDelete` or `onUpdate` behaviour
+
+ if(options.foreignKeyConstraint || options.onDelete || options.onUpdate) {
+
+ // Find primary keys: composite keys not supported with this approach
+ var primaryKeys = Utils._.filter(Utils._.keys(source.rawAttributes), function(key) {
+ return source.rawAttributes[key].primaryKey
+ })
+
+ if(primaryKeys.length == 1) {
+ newAttribute.references = source.tableName,
+ newAttribute.referencesKey = primaryKeys[0]
+ newAttribute.onDelete = options.onDelete,
+ newAttribute.onUpdate = options.onUpdate
+ }
+ }
+ }
+
+}
31 lib/dao-factory-manager.js
View
@@ -1,3 +1,5 @@
+var Toposort = require('toposort-class')
+
module.exports = (function() {
var DAOFactoryManager = function(sequelize) {
this.daos = []
@@ -31,5 +33,34 @@ module.exports = (function() {
return this.daos
})
+ /**
+ * Iterate over DAOs in an order suitable for e.g. creating tables. Will
+ * take foreign key constraints into account so that dependencies are visited
+ * before dependents.
+ */
+ DAOFactoryManager.prototype.forEachDAO = function(iterator) {
+ var daos = {}
+ , sorter = new Toposort()
+
+ this.daos.forEach(function(dao) {
+ daos[dao.tableName] = dao
+ var deps = []
+
+ for(var attrName in dao.rawAttributes) {
+ if(dao.rawAttributes.hasOwnProperty(attrName)) {
+ if(dao.rawAttributes[attrName].references) {
+ deps.push(dao.rawAttributes[attrName].references)
+ }
+ }
+ }
+
+ sorter.add(dao.tableName, deps)
+ })
+
+ sorter.sort().reverse().forEach(function(name) {
+ iterator(daos[name])
+ })
+ }
+
return DAOFactoryManager
})()
42 lib/dialects/mysql/query-generator.js
View
@@ -45,6 +45,7 @@ module.exports = (function() {
var query = "CREATE TABLE IF NOT EXISTS <%= table %> (<%= attributes%>) ENGINE=<%= engine %> <%= charset %>"
, primaryKeys = []
+ , foreignKeys = {}
, attrStr = []
for (var attr in attributes) {
@@ -54,6 +55,11 @@ module.exports = (function() {
if (Utils._.includes(dataType, 'PRIMARY KEY')) {
primaryKeys.push(attr)
attrStr.push(QueryGenerator.addQuotes(attr) + " " + dataType.replace(/PRIMARY KEY/, ''))
+ } else if (Utils._.includes(dataType, 'REFERENCES')) {
+ // MySQL doesn't support inline REFERENCES declarations: move to the end
+ var m = dataType.match(/^(.+) (REFERENCES.*)$/)
+ attrStr.push(QueryGenerator.addQuotes(attr) + " " + m[1])
+ foreignKeys[attr] = m[2]
} else {
attrStr.push(QueryGenerator.addQuotes(attr) + " " + dataType)
}
@@ -72,6 +78,12 @@ module.exports = (function() {
values.attributes += ", PRIMARY KEY (" + pkString + ")"
}
+ for (var fkey in foreignKeys) {
+ if(foreignKeys.hasOwnProperty(fkey)) {
+ values.attributes += ", FOREIGN KEY (" + QueryGenerator.addQuotes(fkey) + ") " + foreignKeys[fkey]
+ }
+ }
+
return Utils._.template(query)(values).trim() + ";"
},
@@ -438,6 +450,26 @@ module.exports = (function() {
template += " PRIMARY KEY"
}
+ if(dataType.references) {
+ template += " REFERENCES " + Utils.addTicks(dataType.references)
+
+
+ if(dataType.referencesKey) {
+ template += " (" + Utils.addTicks(dataType.referencesKey) + ")"
+ } else {
+ template += " (" + Utils.addTicks('id') + ")"
+ }
+
+ if(dataType.onDelete) {
+ template += " ON DELETE " + dataType.onDelete.toUpperCase()
+ }
+
+ if(dataType.onUpdate) {
+ template += " ON UPDATE " + dataType.onUpdate.toUpperCase()
+ }
+
+ }
+
result[name] = template
} else {
result[name] = dataType
@@ -463,6 +495,16 @@ module.exports = (function() {
return fields
},
+ enableForeignKeyConstraintsQuery: function() {
+ var sql = "SET FOREIGN_KEY_CHECKS = 1;"
+ return Utils._.template(sql, {})
+ },
+
+ disableForeignKeyConstraintsQuery: function() {
+ var sql = "SET FOREIGN_KEY_CHECKS = 0;"
+ return Utils._.template(sql, {})
+ },
+
addQuotes: function(s, quoteChar) {
return Utils.addTicks(s, quoteChar)
},
39 lib/dialects/postgres/query-generator.js
View
@@ -62,7 +62,7 @@ module.exports = (function() {
var values = {
table: QueryGenerator.addQuotes(tableName),
- attributes: attrStr.join(", "),
+ attributes: attrStr.join(", ")
}
var pks = primaryKeys[tableName].map(function(pk){
@@ -78,9 +78,10 @@ module.exports = (function() {
dropTableQuery: function(tableName, options) {
options = options || {}
- var query = "DROP TABLE IF EXISTS <%= table %>;"
+ var query = "DROP TABLE IF EXISTS <%= table %><%= cascade %>;"
return Utils._.template(query)({
- table: QueryGenerator.addQuotes(tableName)
+ table: QueryGenerator.addQuotes(tableName),
+ cascade: options.cascade? " CASCADE" : ""
})
},
@@ -556,6 +557,28 @@ module.exports = (function() {
template += " PRIMARY KEY"
}
+ if(dataType.references) {
+ template += " REFERENCES <%= referencesTable %> (<%= referencesKey %>)"
+ replacements.referencesTable = QueryGenerator.addQuotes(dataType.references)
+
+ if(dataType.referencesKey) {
+ replacements.referencesKey = QueryGenerator.addQuotes(dataType.referencesKey)
+ } else {
+ replacements.referencesKey = QueryGenerator.addQuotes('id')
+ }
+
+ if(dataType.onDelete) {
+ template += " ON DELETE <%= onDeleteAction %>"
+ replacements.onDeleteAction = dataType.onDelete.toUpperCase()
+ }
+
+ if(dataType.onUpdate) {
+ template += " ON UPDATE <%= onUpdateAction %>"
+ replacements.onUpdateAction = dataType.onUpdate.toUpperCase()
+ }
+
+ }
+
result[name] = Utils._.template(template)(replacements)
} else {
result[name] = dataType
@@ -579,8 +602,16 @@ module.exports = (function() {
return fields
},
+ enableForeignKeyConstraintsQuery: function() {
+ return false // not supported by dialect
+ },
+
+ disableForeignKeyConstraintsQuery: function() {
+ return false // not supported by dialect
+ },
+
databaseConnectionUri: function(config) {
- var template = '<%= protocol %>://<%= user %>:<%= password %>@<%= host %><% if(port) { %>:<%= port %><% } %>/<%= database %>';
+ var template = '<%= protocol %>://<%= user %>:<%= password %>@<%= host %><% if(port) { %>:<%= port %><% } %>/<%= database %>'
return Utils._.template(template)({
user: encodeURIComponent(config.username),
15 lib/dialects/query-generator.js
View
@@ -229,7 +229,22 @@ module.exports = (function() {
*/
findAutoIncrementField: function(factory) {
throwMethodUndefined('findAutoIncrementField')
+ },
+
+ /*
+ Globally enable foreign key constraints
+ */
+ enableForeignKeyConstraintsQuery: function() {
+ throwMethodUndefined('enableForeignKeyConstraintsQuery')
+ },
+
+ /*
+ Globally disable foreign key constraints
+ */
+ disableForeignKeyConstraintsQuery: function() {
+ throwMethodUndefined('disableForeignKeyConstraintsQuery')
}
+
}
var throwMethodUndefined = function(methodName) {
8 lib/dialects/sqlite/connector-manager.js
View
@@ -5,7 +5,13 @@ var Utils = require("../../utils")
module.exports = (function() {
var ConnectorManager = function(sequelize) {
this.sequelize = sequelize
- this.database = new sqlite3.Database(sequelize.options.storage || ':memory:')
+ this.database = db = new sqlite3.Database(sequelize.options.storage || ':memory:', function(err) {
+ if(!err && sequelize.options.foreignKeys !== false) {
+ // Make it possible to define and use foreign key constraints unless
+ // explicitly disallowed. It's still opt-in per relation
+ db.run('PRAGMA FOREIGN_KEYS=ON')
+ }
+ })
}
Utils._.extend(ConnectorManager.prototype, require("../connector-manager").prototype)
32 lib/dialects/sqlite/query-generator.js
View
@@ -217,6 +217,28 @@ module.exports = (function() {
}
}
+ if(dataType.references) {
+ template += " REFERENCES <%= referencesTable %> (<%= referencesKey %>)"
+ replacements.referencesTable = Utils.addTicks(dataType.references)
+
+ if(dataType.referencesKey) {
+ replacements.referencesKey = Utils.addTicks(dataType.referencesKey)
+ } else {
+ replacements.referencesKey = Utils.addTicks('id')
+ }
+
+ if(dataType.onDelete) {
+ template += " ON DELETE <%= onDeleteAction %>"
+ replacements.onDeleteAction = dataType.onDelete.toUpperCase()
+ }
+
+ if(dataType.onUpdate) {
+ template += " ON UPDATE <%= onUpdateAction %>"
+ replacements.onUpdateAction = dataType.onUpdate.toUpperCase()
+ }
+
+ }
+
result[name] = Utils._.template(template)(replacements)
} else {
result[name] = dataType
@@ -242,6 +264,16 @@ module.exports = (function() {
return fields
},
+ enableForeignKeyConstraintsQuery: function() {
+ var sql = "PRAGMA foreign_keys = ON;"
+ return Utils._.template(sql, {})
+ },
+
+ disableForeignKeyConstraintsQuery: function() {
+ var sql = "PRAGMA foreign_keys = OFF;"
+ return Utils._.template(sql, {})
+ },
+
hashToWhereConditions: function(hash) {
for (var key in hash) {
if (hash.hasOwnProperty(key)) {
38 lib/query-interface.js
View
@@ -79,8 +79,8 @@ module.exports = (function() {
return queryAndEmit.call(this, sql, 'createTable')
}
- QueryInterface.prototype.dropTable = function(tableName) {
- var sql = this.QueryGenerator.dropTableQuery(tableName)
+ QueryInterface.prototype.dropTable = function(tableName, options) {
+ var sql = this.QueryGenerator.dropTableQuery(tableName, options)
return queryAndEmit.call(this, sql, 'dropTable')
}
@@ -91,11 +91,17 @@ module.exports = (function() {
var chainer = new Utils.QueryChainer()
self.showAllTables().success(function(tableNames) {
+
+ chainer.add(self, 'disableForeignKeyConstraints', [])
+
tableNames.forEach(function(tableName) {
- chainer.add(self.dropTable(tableName))
+ chainer.add(self, 'dropTable', [tableName, {cascade: true}])
})
+
+ chainer.add(self, 'enableForeignKeyConstraints', [])
+
chainer
- .run()
+ .runSerially()
.success(function() {
self.emit('dropAllTables', null)
emitter.emit('success', null)
@@ -313,6 +319,30 @@ module.exports = (function() {
}).run()
}
+ QueryInterface.prototype.enableForeignKeyConstraints = function() {
+ var sql = this.QueryGenerator.enableForeignKeyConstraintsQuery()
+ if(sql) {
+ return queryAndEmit.call(this, sql, 'enableForeignKeyConstraints')
+ } else {
+ return new Utils.CustomEventEmitter(function(emitter) {
+ this.emit('enableForeignKeyConstraints', null)
+ emitter.emit('success')
+ }).run()
+ }
+ }
+
+ QueryInterface.prototype.disableForeignKeyConstraints = function() {
+ var sql = this.QueryGenerator.disableForeignKeyConstraintsQuery()
+ if(sql){
+ return queryAndEmit.call(this, sql, 'disableForeignKeyConstraints')
+ } else {
+ return new Utils.CustomEventEmitter(function(emitter) {
+ this.emit('disableForeignKeyConstraints', null)
+ emitter.emit('success')
+ }).run()
+ }
+ }
+
// private
var queryAndEmit = function(sqlOrQueryParams, methodName, options, emitter) {
9 lib/sequelize.js
View
@@ -261,11 +261,14 @@ module.exports = (function() {
var chainer = new Utils.QueryChainer()
- this.daoFactoryManager.daos.forEach(function(dao) {
- chainer.add(dao.sync(options))
+ // Topologically sort by foreign key constraints to give us an appropriate
+ // creation order
+
+ this.daoFactoryManager.forEachDAO(function(dao) {
+ chainer.add(dao, 'sync', [options])
})
- return chainer.run()
+ return chainer.runSerially()
}
Sequelize.prototype.drop = function() {
3  package.json
View
@@ -36,7 +36,8 @@
"moment": "~1.7.0",
"commander": "~0.6.0",
"generic-pool": "1.0.9",
- "dottie": "0.0.6-1"
+ "dottie": "0.0.6-1",
+ "toposort-class": "0.1.4"
},
"devDependencies": {
"jasmine-node": "1.5.0",
64 spec-jasmine/mysql/query-generator.spec.js
View
@@ -10,6 +10,62 @@ describe('QueryGenerator', function() {
afterEach(function() { Helpers.drop() })
var suites = {
+
+ attributesToSQL: [
+ {
+ arguments: [{id: 'INTEGER'}],
+ expectation: {id: 'INTEGER'}
+ },
+ {
+ arguments: [{id: 'INTEGER', foo: 'VARCHAR(255)'}],
+ expectation: {id: 'INTEGER', foo: 'VARCHAR(255)'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER'}}],
+ expectation: {id: 'INTEGER'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', allowNull: false}}],
+ expectation: {id: 'INTEGER NOT NULL'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', allowNull: true}}],
+ expectation: {id: 'INTEGER'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', primaryKey: true, autoIncrement: true}}],
+ expectation: {id: 'INTEGER auto_increment PRIMARY KEY'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', defaultValue: 0}}],
+ expectation: {id: 'INTEGER DEFAULT 0'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', unique: true}}],
+ expectation: {id: 'INTEGER UNIQUE'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', references: 'Bar'}}],
+ expectation: {id: 'INTEGER REFERENCES `Bar` (`id`)'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', references: 'Bar', referencesKey: 'pk'}}],
+ expectation: {id: 'INTEGER REFERENCES `Bar` (`pk`)'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', references: 'Bar', onDelete: 'CASCADE'}}],
+ expectation: {id: 'INTEGER REFERENCES `Bar` (`id`) ON DELETE CASCADE'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', references: 'Bar', onUpdate: 'RESTRICT'}}],
+ expectation: {id: 'INTEGER REFERENCES `Bar` (`id`) ON UPDATE RESTRICT'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', allowNull: false, autoIncrement: true, defaultValue: 1, references: 'Bar', onDelete: 'CASCADE', onUpdate: 'RESTRICT'}}],
+ expectation: {id: 'INTEGER NOT NULL auto_increment DEFAULT 1 REFERENCES `Bar` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT'}
+ },
+ ],
+
createTableQuery: [
{
arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)'}],
@@ -26,6 +82,14 @@ describe('QueryGenerator', function() {
{
arguments: ['myTable', {title: 'ENUM("A", "B", "C")', name: 'VARCHAR(255)'}, {charset: 'latin1'}],
expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` ENUM(\"A\", \"B\", \"C\"), `name` VARCHAR(255)) ENGINE=InnoDB DEFAULT CHARSET=latin1;"
+ },
+ {
+ arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)', id: 'INTEGER PRIMARY KEY'}],
+ expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255), `id` INTEGER , PRIMARY KEY (`id`)) ENGINE=InnoDB;"
+ },
+ {
+ arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)', otherId: 'INTEGER REFERENCES `otherTable` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION'}],
+ expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255), `otherId` INTEGER, FOREIGN KEY (`otherId`) REFERENCES `otherTable` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION) ENGINE=InnoDB;"
}
],
72 spec-jasmine/postgres/query-generator.spec.js
View
@@ -14,6 +14,62 @@ describe('QueryGenerator', function() {
afterEach(function() { Helpers.drop() })
var suites = {
+
+ attributesToSQL: [
+ {
+ arguments: [{id: 'INTEGER'}],
+ expectation: {id: 'INTEGER'}
+ },
+ {
+ arguments: [{id: 'INTEGER', foo: 'VARCHAR(255)'}],
+ expectation: {id: 'INTEGER', foo: 'VARCHAR(255)'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER'}}],
+ expectation: {id: 'INTEGER'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', allowNull: false}}],
+ expectation: {id: 'INTEGER NOT NULL'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', allowNull: true}}],
+ expectation: {id: 'INTEGER'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', primaryKey: true, autoIncrement: true}}],
+ expectation: {id: 'INTEGER SERIAL PRIMARY KEY'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', defaultValue: 0}}],
+ expectation: {id: 'INTEGER DEFAULT 0'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', unique: true}}],
+ expectation: {id: 'INTEGER UNIQUE'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', references: 'Bar'}}],
+ expectation: {id: 'INTEGER REFERENCES "Bar" ("id")'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', references: 'Bar', referencesKey: 'pk'}}],
+ expectation: {id: 'INTEGER REFERENCES "Bar" ("pk")'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', references: 'Bar', onDelete: 'CASCADE'}}],
+ expectation: {id: 'INTEGER REFERENCES "Bar" ("id") ON DELETE CASCADE'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', references: 'Bar', onUpdate: 'RESTRICT'}}],
+ expectation: {id: 'INTEGER REFERENCES "Bar" ("id") ON UPDATE RESTRICT'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', allowNull: false, defaultValue: 1, references: 'Bar', onDelete: 'CASCADE', onUpdate: 'RESTRICT'}}],
+ expectation: {id: 'INTEGER NOT NULL DEFAULT 1 REFERENCES "Bar" ("id") ON DELETE CASCADE ON UPDATE RESTRICT'}
+ },
+ ],
+
createTableQuery: [
{
arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)'}],
@@ -26,6 +82,14 @@ describe('QueryGenerator', function() {
{
arguments: ['myTable', {title: 'ENUM("A", "B", "C")', name: 'VARCHAR(255)'}],
expectation: "DROP TYPE IF EXISTS \"enum_myTable_title\"; CREATE TYPE \"enum_myTable_title\" AS ENUM(\"A\", \"B\", \"C\"); CREATE TABLE IF NOT EXISTS \"myTable\" (\"title\" \"enum_myTable_title\", \"name\" VARCHAR(255));"
+ },
+ {
+ arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)', id: 'INTEGER PRIMARY KEY'}],
+ expectation: "CREATE TABLE IF NOT EXISTS \"myTable\" (\"title\" VARCHAR(255), \"name\" VARCHAR(255), \"id\" INTEGER , PRIMARY KEY (\"id\"));"
+ },
+ {
+ arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)', otherId: 'INTEGER REFERENCES "otherTable" ("id") ON DELETE CASCADE ON UPDATE NO ACTION'}],
+ expectation: "CREATE TABLE IF NOT EXISTS \"myTable\" (\"title\" VARCHAR(255), \"name\" VARCHAR(255), \"otherId\" INTEGER REFERENCES \"otherTable\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION);"
}
],
@@ -37,6 +101,14 @@ describe('QueryGenerator', function() {
{
arguments: ['mySchema.myTable'],
expectation: "DROP TABLE IF EXISTS \"mySchema\".\"myTable\";"
+ },
+ {
+ arguments: ['myTable', {cascade: true}],
+ expectation: "DROP TABLE IF EXISTS \"myTable\" CASCADE;"
+ },
+ {
+ arguments: ['mySchema.myTable', {cascade: true}],
+ expectation: "DROP TABLE IF EXISTS \"mySchema\".\"myTable\" CASCADE;"
}
],
75 spec-jasmine/sqlite/query-generator.spec.js
View
@@ -9,6 +9,81 @@ describe('QueryGenerator', function() {
afterEach(function() { Helpers.drop() })
var suites = {
+
+ attributesToSQL: [
+ {
+ arguments: [{id: 'INTEGER'}],
+ expectation: {id: 'INTEGER'}
+ },
+ {
+ arguments: [{id: 'INTEGER', foo: 'VARCHAR(255)'}],
+ expectation: {id: 'INTEGER', foo: 'VARCHAR(255)'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER'}}],
+ expectation: {id: 'INTEGER'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', allowNull: false}}],
+ expectation: {id: 'INTEGER NOT NULL'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', allowNull: true}}],
+ expectation: {id: 'INTEGER'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', primaryKey: true, autoIncrement: true}}],
+ expectation: {id: 'INTEGER PRIMARY KEY AUTOINCREMENT'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', defaultValue: 0}}],
+ expectation: {id: 'INTEGER DEFAULT 0'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', unique: true}}],
+ expectation: {id: 'INTEGER UNIQUE'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', references: 'Bar'}}],
+ expectation: {id: 'INTEGER REFERENCES `Bar` (`id`)'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', references: 'Bar', referencesKey: 'pk'}}],
+ expectation: {id: 'INTEGER REFERENCES `Bar` (`pk`)'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', references: 'Bar', onDelete: 'CASCADE'}}],
+ expectation: {id: 'INTEGER REFERENCES `Bar` (`id`) ON DELETE CASCADE'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', references: 'Bar', onUpdate: 'RESTRICT'}}],
+ expectation: {id: 'INTEGER REFERENCES `Bar` (`id`) ON UPDATE RESTRICT'}
+ },
+ {
+ arguments: [{id: {type: 'INTEGER', allowNull: false, defaultValue: 1, references: 'Bar', onDelete: 'CASCADE', onUpdate: 'RESTRICT'}}],
+ expectation: {id: 'INTEGER NOT NULL DEFAULT 1 REFERENCES `Bar` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT'}
+ },
+ ],
+
+ createTableQuery: [
+ {
+ arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)'}],
+ expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255));"
+ },
+ {
+ arguments: ['myTable', {title: 'ENUM("A", "B", "C")', name: 'VARCHAR(255)'}],
+ expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` ENUM(\"A\", \"B\", \"C\"), `name` VARCHAR(255));"
+ },
+ {
+ arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)', id: 'INTEGER PRIMARY KEY'}],
+ expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255), `id` INTEGER PRIMARY KEY);"
+ },
+ {
+ arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)', otherId: 'INTEGER REFERENCES `otherTable` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION'}],
+ expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255), `otherId` INTEGER REFERENCES `otherTable` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION);"
+ }
+ ],
+
insertQuery: [
{
arguments: ['myTable', { name: 'foo' }],
132 spec/associations/belongs-to.spec.js
View
@@ -47,4 +47,136 @@ describe(Helpers.getTestDialectTeaser("BelongsTo"), function() {
})
})
})
+
+ describe("Foreign key constraints", function() {
+
+ it("are not enabled by default", function(done) {
+ var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
+ , User = this.sequelize.define('User', { username: Sequelize.STRING })
+
+ Task.belongsTo(User)
+
+ this.sequelize.sync({ force: true }).success(function() {
+ User.create({ username: 'foo' }).success(function(user) {
+ Task.create({ title: 'task' }).success(function(task) {
+ task.setUser(user).success(function() {
+ user.destroy().success(function() {
+ Task.findAll().success(function(tasks) {
+ expect(tasks.length).toEqual(1)
+ done()
+ })
+ })
+ })
+ })
+ })
+ })
+ })
+
+ it("can cascade deletes", function(done) {
+ var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
+ , User = this.sequelize.define('User', { username: Sequelize.STRING })
+
+ Task.belongsTo(User, {onDelete: 'cascade'})
+
+ this.sequelize.sync({ force: true }).success(function() {
+ User.create({ username: 'foo' }).success(function(user) {
+ Task.create({ title: 'task' }).success(function(task) {
+ task.setUser(user).success(function() {
+ user.destroy().success(function() {
+ Task.findAll().success(function(tasks) {
+ expect(tasks.length).toEqual(0)
+ done()
+ })
+ })
+ })
+ })
+ })
+ })
+ })
+
+ it("can restrict deletes", function(done) {
+ var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
+ , User = this.sequelize.define('User', { username: Sequelize.STRING })
+
+ Task.belongsTo(User, {onDelete: 'restrict'})
+
+ this.sequelize.sync({ force: true }).success(function() {
+ User.create({ username: 'foo' }).success(function(user) {
+ Task.create({ title: 'task' }).success(function(task) {
+ task.setUser(user).success(function() {
+ user.destroy().error(function() {
+ // Should fail due to FK restriction
Jan Aagaard Meier Owner
janmeier added a note

We might want to check err.code here to make 100% sure that the right error is triggered

Martin Aspeli
optilude added a note

Do we know what it's supposed to be (in a database-agnostic way)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ Task.findAll().success(function(tasks) {
+ expect(tasks.length).toEqual(1)
+ done()
+ })
+ })
+ })
+ })
+ })
+ })
+ })
+
+ it("can cascade updates", function(done) {
+ var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
+ , User = this.sequelize.define('User', { username: Sequelize.STRING })
+
+ Task.belongsTo(User, {onUpdate: 'cascade'})
+
+ this.sequelize.sync({ force: true }).success(function() {
+ User.create({ username: 'foo' }).success(function(user) {
+ Task.create({ title: 'task' }).success(function(task) {
+ task.setUser(user).success(function() {
+
+ // Changing the id of a DAO requires a little dance since
+ // the `UPDATE` query generated by `save()` uses `id` in the
+ // `WHERE` clause
+
+ var tableName = user.QueryInterface.QueryGenerator.addSchema(user.__factory)
+ user.QueryInterface.update(user, tableName, {id: 999}, user.id)
+ .success(function() {
+ Task.findAll().success(function(tasks) {
+ expect(tasks.length).toEqual(1)
+ expect(tasks[0].UserId).toEqual(999)
+ done()
+ })
+ })
+ })
+ })
+ })
+ })
+ })
+
+ it("can restrict updates", function(done) {
+ var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
+ , User = this.sequelize.define('User', { username: Sequelize.STRING })
+
+ Task.belongsTo(User, {onUpdate: 'restrict'})
+
+ this.sequelize.sync({ force: true }).success(function() {
+ User.create({ username: 'foo' }).success(function(user) {
+ Task.create({ title: 'task' }).success(function(task) {
+ task.setUser(user).success(function() {
+
+ // Changing the id of a DAO requires a little dance since
+ // the `UPDATE` query generated by `save()` uses `id` in the
+ // `WHERE` clause
+
+ var tableName = user.QueryInterface.QueryGenerator.addSchema(user.__factory)
+ user.QueryInterface.update(user, tableName, {id: 999}, user.id)
+ .error(function() {
+ // Should fail due to FK restriction
+ Task.findAll().success(function(tasks) {
+ expect(tasks.length).toEqual(1)
+ done()
+ })
+ })
+ })
+ })
+ })
+ })
+ })
+
+ })
+
})
133 spec/associations/has-many.spec.js
View
@@ -288,7 +288,7 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() {
var add = this.spy()
- this.stub(Sequelize.Utils, 'QueryChainer').returns({ add: add, run: function(){} })
+ this.stub(Sequelize.Utils, 'QueryChainer').returns({ add: add, runSerially: function(){} })
this.sequelize.sync({ force: true })
expect(add).toHaveBeenCalledThrice()
@@ -323,4 +323,135 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() {
})
})
})
+
+ describe("Foreign key constraints", function() {
+
+ it("are not enabled by default", function(done) {
+ var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
+ , User = this.sequelize.define('User', { username: Sequelize.STRING })
+
+ User.hasMany(Task)
+
+ this.sequelize.sync({ force: true }).success(function() {
+ User.create({ username: 'foo' }).success(function(user) {
+ Task.create({ title: 'task' }).success(function(task) {
+ user.setTasks([task]).success(function() {
+ user.destroy().success(function() {
+ Task.findAll().success(function(tasks) {
+ expect(tasks.length).toEqual(1)
+ done()
+ })
+ })
+ })
+ })
+ })
+ })
+ })
+
+ it("can cascade deletes", function(done) {
+ var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
+ , User = this.sequelize.define('User', { username: Sequelize.STRING })
+
+ User.hasMany(Task, {onDelete: 'cascade'})
+
+ this.sequelize.sync({ force: true }).success(function() {
+ User.create({ username: 'foo' }).success(function(user) {
+ Task.create({ title: 'task' }).success(function(task) {
+ user.setTasks([task]).success(function() {
+ user.destroy().success(function() {
+ Task.findAll().success(function(tasks) {
+ expect(tasks.length).toEqual(0)
+ done()
+ })
+ })
+ })
+ })
+ })
+ })
+ })
+
+ it("can restrict deletes", function(done) {
+ var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
+ , User = this.sequelize.define('User', { username: Sequelize.STRING })
+
+ User.hasMany(Task, {onDelete: 'restrict'})
+
+ this.sequelize.sync({ force: true }).success(function() {
+ User.create({ username: 'foo' }).success(function(user) {
+ Task.create({ title: 'task' }).success(function(task) {
+ user.setTasks([task]).success(function() {
+ user.destroy().error(function() {
+ // Should fail due to FK restriction
+ Task.findAll().success(function(tasks) {
+ expect(tasks.length).toEqual(1)
+ done()
+ })
+ })
+ })
+ })
+ })
+ })
+ })
+
+ it("can cascade updates", function(done) {
+ var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
+ , User = this.sequelize.define('User', { username: Sequelize.STRING })
+
+ User.hasMany(Task, {onUpdate: 'cascade'})
+
+ this.sequelize.sync({ force: true }).success(function() {
+ User.create({ username: 'foo' }).success(function(user) {
+ Task.create({ title: 'task' }).success(function(task) {
+ user.setTasks([task]).success(function() {
+
+ // Changing the id of a DAO requires a little dance since
Jan Aagaard Meier Owner
janmeier added a note

I'm not quite sure what this dance is about - could you give an example of the dance-/no-dance-queries?

Martin Aspeli
optilude added a note

So basically, my first attempt was:

user.id = 9999
user.save().success(....)

The problem is that save() generates an UPDATE query like this:

UPDATE Users SET id id = 999 WHERE id = 999

because id is the primary key. So, I need to use the lower level interface.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ // the `UPDATE` query generated by `save()` uses `id` in the
+ // `WHERE` clause
+
+ var tableName = user.QueryInterface.QueryGenerator.addSchema(user.__factory)
+ user.QueryInterface.update(user, tableName, {id: 999}, user.id)
+ .success(function() {
+ Task.findAll().success(function(tasks) {
+ expect(tasks.length).toEqual(1)
+ expect(tasks[0].UserId).toEqual(999)
+ done()
+ })
+ })
+ })
+ })
+ })
+ })
+ })
+
+ it("can restrict updates", function(done) {
+ var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
+ , User = this.sequelize.define('User', { username: Sequelize.STRING })
+
+ User.hasMany(Task, {onUpdate: 'restrict'})
+
+ this.sequelize.sync({ force: true }).success(function() {
+ User.create({ username: 'foo' }).success(function(user) {
+ Task.create({ title: 'task' }).success(function(task) {
+ user.setTasks([task]).success(function() {
+
+ // Changing the id of a DAO requires a little dance since
+ // the `UPDATE` query generated by `save()` uses `id` in the
+ // `WHERE` clause
+
+ var tableName = user.QueryInterface.QueryGenerator.addSchema(user.__factory)
+ user.QueryInterface.update(user, tableName, {id: 999}, user.id)
+ .error(function() {
+ // Should fail due to FK restriction
+ Task.findAll().success(function(tasks) {
+ expect(tasks.length).toEqual(1)
+ done()
+ })
+ })
+ })
+ })
+ })
+ })
+ })
+
+ })
})
132 spec/associations/has-one.spec.js
View
@@ -47,4 +47,136 @@ describe(Helpers.getTestDialectTeaser("HasOne"), function() {
})
})
})
+
+ describe("Foreign key constraints", function() {
+
+ it("are not enabled by default", function(done) {
+ var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
+ , User = this.sequelize.define('User', { username: Sequelize.STRING })
+
+ User.hasOne(Task)
+
+ this.sequelize.sync({ force: true }).success(function() {
+ User.create({ username: 'foo' }).success(function(user) {
+ Task.create({ title: 'task' }).success(function(task) {
+ user.setTask(task).success(function() {
+ user.destroy().success(function() {
+ Task.findAll().success(function(tasks) {
+ expect(tasks.length).toEqual(1)
+ done()
+ })
+ })
+ })
+ })
+ })
+ })
+ })
+
+ it("can cascade deletes", function(done) {
+ var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
+ , User = this.sequelize.define('User', { username: Sequelize.STRING })
+
+ User.hasOne(Task, {onDelete: 'cascade'})
+
+ this.sequelize.sync({ force: true }).success(function() {
+ User.create({ username: 'foo' }).success(function(user) {
+ Task.create({ title: 'task' }).success(function(task) {
+ user.setTask(task).success(function() {
+ user.destroy().success(function() {
+ Task.findAll().success(function(tasks) {
+ expect(tasks.length).toEqual(0)
+ done()
+ })
+ })
+ })
+ })
+ })
+ })
+ })
+
+ it("can restrict deletes", function(done) {
+ var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
+ , User = this.sequelize.define('User', { username: Sequelize.STRING })
+
+ User.hasOne(Task, {onDelete: 'restrict'})
+
+ this.sequelize.sync({ force: true }).success(function() {
+ User.create({ username: 'foo' }).success(function(user) {
+ Task.create({ title: 'task' }).success(function(task) {
+ user.setTask(task).success(function() {
+ user.destroy().error(function() {
+ // Should fail due to FK restriction
+ Task.findAll().success(function(tasks) {
+ expect(tasks.length).toEqual(1)
+ done()
+ })
+ })
+ })
+ })
+ })
+ })
+ })
+
+ it("can cascade updates", function(done) {
+ var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
+ , User = this.sequelize.define('User', { username: Sequelize.STRING })
+
+ User.hasOne(Task, {onUpdate: 'cascade'})
+
+ this.sequelize.sync({ force: true }).success(function() {
+ User.create({ username: 'foo' }).success(function(user) {
+ Task.create({ title: 'task' }).success(function(task) {
+ user.setTask(task).success(function() {
+
+ // Changing the id of a DAO requires a little dance since
+ // the `UPDATE` query generated by `save()` uses `id` in the
+ // `WHERE` clause
+
+ var tableName = user.QueryInterface.QueryGenerator.addSchema(user.__factory)
+ user.QueryInterface.update(user, tableName, {id: 999}, user.id)
+ .success(function() {
+ Task.findAll().success(function(tasks) {
+ expect(tasks.length).toEqual(1)
+ expect(tasks[0].UserId).toEqual(999)
+ done()
+ })
+ })
+ })
+ })
+ })
+ })
+ })
+
+ it("can restrict updates", function(done) {
+ var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
+ , User = this.sequelize.define('User', { username: Sequelize.STRING })
+
+ User.hasOne(Task, {onUpdate: 'restrict'})
+
+ this.sequelize.sync({ force: true }).success(function() {
+ User.create({ username: 'foo' }).success(function(user) {
+ Task.create({ title: 'task' }).success(function(task) {
+ user.setTask(task).success(function() {
+
+ // Changing the id of a DAO requires a little dance since
+ // the `UPDATE` query generated by `save()` uses `id` in the
+ // `WHERE` clause
+
+ var tableName = user.QueryInterface.QueryGenerator.addSchema(user.__factory)
+ user.QueryInterface.update(user, tableName, {id: 999}, user.id)
+ .error(function() {
+ // Should fail due to FK restriction
+ Task.findAll().success(function(tasks) {
+ expect(tasks.length).toEqual(1)
+ done()
+ })
+ })
+ })
+ })
+ })
+ })
+ })
+
+ })
+
})
Something went wrong with that request. Please try again.