不要怂,就是干,撸起袖子干!

Commit b61c4b83 by Sascha Depold

Merge branch 'master' into milestones/2.0.0

2 parents 574389ab d67cc9b9
...@@ -18,6 +18,7 @@ env: ...@@ -18,6 +18,7 @@ env:
- DB=mysql DIALECT=postgres - DB=mysql DIALECT=postgres
- DB=mysql DIALECT=postgres-native - DB=mysql DIALECT=postgres-native
- DB=mysql DIALECT=sqlite - DB=mysql DIALECT=sqlite
- DB=mysql DIALECT=mariadb
language: node_js language: node_js
......
...@@ -19,6 +19,8 @@ test: ...@@ -19,6 +19,8 @@ test:
fi fi
mariadb:
@DIALECT=mariadb make test
sqlite: sqlite:
@DIALECT=sqlite make test @DIALECT=sqlite make test
mysql: mysql:
...@@ -37,6 +39,6 @@ postgresn: postgres-native ...@@ -37,6 +39,6 @@ postgresn: postgres-native
# test all the dialects \o/ # test all the dialects \o/
all: sqlite mysql postgres postgres-native all: sqlite mysql postgres postgres-native mariadb
.PHONY: sqlite mysql postgres pgsql postgres-native postgresn all test .PHONY: sqlite mysql postgres pgsql postgres-native postgresn all test
...@@ -85,6 +85,7 @@ ...@@ -85,6 +85,7 @@
- [FEATURE] Support for hooks. [#894](https://github.com/sequelize/sequelize/pull/894). Thanks to durango. - [FEATURE] Support for hooks. [#894](https://github.com/sequelize/sequelize/pull/894). Thanks to durango.
- [FEATURE] Support for literals and casts. [#950](https://github.com/sequelize/sequelize/pull/950). Thanks to durango. - [FEATURE] Support for literals and casts. [#950](https://github.com/sequelize/sequelize/pull/950). Thanks to durango.
- [FEATURE] Model#findOrBuild. [#960](https://github.com/sequelize/sequelize/pull/960). Thanks to durango. - [FEATURE] Model#findOrBuild. [#960](https://github.com/sequelize/sequelize/pull/960). Thanks to durango.
- [FEATURE] Support for MariaDB. [#948](https://github.com/sequelize/sequelize/pull/948). Thanks to reedog117 and janmeier.
- [REFACTORING] hasMany now uses a single SQL statement when creating and destroying associations, instead of removing each association seperately [690](https://github.com/sequelize/sequelize/pull/690). Inspired by [#104](https://github.com/sequelize/sequelize/issues/104). janmeier - [REFACTORING] hasMany now uses a single SQL statement when creating and destroying associations, instead of removing each association seperately [690](https://github.com/sequelize/sequelize/pull/690). Inspired by [#104](https://github.com/sequelize/sequelize/issues/104). janmeier
- [REFACTORING] Consistent handling of offset across dialects. Offset is now always applied, and limit is set to max table size of not limit is given [#725](https://github.com/sequelize/sequelize/pull/725). janmeier - [REFACTORING] Consistent handling of offset across dialects. Offset is now always applied, and limit is set to max table size of not limit is given [#725](https://github.com/sequelize/sequelize/pull/725). janmeier
- [REFACTORING] Moved Jasmine to Buster and then Buster to Mocha + Chai. sdepold and durango - [REFACTORING] Moved Jasmine to Buster and then Buster to Mocha + Chai. sdepold and durango
......
...@@ -1016,7 +1016,7 @@ module.exports = (function() { ...@@ -1016,7 +1016,7 @@ module.exports = (function() {
DAOFactory.prototype.dataset = function() { DAOFactory.prototype.dataset = function() {
if (!this.__sql) { if (!this.__sql) {
this.__sql = sql.setDialect(this.daoFactoryManager.sequelize.options.dialect) this.__setSqlDialect()
} }
var instance = this.__sql.define({ name: this.tableName, columns: [] }) var instance = this.__sql.define({ name: this.tableName, columns: [] })
...@@ -1029,6 +1029,11 @@ module.exports = (function() { ...@@ -1029,6 +1029,11 @@ module.exports = (function() {
return instance return instance
} }
DAOFactory.prototype.__setSqlDialect = function() {
var dialect = this.daoFactoryManager.sequelize.options.dialect
this.__sql = sql.setDialect(dialect === 'mariadb' ? 'mysql' : dialect)
}
// private // private
var paranoidClause = function(options) { var paranoidClause = function(options) {
......
...@@ -132,7 +132,7 @@ module.exports = (function() { ...@@ -132,7 +132,7 @@ module.exports = (function() {
var definition = self.daoFactory.rawAttributes[attrName] var definition = self.daoFactory.rawAttributes[attrName]
, isHstore = !!definition.type && !!definition.type.type && definition.type.type === DataTypes.HSTORE.type , isHstore = !!definition.type && !!definition.type.type && definition.type.type === DataTypes.HSTORE.type
, isEnum = definition.type && (definition.type.toString() === DataTypes.ENUM.toString()) , isEnum = definition.type && (definition.type.toString() === DataTypes.ENUM.toString())
, isMySQL = self.daoFactory.daoFactoryManager.sequelize.options.dialect === "mysql" , isMySQL = ['mysql', 'mariadb'].indexOf(self.daoFactory.daoFactoryManager.sequelize.options.dialect) !== -1
, ciCollation = !!self.daoFactory.options.collate && self.daoFactory.options.collate.match(/_ci$/i) , ciCollation = !!self.daoFactory.options.collate && self.daoFactory.options.collate.match(/_ci$/i)
// Unfortunately for MySQL CI collation we need to map/lowercase values again // Unfortunately for MySQL CI collation we need to map/lowercase values again
......
var mariadb
, Pooling = require('generic-pool')
, Query = require("./query")
, Utils = require("../../utils")
, without = function(arr, elem) { return arr.filter(function(e) { return e != elem }) }
module.exports = (function() {
var ConnectorManager = function(sequelize, config) {
try {
if (config.dialectModulePath) {
mariadb = require(config.dialectModulePath)
} else {
mariadb = require('mariasql')
}
} catch (err) {
console.log('You need to install mariasql package manually')
}
this.sequelize = sequelize
this.client = null
this.config = config || {}
this.config.port = this.config.port || 3306
this.disconnectTimeoutId = null
this.queue = []
this.activeQueue = []
this.maxConcurrentQueries = (this.config.maxConcurrentQueries || 50)
this.poolCfg = Utils._.defaults(this.config.pool, {
maxConnections: 10,
minConnections: 0,
maxIdleTime: 1000,
handleDisconnects: false,
validate: function(client) {
return client && client.connected
}
})
this.pendingQueries = 0
this.useReplicaton = !!config.replication
this.useQueue = config.queue !== undefined ? config.queue : true
var self = this
if (this.useReplicaton) {
var reads = 0
, writes = 0
// Init configs with options from config if not present
for (var i in config.replication.read) {
config.replication.read[i] = Utils._.defaults(config.replication.read[i], {
host: this.config.host,
port: this.config.port,
username: this.config.username,
password: this.config.password,
database: this.config.database
})
}
config.replication.write = Utils._.defaults(config.replication.write, {
host: this.config.host,
port: this.config.port,
username: this.config.username,
password: this.config.password,
database: this.config.database
})
// I'll make my own pool, with blackjack and hookers!
this.pool = {
release: function (client) {
if (client.queryType == 'read') {
return this.read.release(client)
} else {
return this.write.release(client)
}
},
acquire: function (callback, priority, queryType) {
if (queryType == 'SELECT') {
this.read.acquire(callback, priority)
} else {
this.write.acquire(callback, priority)
}
},
drain: function () {
this.read.drain()
this.write.drain()
},
read: Pooling.Pool({
name: 'sequelize-read',
create: function (done) {
if (reads >= self.config.replication.read.length) {
reads = 0
}
var config = self.config.replication.read[reads++]
connect.call(self, function (err, connection) {
connection.queryType = 'read'
done(null, connection)
}, config)
},
destroy: function(client) {
disconnect.call(self, client)
},
validate: self.poolCfg.validate,
max: self.poolCfg.maxConnections,
min: self.poolCfg.minConnections,
idleTimeoutMillis: self.poolCfg.maxIdleTime
}),
write: Pooling.Pool({
name: 'sequelize-write',
create: function (done) {
connect.call(self, function (err, connection) {
connection.queryType = 'write'
done(null, connection)
}, self.config.replication.write)
},
destroy: function(client) {
disconnect.call(self, client)
},
validate: self.poolCfg.validate,
max: self.poolCfg.maxConnections,
min: self.poolCfg.minConnections,
idleTimeoutMillis: self.poolCfg.maxIdleTime
})
};
} else if (this.poolCfg) {
//the user has requested pooling, so create our connection pool
this.pool = Pooling.Pool({
name: 'sequelize-mariadb',
create: function (done) {
connect.call(self, done)
},
destroy: function(client) {
disconnect.call(self, client)
},
max: self.poolCfg.maxConnections,
min: self.poolCfg.minConnections,
validate: self.poolCfg.validate,
idleTimeoutMillis: self.poolCfg.maxIdleTime
})
}
process.on('exit', function () {
//be nice & close our connections on exit
if (self.pool) {
self.pool.drain()
} else if (self.client) {
disconnect(self.client)
}
return
})
}
Utils._.extend(ConnectorManager.prototype, require("../connector-manager").prototype)
ConnectorManager.prototype.query = function(sql, callee, options) {
if (!this.isConnected && !this.pool) {
this.connect()
}
if (this.useQueue) {
var queueItem = {
query: new Query(this.client, this.sequelize, callee, options || {}),
client: this.client,
sql: sql
}
enqueue.call(this, queueItem, options)
return queueItem.query
}
var self = this
, query = new Query(this.client, this.sequelize, callee, options || {})
this.pendingQueries++
query.done(function() {
self.pendingQueries--
if (self.pool) {
self.pool.release(query.client)
} else {
if (self.pendingQueries === 0) {
setTimeout(function() {
self.pendingQueries === 0 && self.disconnect.call(self)
}, 100)
}
}
})
if (!this.pool) {
query.run(sql)
} else {
this.pool.acquire(function(err, client) {
if (err) {
return query.emit('error', err)
}
query.client = client
query.run(sql)
return
}, undefined, options.type)
}
return query
}
ConnectorManager.prototype.connect = function() {
var self = this
// in case database is slow to connect, prevent orphaning the client
if (this.isConnecting || this.pool) {
return
}
connect.call(self, function(err, client) {
self.client = client
return
})
return
}
ConnectorManager.prototype.disconnect = function() {
if (this.client) {
disconnect.call(this, this.client)
}
return
}
// private
var disconnect = function(client) {
var self = this
if (!this.useQueue) {
this.client = null
client.end()
return
}
var intervalObj = null
var cleanup = function () {
// make sure to let queued items be finish before calling end
if (self && self.hasQueuedItems) {
return
}
client.end()
if (self && self.client) {
self.client = null
}
clearInterval(intervalObj)
}
intervalObj = setInterval(cleanup, 10)
cleanup()
}
var connect = function(done, config) {
config = config || this.config
var self = this
, client
this.isConnecting = true
var connectionConfig = {
host: config.host,
port: config.port,
user: config.username,
password: config.password,
db: config.database,
metadata: true
}
if (config.dialectOptions) {
Object.keys(config.dialectOptions).forEach(function (key) {
connectionConfig[key] = config.dialectOptions[key];
})
}
client = new mariadb()
client.connect(connectionConfig)
client
.on('error', function (err) {
self.isConnecting = false
done(err)
})
.on('connect', function () {
client.query("SET time_zone = '+0:00'").on('result', function (res) {
res.on('end', function () {
client.setMaxListeners(self.maxConcurrentQueries)
self.isConnecting = false
if (config.pool.handleDisconnects) {
handleDisconnect(self.pool, client)
}
done(null, client)
})
})
})
.on('close', function() {
disconnect.call(self, client)
})
}
var handleDisconnect = function(pool, client) {
client.on('error', function(err) {
if (err.code !== 'PROTOCOL_CONNECTION_LOST') {
throw err
}
pool.destroy(client)
})
}
var enqueue = function(queueItem, options) {
options = options || {}
if (this.activeQueue.length < this.maxConcurrentQueries) {
this.activeQueue.push(queueItem)
if (this.pool) {
var self = this
this.pool.acquire(function(err, client) {
if (err) {
queueItem.query.emit('error', err)
return
}
//we set the client here, asynchronously, when getting a pooled connection
//allowing the ConnectorManager.query method to remain synchronous
queueItem.query.client = client
queueItem.client = client
execQueueItem.call(self, queueItem)
return
}, undefined, options.type)
} else {
execQueueItem.call(this, queueItem)
}
} else {
this.queue.push(queueItem)
}
}
var dequeue = function(queueItem) {
//return the item's connection to the pool
if (this.pool) {
this.pool.release(queueItem.client)
}
this.activeQueue = without(this.activeQueue, queueItem)
}
var transferQueuedItems = function(count) {
for(var i = 0; i < count; i++) {
var queueItem = this.queue.shift()
if (queueItem) {
enqueue.call(this, queueItem)
}
}
}
var afterQuery = function(queueItem) {
dequeue.call(this, queueItem)
transferQueuedItems.call(this, this.maxConcurrentQueries - this.activeQueue.length)
disconnectIfNoConnections.call(this)
}
var execQueueItem = function(queueItem) {
var self = this
queueItem.query
.success(function(){ afterQuery.call(self, queueItem) })
.error(function(){ afterQuery.call(self, queueItem) })
queueItem.query.run(queueItem.sql, queueItem.client)
}
ConnectorManager.prototype.__defineGetter__('hasQueuedItems', function() {
return (this.queue.length > 0) || (this.activeQueue.length > 0) || (this.client && this.client._queue && (this.client._queue.length > 0))
})
// legacy
ConnectorManager.prototype.__defineGetter__('hasNoConnections', function() {
return !this.hasQueuedItems
})
ConnectorManager.prototype.__defineGetter__('isConnected', function() {
return this.client != null
})
var disconnectIfNoConnections = function() {
var self = this
this.disconnectTimeoutId && clearTimeout(this.disconnectTimeoutId)
this.disconnectTimeoutId = setTimeout(function() {
self.isConnected && !self.hasQueuedItems && self.disconnect()
}, 100)
}
return ConnectorManager
})()
var Utils = require("../../utils")
module.exports = (function() {
var QueryGenerator = {
dialect: 'mariadb'
}
// "MariaDB is a drop-in replacement for MySQL." - so thats exactly what we do, drop in the mysql query generator
return Utils._.extend(Utils._.clone(require("../mysql/query-generator")), QueryGenerator)
})()
var Utils = require("../../utils")
, AbstractQuery = require('../abstract/query')
module.exports = (function() {
var Query = function(client, sequelize, callee, options) {
this.client = client
this.callee = callee
this.sequelize = sequelize
this.options = Utils._.extend({
logging: console.log,
plain: false,
raw: false
}, options || {})
this.checkLoggingOption()
}
Utils.inherit(Query, AbstractQuery)
Query.prototype.run = function(sql) {
this.sql = sql
if (this.options.logging !== false) {
this.options.logging('Executing: ' + this.sql)
}
var resultSet = [],
errorDetected = false,
alreadyEnded = false, // This is needed because CALL queries emit 'end' twice...
self = this
this.client.query(this.sql)
.on('result', function(results) {
results
.on('row', function(row, metadata) {
for (var prop in row) {
if (row.hasOwnProperty(prop)) {
if (row[prop] === null) {
continue
}
type = metadata.types[prop]
switch (type) {
case "TINYINT":
case "SMALLINT":
case "INTEGER":
case "MEDIUMINT":
case "BIGINT":
case "YEAR":
row[prop] = parseInt(row[prop], 10)
break
case "DECIMAL":
case "FLOAT":
case "DOUBLE":
row[prop] = parseFloat(row[prop])
break
case "DATE":
case "TIMESTAMP":
case "DATETIME":
row[prop] = new Date(row[prop] + 'Z')
break
case "BIT":
case "BLOB":
case "TINYBLOB":
case "MEDIUMBLOB":
case "LONGBLOB":
if (metadata.charsetNrs[prop] === 63) { // binary
row[prop] = new Buffer(row[prop])
}
break
case "TIME":
case "CHAR":
case "VARCHAR":
case "SET":
case "ENUM":
case "GEOMETRY":
case "NULL":
break
default:
// blank
}
}
}
resultSet.push(row)
})
.on('error', function(err) {
errorDetected = true
self.emit('sql', self.sql)
self.emit('error', err, self.callee)
})
.on('end', function(info) {
if (alreadyEnded || errorDetected) {
return
}
alreadyEnded = true
self.emit('sql', self.sql)
// we need to figure out whether to send the result set
// or info depending upon the type of query
if (/^call/.test(self.sql.toLowerCase())) {
self.emit('success', resultSet)
} else if( /^show/.test(self.sql.toLowerCase()) ||
/^select/.test(self.sql.toLowerCase()) ||
/^describe/.test(self.sql.toLowerCase())) {
self.emit('success', self.formatResults(resultSet))
} else {
self.emit('success', self.formatResults(info))
}
})
})
.on('error', function(err) {
if (errorDetected) {
return
}
errorDetected = true
self.emit('sql', self.sql)
self.emit('error', err, self.callee)
})
.setMaxListeners(100)
return this
}
return Query
})()
var Utils = require("../../utils") var Utils = require("../../utils")
, DataTypes = require("../../data-types") , DataTypes = require("../../data-types")
, SqlString = require("../../sql-string")
, util = require("util") , util = require("util")
module.exports = (function() { module.exports = (function() {
......
...@@ -55,7 +55,7 @@ module.exports = (function() { ...@@ -55,7 +55,7 @@ module.exports = (function() {
return Utils._.includes(definition, 'PRIMARY KEY') return Utils._.includes(definition, 'PRIMARY KEY')
}).length > 1) }).length > 1)
, attrStr = [] , attrStr = []
, modifierLastIndex = -1
for (var attr in attributes) { for (var attr in attributes) {
if (attributes.hasOwnProperty(attr)) { if (attributes.hasOwnProperty(attr)) {
...@@ -65,6 +65,26 @@ module.exports = (function() { ...@@ -65,6 +65,26 @@ module.exports = (function() {
dataType = dataType.replace(/BIGINT/, 'INTEGER') dataType = dataType.replace(/BIGINT/, 'INTEGER')
} }
// SQLite thinks that certain modifiers should come before the length declaration,
// whereas other dialects want them after, see http://www.sqlite.org/lang_createtable.html.
// Start by finding the index of the last of the modifiers
['UNSIGNED', 'BINARY', 'ZEROFILL'].forEach(function (modifier) {
var tmpIndex = dataType.indexOf(modifier)
if (tmpIndex > modifierLastIndex) {
modifierLastIndex = tmpIndex + modifier.length
}
})
if (modifierLastIndex) {
// If a modifier was found, and a lenght declaration is given before the modifier, move the length
var length = dataType.match(/\(\s*\d+(\s*,\s*\d)?\s*\)/)
if (length && length.index < modifierLastIndex) {
dataType = dataType.replace(length[0], '')
dataType = Utils._.insert(dataType, modifierLastIndex, length[0])
}
}
if (Utils._.includes(dataType, 'PRIMARY KEY') && needsMultiplePrimaryKeys) { if (Utils._.includes(dataType, 'PRIMARY KEY') && needsMultiplePrimaryKeys) {
primaryKeys.push(attr) primaryKeys.push(attr)
attrStr.push(this.quoteIdentifier(attr) + " " + dataType.replace(/PRIMARY KEY/, 'NOT NULL')) attrStr.push(this.quoteIdentifier(attr) + " " + dataType.replace(/PRIMARY KEY/, 'NOT NULL'))
......
...@@ -81,6 +81,7 @@ module.exports = (function() { ...@@ -81,6 +81,7 @@ module.exports = (function() {
queue: true, queue: true,
native: false, native: false,
replication: false, replication: false,
ssl: undefined,
pool: {}, pool: {},
quoteIdentifiers: true, quoteIdentifiers: true,
language: 'en' language: 'en'
...@@ -101,12 +102,13 @@ module.exports = (function() { ...@@ -101,12 +102,13 @@ module.exports = (function() {
protocol: this.options.protocol, protocol: this.options.protocol,
queue : this.options.queue, queue : this.options.queue,
native : this.options.native, native : this.options.native,
ssl : this.options.ssl,
replication: this.options.replication, replication: this.options.replication,
dialectModulePath: this.options.dialectModulePath, dialectModulePath: this.options.dialectModulePath,
maxConcurrentQueries: this.options.maxConcurrentQueries, maxConcurrentQueries: this.options.maxConcurrentQueries,
dialectOptions: this.options.dialectOptions, dialectOptions: this.options.dialectOptions,
} }
try { try {
var ConnectorManager = require("./dialects/" + this.options.dialect + "/connector-manager") var ConnectorManager = require("./dialects/" + this.options.dialect + "/connector-manager")
} catch(err) { } catch(err) {
...@@ -191,7 +193,7 @@ module.exports = (function() { ...@@ -191,7 +193,7 @@ module.exports = (function() {
attributes[name].validate = attributes[name].validate || { attributes[name].validate = attributes[name].validate || {
_checkEnum: function(value, next) { _checkEnum: function(value, next) {
var hasValue = value !== undefined var hasValue = value !== undefined
, isMySQL = self.options.dialect === "mysql" , isMySQL = ['mysql', 'mariadb'].indexOf(self.options.dialect) !== -1
, ciCollation = !!options.collate && options.collate.match(/_ci$/i) !== null , ciCollation = !!options.collate && options.collate.match(/_ci$/i) !== null
, valueOutOfScope , valueOutOfScope
...@@ -244,12 +246,11 @@ module.exports = (function() { ...@@ -244,12 +246,11 @@ module.exports = (function() {
Sequelize.prototype.import = function(path) { Sequelize.prototype.import = function(path) {
// is it a relative path? // is it a relative path?
if (url.parse(path).pathname.indexOf('/') !== 0) { if (Path.normalize(path).indexOf(path.sep) !== 0) {
// make path relative to the caller // make path relative to the caller
var callerFilename = Utils.stack()[1].getFileName() var callerFilename = Utils.stack()[1].getFileName()
, callerMatch = callerFilename.match(/(.+\/).+?$/) , callerPath = Path.dirname(callerFilename)
, callerPath = callerMatch[1]
path = Path.resolve(callerPath, path) path = Path.resolve(callerPath, path)
} }
......
...@@ -57,7 +57,8 @@ ...@@ -57,7 +57,8 @@
"chai": "~1.8.0", "chai": "~1.8.0",
"mocha": "~1.13.0", "mocha": "~1.13.0",
"chai-datetime": "~1.1.1", "chai-datetime": "~1.1.1",
"sinon": "~1.7.3" "sinon": "~1.7.3",
"mariasql": "git://github.com/sequelize/node-mariasql.git"
}, },
"keywords": [ "keywords": [
"mysql", "mysql",
......
...@@ -38,5 +38,17 @@ module.exports = { ...@@ -38,5 +38,17 @@ module.exports = {
maxConnections: process.env.SEQ_PG_POOL_MAX || process.env.SEQ_POOL_MAX || 5, maxConnections: process.env.SEQ_PG_POOL_MAX || process.env.SEQ_POOL_MAX || 5,
maxIdleTime: process.env.SEQ_PG_POOL_IDLE || process.env.SEQ_POOL_IDLE || 3000 maxIdleTime: process.env.SEQ_PG_POOL_IDLE || process.env.SEQ_POOL_IDLE || 3000
} }
},
mariadb: {
database: process.env.SEQ_MYSQL_DB || process.env.SEQ_DB || 'sequelize_test',
username: process.env.SEQ_MYSQL_USER || process.env.SEQ_USER || "root",
password: process.env.SEQ_MYSQL_PW || process.env.SEQ_PW || null,
host: process.env.SEQ_MYSQL_HOST || process.env.SEQ_HOST || '127.0.0.1',
port: process.env.SEQ_MYSQL_PORT || process.env.SEQ_PORT || 3306,
pool: {
maxConnections: process.env.SEQ_MYSQL_POOL_MAX || process.env.SEQ_POOL_MAX || 1,
maxIdleTime: process.env.SEQ_MYSQL_POOL_IDLE || process.env.SEQ_POOL_IDLE || 3000
}
} }
} }
...@@ -440,7 +440,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -440,7 +440,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
describe('create', function() { describe('create', function() {
it('is possible to use casting when creating an instance', function (done) { it('is possible to use casting when creating an instance', function (done) {
var self = this var self = this
, type = dialect === "mysql" ? 'signed' : 'integer' , type = Support.dialectIsMySQL() ? 'signed' : 'integer'
, _done = _.after(2, function() { , _done = _.after(2, function() {
done() done()
}) })
...@@ -466,14 +466,14 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -466,14 +466,14 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
done() done()
}) })
if (dialect === "mysql") { if (Support.dialectIsMySQL()) {
type = this.sequelize.cast(this.sequelize.cast(this.sequelize.literal('1-2'), 'unsigned'), 'signed') type = this.sequelize.cast(this.sequelize.cast(this.sequelize.literal('1-2'), 'unsigned'), 'signed')
} }
this.User.create({ this.User.create({
intVal: type intVal: type
}).on('sql', function (sql) { }).on('sql', function (sql) {
if (dialect === "mysql") { if (Support.dialectIsMySQL()) {
expect(sql).to.contain('CAST(CAST(1-2 AS UNSIGNED) AS SIGNED)') expect(sql).to.contain('CAST(CAST(1-2 AS UNSIGNED) AS SIGNED)')
} else { } else {
expect(sql).to.contain('CAST(CAST(1-2 AS INTEGER) AS INTEGER)') expect(sql).to.contain('CAST(CAST(1-2 AS INTEGER) AS INTEGER)')
...@@ -492,7 +492,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -492,7 +492,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
var self = this var self = this
this.User.create({ this.User.create({
intVal: this.sequelize.literal('CAST(1-2 AS ' + (dialect === "mysql" ? 'SIGNED' : 'INTEGER') + ')') intVal: this.sequelize.literal('CAST(1-2 AS ' + (Support.dialectIsMySQL() ? 'SIGNED' : 'INTEGER') + ')')
}).success(function (user) { }).success(function (user) {
self.User.find(user.id).success(function (user) { self.User.find(user.id).success(function (user) {
expect(user.intVal).to.equal(-1) expect(user.intVal).to.equal(-1)
...@@ -505,7 +505,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -505,7 +505,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
var self = this var self = this
this.User.create({ this.User.create({
intVal: this.sequelize.literal(this.sequelize.cast('1-2', (dialect === "mysql" ? 'SIGNED' : 'INTEGER'))) intVal: this.sequelize.literal(this.sequelize.cast('1-2', (Support.dialectIsMySQL() ? 'SIGNED' : 'INTEGER')))
}).success(function (user) { }).success(function (user) {
self.User.find(user.id).success(function (user) { self.User.find(user.id).success(function (user) {
expect(user.intVal).to.equal(-1) expect(user.intVal).to.equal(-1)
...@@ -580,7 +580,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -580,7 +580,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
}) })
it("casts empty arrays correctly for postgresql insert", function(done) { it("casts empty arrays correctly for postgresql insert", function(done) {
if (dialect !== "postgres" && dialect !== "postgresql-native") { if (dialect !== "postgres") {
expect('').to.equal('') expect('').to.equal('')
return done() return done()
} }
...@@ -599,7 +599,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -599,7 +599,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
}) })
}) })
it("casts empty array correct for postgres update", function(done) { it("casts empty array correct for postgres update", function(done) {
if (dialect !== "postgres" && dialect !== "postgresql-native") { if (dialect !== "postgres") {
expect('').to.equal('') expect('').to.equal('')
return done() return done()
} }
...@@ -636,7 +636,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -636,7 +636,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
if (dialect === "sqlite") { if (dialect === "sqlite") {
expect(err.message).to.match(/.*SQLITE_CONSTRAINT.*/) expect(err.message).to.match(/.*SQLITE_CONSTRAINT.*/)
} }
else if (dialect === "mysql") { else if (Support.dialectIsMySQL()) {
expect(err.message).to.match(/.*Duplicate\ entry.*/) expect(err.message).to.match(/.*Duplicate\ entry.*/)
} else { } else {
expect(err.message).to.match(/.*duplicate\ key\ value.*/) expect(err.message).to.match(/.*duplicate\ key\ value.*/)
...@@ -660,7 +660,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -660,7 +660,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
UserNull.create({ username: 'foo2', smth: null }).error(function(err) { UserNull.create({ username: 'foo2', smth: null }).error(function(err) {
expect(err).to.exist expect(err).to.exist
if (dialect === "mysql") { if (Support.dialectIsMySQL()) {
// We need to allow two different errors for MySQL, see: // We need to allow two different errors for MySQL, see:
// http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html#sqlmode_strict_trans_tables // http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html#sqlmode_strict_trans_tables
expect(err.message).to.match(/(Column 'smth' cannot be null|Field 'smth' doesn't have a default value)/) expect(err.message).to.match(/(Column 'smth' cannot be null|Field 'smth' doesn't have a default value)/)
...@@ -678,7 +678,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -678,7 +678,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
if (dialect === "sqlite") { if (dialect === "sqlite") {
expect(err.message).to.match(/.*SQLITE_CONSTRAINT.*/) expect(err.message).to.match(/.*SQLITE_CONSTRAINT.*/)
} }
else if (dialect === "mysql") { else if (Support.dialectIsMySQL()) {
expect(err.message).to.match(/Duplicate entry 'foo' for key 'username'/) expect(err.message).to.match(/Duplicate entry 'foo' for key 'username'/)
} else { } else {
expect(err.message).to.match(/.*duplicate key value violates unique constraint.*/) expect(err.message).to.match(/.*duplicate key value violates unique constraint.*/)
...@@ -1487,7 +1487,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -1487,7 +1487,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
it('should be able to handle false/true values just fine...', function(done) { it('should be able to handle false/true values just fine...', function(done) {
var User = this.User var User = this.User
, escapeChar = (dialect === "postgres" || dialect === "postgres-native") ? '"' : '`' , escapeChar = (dialect === "postgres") ? '"' : '`'
User.bulkCreate([ User.bulkCreate([
{username: 'boo5', aBool: false}, {username: 'boo5', aBool: false},
...@@ -1508,7 +1508,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -1508,7 +1508,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
it('should be able to handle false/true values through associations as well...', function(done) { it('should be able to handle false/true values through associations as well...', function(done) {
var User = this.User var User = this.User
, escapeChar = (dialect === "postgres" || dialect === "postgres-native") ? '"' : '`' , escapeChar = (dialect === "postgres") ? '"' : '`'
var Passports = this.sequelize.define('Passports', { var Passports = this.sequelize.define('Passports', {
isActive: Sequelize.BOOLEAN isActive: Sequelize.BOOLEAN
}) })
...@@ -3626,12 +3626,12 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -3626,12 +3626,12 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
expect(schemas[0]).to.be.instanceof(Array) expect(schemas[0]).to.be.instanceof(Array)
// sqlite & MySQL doesn't actually create schemas unless Model.sync() is called // sqlite & MySQL doesn't actually create schemas unless Model.sync() is called
// Postgres supports schemas natively // Postgres supports schemas natively
expect(schemas[0]).to.have.length((dialect === "postgres" || dialect === "postgres-native" ? 2 : 1)) expect(schemas[0]).to.have.length((dialect === "postgres" ? 2 : 1))
done() done()
}) })
}) })
if (dialect === "mysql" || dialect === "sqlite") { if (Support.dialectIsMySQL() || dialect === "sqlite") {
it("should take schemaDelimiter into account if applicable", function(done){ it("should take schemaDelimiter into account if applicable", function(done){
var UserSpecialUnderscore = this.sequelize.define('UserSpecialUnderscore', {age: Sequelize.INTEGER}, {schema: 'hello', schemaDelimiter: '_'}) var UserSpecialUnderscore = this.sequelize.define('UserSpecialUnderscore', {age: Sequelize.INTEGER}, {schema: 'hello', schemaDelimiter: '_'})
var UserSpecialDblUnderscore = this.sequelize.define('UserSpecialDblUnderscore', {age: Sequelize.INTEGER}) var UserSpecialDblUnderscore = this.sequelize.define('UserSpecialDblUnderscore', {age: Sequelize.INTEGER})
...@@ -3665,26 +3665,26 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -3665,26 +3665,26 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
UserPublic.schema('special').sync({ force: true }).success(function() { UserPublic.schema('special').sync({ force: true }).success(function() {
self.sequelize.queryInterface.describeTable('Publics') self.sequelize.queryInterface.describeTable('Publics')
.on('sql', function(sql) { .on('sql', function(sql) {
if (dialect === "sqlite" || dialect === "mysql") { if (dialect === "sqlite" || Support.dialectIsMySQL()) {
expect(sql).to.not.contain('special') expect(sql).to.not.contain('special')
_done() _done()
} }
}) })
.success(function(table) { .success(function(table) {
if (dialect === "postgres" || dialect === "postgres-native") { if (dialect === "postgres") {
expect(table.id.defaultValue).to.not.contain('special') expect(table.id.defaultValue).to.not.contain('special')
_done() _done()
} }
self.sequelize.queryInterface.describeTable('Publics', 'special') self.sequelize.queryInterface.describeTable('Publics', 'special')
.on('sql', function(sql) { .on('sql', function(sql) {
if (dialect === "sqlite" || dialect === "mysql") { if (dialect === "sqlite" || Support.dialectIsMySQL()) {
expect(sql).to.contain('special') expect(sql).to.contain('special')
_done() _done()
} }
}) })
.success(function(table) { .success(function(table) {
if (dialect === "postgres" || dialect === "postgres-native") { if (dialect === "postgres") {
expect(table.id.defaultValue).to.contain('special') expect(table.id.defaultValue).to.contain('special')
_done() _done()
} }
...@@ -3814,7 +3814,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -3814,7 +3814,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
Post.sync().on('sql', function(sql) { Post.sync().on('sql', function(sql) {
if (dialect === 'postgres') { if (dialect === 'postgres') {
expect(sql).to.match(/"authorId" INTEGER REFERENCES "authors" \("id"\)/) expect(sql).to.match(/"authorId" INTEGER REFERENCES "authors" \("id"\)/)
} else if (dialect === 'mysql') { } else if (Support.dialectIsMySQL()) {
expect(sql).to.match(/FOREIGN KEY \(`authorId`\) REFERENCES `authors` \(`id`\)/) expect(sql).to.match(/FOREIGN KEY \(`authorId`\) REFERENCES `authors` \(`id`\)/)
} else if (dialect === 'sqlite') { } else if (dialect === 'sqlite') {
expect(sql).to.match(/`authorId` INTEGER REFERENCES `authors` \(`id`\)/) expect(sql).to.match(/`authorId` INTEGER REFERENCES `authors` \(`id`\)/)
...@@ -3844,7 +3844,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -3844,7 +3844,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
Post.sync().on('sql', function(sql) { Post.sync().on('sql', function(sql) {
if (dialect === 'postgres') { if (dialect === 'postgres') {
expect(sql).to.match(/"authorId" INTEGER REFERENCES "authors" \("id"\)/) expect(sql).to.match(/"authorId" INTEGER REFERENCES "authors" \("id"\)/)
} else if (dialect === 'mysql') { } else if (Support.dialectIsMySQL()) {
expect(sql).to.match(/FOREIGN KEY \(`authorId`\) REFERENCES `authors` \(`id`\)/) expect(sql).to.match(/FOREIGN KEY \(`authorId`\) REFERENCES `authors` \(`id`\)/)
} else if (dialect === 'sqlite') { } else if (dialect === 'sqlite') {
expect(sql).to.match(/`authorId` INTEGER REFERENCES `authors` \(`id`\)/) expect(sql).to.match(/`authorId` INTEGER REFERENCES `authors` \(`id`\)/)
...@@ -3882,8 +3882,10 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -3882,8 +3882,10 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
done() done()
} }
}).error(function(err) { }).error(function(err) {
if (dialect === 'mysql') { if (Support.dialectIsMySQL(true)) {
expect(err.message).to.match(/ER_CANNOT_ADD_FOREIGN|ER_CANT_CREATE_TABLE/) expect(err.message).to.match(/ER_CANNOT_ADD_FOREIGN|ER_CANT_CREATE_TABLE/)
} else if (dialect === 'mariadb') {
expect(err.message).to.match(/Can\'t create table/)
} else if (dialect === 'sqlite') { } else if (dialect === 'sqlite') {
// the parser should not end up here ... see above // the parser should not end up here ... see above
expect(1).to.equal(2) expect(1).to.equal(2)
...@@ -3913,7 +3915,9 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -3913,7 +3915,9 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
describe("dataset", function() { describe("dataset", function() {
it("returns a node-sql instance with the correct dialect", function() { it("returns a node-sql instance with the correct dialect", function() {
expect(this.User.dataset().sql.dialectName).to.equal(dialect) var _dialect = dialect === 'mariadb' ? 'mysql' : dialect
expect(this.User.dataset().sql.dialectName).to.equal(_dialect)
}) })
it("allows me to generate sql queries", function() { it("allows me to generate sql queries", function() {
...@@ -3936,6 +3940,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -3936,6 +3940,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
var sql = this.User.select("username").toSql() var sql = this.User.select("username").toSql()
var sqlMap = { var sqlMap = {
postgres: 'SELECT username FROM "' + this.User.tableName + '";', postgres: 'SELECT username FROM "' + this.User.tableName + '";',
mariadb: 'SELECT username FROM `' + this.User.tableName + '`;',
mysql: 'SELECT username FROM `' + this.User.tableName + '`;', mysql: 'SELECT username FROM `' + this.User.tableName + '`;',
sqlite: 'SELECT username FROM "' + this.User.tableName + '";' sqlite: 'SELECT username FROM "' + this.User.tableName + '";'
} }
...@@ -3946,6 +3951,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () { ...@@ -3946,6 +3951,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
var sql = this.User.select("username").select("firstName").group("username").toSql() var sql = this.User.select("username").select("firstName").group("username").toSql()
var sqlMap = { var sqlMap = {
postgres: 'SELECT username, firstName FROM "' + this.User.tableName + '" GROUP BY username;', postgres: 'SELECT username, firstName FROM "' + this.User.tableName + '" GROUP BY username;',
mariadb: 'SELECT username, firstName FROM `' + this.User.tableName + '` GROUP BY username;',
mysql: 'SELECT username, firstName FROM `' + this.User.tableName + '` GROUP BY username;', mysql: 'SELECT username, firstName FROM `' + this.User.tableName + '` GROUP BY username;',
sqlite: 'SELECT username, firstName FROM "' + this.User.tableName + '" GROUP BY username;' sqlite: 'SELECT username, firstName FROM "' + this.User.tableName + '" GROUP BY username;'
} }
......
...@@ -1032,7 +1032,7 @@ describe(Support.getTestDialectTeaser("DAO"), function () { ...@@ -1032,7 +1032,7 @@ describe(Support.getTestDialectTeaser("DAO"), function () {
if (dialect === "postgres" || dialect === "postgres-native") { if (dialect === "postgres" || dialect === "postgres-native") {
expect(sql).to.equal('DELETE FROM "UserDestroys" WHERE "newId" IN (SELECT "newId" FROM "UserDestroys" WHERE "newId"=\'123ABC\' LIMIT 1)') expect(sql).to.equal('DELETE FROM "UserDestroys" WHERE "newId" IN (SELECT "newId" FROM "UserDestroys" WHERE "newId"=\'123ABC\' LIMIT 1)')
} }
else if (dialect === "mysql") { else if (Support.dialectIsMySQL()) {
expect(sql).to.equal("DELETE FROM `UserDestroys` WHERE `newId`='123ABC' LIMIT 1") expect(sql).to.equal("DELETE FROM `UserDestroys` WHERE `newId`='123ABC' LIMIT 1")
} else { } else {
expect(sql).to.equal("DELETE FROM `UserDestroys` WHERE `newId`='123ABC'") expect(sql).to.equal("DELETE FROM `UserDestroys` WHERE `newId`='123ABC'")
......
...@@ -7,7 +7,7 @@ var chai = require('chai') ...@@ -7,7 +7,7 @@ var chai = require('chai')
chai.Assertion.includeStack = true chai.Assertion.includeStack = true
if (dialect.match(/^mysql/)) { if (Support.dialectIsMySQL()) {
describe('[MYSQL Specific] Associations', function() { describe('[MYSQL Specific] Associations', function() {
describe('many-to-many', function() { describe('many-to-many', function() {
describe('where tables have the same prefix', function() { describe('where tables have the same prefix', function() {
......
...@@ -7,7 +7,7 @@ var chai = require('chai') ...@@ -7,7 +7,7 @@ var chai = require('chai')
chai.Assertion.includeStack = true chai.Assertion.includeStack = true
if (dialect.match(/^mysql/)) { if (Support.dialectIsMySQL()) {
describe('[MYSQL Specific] Connector Manager', function() { describe('[MYSQL Specific] Connector Manager', function() {
it('works correctly after being idle', function(done) { it('works correctly after being idle', function(done) {
var User = this.sequelize.define('User', { username: DataTypes.STRING }) var User = this.sequelize.define('User', { username: DataTypes.STRING })
......
...@@ -8,7 +8,7 @@ var chai = require('chai') ...@@ -8,7 +8,7 @@ var chai = require('chai')
chai.Assertion.includeStack = true chai.Assertion.includeStack = true
if (dialect.match(/^mysql/)) { if (Support.dialectIsMySQL()) {
describe("[MYSQL Specific] DAOFactory", function () { describe("[MYSQL Specific] DAOFactory", function () {
describe('constructor', function() { describe('constructor', function() {
it("handles extended attributes (unique)", function(done) { it("handles extended attributes (unique)", function(done) {
......
...@@ -9,7 +9,7 @@ var chai = require('chai') ...@@ -9,7 +9,7 @@ var chai = require('chai')
chai.Assertion.includeStack = true chai.Assertion.includeStack = true
if (dialect.match(/^mysql/)) { if (Support.dialectIsMySQL()) {
describe("[MYSQL Specific] QueryGenerator", function () { describe("[MYSQL Specific] QueryGenerator", function () {
var suites = { var suites = {
attributesToSQL: [ attributesToSQL: [
......
...@@ -18,7 +18,7 @@ describe(Support.getTestDialectTeaser("QueryGenerators"), function () { ...@@ -18,7 +18,7 @@ describe(Support.getTestDialectTeaser("QueryGenerators"), function () {
User.sync({ force: true }).success(function() { User.sync({ force: true }).success(function() {
var sql = '' var sql = ''
if (dialect === "mysql") { if (Support.dialectIsMySQL()) {
sql = 'SELECT COLUMN_COMMENT as cmt FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = \'' + self.sequelize.config.database + '\' AND TABLE_NAME = \'Users\' AND COLUMN_NAME = \'username\''; sql = 'SELECT COLUMN_COMMENT as cmt FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = \'' + self.sequelize.config.database + '\' AND TABLE_NAME = \'Users\' AND COLUMN_NAME = \'username\'';
} }
else if (dialect === "postgres" || dialect === "postgres-native") { else if (dialect === "postgres" || dialect === "postgres-native") {
......
...@@ -13,7 +13,7 @@ chai.Assertion.includeStack = true ...@@ -13,7 +13,7 @@ chai.Assertion.includeStack = true
var qq = function(str) { var qq = function(str) {
if (dialect == 'postgres' || dialect == 'sqlite') { if (dialect == 'postgres' || dialect == 'sqlite') {
return '"' + str + '"' return '"' + str + '"'
} else if (dialect == 'mysql') { } else if (Support.dialectIsMySQL()) {
return '`' + str + '`' return '`' + str + '`'
} else { } else {
return str return str
...@@ -147,7 +147,7 @@ describe(Support.getTestDialectTeaser("Sequelize"), function () { ...@@ -147,7 +147,7 @@ describe(Support.getTestDialectTeaser("Sequelize"), function () {
}) })
}) })
if (dialect == 'mysql') { if (Support.dialectIsMySQL()) {
it('executes stored procedures', function(done) { it('executes stored procedures', function(done) {
var self = this var self = this
self.sequelize.query(this.insertQuery).success(function() { self.sequelize.query(this.insertQuery).success(function() {
......
...@@ -91,6 +91,10 @@ if (dialect === 'sqlite') { ...@@ -91,6 +91,10 @@ if (dialect === 'sqlite') {
expectation: "CREATE TABLE IF NOT EXISTS `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) BINARY', number: 'INTEGER(5) UNSIGNED'}],
expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR BINARY(255), `number` INTEGER UNSIGNED(5));"
},
{
arguments: ['myTable', {title: 'ENUM("A", "B", "C")', 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));" expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` ENUM(\"A\", \"B\", \"C\"), `name` VARCHAR(255));"
}, },
......
...@@ -101,6 +101,19 @@ var Support = { ...@@ -101,6 +101,19 @@ var Support = {
return envDialect return envDialect
}, },
dialectIsMySQL: function(strict) {
var envDialect = process.env.DIALECT || 'mysql'
if (strict === undefined) {
strict = false
}
if (strict) {
return envDialect === 'mysql'
} else {
return ['mysql', 'mariadb'].indexOf(envDialect) !== -1
}
},
getTestDialectTeaser: function(moduleName) { getTestDialectTeaser: function(moduleName) {
var dialect = this.getTestDialect() var dialect = this.getTestDialect()
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!