Skip to content

Commit

Permalink
Merge pull request #90 from synatic/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
filepounder authored Jan 27, 2023
2 parents 625afb1 + fb4de63 commit 620f552
Show file tree
Hide file tree
Showing 11 changed files with 237 additions and 65 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ Functions in WHERE statements require explicit definition and can't use a comput
select `Address.City` as City,abs(`id`) as absId from `customers` where `First Name` like 'm%' and abs(`id`) > 1 order by absId

--Won't work
select `Address.City` as City from `customers` where `First Name` like 'm%' and absId > 1
select `Address.City` as City,abs(`id`) as absId from `customers` where `First Name` like 'm%' and absId > 1
```

ORDER BY requires field to be part of select
Expand Down
101 changes: 59 additions & 42 deletions lib/canQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,65 @@ const _allowableFunctions = require('./MongoFunctions');
const {isSelectAll: checkIfIsSelectAll} = require('./isSelectAll');
const {parseSQLtoAST} = require('./parseSQLtoAST');

/**
* Checks whether the expression is null or its type is null
*
* @param {any} val - the expression value to check
* @returns {boolean} - whether it is null or not
* @private
*/
function _checkNullOrEmptyType(val) {
return !val || (val && !val.type);
}

/**
* Returns a function to check whether the column supports a function
*
* @param {import('./types').Column} column - the column to check
* @returns {()=>*} - the function to check the column type
*/
function findFnFromColumnType(column) {
return (fn) =>
fn.name === column.expr.name.toLowerCase() &&
(!fn.type || fn.type === column.expr.type) &&
fn.allowQuery;
}

/**
* Checks whether the column contains an allowed function
*
* @param {import('./types').Column} column - the column to check
* @returns {boolean} - whether the column contains an allowed query function
*/
function checkIfContainsAllowedFunctions(column) {
return (
column.expr.type === 'function' &&
!_allowableFunctions.functionMappings.find(findFnFromColumnType(column))
);
}

/**
* Checks whether the column contains an allowed aggregate function
*
* @param {import('./types').Column} column - the column to check
* @returns {boolean} - whether the column contains an allowed aggregate function
*/
function checkIfContainsAllowedAggregateFunctions(column) {
if (column.expr.type !== 'aggr_func') {
return false;
}
const someValue = _allowableFunctions.functionMappings.find(
findFnFromColumnType(column)
);
return !someValue;
}

/**
* Checks whether a mongo query can be performed or an aggregate is required
*
* @param {import('./types').ParserInput} sqlOrAST - the SQL statement or AST to parse
* @param {import('./types').ParserOptions} [options] - the parser options
* @returns {boolean}
* @returns {boolean} - if the sql or ast can be executed as a query
* @throws
*/
function canQuery(sqlOrAST, options = {isArray: false}) {
Expand All @@ -21,9 +70,11 @@ function canQuery(sqlOrAST, options = {isArray: false}) {
const isSelectAll = checkIfIsSelectAll(ast.columns);
/** @type{import('./types').Column[]} */
const columns = typeof ast.columns === 'string' ? null : ast.columns;

const asColumns = isSelectAll
? []
: columns.map((c) => c.as).filter((c) => !!c);

const checkAsUsedInWhere = (expr) => {
if (!expr) {
return false;
Expand Down Expand Up @@ -62,6 +113,8 @@ function canQuery(sqlOrAST, options = {isArray: false}) {
asColumns.length > 0 && checkAsUsedInWhere(ast.where);
const fromHasExpr = ast.from.findIndex((f) => !!f.expr) > -1;
const hasUnion = !!ast.union;
const hasTableAlias = !!(ast.from && ast.from[0] && ast.from[0].as);

const isAggregate =
moreThanOneFrom ||
hasNoTable ||
Expand All @@ -74,52 +127,16 @@ function canQuery(sqlOrAST, options = {isArray: false}) {
fromHasExpr ||
asColumnsUsedInWhere ||
whereContainsOtherTable ||
hasUnion;
hasUnion ||
hasTableAlias;
return !isAggregate;
}
/**
*
* @param {import('./types').Column} column
* @returns {boolean}
*/
function checkIfContainsAllowedFunctions(column) {
return (
column.expr.type === 'function' &&
!_allowableFunctions.functionMappings.find(findFnFromColumnType(column))
);
}

/**
*
* @param {import('./types').Column} column
* @returns {boolean}
*/
function checkIfContainsAllowedAggregateFunctions(column) {
if (column.expr.type !== 'aggr_func') {
return false;
}
const someValue = _allowableFunctions.functionMappings.find(
findFnFromColumnType(column)
);
return !someValue;
}

/**
*
* @param {import('./types').Column} column
* @returns {()=>*}
*/
function findFnFromColumnType(column) {
return (fn) =>
fn.name === column.expr.name.toLowerCase() &&
(!fn.type || fn.type === column.expr.type) &&
fn.allowQuery;
}

/**
* Checks whether the expression statement contains other tables to execute a sub select
*
* @param {import('./types').Expression} expr
* @returns {boolean}
* @param {import('./types').Expression} expr - the expressions to check
* @returns {boolean} - whether the expression contains other tables
*/
function checkWhereContainsOtherTable(expr) {
if (!expr) {
Expand Down
2 changes: 1 addition & 1 deletion lib/make/filter-queries.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Finds all the queries in a an AST where statement that are AST's themselves
* Finds all the queries in an AST where statement that are AST's themselves
*
* @param {import('../types').Expression} where
* @returns {{column:string,ast:import('../types').AST[]}[]}
Expand Down
44 changes: 35 additions & 9 deletions lib/make/makeAggregatePipeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,15 @@ const _allowableFunctions = require('../MongoFunctions');

exports.makeAggregatePipeline = makeAggregatePipeline;

function forceGroupBy(ast) {
/**
*
* Checks whether the query needs to force a group by
*
* @param {import('../types').AST} ast - the ast to check if a group by needs to be forced
* @returns {boolean} - whether a group by needs to be forced
* @private
*/
function _forceGroupBy(ast) {
if (ast.groupby) {
return false;
}
Expand All @@ -34,7 +42,14 @@ function forceGroupBy(ast) {
);
}

function hasIdCol(columns) {
/**
* Checks whether an _id column is specified
*
* @param {Array} columns - the columns to check
* @returns {boolean} - whether an _id column is specified
* @private
*/
function _hasIdCol(columns) {
if (!columns || columns.length === 0) {
return false;
}
Expand Down Expand Up @@ -132,7 +147,8 @@ function makeAggregatePipeline(ast, options = {}) {
ast.where,
false,
[],
false
false,
ast.from && ast.from[0] ? ast.from[0].as : null
),
};
}
Expand All @@ -145,13 +161,19 @@ function makeAggregatePipeline(ast, options = {}) {
const pipeLineJoin = makeJoinForPipelineModule.makeJoinForPipeline(ast);
if (pipeLineJoin.length > 0) {
pipeline = pipeline.concat(pipeLineJoin);
// todo check where this gets inserted
if (wherePiece) {
pipeline.push(wherePiece);
wherePiece = null;
}
} else {
if (wherePiece) {
pipeline.push(wherePiece);
wherePiece = null;
}
}

const checkForceGroupBy = forceGroupBy(ast);
const checkForceGroupBy = _forceGroupBy(ast);

if (ast.groupby || checkForceGroupBy) {
if (isSelectAll(ast.columns)) {
Expand Down Expand Up @@ -196,7 +218,11 @@ function makeAggregatePipeline(ast, options = {}) {
// @ts-ignore
const columns = ast.columns;
columns.forEach((column) => {
projectColumnParserModule.projectColumnParser(column, result);
projectColumnParserModule.projectColumnParser(
column,
result,
ast.from && ast.from[0] ? ast.from[0].as : null
);
});
if (result.count.length > 0) {
result.count.forEach((countStep) => pipeline.push(countStep));
Expand Down Expand Up @@ -242,9 +268,9 @@ function makeAggregatePipeline(ast, options = {}) {
}
}

if (wherePiece) {
pipeline.unshift(wherePiece);
}
// if (wherePiece) {
// pipeline.unshift(wherePiece);
// }

// for if initial query is subquery
if (!ast.from[0].table && ast.from[0].expr && ast.from[0].expr.ast) {
Expand Down Expand Up @@ -281,7 +307,7 @@ function makeAggregatePipeline(ast, options = {}) {
if (
options.unsetId &&
!isSelectAll(ast.columns) &&
!hasIdCol(ast.columns)
!_hasIdCol(ast.columns)
) {
pipeline.push({$unset: '_id'});
}
Expand Down
6 changes: 6 additions & 0 deletions lib/make/makeFilterCondition.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const operatorMap = {
'!=': '$ne',
AND: '$and',
OR: '$or',
IS: '$eq',
'IS NOT': '$ne',
};
/**
* Creates a filter expression from a query part
Expand Down Expand Up @@ -128,6 +130,10 @@ function makeFilterCondition(
return queryPart.value;
}

if (queryPart.type === 'null') {
return null;
}

throw new Error(
`invalid expression type for array sub select:${queryPart.type}`
);
Expand Down
27 changes: 18 additions & 9 deletions lib/make/makeQueryPart.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ exports.makeQueryPart = makeQueryPart;
* @param {boolean} [ignorePrefix] - Ignore the table prefix
* @param {Array} [allowedTypes] - Expression types to allow
* @param {boolean} [includeThis] - include $$this in expresions
* @param {string} [tableAlias] - a table alias to check if it hasn't been specified
* @returns {any} - the mongo query/match
*/
function makeQueryPart(
queryPart,
ignorePrefix,
allowedTypes = [],
includeThis = false
includeThis = false,
tableAlias = ''
) {
if (allowedTypes.length > 0 && !allowedTypes.includes(queryPart.type)) {
throw new Error(`Type not allowed for query:${queryPart.type}`);
Expand All @@ -29,11 +31,12 @@ function makeQueryPart(
queryPartToUse = queryPart.left;
}

const table = queryPartToUse.table || tableAlias;
if (queryPartToUse.column) {
return (
(includeThis ? '$$this.' : '') +
(queryPartToUse.table && !ignorePrefix
? `${queryPartToUse.table}.${queryPartToUse.column}`
(table && !ignorePrefix
? `${table}.${queryPartToUse.column}`
: queryPartToUse.column)
);
} else {
Expand All @@ -46,13 +49,15 @@ function makeQueryPart(
queryPart.left,
ignorePrefix,
allowedTypes,
includeThis
includeThis,
tableAlias
);
const right = makeQueryPart(
queryPart.right,
ignorePrefix,
allowedTypes,
includeThis
includeThis,
tableAlias
);
if ($check.string(left) && !left.startsWith('$')) {
return {[left]: {[op]: right}};
Expand All @@ -75,13 +80,15 @@ function makeQueryPart(
queryPart.left,
ignorePrefix,
allowedTypes,
includeThis
includeThis,
tableAlias
),
makeQueryPart(
queryPart.right,
ignorePrefix,
allowedTypes,
includeThis
includeThis,
tableAlias
),
],
};
Expand All @@ -93,13 +100,15 @@ function makeQueryPart(
queryPart.left,
ignorePrefix,
allowedTypes,
includeThis
includeThis,
tableAlias
),
makeQueryPart(
queryPart.right,
ignorePrefix,
allowedTypes,
includeThis
includeThis,
tableAlias
),
],
};
Expand Down
7 changes: 5 additions & 2 deletions lib/make/projectColumnParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ exports.projectColumnParser = projectColumnParser;
/**
* @param {import('../types').Column} column The column to parse
* @param {import('../types').ColumnParseResult} result the result object
* @param {string} [tableAlias] - a table alias to check if it hasn't been specified
* @returns {void}
*/
function projectColumnParser(column, result) {
function projectColumnParser(column, result, tableAlias = '') {
if (column.expr.type === 'column_ref') {
const columnTable = column.expr.table || tableAlias;

if (column.as && column.as.toUpperCase() === '$$ROOT') {
result.replaceRoot = {
$replaceRoot: {newRoot: `$${column.expr.column}`},
Expand All @@ -32,7 +35,7 @@ function projectColumnParser(column, result) {
return;
}
result.parsedProject.$project[column.as || column.expr.column] = `$${
column.expr.table ? column.expr.table + '.' : ''
columnTable ? columnTable + '.' : ''
}${column.expr.column}`;
return;
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@synatic/sql-to-mongo",
"version": "1.1.4",
"version": "1.1.5",
"description": "Convert SQL to mongo queries or aggregates",
"main": "index.js",
"files": [
Expand Down
Loading

0 comments on commit 620f552

Please sign in to comment.