UNPKG

@naturalcycles/mysql-lib

Version:

MySQL client implementing CommonDB interface

204 lines (203 loc) 6.21 kB
import { _hb } from '@naturalcycles/js-lib'; import { white, yellow } from '@naturalcycles/nodejs-lib/colors'; import * as mysql from 'mysql'; import { mapNameToMySQL } from './schema/mysql.schema.util.js'; const MAX_PACKET_SIZE = 1024 * 1024; // 1Mb const MAX_ROW_SIZE = 800 * 1024; // 1Mb - margin /** * Returns `null` if it detects that 0 rows will be returned, * e.g when `IN ()` (empty array) is used. */ export function dbQueryToSQLSelect(q) { const tokens = selectTokens(q); // filters const whereTokens = getWhereTokens(q); if (!whereTokens) return null; tokens.push(...whereTokens); // order tokens.push(...groupOrderTokens(q)); // offset/limit tokens.push(...offsetLimitTokens(q)); return tokens.join(' '); } /** * Returns null in "0 rows" case. */ export function dbQueryToSQLDelete(q) { const tokens = [`DELETE FROM`, mysql.escapeId(q.table)]; // filters const whereTokens = getWhereTokens(q); if (!whereTokens) return null; tokens.push(...whereTokens); // offset/limit tokens.push(...offsetLimitTokens(q)); return tokens.join(' '); } /** * Returns array of sql statements to respect the max sql size. */ export function insertSQL(table, rows, verb = 'INSERT', logger = console) { // INSERT INTO table_name (column1, column2, column3, ...) // VALUES (value1, value2, value3, ...); // eslint-disable-next-line unicorn/no-array-reduce const fieldSet = rows.reduce((set, row) => { for (const field of Object.keys(row)) { set.add(field); } return set; }, new Set()); const fields = [...fieldSet]; const start = [ verb, `INTO`, mysql.escapeId(table), `(` + [...fields].map(f => mysql.escapeId(mapNameToMySQL(f))).join(',') + `)`, `VALUES\n`, ].join(' '); const valueRows = rows.map(rec => { return `(` + fields.map(k => mysql.escape(rec[k])).join(',') + `)`; }); const full = start + valueRows.join(',\n'); if (full.length < MAX_PACKET_SIZE) return [full]; const sqls = []; let sql; for (const vrow of valueRows) { if (!sql) { sql = start + vrow; } else { if (sql.length + vrow.length >= MAX_ROW_SIZE) { sqls.push(sql); sql = start + vrow; // reset } else { sql += ',\n ' + vrow; // add } } } if (sql) { sqls.push(sql); // last one } logger.log(`${white(table)} large sql query (${yellow(_hb(full.length))}) was split into ${yellow(sqls.length)} chunks`); return sqls; // todo: handle "upsert" later } export function insertSQLSingle(table, record) { // INSERT INTO table_name (column1, column2, column3, ...) // VALUES (value1, value2, value3, ...); const tokens = [ `INSERT INTO`, mysql.escapeId(table), `(` + Object.keys(record) .map(f => mysql.escapeId(mapNameToMySQL(f))) .join(',') + `)`, `\nVALUES`, `(` + Object.values(record) .map(v => mysql.escapeId(v)) .join(',') + `)`, ]; return tokens.join(' '); } export function insertSQLSetSingle(table, record) { return { sql: `INSERT INTO ${mysql.escapeId(table)} SET ?`, values: [record], }; } export function dbQueryToSQLUpdate(q, patch) { // var sql = mysql.format('UPDATE posts SET modified = ? WHERE id = ?', [CURRENT_TIMESTAMP, 42]); const whereTokens = getWhereTokens(q); if (!whereTokens) return null; const tokens = [ `UPDATE`, mysql.escapeId(q.table), `SET`, Object.keys(patch) .map(f => mysql.escapeId(mapNameToMySQL(f)) + ' = ?') .join(', '), ...whereTokens, ]; return mysql.format(tokens.join(' '), Object.values(patch)); } function selectTokens(q) { let fields = ['*']; if (q._selectedFieldNames) { fields = q._selectedFieldNames.length ? q._selectedFieldNames : ['id']; } // We don't do `escapeId` cause it'll ruin e.g SELECT `count *` FROM ... return [ `SELECT`, q._distinct && 'DISTINCT', fields.map(f => mapNameToMySQL(f)).join(', '), `FROM`, mysql.escapeId(q.table), ].filter(Boolean); } function offsetLimitTokens(q) { const tokens = []; if (q._limitValue) { tokens.push(`LIMIT`, String(q._limitValue)); // In SQL OFFSET is only allowed if LIMIT is set if (q._offsetValue) { tokens.push(`OFFSET`, String(q._offsetValue)); } } return tokens; } function groupOrderTokens(q) { const t = []; if (q._groupByFieldNames?.length) { t.push(`GROUP BY`, q._groupByFieldNames.map(c => `\`${c}\``).join(', ')); } if (q._orders.length) { t.push(`ORDER BY`, q._orders.map(o => `\`${o.name}\` ${o.descending ? 'DESC' : 'ASC'}`).join(', ')); } return t; } const OP_MAP = { '==': '=', }; /** * Returns `null` for "guaranteed 0 rows" cases. */ function getWhereTokens(q) { if (!q._filters.length) return []; let returnNull = false; const tokens = [ `WHERE`, q._filters .map(f => { if (f.val === null || f.val === undefined) { // special treatment return [ mysql.escapeId(mapNameToMySQL(f.name)), f.op === '==' ? 'IS NULL' : 'IS NOT NULL', ].join(' '); } if (Array.isArray(f.val)) { // special case for arrays if (!f.val.length) returnNull = true; return `${mysql.escapeId(mapNameToMySQL(f.name))} IN (${mysql.escape(f.val)})`; } return [ mysql.escapeId(mapNameToMySQL(f.name)), OP_MAP[f.op] || f.op, mysql.escape(f.val), ].join(' '); }) .join(' AND '), ]; if (returnNull) return null; return tokens; }