jones-mysql/impl/MySQLDictionary.js (420 lines of code) (raw):

/* Copyright (c) 2012, 2018, Oracle and/or its affiliates. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ "use strict"; /* Requires version 2.0 of Felix Geisendoerfer's MySQL client */ var util = require('util'), path = require("path"), fs = require("fs"), assert = require("assert"), config = require("./path_config"), jones = require("database-jones"), child_process = require("child_process"), existsSync = fs.existsSync || path.existsSync, udebug = unified_debug.getLogger("MySQLDictionary.js"); exports.DataDictionary = function(pooledConnection, dbConnectionPool) { this.connection = pooledConnection; // need connection pool only for type converters this.dbConnectionPool = dbConnectionPool; }; exports.DataDictionary.prototype.listTables = function(databaseName, user_callback) { var callback = user_callback; var showTables_callback = function(err, rows) { if (err) { err.sqlstate = err.sqlState; callback(err); } else { var result = []; var propertyName = 'Tables_in_' + databaseName; rows.forEach(function(row) { result.push(row[propertyName]); }); udebug.log('listTables function result:', result); callback(err, result); } }; this.connection.query('show tables', showTables_callback); }; function getChainedConverter(databaseTypeConverter, domainTypeConverter) { var converter = {}; if(databaseTypeConverter === undefined) { return domainTypeConverter; } if(domainTypeConverter === undefined) { return databaseTypeConverter; } converter.fromDB = function(value) { return domainTypeConverter.fromDB(databaseTypeConverter.fromDB(value)); }; converter.toDB = function(value) { return databaseTypeConverter.toDB(domainTypeConverter.toDB(value)); }; return converter; } exports.DataDictionary.prototype.getTableMetadata = function(databaseName, tableName, user_callback) { var dbConnectionPool = this.dbConnectionPool; // get precision from columnSize e.g. 10,2 var getPrecision = function(columnSize) { var precision = columnSize.split(',')[0]; return parseInt(precision, 10); }; // get scale from columnSize e.g. 10,2 var getScale = function(columnSize) { var scale = columnSize.split(',')[1]; return parseInt(scale, 10); }; var decodeIndexColumnNames = function(columnNames) { var columnNamesSplit = columnNames.split('`'); var indexColumnNames = []; var k; udebug.log_detail('decodeIndexColumnNames columnNamesSplit: ', columnNamesSplit.length, ' ', columnNamesSplit); for (k = 1; k < columnNamesSplit.length; k += 2) { indexColumnNames.push(columnNamesSplit[k]); } udebug.log_detail('decodeIndexColumnNames indexColumnNames:', indexColumnNames); return indexColumnNames; }; var convertColumnNamesToNumbers = function(columnNames, columns) { var result = []; var i, j; for (i = 0; i < columnNames.length; ++i) { udebug.log_detail('convertColumnNamesToNumbers looking for: ', columnNames[i]); for (j = 0; j < columns.length; ++j) { if (columnNames[i] === columns[j].name) { result.push(j); break; } } } return result; }; var parseCreateTable = function(tableName, statement) { udebug.log_detail('parseCreateTable: ', statement); var columns = []; var indexes = []; var foreignKeys = []; // PRIMARY unique index must be the first index indexes.push({'name': 'PRIMARY PLACEHOLDER'}); var index, indexName, usingHash; var result = {'name' : tableName, 'database' : databaseName, 'columns' : columns, 'indexes' : indexes, 'foreignKeys': foreignKeys, 'sparseContainer': null, 'invalidateCallbacks': [], 'registerInvalidateCallback': function(cb) { result.invalidateCallbacks.push(cb); }, 'invalidate': function() { result.invalidateCallbacks.forEach(function (cb) { cb(result); }); result.invalidateCallbacks = []; } }; // split lines by '\n' var lines = statement.split('\n'); var i; var foreignKey, foreignKeyName, foreignKeyColumnNames; var foreignKeyTargetTable, foreignKeyTargetDatabase, foreignKeyTargetWithDatabase; var foreignKeyTargetColumnNames; var columnNumber = 0; var columnNames, indexColumnNames, indexColumnNumbers; var column, columnName, columnNumberIndex, columnTypeAndSize, columnTypeAndSizeSplit, columnSize, columnType, unsigned, nullable, defaultValue, rawDefaultValue, line, tokens, token, j, unique; var databaseTypeConverter, domainTypeConverter, charset, collation; // first line has table name which we ignore because we already know it for (i = 1; i < lines.length; ++i) { // var defaultValue; // if DEFAULT is not specified, defaultValue is undefined // var rawDefaultValue; // if DEFAULT is specified, this is the raw text following DEFAULT line = lines[i]; if (line[line.length - 1] === ',') { // remove trailing comma from line line = line.substr(0, line.length - 1); } udebug.log_detail('\n parseCreateTable:', line); tokens = line.split(' '); j = 0; // index into tokens in the line token = tokens[j]; // remove empty tokens while (token.length === 0) { token = tokens[++j]; } unique = false; udebug.log_detail('parseCreateTable token:', token); switch (token) { case 'PRIMARY': // found primary key definition j+= 2; // skip 'PRIMARY KEY' index = {}; index.name = 'PRIMARY'; udebug.log_detail('parseCreateTable PRIMARY:', token); index.isPrimaryKey = true; index.isUnique = true; index.isOrdered = true; columnNames = tokens[j]; indexColumnNames = decodeIndexColumnNames(columnNames); udebug.log_detail('parseCreateTable PRIMARY indexColumnNames:', indexColumnNames); indexColumnNumbers = convertColumnNamesToNumbers(indexColumnNames, result.columns); udebug.log_detail('parseCreateTable PRIMARY indexColumnNumbers: ', indexColumnNumbers); index.columnNumbers = indexColumnNumbers; // mark primary key index columns with 'isInPrimaryKey' for (columnNumberIndex = 0; columnNumberIndex < indexColumnNumbers.length; ++columnNumberIndex) { columnNumber = indexColumnNumbers[columnNumberIndex]; column = columns[columnNumber]; udebug.log_detail('parseCreateTable marking column', columnNumber, columns[columnNumber].name); column.isInPrimaryKey = true; } indexes[0] =index; break; case 'UNIQUE': // found unique key definition udebug.log_detail('parseCreateTable UNIQUE:', token); unique = true; ++j; // continue with KEY handling case 'KEY': ++j; // found key definition, same as unique index = {}; indexName = tokens[j].split('`')[1]; index.name = indexName; if (unique) { index.isUnique = true; } // get column names columnNames = tokens[++j]; indexColumnNames = decodeIndexColumnNames(columnNames); udebug.log_detail('parseCreateTable KEY indexColumnNames:', indexColumnNames); indexColumnNumbers = convertColumnNamesToNumbers(indexColumnNames, result.columns); udebug.log_detail('parseCreateTable KEY indexColumnNumbers:', indexColumnNumbers); index.columnNumbers = indexColumnNumbers; usingHash = false; index.isOrdered = false; // get using statement if (++j < tokens.length) { // more tokens usingHash = -1 !== tokens[++j].indexOf('HASH'); } if (!usingHash) { // TODO create two index objects for unique btree index index.isOrdered = true; } udebug.log_detail('parseCreateTable for ', indexName, 'KEY USING HASH:', usingHash); indexes.push(index); break; case ')': // TODO found engine; get default charset break; case 'CONSTRAINT': foreignKey = {}; ++j; foreignKeyName = tokens[j++].split('`')[1]; // remove surrounding ticks foreignKey.name = foreignKeyName; // verify it is a FOREIGN KEY if (tokens[j] !== 'FOREIGN') { // unknown CONSTRAINT type; ignore it for now udebug.log_detail('ignoring unknown CONSTRAINT type: ', tokens[j], tokens[j+1], '...'); break; } j += 1; // skip past FOREIGN if (tokens[j] === 'KEY') {j += 1;} // there may be an extra blank after FOREIGN KEY before column names columnNames = tokens[j]; foreignKeyColumnNames = decodeIndexColumnNames(columnNames); udebug.log_detail('parseCreateTable FOREIGN KEY foreignKeyColumnNames:', foreignKeyColumnNames); foreignKey.columnNames = foreignKeyColumnNames; j += 1; // skip past (`columnName`, ...) if (tokens[j] !== 'REFERENCES') { // error udebug.log_detail('unexpected missing REFERENCES clause for FOREIGN KEY', tokens[j], tokens[j+1], tokens[j+2]); break; } j += 1; // skip past REFERENCES foreignKeyTargetWithDatabase = tokens[j].split('.'); // split database and table from `database`.`table` if (foreignKeyTargetWithDatabase.length == 2) { foreignKeyTargetDatabase = foreignKeyTargetWithDatabase[0].split('`')[1]; // remove surrounding ticks foreignKeyTargetTable = foreignKeyTargetWithDatabase[1].split('`')[1]; // remove surrounding ticks } else { foreignKeyTargetDatabase = databaseName; foreignKeyTargetTable = foreignKeyTargetWithDatabase[0].split('`')[1]; // remove surrounding ticks } foreignKey.targetDatabase = foreignKeyTargetDatabase; foreignKey.targetTable = foreignKeyTargetTable; j += 1; // skip past target table name columnNames = tokens[j]; foreignKeyTargetColumnNames = decodeIndexColumnNames(columnNames); udebug.log_detail('parseCreateTable REFERENCES foreignKeyTargetColumnNames:', foreignKeyTargetColumnNames); foreignKey.targetColumnNames = foreignKeyTargetColumnNames; foreignKeys.push(foreignKey); break; default: // found column definition? columnName = (token.split('`'))[1]; if (columnName === undefined) { // not a column; might be e.g. /*!50100 PARTITION BY KEY(i) */ udebug.log_detail('parseCreateTable ignoring token', token); break; } udebug.log_detail('parseCreateTable: columnName:', columnName); nullable = true; // default if no 'NOT NULL' clause unsigned = false; // default if no 'unsigned' clause column = {}; column.columnNumber = columnNumber++; // decode the column name column.name = columnName; if(columnName === "SPARSE_FIELDS") { // Note: NDB also requires (VARCHAR + UNICODE) or (VARBINARY) result.sparseContainer = columnName; } // analyze column type columnTypeAndSize = tokens[++j]; udebug.log_detail('parseCreateTable: columnDefinition:', columnTypeAndSize); columnTypeAndSizeSplit = columnTypeAndSize.split('('); columnType = columnTypeAndSizeSplit[0]; udebug.log_detail('parseCreateTable for: ', columnName, ': columnType: ', columnType); column.columnType = columnType.toLocaleUpperCase(); if (columnTypeAndSizeSplit.length > 1) { columnSize = columnTypeAndSizeSplit[1].split(')')[0]; udebug.log_detail('parseCreateTable for: ', columnName, ': columnSize: ', columnSize); } ++j; // check for unsigned if (tokens[j] === 'unsigned') { unsigned = true; ++j; } udebug.log_detail('parseCreateTable for:', columnName, ': unsigned: ', unsigned); column.isUnsigned = unsigned; // add extra metadata specific to type switch (columnType) { case 'tinyint': column.intSize = 1; column.isIntegral = true; break; case 'smallint': column.intSize = 2; column.isIntegral = true; break; case 'mediumint': column.intSize = 3; column.isIntegral = true; break; case 'int': column.intSize = 4; column.isIntegral = true; break; case 'bigint': column.intSize = 8; column.isIntegral = true; break; case 'json': break; case 'decimal' : column.precision = getPrecision(columnSize); column.scale = getScale(columnSize); break; case 'binary': case 'varbinary': column.isBinary = true; column.length = parseInt(columnSize, 10); break; case 'char': case 'varchar': column.length = parseInt(columnSize, 10); break; case 'blob': column.isBinary = true; break; case 'bit': column.length = parseInt(columnSize, 10); column.isIntegral = true; break; default: udebug.log('unknown column type', columnType, '\n', column); } // set the type converter for the column type databaseTypeConverter = dbConnectionPool.getDatabaseTypeConverter(column.columnType); domainTypeConverter = dbConnectionPool.getDomainTypeConverter(column.columnType); if (databaseTypeConverter || domainTypeConverter) { column.typeConverter = getChainedConverter(databaseTypeConverter, domainTypeConverter); } // continue parsing the rest of the column definition line // check for character set if (tokens[j] === 'CHARACTER') { charset = tokens[j + 2]; udebug.log_detail('parseCreateTable for:', columnName, ': charset: ', charset); j += 3; // skip 'CHARACTER SET charset' column.charsetName = charset; // check for collation if (tokens[j] === 'COLLATE') { collation = tokens[j + 1]; udebug.log_detail('parseCreateTable for: ', columnName, ': collation: ', collation); column.collationName = collation; j+= 2; // skip 'COLLATE collation' } } if (tokens[j] === 'NOT') { // 'NOT NULL' clause nullable = false; j += 2; // skip 'not null' } udebug.log_detail('parseCreateTable for: ', columnName, ' NOT NULL: ', !nullable); column.isNullable = nullable; if (tokens[j] === 'DEFAULT') { rawDefaultValue = tokens[j + 1]; if (rawDefaultValue === 'NULL') { // default value is null defaultValue = null; } else { // default value is a quoted string, so separate by \' // this will return the first (and presumed only) quoted string in the line rawDefaultValue = line.split('\'')[1]; if (column.isIntegral) { defaultValue = parseInt(rawDefaultValue, 10); } else { defaultValue = rawDefaultValue; } } udebug.log_detail('parseCreateTable for:', columnName, 'DEFAULT:', rawDefaultValue, 'defaultValue: (', typeof defaultValue, ')', defaultValue); // add defaultValue to model column.defaultValue = defaultValue; j += 2; // skip 'DEFAULT <value>' } if (tokens[j] === 'AUTO_INCREMENT') { column.isAutoincrement = true; j++; // skip 'AUTO_INCREMENT' } // add the column description metadata columns.push(column); break; } } // for each index that is both unique and ordered, make one ordered and a second index unique var ordered; for (i = 0; i < result.indexes.length; ++i) { index = result.indexes[i]; if (index.isUnique && index.isOrdered) { index.isOrdered = false; ordered = {}; ordered.isOrdered = true; ordered.isUnique = false; ordered.name = index.name; ordered.columnNames = index.columnNames; ordered.indexColumnNames = index.indexColumnNames; ordered.indexColumnNumbers = index.indexColumnNumbers; ordered.columnNumbers = index.columnNumbers; udebug.log_detail('MySQLDictionary creating second ordered index from unique btree index', index.name); indexes.push(ordered); } } return result; }; var callback = user_callback; var showCreateTable_callback = function(err, rows) { var result; if (err) { err.sqlstate = err.sqlState; udebug.log_detail('MySQLDictonary error from SHOW CREATE TABLE: ' + err); callback(err); } else { udebug.log_detail(rows); var row = rows[0]; // result of show create table is of the form: // [ { Table: 'tbl1', // 'Create Table': 'CREATE TABLE `tbl1` (\n `i` int(11) NOT NULL,\n `j` int(11) DEFAULT NULL,\n PRIMARY KEY (`i`)\n) ENGINE=ndbcluster DEFAULT CHARSET=latin1' } ] // the create table statement is the attribute named 'Create Table' var createTableStatement = row['Create Table']; var metadata = parseCreateTable(tableName, createTableStatement); udebug.log_detail('showCreateTable_callback.forEach metadata:', metadata); result = metadata; callback(null, result); } }; this.connection.query('show create table ' + databaseName + '.' + tableName, showCreateTable_callback); }; /* SQL DDL Utilities */ function runSQL(connectionProperties, sqlPath, callback) { var engine = connectionProperties.mysql_storage_engine || "ndb"; var statement = "set default_storage_engine=" + engine + ";\n"; statement += fs.readFileSync(sqlPath, "ASCII"); jones.openSession(connectionProperties, function(err, session) { udebug.log("MySQLMetadataManager::onSession"); if(session) { var driver = session.dbSession.pooledConnection; assert(driver); driver.query(statement, function(err) { udebug.log("MySQLMetadataManager::onQuery // err:", err); session.close(); callback(err); }); } else { callback(err); } }); } function findMetadataScript(suiteName, suitePath, file) { var path1, path2, path3; path1 = path.join(config.suites_dir, "standard", suiteName + "-" + file); path2 = path.join(config.suites_dir, suiteName, file); path3 = path.join(suitePath, file); if(existsSync(path1)) {return path1;} if(existsSync(path2)) {return path2;} if(existsSync(path3)) {return path3;} console.log("No path to:", suiteName, file); } function MySQLMetadataManager(properties) { this.sqlConnectionProperties = new jones.ConnectionProperties(properties); this.sqlConnectionProperties.isMetadataOnlyConnection = true; } MySQLMetadataManager.prototype.createTestTables = function(suiteName, suitePath, callback) { udebug.log("createTestTables", suiteName); var sqlPath = findMetadataScript(suiteName, suitePath, 'create.sql'); runSQL(this.sqlConnectionProperties, sqlPath, callback); }; MySQLMetadataManager.prototype.dropTestTables = function(suiteName, suitePath, callback) { udebug.log("dropTestTables", suiteName); var sqlPath = findMetadataScript(suiteName, suitePath, 'drop.sql'); runSQL(this.sqlConnectionProperties, sqlPath, callback); }; exports.MetadataManager = function(properties) { return new MySQLMetadataManager(properties); };