UNPKG

globalstorage

Version:

Global Storage is a Global Distributed Data Warehouse

230 lines (204 loc) 6.05 kB
'use strict'; const { extractDecorator } = require('metaschema'); const transformations = require('./transformations'); const escapeString = value => { let backslash = false; let escaped = "'"; for (const character of value) { if (character === "'") { escaped += "''"; } else if (character === '\\') { escaped += '\\\\'; backslash = true; } else { escaped += character; } } escaped += "'"; return backslash ? ` E${escaped}` : escaped; }; const escapeIdentifier = name => `"${name}"`; const escapeKey = key => key .split('.') .map(k => (k === '*' ? '*' : escapeIdentifier(k))) .join('.'); const escapeValue = value => { const type = typeof value; if (type === 'number') return value; if (type === 'string') return escapeString(value); if (value instanceof Date) return `'${value.toISOString()}'`; throw new TypeError('Unsupported value (${value}) type'); }; const PREDEFINED_DOMAINS = { Time: 'time with time zone', DateDay: 'date', DateTime: 'timestamp with time zone', JSON: 'jsonb', Money: 'money', }; const IGNORED_DOMAINS = ['List', 'Enum', 'HashMap', 'HashSet']; // https://tools.ietf.org/html/rfc3629#section-3 const utf8bytesLastCodePoints = { 1: 0x007f, 2: 0x07ff, 3: 0xffff, }; const utf8codePointSize = codePoint => { for (let byteCount = 1; byteCount <= 3; byteCount++) { if (codePoint <= utf8bytesLastCodePoints[byteCount]) { return byteCount; } } return 4; }; const asciiCP = { aToZLower: ['a'.codePointAt(0), 'z'.codePointAt(0)], aToZUpper: ['A'.codePointAt(0), 'Z'.codePointAt(0)], underscore: '_'.codePointAt(0), numbers: ['0'.codePointAt(0), '9'.codePointAt(0)], dollar: '$'.codePointAt(0), }; const classMapping = { Uint8Array: 'bytea', Date: 'timestamp with time zone', Uint64: 'bigint', }; // https://github.com/postgres/postgres/blob/5f6b0e6d69f1087847c8456b3f69761c950d52c6/src/backend/utils/adt/misc.c#L723 const isValidIdentifierStart = cp => cp === asciiCP.underscore || (cp >= asciiCP.aToZLower[0] && cp <= asciiCP.aToZLower[1]) || (cp >= asciiCP.aToZUpper[0] && cp <= asciiCP.aToZUpper[1]) || cp >= 0x80; // https://github.com/postgres/postgres/blob/5f6b0e6d69f1087847c8456b3f69761c950d52c6/src/backend/utils/adt/misc.c#L741 const isValidIdentifierCont = cp => (cp >= asciiCP.numbers[0] && cp <= asciiCP.numbers[1]) || cp === asciiCP.dollar || isValidIdentifierStart(cp); const singleUnitUtf16 = (1 << 16) - 1; // https://www.postgresql.org/docs/10/static/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS const isValidIdentifier = name => { let length = 0; for (let i = 0; i < name.length; i++) { const codePoint = name.codePointAt(i); if (i === 0) { if (!isValidIdentifierStart(codePoint)) { return false; } } else if (!isValidIdentifierCont(codePoint)) { return false; } length += utf8codePointSize(codePoint); if (codePoint > singleUnitUtf16) i++; } return length < 64; }; const validateIdentifier = (name, type, prefix = '') => { if (!isValidIdentifier(name)) { throw new Error( `Cannot create ${type} ${prefix}${name} ` + 'because it is not a valid identifier' ); } }; const generateQueryParams = (count, start = 1) => { let params = ''; for (let i = start; i < start + count; i++) { if (i !== start) { params += ', '; } params += `$${i}`; } return params; }; const generateLinkQueryParams = (count, start = 1) => { let params = ''; for (let i = start + 1; i < start + 1 + count; i++) { if (i !== start + 1) { params += ', '; } params += `($${start}, $${i})`; } return params; }; const buildWhere = query => { const constraints = transformations.constraints(query); const constraintsKeys = Object.keys(constraints); if (constraintsKeys.length === 0) { return ['', []]; } const params = new Array(constraintsKeys.length); return [ ' WHERE ' + constraintsKeys .map((key, i) => { const [cond, value] = constraints[key]; params[i] = value; return `${escapeKey(key)} ${cond === '!' ? '!=' : cond} $${i + 1}`; }) .join(' AND '), params, ]; }; const generateDeleteQuery = (category, includedCategories, query) => { const escapedCategory = escapeIdentifier(category); let deleteQuery = `WITH ToDelete AS (SELECT ${escapedCategory}."Id"` + ` FROM ${escapedCategory}`; includedCategories.forEach(category => { const includedCategory = escapeIdentifier(category); deleteQuery += ` INNER JOIN ${includedCategory} ON` + ` ${escapedCategory}."Id" = ${includedCategory}."Id"`; }); const [where, whereParams] = buildWhere(query); deleteQuery += where + ')'; includedCategories.forEach(category => { const includedCategory = escapeIdentifier(category); deleteQuery += `, ${category} AS (DELETE FROM ${includedCategory}` + ' WHERE "Id" IN (SELECT "Id" FROM ToDelete))'; }); deleteQuery += ` DELETE FROM ${escapedCategory}` + ' WHERE "Id" IN (SELECT "Id" FROM ToDelete)'; return [deleteQuery, whereParams]; }; const fitInSchema = (object, schema) => { const result = {}; if (object.Id) { result.Id = object.Id; } for (const prop in schema) { const field = schema[prop]; const value = object[prop]; if (value !== undefined) { result[prop] = value; } else if (extractDecorator(field) === 'Include') { result[prop] = fitInSchema(object, field.definition); } } return result; }; const recreateIdTrigger = Symbol('recreateIdTrigger'); const uploadMetadata = Symbol('uploadMetadata'); module.exports = { escapeString, escapeValue, escapeIdentifier, escapeKey, PREDEFINED_DOMAINS, IGNORED_DOMAINS, isValidIdentifier, validateIdentifier, generateQueryParams, generateLinkQueryParams, buildWhere, generateDeleteQuery, classMapping, fitInSchema, symbols: { recreateIdTrigger, uploadMetadata, }, };