UNPKG

globalstorage

Version:

Global Storage is a Global Distributed Data Warehouse

667 lines (570 loc) 22.2 kB
'use strict'; function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } function _iterableToArrayLimit(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } var _require = require('@metarhia/common'), iter = _require.iter; var _require2 = require('metaschema'), extractDecorator = _require2.extractDecorator; var _require3 = require('./pg.utils'), escapeString = _require3.escapeString, escapeIdentifier = _require3.escapeIdentifier, validateIdentifier = _require3.validateIdentifier, PREDEFINED_DOMAINS = _require3.PREDEFINED_DOMAINS, IGNORED_DOMAINS = _require3.IGNORED_DOMAINS, classMapping = _require3.classMapping; var _require4 = require('./schema.utils'), getCategoryType = _require4.getCategoryType, isGlobalCategory = _require4.isGlobalCategory; var _require5 = require('./ddl.utils.js'), manyToManyTableName = _require5.manyToManyTableName; var pad = function pad(name, length) { var symbol = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ' '; return name + symbol.repeat(length - name.length); }; var padProperty = function padProperty(name, length) { return pad("\"".concat(name, "\""), length + 2); }; var verticalPad = function verticalPad(text) { return text ? "\n\n".concat(text) : ''; }; var createComment = function createComment(string) { var separator = "-- ".concat(string, " "); return '\n' + pad(separator, 80, '-') + '\n\n'; }; // Generates SQL to define an Enum // domainName - <string> // values - <Array>, values of an Enum // // Returns: <Object> // type - <string> // sql - <string> var createEnum = function createEnum(domainName, values) { validateIdentifier(domainName, 'enum'); var type = escapeIdentifier(domainName); var sql = createComment("Enum: ".concat(type)) + "CREATE TYPE ".concat(type, " AS ENUM (\n") + " ".concat(values.map(escapeString).join(',\n '), "\n);"); return { type: type, sql: sql }; }; var typeFromDomain = { bigint: function bigint() { return 'bigint'; }, number: function number(domain) { return domain.subtype === 'int' ? 'integer' : 'double precision'; }, string: function string() { return 'text'; }, function: function _function() { return 'text'; }, boolean: function boolean() { return 'boolean'; }, object: function object(domain, domainKind) { var type = classMapping[domain.class]; if (!type) { throw new Error("Unsupported domain class '".concat(domain.class, "' in domain '").concat(domainKind, "'")); } return type; } }; // Generates SQL to define a domain // domainName - <string> // domain - <Object> // // Returns: <Object> // type - <string> // sql - <string>, optional // // Throws: <Error>, if `domainName` is not supported // or `domain` has too many values var generateType = function generateType(domainName, domain) { var domainKind = extractDecorator(domain); if (domainKind === 'Object') { var type = PREDEFINED_DOMAINS[domainName] || typeFromDomain[domain.type](domain, domainName); return { type: type }; } if (domainKind === 'Enum') { return createEnum(domainName, domain.values); } if (domainKind === 'Flags') { if (domain.values.length <= 16) { return { type: 'smallint' }; } if (domain.values.length <= 32) { return { type: 'integer' }; } if (domain.values.length <= 64) { return { type: 'bigint' }; } throw new Error("Too many flags in ".concat(domainName, ", must not be bigger than 64")); } throw new Error("Unsupported domain: ".concat(domainName)); }; // Generates Map of domain name to SQL definition of that domain // domains - <Map>, domain definitions // // Returns: <Object> // types - <Map>, domain -> SQL type definition // typesSQL - <string> var generateTypes = function generateTypes(domains) { var sqlQueries = []; var types = iter(domains).filter(function (_ref) { var _ref2 = _slicedToArray(_ref, 1), domainName = _ref2[0]; return !IGNORED_DOMAINS.includes(domainName); }).map(function (_ref3) { var _ref4 = _slicedToArray(_ref3, 2), domainName = _ref4[0], domain = _ref4[1]; var _generateType = generateType(domainName, domain), type = _generateType.type, sql = _generateType.sql; if (sql) { sqlQueries.push(sql); } return [domainName, type]; }).collectTo(Map); return { types: types, typesSQL: sqlQueries.join('\n') }; }; // Determines if a property should have a FOREIGN KEY constraint // categories - <Map>, categories generated by metaschema // from - <string>, name of a source category // // Returns: <boolean> var requiresForeignKey = function requiresForeignKey() { return true; }; // Categorizes schema entries into indexes, unique, links, and properties // category - <Object>, category definition // categoryName - <string>, name of a category // categories - <Map>, categories generated by metaschema // // Returns: <Object> // indexes - <Object[]>, with structure { name, property } // unique - <Object[]>, with structure { name, property } // links - <Object[]>, with structure { name, property, required } // properties - <Object[]>, with structure { name, property } var categorizeEntries = function categorizeEntries(category, categoryName, categories) { var indexes = []; var unique = []; var links = []; var properties = []; iter(Object.keys(category)).filter(function (name) { return typeof category[name] !== 'function'; }).each(function (name) { validateIdentifier(name, 'entry', "".concat(categoryName, ".")); var property = category[name]; var type = extractDecorator(property); if (type === 'Include') { return; } var entry = { name: name, property: property }; if (property.index) { indexes.push(entry); } else if (property.unique) { unique.push(entry); } if (property.domain) { properties.push(entry); } else if (property.category) { entry.foreignKey = requiresForeignKey(categories, categoryName); entry.destination = isGlobalCategory(property.definition) ? 'Identifier' : property.category; if (type !== 'Many') { properties.push(entry); } if (type === 'Many' || entry.foreignKey) { links.push(entry); } } else if (type === 'Index') { indexes.push(entry); } else if (type === 'Unique') { unique.push(entry); } }); return { indexes: indexes, unique: unique, links: links, properties: properties }; }; // Calculates max length of properties in a category // properties - <Object[]>, with structure { name } // // Returns: <number> var getMaxPropLength = function getMaxPropLength(properties) { return iter(properties).map(function (property) { return property.name.length; }).reduce(Math.max); }; // Generates SQL to define properties in a table // category - <string>, category name // properties - <Array>, properties in a category // types - <Map>, domain -> SQL type definition // maxPropLength - <number>, length of properties to be padded to // // Returns: <string> var generateProperties = function generateProperties(category, properties, types, maxPropLength) { return properties.map(function (_ref5) { var name = _ref5.name, property = _ref5.property; var type = property.domain ? types.get(property.domain) : 'bigint'; var sql = "".concat(padProperty(name, maxPropLength), " ").concat(type); if (property.required) { sql += ' NOT NULL'; } if (property.default !== undefined) { sql += " DEFAULT ".concat(escapeString(property.default.toString())); } return sql; }); }; var ON_DELETE_RESTRICT_TYPES = new Set(['Object', 'Catalog', 'Subsystem', 'Hierarchy']); // Generates SQL to define a link in a table // from - <string>, name of a source category // to - <string>, name of a destination category // name - <string>, name of a link // type - <string>, type of a link // destination - <string>, name of a table to create `FOREIGN KEY` to // // Returns: <string> var generateLink = function generateLink(from, to, name, type, destination) { var constraint = "fk".concat(from).concat(name); validateIdentifier(constraint, 'constraint'); var sql = "ALTER TABLE ".concat(escapeIdentifier(from), " ") + "ADD CONSTRAINT ".concat(escapeIdentifier(constraint), " ") + "FOREIGN KEY (".concat(escapeIdentifier(name), ") "); sql += "REFERENCES \"".concat(destination, "\" (").concat(escapeIdentifier('Id'), ") ") + 'ON UPDATE RESTRICT ON DELETE '; if (ON_DELETE_RESTRICT_TYPES.has(type)) { sql += 'RESTRICT'; } else if (type === 'Master') { sql += 'CASCADE'; } else { throw new Error("".concat(type, " decorator is not supported")); } return sql + ';'; }; // Generates SQL to define many to many relationship // from - <string>, name of a source category // to - <string>, name of a destination category // name - <string>, name of a link // categories - <Map>, categories generated by metaschema // destination - <string>, name of a table to create the link to // // Returns: <string> var generateManyToMany = function generateManyToMany(from, to, name, categories, destination) { var propLength = from.length > name.length ? from.length : name.length; var tableName = manyToManyTableName(from, to, name); var table = "\nCREATE TABLE ".concat(escapeIdentifier(tableName), " (\n") + " ".concat(padProperty(from, propLength), " bigint NOT NULL,\n") + " ".concat(padProperty(name, propLength), " bigint NOT NULL\n);"); var lines = [table]; if (requiresForeignKey(categories, from)) { lines.push(generateLink(tableName, from, from, 'Master', from)); lines.push(generateLink(tableName, to, name, 'Master', destination)); } var primary = "pk".concat(name).concat(from).concat(to); validateIdentifier(primary, 'constraint'); lines.push("ALTER TABLE ".concat(escapeIdentifier(tableName), " ") + "ADD CONSTRAINT ".concat(escapeIdentifier(primary), " ") + "PRIMARY KEY (".concat(escapeIdentifier(from), ", ").concat(escapeIdentifier(name), ");")); return lines.join('\n'); }; // Generates SQL to define links in a table // links - <Array>, links // categories - <Map>, categories generated by metaschema // // Returns: <string> var generateLinks = function generateLinks(links, categories) { return links.sort(function (_ref6, _ref7) { var l1 = _ref6.link; var l2 = _ref7.link; var d1 = extractDecorator(l1); var d2 = extractDecorator(l2); if (d1 !== 'Many' && d2 === 'Many') return -1; if (d1 === 'Many' && d2 !== 'Many') return 1; return 0; }).map(function (_ref8) { var from = _ref8.from, to = _ref8.to, name = _ref8.name, link = _ref8.link, destination = _ref8.destination; var type = extractDecorator(link); return type === 'Many' ? generateManyToMany(from, to, name, categories, destination) : generateLink(from, to, name, type, destination); }).join('\n'); }; // Generates SQL to define indexes in a table // indexes - <Array>, entries that require an index // table - <string>, name of a table // // Returns: <string> var generateIndexes = function generateIndexes(indexes, table) { return indexes.map(function (_ref9) { var name = _ref9.name, index = _ref9.property; var type = extractDecorator(index); var prop = type === 'Index' ? index.fields.map(escapeIdentifier).join(', ') : escapeIdentifier(name); var indexName = "idx".concat(table).concat(name); validateIdentifier(name, 'index'); return "CREATE INDEX ".concat(escapeIdentifier(indexName), " ") + "on ".concat(escapeIdentifier(table), " (").concat(prop, ");"); }).join('\n'); }; // Generates SQL to define UNIQUE constraints in a table // unique - <Array>, entries that require UNIQUE constraint // table - <string>, name of a table // // Returns: <string> var generateUnique = function generateUnique(unique, table) { return unique.map(function (_ref10) { var name = _ref10.name, index = _ref10.property; var type = extractDecorator(index); var prop = type === 'Unique' ? index.fields.map(escapeIdentifier).join(', ') : escapeIdentifier(name); var constraint = "ak".concat(table).concat(name); validateIdentifier(constraint, 'constraint'); return "ALTER TABLE ".concat(escapeIdentifier(table), " ") + "ADD CONSTRAINT ".concat(escapeIdentifier(constraint), " UNIQUE (").concat(prop, ");"); }).join('\n'); }; // Generates SQL to define id in a table // type - <string>, type of a category // length - <number>, length to pad id to // // Returns: <string> var generateId = function generateId(type, length) { return padProperty('Id', length) + (type === 'Global' ? ' bigint' : ' bigserial'); }; // Categorizes links into resolvable and unresolvable. // category - <string>, name of category to resolve links to and from // unresolvedLinks - <Map>, unresolved links // links - <Array>, links to be categorized // existingTables - <Array>, names of defined tables // // Returns: [ <Array>, <Array> ] // unresolved - <Array>, links that can not be resolved at the moment // resolvableLinks - <Array>, links that can be resolved var getResolvableLinks = function getResolvableLinks(category, unresolvedLinks, links, existingTables) { var resolvableLinks = unresolvedLinks.get(category) || []; var unresolved = links.filter(function (_ref11) { var name = _ref11.name, link = _ref11.property, destination = _ref11.destination; if (destination === category || existingTables.includes(destination)) { resolvableLinks.push({ from: category, to: link.category, name: name, link: link, destination: destination }); return false; } return true; }); return [unresolved, resolvableLinks]; }; // Generates SQL to define a table. // name - <string>, name of a category // category - <Object>, category definition // type - <string>, type of a category // types - <Map>, domain -> SQL type definition // unresolvedLinks - <Map>, unresolved links // existingTables - <Array>, names of defined tables // categories - <Map>, categories generated by metaschema // // Returns: <Object> // sql - <string> // unresolved - <Array> var generateTable = function generateTable(name, category, type, types, unresolvedLinks, existingTables, categories) { validateIdentifier(name, 'table'); var _categorizeEntries = categorizeEntries(category, name, categories), indexes = _categorizeEntries.indexes, unique = _categorizeEntries.unique, links = _categorizeEntries.links, properties = _categorizeEntries.properties; var maxPropLength = getMaxPropLength(properties); var props = generateProperties(name, properties, types, maxPropLength); props.unshift(generateId(type, maxPropLength)); var _getResolvableLinks = getResolvableLinks(name, unresolvedLinks, links, existingTables), _getResolvableLinks2 = _slicedToArray(_getResolvableLinks, 2), unresolved = _getResolvableLinks2[0], resolvableLinks = _getResolvableLinks2[1]; var pk = "pk".concat(name, "Id"); validateIdentifier(pk, 'constraint'); var primaryKey = "ALTER TABLE ".concat(escapeIdentifier(name), " ") + "ADD CONSTRAINT ".concat(escapeIdentifier(pk), " ") + "PRIMARY KEY (".concat(escapeIdentifier('Id'), ");"); var sql = createComment("Category: ".concat(name)) + "CREATE TABLE ".concat(escapeIdentifier(name), " (\n ").concat(props.join(',\n '), "\n);") + [generateIndexes(indexes, name), generateUnique(unique, name), primaryKey, generateLinks(resolvableLinks, categories)].map(verticalPad).join(''); return { sql: sql, unresolved: unresolved }; }; // Adds new unresolved links to an existing map // links - <Object[]>, with structure { name, property, required } // unresolved - <Map>, unresolved links -> destination // category - <string>, category name var addUnresolved = function addUnresolved(links, unresolved, category) { links.forEach(function (_ref12) { var name = _ref12.name, link = _ref12.property, destination = _ref12.destination; var existingLinks = unresolved.get(destination); var newLink = { from: category, to: link.category, name: name, link: link, destination: destination }; if (existingLinks) { existingLinks.push(newLink); } else { unresolved.set(destination, [newLink]); } }); }; // Generates SQL to define tables based on a schema. // categories - <Map>, categories generated by metaschema // types - <Map>, domain -> SQL type definition // // Returns: <string> var generateTables = function generateTables(categories, types) { var unresolvedLinks = new Map(); /* links: table => [{ name, link }] */ var existingTables = []; return iter(categories).map(function (_ref13) { var _ref14 = _slicedToArray(_ref13, 2), name = _ref14[0], category = _ref14[1]; var type = getCategoryType(category.definition); return { name: name, category: category.definition, type: type }; }).filter(function (_ref15) { var type = _ref15.type; return type !== 'Ignore'; }).map(function (_ref16) { var name = _ref16.name, category = _ref16.category, type = _ref16.type; var _generateTable = generateTable(name, category, type, types, unresolvedLinks, existingTables, categories), sql = _generateTable.sql, unresolved = _generateTable.unresolved; if (unresolved) { addUnresolved(unresolved, unresolvedLinks, name); } existingTables.push(name); return sql; }).toArray().join('\n'); }; var createHistorySchema = function createHistorySchema(schema, domains, categories) { var history = Object.assign({}, schema); var dateTime = domains.get('DateTime'); history._Creation = { domain: 'DateTime', required: true, index: true, definition: dateTime }; history._Effective = { domain: 'DateTime', required: true, index: true, definition: dateTime }; history._Cancel = { domain: 'DateTime', index: true, definition: dateTime }; history._HistoryStatus = { domain: 'HistoryStatus', required: true, definition: dateTime }; history._Identifier = { category: 'Identifier', required: true, index: true, definition: categories.get('Identifier').definition }; return history; }; var preprocessSchema = function preprocessSchema(categories, domains) { var processed = new Map(); var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = categories[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var _step$value = _slicedToArray(_step.value, 2), name = _step$value[0], category = _step$value[1]; processed.set(name, category); var type = extractDecorator(category.definition); if (type === 'History') { var historyName = "".concat(name, "History"); processed.set(historyName, { name: historyName, definition: createHistorySchema(category.definition, domains, categories) }); } } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return != null) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } return processed; }; // Generate SQL to define extensions being used. // names <string[]> extension names // Returns: <string> var generateExtensions = function generateExtensions(names) { return names.map(function (extName) { return "CREATE EXTENSION IF NOT EXISTS ".concat(extName, ";\n"); }).join(''); }; // Generates SQL to define a postgres database structure based on a schema. // schema - <Metaschema> // // Returns: <string> var generateDDL = function generateDDL(schema) { var _generateTypes = generateTypes(schema.domains), types = _generateTypes.types, typesSQL = _generateTypes.typesSQL; var extensionsSQL = generateExtensions(['pgcrypto']); var tablesSQL = generateTables(preprocessSchema(schema.categories, schema.domains), types); return "".concat(extensionsSQL, "\n").concat(typesSQL, "\n").concat(tablesSQL); }; module.exports = { generateDDL: generateDDL, generateExtensions: generateExtensions, preprocessSchema: preprocessSchema, createHistorySchema: createHistorySchema, generateTables: generateTables, generateTable: generateTable, addUnresolved: addUnresolved, getResolvableLinks: getResolvableLinks, generateId: generateId, generateUnique: generateUnique, generateIndexes: generateIndexes, generateLinks: generateLinks, generateManyToMany: generateManyToMany, generateLink: generateLink, generateProperties: generateProperties, getMaxPropLength: getMaxPropLength, categorizeEntries: categorizeEntries, generateTypes: generateTypes, generateType: generateType, createEnum: createEnum, createComment: createComment, verticalPad: verticalPad, padProperty: padProperty, pad: pad };