globalstorage
Version:
Global Storage is a Global Distributed Data Warehouse
230 lines (204 loc) • 6.05 kB
JavaScript
;
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,
},
};