diff --git a/src/table.mjs b/src/table.mjs index b2fe1072..ec4aa9cf 100644 --- a/src/table.mjs +++ b/src/table.mjs @@ -248,20 +248,44 @@ export function makeQueryTemplate(operations, source) { ]; for (let i = 0; i < filter.length; ++i) { appendSql(i ? `\nAND ` : `\nWHERE `, args); - appendWhereEntry(filter[i], args); + appendWhereEntry(filter[i], args, escaper); } for (let i = 0; i < sort.length; ++i) { appendSql(i ? `, ` : `\nORDER BY `, args); - appendOrderBy(sort[i], args); + appendOrderBy(sort[i], args, escaper); } - if (slice.to !== null || slice.from !== null) { - appendSql( - `\nLIMIT ${slice.to !== null ? slice.to - (slice.from || 0) : 1e9}`, - args - ); - } - if (slice.from !== null) { - appendSql(` OFFSET ${slice.from}`, args); + if (source.dialect === "mssql") { + if (slice.to !== null || slice.from !== null) { + if (!sort.length) { + if (!select.columns) + throw new Error( + "at least one column must be explicitly specified. Received '*'." + ); + appendSql(`\nORDER BY `, args); + appendOrderBy( + {column: select.columns[0], direction: "ASC"}, + args, + escaper + ); + } + appendSql(`\nOFFSET ${slice.from || 0} ROWS`, args); + appendSql( + `\nFETCH NEXT ${ + slice.to !== null ? slice.to - (slice.from || 0) : 1e9 + } ROWS ONLY`, + args + ); + } + } else { + if (slice.to !== null || slice.from !== null) { + appendSql( + `\nLIMIT ${slice.to !== null ? slice.to - (slice.from || 0) : 1e9}`, + args + ); + } + if (slice.from !== null) { + appendSql(` OFFSET ${slice.from}`, args); + } } return args; } @@ -282,16 +306,16 @@ function appendSql(sql, args) { strings[strings.length - 1] += sql; } -function appendOrderBy({column, direction}, args) { - appendSql(`t.${column} ${direction.toUpperCase()}`, args); +function appendOrderBy({column, direction}, args, escaper) { + appendSql(`t.${escaper(column)} ${direction.toUpperCase()}`, args); } -function appendWhereEntry({type, operands}, args) { +function appendWhereEntry({type, operands}, args, escaper) { if (operands.length < 1) throw new Error("Invalid operand length"); // Unary operations if (operands.length === 1) { - appendOperand(operands[0], args); + appendOperand(operands[0], args, escaper); switch (type) { case "n": appendSql(` IS NULL`, args); @@ -310,7 +334,7 @@ function appendWhereEntry({type, operands}, args) { // Fallthrough to next parent block. } else if (["c", "nc"].includes(type)) { // TODO: Case (in)sensitive? - appendOperand(operands[0], args); + appendOperand(operands[0], args, escaper); switch (type) { case "c": appendSql(` LIKE `, args); @@ -319,10 +343,10 @@ function appendWhereEntry({type, operands}, args) { appendSql(` NOT LIKE `, args); break; } - appendOperand(likeOperand(operands[1]), args); + appendOperand(likeOperand(operands[1]), args, escaper); return; } else { - appendOperand(operands[0], args); + appendOperand(operands[0], args, escaper); switch (type) { case "eq": appendSql(` = `, args); @@ -345,13 +369,13 @@ function appendWhereEntry({type, operands}, args) { default: throw new Error("Invalid filter operation"); } - appendOperand(operands[1], args); + appendOperand(operands[1], args, escaper); return; } } // List operations - appendOperand(operands[0], args); + appendOperand(operands[0], args, escaper); switch (type) { case "in": appendSql(` IN (`, args); @@ -366,9 +390,9 @@ function appendWhereEntry({type, operands}, args) { appendSql(")", args); } -function appendOperand(o, args) { +function appendOperand(o, args, escaper) { if (o.type === "column") { - appendSql(`t.${o.value}`, args); + appendSql(`t.${escaper(o.value)}`, args); } else { args.push(o.value); args[0].push(""); diff --git a/test/table-test.mjs b/test/table-test.mjs index 462f0bea..7b3184e3 100644 --- a/test/table-test.mjs +++ b/test/table-test.mjs @@ -106,6 +106,53 @@ describe("makeQueryTemplate", () => { assert.deepStrictEqual(params, ["val1"]); }); + it("makeQueryTemplate filter and escape filters column", () => { + const source = {name: "db", dialect: "postgres", escape: (i) => `_${i}_`}; + const operations = { + ...baseOperations, + filter: [ + { + type: "eq", + operands: [ + {type: "column", value: "col2"}, + {type: "resolved", value: "val1"} + ] + } + ] + }; + + const [parts, ...params] = makeQueryTemplate(operations, source); + assert.deepStrictEqual( + parts.join("?"), + "SELECT t._col1_,t._col2_ FROM table1 t\nWHERE t._col2_ = ?" + ); + assert.deepStrictEqual(params, ["val1"]); + }); + + it("makeQueryTemplate filter and escape filters column only once", () => { + const source = {name: "db", dialect: "postgres", escape: (i) => `_${i}_`}; + const operations = { + ...baseOperations, + filter: [ + { + type: "eq", + operands: [ + {type: "column", value: "col2"}, + {type: "resolved", value: "val1"} + ] + } + ] + }; + + makeQueryTemplate(operations, source); + const [parts, ...params] = makeQueryTemplate(operations, source); + assert.deepStrictEqual( + parts.join("?"), + "SELECT t._col1_,t._col2_ FROM table1 t\nWHERE t._col2_ = ?" + ); + assert.deepStrictEqual(params, ["val1"]); + }); + it("makeQueryTemplate filter list", () => { const source = {name: "db", dialect: "postgres"}; const operations = { @@ -164,6 +211,24 @@ describe("makeQueryTemplate", () => { assert.deepStrictEqual(params, []); }); + it("makeQueryTemplate sort and escape sort column", () => { + const source = {name: "db", dialect: "mysql", escape: (i) => `_${i}_`}; + const operations = { + ...baseOperations, + sort: [ + {column: "col1", direction: "asc"}, + {column: "col2", direction: "desc"} + ] + }; + + const [parts, ...params] = makeQueryTemplate(operations, source); + assert.deepStrictEqual( + parts.join("?"), + "SELECT t._col1_,t._col2_ FROM table1 t\nORDER BY t._col1_ ASC, t._col2_ DESC" + ); + assert.deepStrictEqual(params, []); + }); + it("makeQueryTemplate slice", () => { const source = {name: "db", dialect: "mysql"}; const operations = {...baseOperations}; @@ -215,6 +280,87 @@ describe("makeQueryTemplate", () => { assert.deepStrictEqual(parts.join("?"), "SELECT t.col1,t.col2,t.col3 FROM table1 t\nWHERE t.col1 >= ?\nAND t.col2 = ?\nORDER BY t.col1 ASC\nLIMIT 90 OFFSET 10"); assert.deepStrictEqual(params, ["val1", "val2"]); }); + + it("makeQueryTemplate select, slice and escape column name with mssql syntax", () => { + const source = {name: "db", dialect: "mssql", escape: (i) => `_${i}_`}; + const operations = { + ...baseOperations, + select: { + columns: ["col1", "col2", "col3"] + }, + slice: {to: 100} + }; + + const [parts] = makeQueryTemplate(operations, source); + assert.deepStrictEqual( + parts.join("?"), + "SELECT t._col1_,t._col2_,t._col3_ FROM table1 t\nORDER BY t._col1_ ASC\nOFFSET 0 ROWS\nFETCH NEXT 100 ROWS ONLY" + ); + }); + + it("makeQueryTemplate select, sort, slice, filter indexed with mssql syntax", () => { + const source = {name: "db", dialect: "mssql"}; + const operations = { + ...baseOperations, + select: { + columns: ["col1", "col2", "col3"] + }, + sort: [{column: "col2", direction: "desc"}], + slice: {from: 10, to: 100}, + filter: [ + { + type: "gte", + operands: [ + {type: "column", value: "col1"}, + {type: "resolved", value: "val1"} + ] + }, + { + type: "eq", + operands: [ + {type: "column", value: "col2"}, + {type: "resolved", value: "val2"} + ] + } + ] + }; + + const [parts, ...params] = makeQueryTemplate(operations, source); + assert.deepStrictEqual(parts.join("?"), "SELECT t.col1,t.col2,t.col3 FROM table1 t\nWHERE t.col1 >= ?\nAND t.col2 = ?\nORDER BY t.col2 DESC\nOFFSET 10 ROWS\nFETCH NEXT 90 ROWS ONLY"); + assert.deepStrictEqual(params, ["val1", "val2"]); + }); + + it("makeQueryTemplate throw if no columns are explicitly specified for mssql dialect", () => { + const source = {name: "db", dialect: "mssql"}; + const operations = { + ...baseOperations, + select: { + columns: null + }, + sort: [], + slice: {from: 10, to: 100} + }; + + assert.throws(() => { + makeQueryTemplate(operations, source); + }, Error); + }); + + it("makeQueryTemplate the sort and slice if no columns are explicitly BUT sort has value for mssql dialect", () => { + const source = {name: "db", dialect: "mssql"}; + const operations = { + ...baseOperations, + select: { + columns: null + }, + sort: [{column: "col2", direction: "desc"}], + slice: {from: 10, to: 100} + }; + + const [parts, ...params] = makeQueryTemplate(operations, source); + assert.deepStrictEqual(parts.join("?"), "SELECT * FROM table1 t\nORDER BY t.col2 DESC\nOFFSET 10 ROWS\nFETCH NEXT 90 ROWS ONLY"); + assert.deepStrictEqual(params, []); + }); }); describe("__table", () => {