mirror of
https://github.com/mariadb-corporation/mariadb-connector-nodejs.git
synced 2025-08-19 01:23:19 +00:00
adding placeholder implementation
This commit is contained in:
@ -10,6 +10,7 @@ class Command extends EventEmitter {
|
||||
super();
|
||||
this.sequenceNo = 0;
|
||||
this.connEvents = connEvents;
|
||||
this.onPacketReceive = this.start;
|
||||
}
|
||||
|
||||
init(out, opts, info) {
|
||||
|
172
src/cmd/query.js
172
src/cmd/query.js
@ -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;
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
240
test/integration/test-placholders.js
Normal file
240
test/integration/test-placholders.js
Normal 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();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user