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

Commit ddcb3c4e by Matt Broadstone

add ability to test for dialect transaction support

Transaction support is currently very limited in the MSSQL dialect,
this allows us to conditionally skip transaction related tests for
a given dialect. I imagine it's also a nice feature to have when
adding a new dialect, so you don't have to solve all one thousand
errors at the same time
1 parent 49b6352f
......@@ -32,6 +32,7 @@ AbstractDialect.prototype.supports = {
/* What is the dialect's keyword for INSERT IGNORE */
'IGNORE': '',
schemas: false,
transactions: true,
constraints: {
restrict: true
},
......
......@@ -25,6 +25,8 @@ describe(Support.getTestDialectTeaser("BelongsTo"), function() {
})
describe('getAssociation', function() {
if (current.dialect.supports.transactions) {
it('supports transactions', function(done) {
Support.prepareTransactionTest(this.sequelize, function(sequelize) {
var User = sequelize.define('User', { username: Support.Sequelize.STRING })
......@@ -57,6 +59,7 @@ describe(Support.getTestDialectTeaser("BelongsTo"), function() {
})
})
})
}
it('does not modify the passed arguments', function () {
var User = this.sequelize.define('user', {})
......@@ -126,6 +129,8 @@ describe(Support.getTestDialectTeaser("BelongsTo"), function() {
})
describe('setAssociation', function() {
if (current.dialect.supports.transactions) {
it('supports transactions', function(done) {
Support.prepareTransactionTest(this.sequelize, function(sequelize) {
var User = sequelize.define('User', { username: Support.Sequelize.STRING })
......@@ -151,6 +156,7 @@ describe(Support.getTestDialectTeaser("BelongsTo"), function() {
})
})
})
}
it('can set the association with declared primary keys...', function(done) {
var User = this.sequelize.define('UserXYZ', { user_id: {type: DataTypes.INTEGER, primaryKey: true }, username: DataTypes.STRING })
......@@ -277,6 +283,7 @@ describe(Support.getTestDialectTeaser("BelongsTo"), function() {
})
})
if (current.dialect.supports.transactions) {
it('supports transactions', function(done) {
Support.prepareTransactionTest(this.sequelize, function(sequelize) {
var User = sequelize.define('User', { username: Support.Sequelize.STRING })
......@@ -303,6 +310,7 @@ describe(Support.getTestDialectTeaser("BelongsTo"), function() {
})
})
})
}
})
describe("foreign key", function () {
......
......@@ -50,6 +50,7 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
expect(Object.keys(this.Label.rawAttributes).length).to.equal(3);
});
if (current.dialect.supports.transactions) {
it('supports transactions', function() {
var Article, Label, sequelize, article, label, t;
return Support.prepareTransactionTest(this.sequelize).then(function(_sequelize) {
......@@ -87,6 +88,7 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
});
});
});
}
it('does not have any labels assigned to it initially', function() {
return Promise.all([
......@@ -155,6 +157,7 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
return this.sequelize.sync({ force: true });
});
if (current.dialect.supports.transactions) {
it('supports transactions', function () {
return Support.prepareTransactionTest(this.sequelize).bind({}).then(function(sequelize) {
this.sequelize = sequelize;
......@@ -190,6 +193,7 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
return this.t.rollback();
});
});
}
it('answers false if only some labels have been assigned', function() {
return Promise.all([
......@@ -249,6 +253,8 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
});
describe('setAssociations', function() {
if (current.dialect.supports.transactions) {
it('supports transactions', function() {
return Support.prepareTransactionTest(this.sequelize).bind({}).then(function(sequelize) {
this.Article = sequelize.define('Article', { 'title': DataTypes.STRING });
......@@ -279,6 +285,7 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
return this.t.rollback();
});
});
}
it("clears associations when passing null to the set-method", function() {
var User = this.sequelize.define('User', { username: DataTypes.STRING })
......@@ -366,6 +373,7 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
});
describe('addAssociations', function() {
if (current.dialect.supports.transactions) {
it('supports transactions', function() {
return Support.prepareTransactionTest(this.sequelize).bind({}).then(function(sequelize) {
this.Article = sequelize.define('Article', { 'title': DataTypes.STRING });
......@@ -397,6 +405,7 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
return this.t.rollback();
});
});
}
it('supports passing the primary key instead of an object', function () {
var Article = this.sequelize.define('Article', { 'title': DataTypes.STRING })
......@@ -521,6 +530,7 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
});
});
if (current.dialect.supports.transactions) {
it('supports transactions', function() {
return Support.prepareTransactionTest(this.sequelize).bind({}).then(function(sequelize) {
this.sequelize = sequelize;
......@@ -551,6 +561,7 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
return this.t.rollback();
});
});
}
it('supports passing the field option', function () {
var Article = this.sequelize.define('Article', {
......@@ -774,6 +785,7 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
});
});
if (current.dialect.supports.transactions) {
it('supports transactions', function() {
return Support.prepareTransactionTest(this.sequelize).bind({}).then(function(sequelize) {
this.sequelize = sequelize;
......@@ -807,6 +819,7 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
return this.t.rollback();
});
});
}
it('gets all associated objects with all fields', function() {
return this.User.find({where: {username: 'John'}}).then(function (john) {
......@@ -1095,6 +1108,7 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
});
});
if (current.dialect.supports.transactions) {
it('supports transactions', function() {
return Support.prepareTransactionTest(this.sequelize).bind({}).then(function(sequelize) {
this.User = sequelize.define('User', { username: DataTypes.STRING });
......@@ -1125,6 +1139,7 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
return this.t.rollback();
});
});
}
it('supports setting through table attributes', function () {
var User = this.sequelize.define('user', {})
......@@ -1207,6 +1222,7 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
});
});
if (current.dialect.supports.transactions) {
it('supports transactions', function() {
return Support.prepareTransactionTest(this.sequelize).bind({}).then(function(sequelize) {
this.User = sequelize.define('User', { username: DataTypes.STRING });
......@@ -1277,7 +1293,7 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
return this.t.rollback();
});
});
}
it('supports passing the primary key instead of an object', function () {
var User = this.sequelize.define('User', { username: DataTypes.STRING })
......
......@@ -23,6 +23,7 @@ describe(Support.getTestDialectTeaser("HasOne"), function() {
})
describe('getAssocation', function() {
if (current.dialect.supports.transactions) {
it('supports transactions', function(done) {
Support.prepareTransactionTest(this.sequelize, function(sequelize) {
var User = sequelize.define('User', { username: Support.Sequelize.STRING })
......@@ -57,6 +58,7 @@ describe(Support.getTestDialectTeaser("HasOne"), function() {
})
})
})
}
it('does not modify the passed arguments', function () {
var User = this.sequelize.define('user', {})
......@@ -98,6 +100,7 @@ describe(Support.getTestDialectTeaser("HasOne"), function() {
})
describe('setAssociation', function() {
if (current.dialect.supports.transactions) {
it('supports transactions', function(done) {
Support.prepareTransactionTest(this.sequelize, function(sequelize) {
var User = sequelize.define('User', { username: Support.Sequelize.STRING })
......@@ -128,6 +131,7 @@ describe(Support.getTestDialectTeaser("HasOne"), function() {
})
})
})
}
it('can set an association with predefined primary keys', function(done) {
var User = this.sequelize.define('UserXYZZ', { userCoolIdTag: { type: Sequelize.INTEGER, primaryKey: true }, username: Sequelize.STRING })
......@@ -227,6 +231,7 @@ describe(Support.getTestDialectTeaser("HasOne"), function() {
})
})
if (current.dialect.supports.transactions) {
it('supports transactions', function(done) {
Support.prepareTransactionTest(this.sequelize, function(sequelize) {
var User = sequelize.define('User', { username: Sequelize.STRING })
......@@ -255,6 +260,8 @@ describe(Support.getTestDialectTeaser("HasOne"), function() {
})
})
})
}
})
describe('foreign key', function () {
......
......@@ -12,6 +12,8 @@ var chai = require('chai')
, _ = require('lodash')
, moment = require('moment')
, async = require('async')
, current = Support.sequelize;
chai.use(datetime)
chai.config.includeStack = true
......@@ -694,6 +696,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
describe('find', function() {
if (current.dialect.supports.transactions) {
it('supports the transaction option in the first parameter', function(done) {
Support.prepareTransactionTest(this.sequelize, function(sequelize) {
var User = sequelize.define('User', { username: Sequelize.STRING, foo: Sequelize.STRING })
......@@ -710,6 +713,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
})
}
it('should not fail if model is paranoid and where is an empty array', function(done) {
var User = this.sequelize.define('User', { username: Sequelize.STRING }, { paranoid: true })
......@@ -732,6 +736,8 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
describe('findOrInitialize', function() {
if (current.dialect.supports.transactions) {
it('supports transactions', function(done) {
Support.prepareTransactionTest(this.sequelize, function(sequelize) {
var User = sequelize.define('User', { username: Sequelize.STRING, foo: Sequelize.STRING })
......@@ -763,6 +769,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
})
}
describe('returns an instance if it already exists', function() {
it('with a single find field', function (done) {
......@@ -822,6 +829,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
describe('update', function() {
if (current.dialect.supports.transactions) {
it('supports transactions', function(done) {
Support.prepareTransactionTest(this.sequelize, function(sequelize) {
var User = sequelize.define('User', { username: Sequelize.STRING })
......@@ -843,6 +851,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
})
}
it('updates the attributes that we select only without updating createdAt', function(done) {
var User = this.sequelize.define('User1', {
......@@ -1040,6 +1049,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
describe('destroy', function() {
if (current.dialect.supports.transactions) {
it('supports transactions', function(done) {
Support.prepareTransactionTest(this.sequelize, function(sequelize) {
var User = sequelize.define('User', { username: Sequelize.STRING })
......@@ -1061,6 +1071,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
})
}
it('deletes values that match filter', function(done) {
var self = this
......@@ -1436,6 +1447,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
describe('count', function() {
if (current.dialect.supports.transactions) {
it('supports transactions', function(done) {
Support.prepareTransactionTest(this.sequelize, function(sequelize) {
var User = sequelize.define('User', { username: Sequelize.STRING })
......@@ -1455,6 +1467,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
})
}
it('counts all created objects', function(done) {
var self = this
......@@ -1536,6 +1549,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
if (current.dialect.supports.transactions) {
it('supports transactions', function(done) {
Support.prepareTransactionTest(this.sequelize, function(sequelize) {
var User = sequelize.define('User', { age: Sequelize.INTEGER })
......@@ -1555,6 +1569,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
})
}
it("should return the min value", function(done) {
var self = this
......@@ -1625,6 +1640,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
if (current.dialect.supports.transactions) {
it('supports transactions', function(done) {
Support.prepareTransactionTest(this.sequelize, function(sequelize) {
var User = sequelize.define('User', { age: Sequelize.INTEGER })
......@@ -1644,6 +1660,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
})
}
it("should return the max value for a field named the same as an SQL reserved keyword", function(done) {
var self = this
......@@ -2170,6 +2187,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
.then(function() { done() })
})
if (current.dialect.supports.transactions) {
it('supports transactions', function(done) {
Support.prepareTransactionTest(this.sequelize, function(sequelize) {
var User = sequelize.define('User', { username: Sequelize.STRING })
......@@ -2189,6 +2207,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
})
}
it("selects all users with name 'foo'", function(done) {
this
......@@ -2430,7 +2449,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
if (dialect !== 'sqlite') {
if (dialect !== 'sqlite' && current.dialect.supports.transactions) {
it('supports multiple async transactions', function(done) {
this.timeout(25000);
Support.prepareTransactionTest(this.sequelize, function(sequelize) {
......
......@@ -10,6 +10,8 @@ var chai = require('chai')
, datetime = require('chai-datetime')
, _ = require('lodash')
, assert = require('assert')
, current = Support.sequelize;
chai.use(datetime)
chai.config.includeStack = true
......@@ -41,6 +43,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
});
describe('findOrCreate', function () {
if (current.dialect.supports.transactions) {
it("supports transactions", function(done) {
var self = this;
this.sequelize.transaction().then(function(t) {
......@@ -70,6 +73,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
})
}
it("returns instance if already existent. Single find field.", function(done) {
var self = this,
......@@ -137,6 +141,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
if (current.dialect.supports.transactions) {
it("should release transaction when meeting errors", function(){
var self = this
......@@ -160,8 +165,10 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
return test(0);
})
}
describe('several concurrent calls', function () {
if (current.dialect.supports.transactions) {
it('works with a transaction', function () {
return this.sequelize.transaction().bind(this).then(function (transaction) {
return Promise.join(
......@@ -186,6 +193,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
);
});
});
}
// Creating two concurrent transactions and selecting / inserting from the same table throws sqlite off
(dialect !== 'sqlite' ? it : it.skip)('works without a transaction', function () {
......@@ -287,6 +295,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
});
});
if (current.dialect.supports.transactions) {
it('supports transactions', function(done) {
var self = this;
this.sequelize.transaction().then(function(t) {
......@@ -303,6 +312,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
})
}
it('is possible to use casting when creating an instance', function (done) {
var self = this
......@@ -960,6 +970,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
describe('bulkCreate', function() {
if (current.dialect.supports.transactions) {
it("supports transactions", function(done) {
var self = this;
this.sequelize.transaction().then(function(t) {
......@@ -976,6 +987,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
})
}
it('properly handles disparate field lists', function(done) {
var self = this
......
......@@ -11,6 +11,8 @@ var chai = require('chai')
, promised = require("chai-as-promised")
, _ = require('lodash')
, async = require('async')
, current = Support.sequelize;
chai.use(promised);
chai.use(datetime)
......@@ -33,6 +35,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
describe('find', function() {
if (current.dialect.supports.transactions) {
it('supports transactions', function(done) {
Support.prepareTransactionTest(this.sequelize, function(sequelize) {
var User = sequelize.define('User', { username: Sequelize.STRING })
......@@ -59,6 +62,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
})
}
describe('general / basic function', function() {
beforeEach(function(done) {
......
......@@ -11,6 +11,8 @@ var chai = require('chai')
, _ = require('lodash')
, moment = require('moment')
, async = require('async')
, current = Support.sequelize;
chai.use(datetime)
chai.config.includeStack = true
......@@ -33,6 +35,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
describe('findAll', function() {
if (current.dialect.supports.transactions) {
it('supports transactions', function(done) {
Support.prepareTransactionTest(this.sequelize, function(sequelize) {
var User = sequelize.define('User', { username: Sequelize.STRING })
......@@ -58,6 +61,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
})
}
describe('special where conditions/smartWhere object', function() {
beforeEach(function(done) {
......@@ -1364,6 +1368,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
if (current.dialect.supports.transactions) {
it('supports transactions', function(done) {
Support.prepareTransactionTest(this.sequelize, function(sequelize) {
var User = sequelize.define('User', { username: Sequelize.STRING })
......@@ -1384,6 +1389,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
})
}
it("handles where clause [only]", function(done) {
this.User.findAndCountAll({where: "id != " + this.users[0].id}).success(function(info) {
......@@ -1503,6 +1509,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
if (current.dialect.supports.transactions) {
it('supports transactions', function(done) {
Support.prepareTransactionTest(this.sequelize, function(sequelize) {
var User = sequelize.define('User', { username: Sequelize.STRING })
......@@ -1522,6 +1529,7 @@ describe(Support.getTestDialectTeaser("DAOFactory"), function () {
})
})
})
}
it("should return all users", function(done) {
this.User.all().on('success', function(users) {
......
......@@ -9,6 +9,8 @@ var chai = require('chai')
, datetime = require('chai-datetime')
, uuid = require('node-uuid')
, _ = require('lodash')
, current = Support.sequelize;
chai.use(datetime)
chai.config.includeStack = true
......@@ -272,6 +274,7 @@ describe(Support.getTestDialectTeaser("DAO"), function () {
})
})
if (current.dialect.supports.transactions) {
it('supports transactions', function(done) {
Support.prepareTransactionTest(this.sequelize, function(sequelize) {
var User = sequelize.define('User', { number: Support.Sequelize.INTEGER })
......@@ -293,6 +296,7 @@ describe(Support.getTestDialectTeaser("DAO"), function () {
})
})
})
}
it('supports where conditions', function(done) {
var self = this
......@@ -416,6 +420,7 @@ describe(Support.getTestDialectTeaser("DAO"), function () {
this.User.create({ id: 1, aNumber: 0, bNumber: 0 }).complete(done)
})
if (current.dialect.supports.transactions) {
it('supports transactions', function(done) {
Support.prepareTransactionTest(this.sequelize, function(sequelize) {
var User = sequelize.define('User', { number: Support.Sequelize.INTEGER })
......@@ -437,6 +442,7 @@ describe(Support.getTestDialectTeaser("DAO"), function () {
})
})
})
}
it('with array', function(done) {
var self = this
......@@ -544,6 +550,7 @@ describe(Support.getTestDialectTeaser("DAO"), function () {
})
describe('reload', function () {
if (current.dialect.supports.transactions) {
it('supports transactions', function(done) {
Support.prepareTransactionTest(this.sequelize, function(sequelize) {
var User = sequelize.define('User', { username: Support.Sequelize.STRING })
......@@ -565,6 +572,7 @@ describe(Support.getTestDialectTeaser("DAO"), function () {
})
})
})
}
it("should return a reference to the same DAO instead of creating a new one", function(done) {
this.User.create({ username: 'John Doe' }).complete(function(err, originalUser) {
......@@ -748,6 +756,7 @@ describe(Support.getTestDialectTeaser("DAO"), function () {
})
describe('save', function() {
if (current.dialect.supports.transactions) {
it('supports transactions', function(done) {
Support.prepareTransactionTest(this.sequelize, function(sequelize) {
var User = sequelize.define('User', { username: Support.Sequelize.STRING })
......@@ -767,6 +776,7 @@ describe(Support.getTestDialectTeaser("DAO"), function () {
})
})
})
}
it('only updates fields in passed array', function(done) {
var self = this
......@@ -1600,6 +1610,7 @@ describe(Support.getTestDialectTeaser("DAO"), function () {
})
describe('updateAttributes', function() {
if (current.dialect.supports.transactions) {
it('supports transactions', function(done) {
Support.prepareTransactionTest(this.sequelize, function(sequelize) {
var User = sequelize.define('User', { username: Support.Sequelize.STRING })
......@@ -1621,6 +1632,7 @@ describe(Support.getTestDialectTeaser("DAO"), function () {
})
})
})
}
it("updates attributes in the database", function(done) {
this.User.create({ username: 'user' }).success(function(user) {
......@@ -1734,6 +1746,7 @@ describe(Support.getTestDialectTeaser("DAO"), function () {
})
describe('destroy', function() {
if (current.dialect.supports.transactions) {
it('supports transactions', function(done) {
Support.prepareTransactionTest(this.sequelize, function(sequelize) {
var User = sequelize.define('User', { username: Support.Sequelize.STRING })
......@@ -1755,6 +1768,7 @@ describe(Support.getTestDialectTeaser("DAO"), function () {
})
})
})
}
it('deletes a record from the database if dao is not paranoid', function(done) {
var UserDestroy = this.sequelize.define('UserDestroy', {
......
......@@ -11,6 +11,8 @@ var chai = require('chai')
, Transaction = require(__dirname + '/../lib/transaction')
, path = require('path')
, sinon = require('sinon')
, current = Support.sequelize;
chai.config.includeStack = true
......@@ -866,6 +868,7 @@ describe(Support.getTestDialectTeaser("Sequelize"), function () {
})
})
if (current.dialect.supports.transactions) {
describe('transaction', function() {
beforeEach(function(done) {
var self = this
......@@ -1115,5 +1118,6 @@ describe(Support.getTestDialectTeaser("Sequelize"), function () {
})
})
})
}
})
})
......@@ -3,6 +3,10 @@ var chai = require('chai')
, Support = require(__dirname + '/support')
, Promise = require(__dirname + '/../lib/promise')
, Transaction = require(__dirname + '/../lib/transaction')
, current = Support.sequelize;
if (current.dialect.supports.transactions) {
describe(Support.getTestDialectTeaser("Sequelize#transaction"), function () {
this.timeout(4000);
......@@ -182,3 +186,5 @@ describe(Support.getTestDialectTeaser("Sequelize#transaction"), function () {
})
})
})
}
......@@ -4,7 +4,6 @@ var chai = require('chai')
, expect = chai.expect
, Support = require(__dirname + '/support')
, dialect = Support.getTestDialect()
, Transaction = require(__dirname + '/../lib/transaction')
, Sequelize = require(__dirname + '/../index')
, Promise = Sequelize.Promise
, sinon = require('sinon');
......
......@@ -7,6 +7,8 @@ var chai = require('chai')
, sinon = require('sinon')
, current = Support.sequelize;
if (current.dialect.supports.transactions) {
describe(Support.getTestDialectTeaser("Transaction"), function () {
this.timeout(4000);
describe('constructor', function() {
......@@ -173,3 +175,5 @@ describe(Support.getTestDialectTeaser("Transaction"), function () {
});
}
});
}
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!