cassandra-migration
Version:
Simple schema migration tool for Apache Cassandra
364 lines (336 loc) • 14.4 kB
JavaScript
// Generated by CoffeeScript 1.12.7
(function() {
var FS, Q, _, applyMigration, cassandra, createVersionTable, debugMode, durations, firstNodeWithPort, getCassandraClient, getSchemaVersion, listMigrations, logDebug, logError, logInfo, migrate, moduleVersion, parseCassandraHosts, program, quietMode, readConfig, runQuery, runScript, sleep;
_ = require('lodash');
Q = require('q');
FS = require('fs');
program = require('commander');
moduleVersion = require('../package.json').version;
cassandra = require('cassandra-driver');
durations = require('durations');
quietMode = false;
debugMode = false;
sleep = function(ms) {
return new Promise(function(resolve) {
setTimeout(resolve, ms);
});
};
logError = function(message, error) {
var errorMessage, stack;
errorMessage = error != null ? ": " + error : '';
stack = (error != null) && debugMode ? "\n" + error.stack : '';
return console.error("" + message + errorMessage + stack);
};
logInfo = function(message) {
if (!quietMode) {
return console.log(message);
}
};
logDebug = function(message) {
if (!quietMode && debugMode === true) {
return console.log(message);
}
};
readConfig = function(configFile) {
return Q.nfcall(FS.readFile, configFile, 'utf-8').then(function(rawConfig) {
var config, d, error;
d = Q.defer();
try {
config = JSON.parse(rawConfig);
d.resolve(config);
} catch (error1) {
error = error1;
d.reject(error);
}
return d.promise;
}).then(function(config) {
var d;
d = Q.defer();
if (config.cassandra != null) {
d.resolve(config);
} else {
d.reject(new Error("Cassandra configuration not supplied."));
}
return d.promise;
});
};
listMigrations = function(config) {
var d, migrationsDir;
d = Q.defer();
migrationsDir = config.migrationsDir;
if (migrationsDir == null) {
d.reject(new Error("The config did not contain a migrationsDir property."));
} else if (!FS.existsSync(migrationsDir)) {
d.reject(new Error("Migrations directory does not exist."));
} else {
FS.readdir(migrationsDir, function(error, files) {
var migrationFiles;
if (error != null) {
return d.reject(new Error("Error listing migrations directory contents: " + error, error));
} else {
migrationFiles = _(files).filter(function(fileName) {
return _.endsWith(fileName.toLowerCase(), '.cql');
}).filter(function(fileName) {
var filePath;
filePath = migrationsDir + "/" + fileName;
return FS.statSync(filePath).isFile();
}).map(function(fileName) {
var file, version;
version = fileName.split('__')[0];
file = migrationsDir + "/" + fileName;
return [file, version];
}).filter(function(arg) {
var file, version;
file = arg[0], version = arg[1];
return !isNaN(version);
}).map(function(arg) {
var file, version;
file = arg[0], version = arg[1];
return [file, parseInt(version)];
}).value();
if (_(migrationFiles).size() > 0) {
return d.resolve(migrationFiles);
} else {
return d.reject(new Error("No migration files found"));
}
}
});
}
return d.promise.then(function(files) {
return files;
});
};
getCassandraClient = function(config) {
var cassandraConfig, client, d, dcAwareRoundRobinPolicy, error, nodeWhiteList, tokenAwarePolicy, whiteListPolicy;
d = Q.defer();
try {
cassandraConfig = config.cassandra;
if ((cassandraConfig.datacenterName != null) && cassandraConfig.useSingleNode) {
dcAwareRoundRobinPolicy = new cassandra.policies.loadBalancing.DCAwareRoundRobinPolicy(cassandraConfig.datacenterName);
tokenAwarePolicy = new cassandra.policies.loadBalancing.TokenAwarePolicy(dcAwareRoundRobinPolicy);
nodeWhiteList = [firstNodeWithPort(cassandraConfig)];
whiteListPolicy = new cassandra.policies.loadBalancing.WhiteListPolicy(tokenAwarePolicy, nodeWhiteList);
cassandraConfig.policies = {
loadBalancing: whiteListPolicy
};
}
client = new cassandra.Client(cassandraConfig);
client.connect(function(error) {
if (error != null) {
return d.reject(error);
} else {
logDebug("Connected to Cassandra.");
return d.resolve(client);
}
});
} catch (error1) {
error = error1;
d.reject(new Error("Error creating Cassandra client: " + error, error));
}
return d.promise;
};
firstNodeWithPort = function(cassandraConfig) {
var firstNode;
firstNode = cassandraConfig.contactPoints[0];
if (firstNode.match(/[^\:]+:[0-9]+/)) {
return firstNode;
} else {
return firstNode + ":" + cassandraConfig.protocolOptions.port;
}
};
createVersionTable = function(config, client, keyspace) {
var d, versionQuery;
d = Q.defer();
versionQuery = "SELECT release_version FROM system.local";
client.execute(versionQuery, function(error, results) {
var cassandraVersion, isVersion3orAbove, schemaKeyspace, tableNameColumn, tableQuery, tablesTable;
cassandraVersion = _(results.rows).map(function(row) {
return row.release_version;
}).first();
if (error != null) {
d.reject(error);
}
if (cassandraVersion == null) {
return d.reject(new Error("Could not determine the version of Cassandra!"));
} else {
logDebug("Cassandra version: " + cassandraVersion);
isVersion3orAbove = _.startsWith(cassandraVersion, "3.") || _.startsWith(cassandraVersion, "4.");
schemaKeyspace = isVersion3orAbove ? "system_schema" : "system";
tablesTable = isVersion3orAbove ? "tables" : "schema_columnfamilies";
tableNameColumn = isVersion3orAbove ? "table_name" : "columnfamily_name";
logDebug("schemaKeyspace: " + schemaKeyspace);
logDebug("tablesTable: " + tablesTable);
logDebug("tableNameColumn: " + tableNameColumn);
tableQuery = "SELECT " + tableNameColumn + " \nFROM " + schemaKeyspace + "." + tablesTable + " \nWHERE keyspace_name='" + keyspace + "'";
return client.execute(tableQuery, function(error, results) {
var createQuery, tableNames;
tableNames = _(results.rows).map(function(row) {
return row[tableNameColumn];
}).value();
if (error != null) {
return d.reject(error);
} else if (_(tableNames).filter(function(tableName) {
return tableName === 'schema_version';
}).size() > 0) {
logDebug("Table 'schema_version' already exists.");
return d.resolve(client);
} else {
logDebug("");
createQuery = "CREATE TABLE " + keyspace + ".schema_version (\n zero INT,\n version INT,\n migration_timestamp TIMESTAMP, \n\n PRIMARY KEY (zero, version)\n) WITH CLUSTERING ORDER BY (version DESC)";
logDebug("creating the schema_version table...");
return client.execute(createQuery, function(error, results) {
if (error != null) {
return d.reject(new Error("Error creating the schema_version table: " + error, error));
} else {
return d.resolve(client);
}
});
}
});
}
});
return d.promise;
};
getSchemaVersion = function(config, client, keyspace) {
return createVersionTable(config, client, keyspace).then(function() {
var d, versionQuery;
d = Q.defer();
logDebug("Fetching version info...");
versionQuery = "SELECT version FROM " + keyspace + ".schema_version LIMIT 1";
client.execute(versionQuery, function(error, results) {
var ref, ref1, ref2, version;
if (error != null) {
logError("Error reading version information from the version table", error);
return d.reject(new Error("Error reading version information from the version table: " + error, error));
} else if (_(results.rows).size() > 0) {
version = (ref = (ref1 = _(results.rows)) != null ? (ref2 = ref1.first()) != null ? ref2.version : void 0 : void 0) != null ? ref : 0;
version = parseInt(version);
logDebug("Current version is " + version);
return d.resolve(version);
} else {
logDebug("Current version is 0");
return d.resolve(0);
}
});
return d.promise;
});
};
runQuery = function(config, client, query, version) {
var d;
d = Q.defer();
client.execute(query, function(error, results) {
logDebug("running query: " + query);
if (error != null) {
return d.reject(new Error("Error applying migration " + version + ": " + error, error));
} else {
return d.resolve(version);
}
});
return d.promise;
};
applyMigration = function(config, client, keyspace, file, version, interval) {
var cql, queryStrings;
logInfo("Applying migration: " + file);
queryStrings = _.trim(FS.readFileSync(file, 'utf-8')).split('---');
cql = ("INSERT INTO " + keyspace + ".schema_version") + " (zero, version, migration_timestamp)" + (" VALUES (0, " + version + ", '" + (new Date().getTime()) + "');");
queryStrings.push(cql);
return queryStrings.reduce((function(promiseChain, queryString) {
return promiseChain.then(function() {
return runQuery(config, client, queryString, version).then(function() {
return sleep(interval).then(function() {
return version;
});
});
});
}), Promise.resolve());
};
migrate = function(config, client, keyspace, migrationFiles, schemaVersion, interval) {
var migrationFunctions, migrations, versionString, versions;
migrations = _(migrationFiles).filter(function(arg) {
var file, version;
file = arg[0], version = arg[1];
return version > schemaVersion && version <= config.targetVersion;
}).sortBy(function(arg) {
var file, version;
file = arg[0], version = arg[1];
return version;
}).value();
versionString = config.targetVersion === Number.MAX_VALUE ? "unlimited" : config.targetVersion;
logDebug("Migrations to be applied: " + migrations + " (target version is " + versionString + ")");
if (_(migrations).size() > 0) {
versions = _(migrations).map(function(arg) {
var file, version;
file = arg[0], version = arg[1];
return version;
}).value();
versions.unshift(schemaVersion);
logInfo("Migrating database " + (_(versions).join(" -> ")) + " ...");
migrationFunctions = _(migrations).map(function(arg) {
var file, version;
file = arg[0], version = arg[1];
return function() {
return applyMigration(config, client, keyspace, file, version, interval);
};
}).value();
return migrationFunctions.reduce(Q.when, Q(schemaVersion)).then(function(version) {
console.log("All migrations complete. Schema is now at version " + version + ".");
return version;
});
} else {
console.log("No new migrations. Schema version is " + schemaVersion);
return Q(schemaVersion);
}
};
parseCassandraHosts = function(val) {
return val.split(',');
};
runScript = function() {
var cassandraClient, code, configFile;
program.version(moduleVersion).usage('[options] <config_file>').option('-d, --debug', 'Increase verbosity and error detail').option('-h, --hosts <hosts>', 'A comma separated list of cassandra hosts', parseCassandraHosts).option('-k, --keyspace <keyspace>', 'Cassandra keyspace used for migration and schema_version table').option('-q, --quiet', 'Silence non-error output (default is false)').option('-i, --interval <interval>', 'Milliseconds to wait between migration queries are executed').option('-t, --target-version <version>', 'Maximum migration version to apply (default runs all migrations)').parse(process.argv);
configFile = _(program.args).last();
code = 1;
cassandraClient = void 0;
return readConfig(configFile).then(function(config) {
var interval, keyspace, ref, ref1, ref2, ref3, ref4, ref5;
config.quiet = (ref = program.quiet) != null ? ref : config.quiet;
config.debug = (ref1 = program.debug) != null ? ref1 : config.debug;
config.cassandra.keyspace = (ref2 = program.keyspace) != null ? ref2 : config.cassandra.keyspace;
config.targetVersion = (ref3 = program.targetVersion) != null ? ref3 : Number.MAX_VALUE;
config.cassandra.contactPoints = (ref4 = program.hosts) != null ? ref4 : config.cassandra.contactPoints;
quietMode = config.quiet;
debugMode = config.debug;
keyspace = config.cassandra.keyspace;
interval = (ref5 = program.interval) != null ? ref5 : 0;
if (config.auth != null) {
logDebug("Connecting with simple user authentication.");
config.cassandra.authProvider = new cassandra.auth.PlainTextAuthProvider(config.auth.username, config.auth.password);
} else {
logDebug("Connecting without authentication.");
}
return Q.all([listMigrations(config), getCassandraClient(config)]).spread(function(migrationFiles, client) {
cassandraClient = client;
return getSchemaVersion(config, client, keyspace).then(function(schemaVersion) {
return migrate(config, client, keyspace, migrationFiles, schemaVersion, interval);
}).then(function(version) {
return code = 0;
})["catch"](function(error) {
return logError("Migration Error", error);
});
});
})["catch"](function(error) {
return logError("Error reading configuration file", error);
})["finally"](function() {
if (cassandraClient != null) {
cassandraClient.shutdown;
}
return process.exit(code);
});
};
module.exports = {
run: runScript,
listMigrations: listMigrations
};
if (require.main === module) {
runScript();
}
}).call(this);