adding placeholder implementation

This commit is contained in:
rusher
2018-03-27 12:50:57 +02:00
parent 71a82f7311
commit e60291dd3e
5 changed files with 481 additions and 50 deletions

View File

@ -10,6 +10,7 @@ class Command extends EventEmitter {
super();
this.sequenceNo = 0;
this.connEvents = connEvents;
this.onPacketReceive = this.start;
}
init(out, opts, info) {

View File

@ -15,7 +15,7 @@ class Query extends ResultSet {
super(connEvents);
this.opts = options;
this.sql = sql;
this.values = values;
this.initialValues = values;
this.onResult = callback;
}
@ -30,7 +30,7 @@ class Query extends ResultSet {
start(out, opts, info) {
this.configAssign(opts, this.opts);
if (!this.values) {
if (!this.initialValues) {
//shortcut if no parameters
out.startPacket(this);
out.writeInt8(0x03);
@ -40,11 +40,25 @@ class Query extends ResultSet {
return this.readResponsePacket;
}
//TODO handle named placeholder (if option namedPlaceholders is set)
this.queryParts = Query.splitQuery(this.sql);
if (!this.validateParameters(info)) {
return null;
if (this.opts.namedPlaceholders) {
try {
const parsed = Query.splitQueryPlaceholder(
this.sql,
info,
this.initialValues,
this.displaySql.bind(this)
);
this.queryParts = parsed.parts;
this.values = parsed.values;
} catch (err) {
this.emit("send_end");
this.throwError(err);
return null;
}
} else {
this.queryParts = Query.splitQuery(this.sql);
this.values = Array.isArray(this.initialValues) ? this.initialValues : [this.initialValues];
if (!this.validateParameters(info)) return null;
}
out.startPacket(this);
@ -443,6 +457,150 @@ class Query extends ResultSet {
return partList;
}
/**
* Split query according to parameters using placeholder.
*
* @param sql sql with placeholders
* @param info connection information
* @param initialValues placeholder object
* @returns {{parts: Array, values: Array}}
*/
static splitQueryPlaceholder(sql, info, initialValues, displaySql) {
let partList = [];
const State = {
Normal: 1 /* inside query */,
String: 2 /* inside string */,
SlashStarComment: 3 /* inside slash-star comment */,
Escape: 4 /* found backslash */,
EOLComment: 5 /* # comment, or // comment, or -- comment */,
Backtick: 6 /* found backtick */,
Placeholder: 7 /* found placeholder */
};
let values = [];
let state = State.Normal;
let lastChar = "\0";
let singleQuotes = false;
let lastParameterPosition = 0;
let idx = 0;
let car = sql.charAt(idx++);
let placeholderName;
while (car !== "") {
if (state === State.Escape) state = State.String;
switch (car) {
case "*":
if (state === State.Normal && lastChar === "/") state = State.SlashStarComment;
break;
case "/":
if (state === State.SlashStarComment && lastChar === "*") {
state = State.Normal;
} else if (state === State.Normal && lastChar === "/") {
state = State.EOLComment;
}
break;
case "#":
if (state === State.Normal) state = State.EOLComment;
break;
case "-":
if (state === State.Normal && lastChar === "-") {
state = State.EOLComment;
}
break;
case "\n":
if (state === State.EOLComment) {
state = State.Normal;
}
break;
case '"':
if (state === State.Normal) {
state = State.String;
singleQuotes = false;
} else if (state === State.String && !singleQuotes) {
state = State.Normal;
}
break;
case "'":
if (state === State.Normal) {
state = State.String;
singleQuotes = true;
} else if (state === State.String && singleQuotes) {
state = State.Normal;
}
break;
case "\\":
if (state === State.String) state = State.Escape;
break;
case ":":
if (state === State.Normal) {
if (!initialValues) {
throw Utils.createError(
"Placeholder values are not defined\n" + displaySql.call(),
false,
info,
1210,
"HY000"
);
}
partList.push(sql.substring(lastParameterPosition, idx - 1));
placeholderName = "";
while (
((car = sql.charAt(idx++)) !== "" && (car >= "0" && car <= "9")) ||
(car >= "A" && car <= "Z") ||
(car >= "a" && car <= "z") ||
car === "-" ||
car === "_"
) {
placeholderName += car;
}
idx--;
const val = initialValues[placeholderName];
if (val === undefined) {
throw Utils.createError(
"Placeholder '" + placeholderName + "' is not defined\n" + displaySql.call(),
false,
info,
1210,
"HY000"
);
}
values.push(val);
lastParameterPosition = idx;
}
break;
case "`":
if (state === State.Backtick) {
state = State.Normal;
} else if (state === State.Normal) {
state = State.Backtick;
}
break;
}
lastChar = car;
car = sql.charAt(idx++);
}
if (lastParameterPosition === 0) {
partList.push(sql);
} else {
partList.push(sql.substring(lastParameterPosition));
}
return { parts: partList, values: values };
}
}
module.exports = Query;

View File

@ -322,62 +322,48 @@ class ResultSet extends Command {
* @returns {string}
*/
displaySql() {
if (this.opts && this.values && this.values.length > 0) {
if (this.opts && this.initialValues) {
if (this.sql.length > 1024) {
return "sql: " + this.sql.substring(0, 1024) + "...";
}
let sqlMsg = "sql: " + this.sql + " - parameters:[";
if (this.values) {
for (let i = 0; i < this.values.length; i++) {
if (i !== 0) sqlMsg += ",";
let param = this.values[i];
if (!param) {
sqlMsg += param === undefined ? "undefined" : "null";
} else if (param.constructor.name) {
switch (param.constructor.name) {
case "Buffer":
sqlMsg += "0x" + param.toString("hex", 0, Math.floor(1024, param.length)) + "";
break;
let sqlMsg = "sql: " + this.sql + " - parameters:";
case "String":
sqlMsg += "'" + param + "'";
break;
case "Date":
sqlMsg +=
"'" +
("00" + (param.getMonth() + 1)).slice(-2) +
"/" +
("00" + param.getDate()).slice(-2) +
"/" +
param.getFullYear() +
" " +
("00" + param.getHours()).slice(-2) +
":" +
("00" + param.getMinutes()).slice(-2) +
":" +
("00" + param.getSeconds()).slice(-2) +
"." +
("000" + param.getMilliseconds()).slice(-3) +
"'";
break;
default:
sqlMsg += param.toString();
}
if (this.opts.namedPlaceholders) {
sqlMsg += "{";
let first = true;
for (let key in this.initialValues) {
if (first) {
first = false;
} else {
sqlMsg += param.toString();
sqlMsg += ",";
}
sqlMsg += "'" + key + "':";
let param = this.initialValues[key];
sqlMsg = logParam(sqlMsg, param);
if (sqlMsg.length > 1024) {
sqlMsg += "...";
break;
}
}
sqlMsg += "}";
} else {
const values = Array.isArray(this.initialValues)
? this.initialValues
: [this.initialValues];
sqlMsg += "[";
for (let i = 0; i < values.length; i++) {
if (i !== 0) sqlMsg += ",";
let param = values[i];
sqlMsg = logParam(sqlMsg, param);
if (sqlMsg.length > 1024) {
sqlMsg += "...";
break;
}
}
sqlMsg += "]";
}
sqlMsg += "]";
return sqlMsg;
}
@ -385,6 +371,52 @@ class ResultSet extends Command {
}
}
function logParam(sqlMsg, param) {
if (!param) {
sqlMsg += param === undefined ? "undefined" : "null";
} else if (param.constructor.name) {
switch (param.constructor.name) {
case "Buffer":
sqlMsg += "0x" + param.toString("hex", 0, Math.floor(1024, param.length)) + "";
break;
case "String":
sqlMsg += "'" + param + "'";
break;
case "Date":
sqlMsg += getStringDate(param);
break;
default:
sqlMsg += param.toString();
}
} else {
sqlMsg += param.toString();
}
return sqlMsg;
}
function getStringDate(param) {
return (
"'" +
("00" + (param.getMonth() + 1)).slice(-2) +
"/" +
("00" + param.getDate()).slice(-2) +
"/" +
param.getFullYear() +
" " +
("00" + param.getHours()).slice(-2) +
":" +
("00" + param.getMinutes()).slice(-2) +
":" +
("00" + param.getSeconds()).slice(-2) +
"." +
("000" + param.getMilliseconds()).slice(-3) +
"'"
);
}
/**
* Object to store insert/update/delete results
*

View File

@ -160,7 +160,7 @@ class Connection {
if (typeof values === "function") {
_cb = values;
} else if (values !== undefined) {
_values = !Array.isArray(values) ? [values] : values;
_values = values;
_cb = cb;
}

View File

@ -0,0 +1,240 @@
"use strict";
const base = require("../base.js");
const assert = require("chai").assert;
describe("Placeholder", () => {
it("query placeholder basic test", function(done) {
const conn = base.createConnection({ namedPlaceholders: true });
conn.connect(err => {
conn.query(
"select :param1 as val1, :param3 as val3, :param2 as val2",
{ param3: 30, param1: 10, param2: 20 },
(err, rows) => {
if (err) done(err);
assert.deepEqual(rows, [{ val1: 10, val3: 30, val2: 20 }]);
conn.execute(
"select :param1 as val1, :param3 as val3, :param2 as val2",
{ param3: 30, param1: 10, param2: 20 },
(err, rows) => {
if (err) done(err);
assert.deepEqual(rows, [{ val1: 10, val3: 30, val2: 20 }]);
conn.end();
done();
}
);
}
);
});
});
it("query placeholder using option", function(done) {
shareConn.query(
{ namedPlaceholders: true, sql: "select :param1 as val1, :param3 as val3, :param2 as val2" },
{ param3: 30, param1: 10, param2: 20 },
(err, rows) => {
if (err) done(err);
assert.deepEqual(rows, [{ val1: 10, val3: 30, val2: 20 }]);
shareConn.execute(
{
namedPlaceholders: true,
sql: "select :param1 as val1, :param3 as val3, :param2 as val2"
},
{ param3: 30, param1: 10, param2: 20 },
(err, rows) => {
if (err) done(err);
assert.deepEqual(rows, [{ val1: 10, val3: 30, val2: 20 }]);
done();
}
);
}
);
});
it("query ending by placeholder", function(done) {
shareConn.query(
{ namedPlaceholders: true, sql: "select :param-1 as val1, :param-3 as val3, :param-2" },
{ "param-3": 30, "param-1": 10, "param-2": 20 },
(err, rows) => {
if (err) done(err);
assert.deepEqual(rows, [{ val1: 10, val3: 30, "20": 20 }]);
shareConn.execute(
{ namedPlaceholders: true, sql: "select :param-1 as val1, :param-3 as val3, :param-2" },
{ "param-3": 30, "param-1": 10, "param-2": 20 },
(err, rows) => {
if (err) done(err);
assert.deepEqual(rows, [{ val1: 10, val3: 30, "20": 20 }]);
done();
}
);
}
);
});
it("query named parameters logged in error", function(done) {
const handleResult = function(err) {
assert.equal(1146, err.errno);
assert.equal("42S02", err.sqlState);
assert.isFalse(err.fatal);
assert.isTrue(
err.message.includes(
"sql: INSERT INTO falseTable(t1, t2, t3, t4, t5) values (:t1, :t2, :t3, :t4, :t5) - parameters:{'t1':1,'t2':0x01ff,'t3':'hh','t4':'01/01/2001 00:00:00.000','t5':null}"
)
);
};
const conn = base.createConnection({ namedPlaceholders: true });
conn.connect(err => {
conn.query(
"INSERT INTO falseTable(t1, t2, t3, t4, t5) values (:t1, :t2, :t3, :t4, :t5) ",
{
t1: 1,
t2: Buffer.from([0x01, 0xff]),
t3: "hh",
t4: new Date(2001, 0, 1, 0, 0, 0),
t5: null
},
handleResult
);
conn.execute(
"INSERT INTO falseTable(t1, t2, t3, t4, t5) values (:t1, :t2, :t3, :t4, :t5) ",
{
t1: 1,
t2: Buffer.from([0x01, 0xff]),
t3: "hh",
t4: new Date(2001, 0, 1, 0, 0, 0),
t5: null
},
handleResult
);
conn.execute("SELECT 1", (err, rows) => {
assert.deepEqual(rows, [{ "1": 1 }]);
conn.end();
done();
});
});
});
it("query undefined named parameter", function(done) {
const handleResult = function(err) {
assert.equal(err.errno, 1210);
assert.equal(err.sqlState, "HY000");
assert.isFalse(err.fatal);
assert.ok(
err.message.includes(
"Placeholder 'param2' is not defined\n" +
"sql: INSERT INTO undefinedParameter values (:param3, :param1, :param2) - parameters:{'param1':1,'param3':3,'param4':4}"
)
);
};
const conn = base.createConnection({ namedPlaceholders: true });
conn.connect(err => {
conn.query("CREATE TEMPORARY TABLE undefinedParameter (id int, id2 int, id3 int)");
conn.query(
"INSERT INTO undefinedParameter values (:param3, :param1, :param2)",
{ param1: 1, param3: 3, param4: 4 },
handleResult
);
conn.execute(
"INSERT INTO undefinedParameter values (:param3, :param1, :param2)",
{ param1: 1, param3: 3, param4: 4 },
handleResult
);
conn.query("SELECT 1", (err, rows) => {
assert.deepEqual(rows, [{ "1": 1 }]);
conn.end();
done();
});
});
});
it("query missing placeholder parameter", function(done) {
const handleResult = function(err) {
assert.equal(err.errno, 1210);
assert.equal(err.sqlState, "HY000");
assert.isFalse(err.fatal);
assert.ok(
err.message.includes(
"Placeholder 't2' is not defined\n" +
"sql: INSERT INTO execute_missing_parameter values (:t1, :t2, :t3) - parameters:{'t1':1,'t3':3}"
)
);
};
const conn = base.createConnection({ namedPlaceholders: true });
conn.connect(err => {
conn.query("CREATE TEMPORARY TABLE execute_missing_parameter (id int, id2 int, id3 int)");
conn.query(
"INSERT INTO execute_missing_parameter values (:t1, :t2, :t3)",
{ t1: 1, t3: 3 },
handleResult
);
conn.execute(
"INSERT INTO execute_missing_parameter values (:t1, :t2, :t3)",
{ t1: 1, t3: 3 },
handleResult
);
shareConn.query("SELECT 1", (err, rows) => {
assert.deepEqual(rows, [{ "1": 1 }]);
conn.end();
done();
});
});
});
it("query no placeholder parameter", function(done) {
const handleResult = function(err) {
assert.equal(err.errno, 1210);
assert.equal(err.sqlState, "HY000");
assert.isFalse(err.fatal);
assert.ok(
err.message.includes(
"Placeholder 't1' is not defined\n" +
"sql: INSERT INTO execute_no_parameter values (:t1, :t2, :t3) - parameters:{}"
)
);
};
const conn = base.createConnection({ namedPlaceholders: true });
conn.connect(err => {
conn.query("CREATE TEMPORARY TABLE execute_no_parameter (id int, id2 int, id3 int)");
conn.query("INSERT INTO execute_no_parameter values (:t1, :t2, :t3)", [], handleResult);
conn.query("INSERT INTO execute_no_parameter values (:t1, :t2, :t3)", {}, handleResult);
conn.execute("INSERT INTO execute_no_parameter values (:t1, :t2, :t3)", [], handleResult);
conn.execute("INSERT INTO execute_no_parameter values (:t1, :t2, :t3)", {}, handleResult);
conn.query("SELECT 1", (err, rows) => {
assert.deepEqual(rows, [{ "1": 1 }]);
conn.end();
done();
});
});
});
it("query to much placeholder parameter", function(done) {
const conn = base.createConnection({ namedPlaceholders: true });
conn.connect(err => {
conn.query("CREATE TEMPORARY TABLE to_much_parameters (id int, id2 int, id3 int)");
conn.query(
"INSERT INTO to_much_parameters values (:t2, :t0, :t1)",
{ t0: 0, t1: 1, t2: 2, t3: 3 },
err => {
if (err) {
done(err);
} else {
conn.execute(
"INSERT INTO to_much_parameters values (:t2, :t0, :t1) ",
{ t0: 0, t1: 1, t2: 2, t3: 3 },
err => {
conn.end();
if (err) {
done(err);
} else {
done();
}
}
);
}
}
);
});
});
});