sqlpad
Version:
Web app for writing and running SQL queries and visualizing the results. Supports Postgres, MySQL, SQL Server, Crate and Vertica.
375 lines (355 loc) • 11.3 kB
JavaScript
var fs = require('fs')
var pg = require('pg')
var mysql = require('mysql')
var mssql = require('mssql')
var PgCursor = require('pg-cursor')
var vertica = require('vertica')
var crate = require('node-crate')
var presto = require('./presto.js')
var config = require('./config.js')
var QueryResult = require('../models/QueryResult.js')
var createSocksConnection = require('./socks.js')
module.exports = function runQuery(query, connection, callback) {
var queryResult = new QueryResult()
queryResult.timerStart()
var conn = Object.assign({}, connection, {
stream: createSocksConnection(connection)
})
clients[conn.driver](query, conn, queryResult, function(err, queryResult) {
queryResult.timerStop()
if (config.get('debug')) {
var resultRowCount =
queryResult && queryResult.rows && queryResult.rows.length
? queryResult.rows.length
: 0
console.log('\n--- lib/run-query.js ---')
console.log('CONNECTION: ' + conn.name)
console.log('START TIME: ' + queryResult.startTime.toISOString())
console.log('END TIME: ' + queryResult.stopTime.toISOString())
console.log('ELAPSED MS: ' + queryResult.queryRunTime)
console.log('RESULT ROWS: ' + resultRowCount)
console.log('QUERY: ')
console.log(query)
console.log()
}
callback(err, queryResult)
})
}
var clients = {}
clients.mysql = function(query, connection, queryResult, callback) {
var myConnection = mysql.createConnection({
multipleStatements: true,
host: connection.host,
port: connection.port ? connection.port : 3306,
user: connection.username,
password: connection.password,
database: connection.database,
insecureAuth: connection.mysqlInsecureAuth,
timezone: 'Z',
supportBigNumbers: true
})
myConnection.connect(function(err) {
if (err) return callback(err, queryResult)
var rowCounter = 0
var queryError
var resultsSent = false
function continueOn() {
if (!resultsSent) {
resultsSent = true
callback(queryError, queryResult)
}
}
var myQuery = myConnection.query(query)
myQuery
.on('error', function(err) {
// Handle error,
// an 'end' event will be emitted after this as well
// so we'll call the callback there.
queryError = err
})
.on('result', function(row) {
rowCounter++
if (rowCounter <= connection.maxRows) {
// if we haven't hit the max yet add row to results
queryResult.addRow(row)
} else {
// Too many rows! pause that connection.
// It sounds like there is no way to close query stream
// you just have to close the connection
myConnection.pause()
queryResult.incomplete = true
continueOn() // return records to client before closing connection
myConnection.end()
}
})
.on('end', function() {
// all rows have been received
// This will not fire if we end the connection early
continueOn()
myConnection.end()
})
})
}
// TODO - crate driver should honor max rows restriction
clients.crate = function(query, connection, queryResult, callback) {
var crateConfig = {
host: connection.host
}
if (connection.port) {
crate.connect(crateConfig.host, connection.port)
} else {
crate.connect(crateConfig.host)
}
query = query.replace(/;$/, '')
crate
.execute(query)
.success(function(res) {
var results = {
rows: [],
fields: []
}
for (var row in res.rows) {
results.rows[row] = {}
for (var val in res.rows[row]) {
var columnName = res.cols[val]
var type = res.col_types[val]
val = res.rows[row][val]
if (type === 11) {
val = new Date(val)
}
results.rows[row][columnName] = val
results.fields[row] = columnName
}
}
queryResult.addRows(results.rows)
callback(null, queryResult)
})
.error(function(err) {
callback(err.message, queryResult)
})
}
clients.presto = function(query, connection, queryResult, callback) {
const port = connection.port || 8080
const prestoConfig = {
url: `http://${connection.host}:${port}`,
user: connection.username,
catalog: connection.prestoCatalog,
schema: connection.prestoSchema
}
presto
.send(prestoConfig, query)
.then(result => {
if (!result) {
const missingResult = 'No result returned'
return callback(missingResult, queryResult)
}
let { data, columns, error } = result
if (error) {
return callback(error.message, queryResult)
}
if (data.length > connection.maxRows) {
queryResult.incomplete = true
data = data.slice(0, connection.maxRows)
}
for (var r = 0; r < data.length; r++) {
var row = {}
for (var c = 0; c < columns.length; c++) {
row[columns[c].name] = data[r][c]
}
queryResult.addRow(row)
}
return callback(null, queryResult)
})
.catch(error => {
console.error({ error })
return callback(error.message, queryResult)
})
}
clients.postgres = function(query, connection, queryResult, callback) {
var pgConfig = {
user: connection.username,
password: connection.password,
database: connection.database,
host: connection.host,
ssl: connection.postgresSsl,
stream: connection.stream
}
// TODO cache key/cert values
if (connection.postgresKey && connection.postgresCert) {
pgConfig.ssl = {
key: fs.readFileSync(connection.postgresKey),
cert: fs.readFileSync(connection.postgresCert)
}
if (connection.postgresCA) {
pgConfig.ssl['ca'] = fs.readFileSync(connection.postgresCA)
}
}
if (connection.port) pgConfig.port = connection.port
var client = new pg.Client(pgConfig)
client.connect(function(err) {
if (err) {
callback(err, queryResult)
client.end()
} else {
var cursor = client.query(new PgCursor(query))
cursor.read(connection.maxRows + 1, function(err, rows) {
if (err) {
// pg_cursor can't handle multi-statements at the moment
// as a work around we'll retry the query the old way, but we lose the maxRows protection
client.query(query, function(err, result) {
if (result && result.rows) queryResult.addRows(result.rows)
callback(err, queryResult)
client.end()
})
} else {
queryResult.addRows(rows)
if (rows.length === connection.maxRows + 1) {
queryResult.incomplete = true
queryResult.rows.pop() // get rid of that extra record. we only get 1 more than the max to see if there would have been more...
}
callback(err, queryResult)
cursor.close(function(err) {
if (err) {
console.log('error closing pg-cursor:')
console.log(err)
}
client.end()
})
}
})
}
})
}
clients.sqlserver = function(query, connection, queryResult, callback) {
var sqlconfig = {
user: connection.username,
password: connection.password,
server: connection.host,
port: connection.port ? connection.port : 1433,
database: connection.database,
domain: connection.domain,
stream: true,
requestTimeout: 1000 * 60 * 60, // one hour
options: {
encrypt: connection.sqlserverEncrypt
}
}
var mssqlConnection = new mssql.Connection(sqlconfig, function(err) {
if (err) {
callback(err, queryResult)
} else {
var rowCounter = 0
var queryError
var resultsSent = false
var tooManyHandled = false
// For SQL Server, this can be called more than once safely
var continueOn = function() {
if (!resultsSent) {
resultsSent = true
callback(queryError, queryResult)
}
}
var request = new mssql.Request(mssqlConnection)
request.query(query)
request.on('row', function(row) {
// special handling if columns were not given names
if (row[''] && row[''].length) {
for (var i = 0; i < row[''].length; i++) {
row['UNNAMED COLUMN ' + (i + 1)] = row[''][i]
}
delete row['']
}
rowCounter++
if (rowCounter <= connection.maxRows) {
// if we haven't hit the max yet add row to results
queryResult.addRow(row)
} else {
if (!tooManyHandled) {
tooManyHandled = true
// Too many rows!
queryResult.incomplete = true
continueOn()
console.log('Row limit hit - Attempting to cancel query...')
request.cancel() // running this will yeild a cancel error
}
}
})
request.on('error', function(err) {
// May be emitted multiple times
// for now I guess we just set queryError to be the most recent error?
if (err.code === 'ECANCEL') {
console.log('Query cancelled successfully')
} else {
console.log('mssql query error:')
console.log(err)
queryError = err
}
})
request.on('done', function(returnValue) {
// Always emitted as the last one
continueOn()
mssqlConnection.close() // I don't think this does anything using the tedious driver. but maybe someday it will
})
}
})
}
clients.vertica = function(query, connection, queryResult, callback) {
var params = {
host: connection.host,
port: connection.port ? connection.port : 5433,
user: connection.username,
password: connection.password,
database: connection.database
}
var client = vertica.connect(params, function(err) {
if (err) {
callback(err, queryResult)
client.disconnect()
} else {
var finished = false
var rowCounter = 0
var fields = []
var verticaQuery = client.query(query)
verticaQuery.on('fields', function(f) {
for (var i in f) {
if (f.hasOwnProperty(i)) {
fields.push(f[i]['name'])
}
}
})
verticaQuery.on('row', function(row) {
if (rowCounter < connection.maxRows) {
var resultRow = {}
for (var item in row) {
if (row.hasOwnProperty(item)) {
resultRow[fields[item]] = row[item]
}
}
queryResult.addRow(resultRow)
rowCounter++
} else {
if (!finished) {
finished = true
client.disconnect()
queryResult.incomplete = true
callback(err, queryResult)
}
}
})
verticaQuery.on('end', function() {
if (!finished) {
finished = true
client.disconnect()
callback(err, queryResult)
}
})
verticaQuery.on('error', function(err) {
if (!finished) {
finished = true
client.disconnect()
callback(err, queryResult)
}
})
}
})
}