Skip to content

Commit

Permalink
Add CONTAINS operator to query language (hyperledger-archives#2471)
Browse files Browse the repository at this point in the history
* Add CONTAINS operator to Composer query language

Signed-off-by: Simon Stone <sstone1@uk.ibm.com>

* Stop resetting environment inbetween query tests

Signed-off-by: Simon Stone <sstone1@uk.ibm.com>
  • Loading branch information
Simon Stone authored and nklincoln committed Oct 25, 2017
1 parent 0553288 commit 5931401
Show file tree
Hide file tree
Showing 14 changed files with 979 additions and 66 deletions.
10 changes: 9 additions & 1 deletion packages/composer-common/lib/query/parser.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,14 @@ LogicalORExpressionNoIn
LogicalOROperator
= "OR"

ContainsExpression
= first:LogicalORExpression
rest:(__ ContainsOperator __ LogicalORExpression)*
{ return buildBinaryExpression(first, rest); }

ContainsOperator
= "CONTAINS"

ConditionalExpression
= test:LogicalORExpression __
"?" __ consequent:AssignmentExpression __
Expand All @@ -836,7 +844,7 @@ ConditionalExpression
alternate: alternate
};
}
/ LogicalORExpression
/ ContainsExpression

ConditionalExpressionNoIn
= test:LogicalORExpressionNoIn __
Expand Down
95 changes: 91 additions & 4 deletions packages/composer-common/lib/query/queryanalyzer.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ class QueryAnalyzer {
result = this.visitIdentifier(thing, parameters);
} else if (thing.type === 'Literal') {
result = this.visitLiteral(thing, parameters);
} else if (thing.type === 'ArrayExpression') {
result = this.visitArrayExpression(thing, parameters);
} else if (thing.type === 'MemberExpression') {
result = this.visitMemberExpression(thing, parameters);
} else {
Expand Down Expand Up @@ -251,6 +253,8 @@ class QueryAnalyzer {
let result;
if (arrayCombinationOperators.indexOf(ast.operator) !== -1) {
result = this.visitArrayCombinationOperator(ast, parameters);
} else if (ast.operator === 'CONTAINS') {
result = this.visitContainsOperator(ast, parameters);
} else {
result = this.visitConditionOperator(ast, parameters);
}
Expand Down Expand Up @@ -279,6 +283,56 @@ class QueryAnalyzer {
return result;
}

/**
* Visitor design pattern; handle an contains operator.
* @param {Object} ast The abstract syntax tree being visited.
* @param {Object} parameters The parameters.
* @return {Object} The result of visiting, or null.
* @private
*/
visitContainsOperator(ast, parameters) {
const method = 'visitContainsOperator';
LOG.entry(method, ast, parameters);

// Check we haven't already entered a scope - let's keep it simple!
if (parameters.validationDisabled) {
throw new Error('A CONTAINS expression cannot be nested within another CONTAINS expression');
}

// Disable validation.
parameters.validationDisabled = true;

// Resolve both the left and right sides of the expression.
let left = this.visit(ast.left, parameters);
let right = this.visit(ast.right, parameters);

// Enable validation again.
parameters.validationDisabled = false;

// Initialize the scopes array.
parameters.scopes = parameters.scopes || [];

// Look for a scope name.
if (typeof left === 'string' && !left.startsWith('_$')) {
parameters.scopes.push(left);
} else if (typeof right === 'string' && !right.startsWith('_$')) {
parameters.scopes.push(right);
} else {
throw new Error('A property name is required on one side of a CONTAINS expression');
}

// Re-resolve both the left and right sides of the expression.
left = this.visit(ast.left, parameters);
right = this.visit(ast.right, parameters);

// Pop the scope name off again.
parameters.scopes.pop();

const result = [left, right];
LOG.exit(method, result);
return result;
}

/**
* Visitor design pattern; handle a condition operator.
* Condition operators are operators that compare two pieces of data, such
Expand All @@ -298,19 +352,27 @@ class QueryAnalyzer {
const rhs = this.visit(ast.right, parameters);
const lhs = this.visit(ast.left, parameters);

// Bypass the following validation if required. This will be set during
// the first pass of a CONTAINS when we are trying to figure out the name
// of the current scope so we can correctly validate model references.
if (parameters.validationDisabled) {
LOG.exit(method, result);
return result;
}

// if the rhs is a string, it is the name of a property
// and we infer the type of the lhs from the model
// if the lhs is a parameter
if (typeof rhs === 'string' && (lhs instanceof Array && lhs.length > 0)) {
lhs[0].type = this.getParameterType(rhs);
lhs[0].type = this.getParameterType(rhs, parameters);
result = result.concat(lhs);
}

// if the lhs is a string, it is the name of a property
// and we infer the type of the rhs from the model
// if the rhs is a parameter
if (typeof lhs === 'string' && (rhs instanceof Array && rhs.length > 0)) {
rhs[0].type = this.getParameterType(lhs);
rhs[0].type = this.getParameterType(lhs, parameters);
result = result.concat(rhs);
}

Expand Down Expand Up @@ -368,6 +430,23 @@ class QueryAnalyzer {
return result;
}

/**
* Visitor design pattern; handle an array expression.
* @param {Object} ast The abstract syntax tree being visited.
* @param {Object} parameters The parameters.
* @return {Object} The result of visiting, or null.
* @private
*/
visitArrayExpression(ast, parameters) {
const method = 'visitArrayExpression';
LOG.entry(method, ast, parameters);
const result = ast.elements.map((element) => {
return this.visit(element, parameters);
});
LOG.exit(method, result);
return result;
}

/**
* Visitor design pattern; handle a member expression.
* @param {Object} ast The abstract syntax tree being visited.
Expand All @@ -388,16 +467,24 @@ class QueryAnalyzer {
/**
* Get the parameter type for a property path on a resource
* @param {string} parameterName The parameter name or name with nested structure e.g A.B.C
* @param {object} parameters The parameters
* @return {string} The type to use for the parameter
* @throws {Error} if the property does not exist or is of an unsupported type
* @private
*/
getParameterType(parameterName) {
getParameterType(parameterName, parameters) {
const method = 'getParameterType';
LOG.entry(method, parameterName);

// If we have entered a scope, for example a CONTAINS, then we need
// to prepend the current scope to the property name.
let actualParameterName = parameterName;
if (parameters.scopes && parameters.scopes.length) {
actualParameterName = parameters.scopes.concat(parameterName).join('.');
}

const classDeclaration = this.query.getSelect().getResourceClassDeclaration();
const property = classDeclaration.getNestedProperty(parameterName);
const property = classDeclaration.getNestedProperty(actualParameterName);

let result = null;

Expand Down
114 changes: 97 additions & 17 deletions packages/composer-common/lib/query/whereastvalidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const InvalidQueryException = require('./invalidqueryexception');
const Globalize = require('../globalize');

const BOOLEAN_OPERATORS = ['==', '!='];
const OPERATORS = ['>', '>=', '==', '!=', '<=', '<'];
const OPERATORS = ['>', '>=', '==', '!=', '<=', '<', 'CONTAINS'];

/**
* The query validator visits the AST for a WHERE and checks that all the model references exist
Expand Down Expand Up @@ -59,6 +59,8 @@ class WhereAstValidator {
result = this.visitIdentifier(thing, parameters);
} else if (thing.type === 'Literal') {
result = this.visitLiteral(thing, parameters);
} else if (thing.type === 'ArrayExpression') {
result = this.visitArrayExpression(thing, parameters);
} else if (thing.type === 'MemberExpression') {
result = this.visitMemberExpression(thing, parameters);
} else {
Expand All @@ -84,6 +86,8 @@ class WhereAstValidator {
const arrayCombinationOperators = ['AND', 'OR'];
if (arrayCombinationOperators.indexOf(ast.operator) !== -1) {
this.visitArrayCombinationOperator(ast, parameters);
} else if (ast.operator === 'CONTAINS') {
this.visitContainsOperator(ast, parameters);
} else {
this.visitConditionOperator(ast, parameters);
}
Expand Down Expand Up @@ -111,6 +115,55 @@ class WhereAstValidator {
return null;
}

/**
* Visitor design pattern; handle an contains operator.
* @param {Object} ast The abstract syntax tree being visited.
* @param {Object} parameters The parameters.
* @return {Object} The result of visiting, or null.
* @private
*/
visitContainsOperator(ast, parameters) {
const method = 'visitContainsOperator';
LOG.entry(method, ast, parameters);

// Check we haven't already entered a scope - let's keep it simple!
if (parameters.validationDisabled) {
throw new Error('A CONTAINS expression cannot be nested within another CONTAINS expression');
}

// Disable validation.
parameters.validationDisabled = true;

// Resolve both the left and right sides of the expression.
let left = this.visit(ast.left, parameters);
let right = this.visit(ast.right, parameters);

// Enable validation again.
parameters.validationDisabled = false;

// Initialize the scopes array.
parameters.scopes = parameters.scopes || [];

// Look for a scope name.
if (typeof left === 'string' && !left.startsWith('_$')) {
parameters.scopes.push(left);
} else if (typeof right === 'string' && !right.startsWith('_$')) {
parameters.scopes.push(right);
} else {
throw new Error('A property name is required on one side of a CONTAINS expression');
}

// Re-resolve both the left and right sides of the expression.
left = this.visit(ast.left, parameters);
right = this.visit(ast.right, parameters);

// Pop the scope name off again.
parameters.scopes.pop();

LOG.exit(method);
return null;
}

/**
* Visitor design pattern; handle a condition operator.
* Condition operators are operators that compare two pieces of data, such
Expand All @@ -123,32 +176,37 @@ class WhereAstValidator {
visitConditionOperator(ast, parameters) {
const method = 'visitConditionOperator';
LOG.entry(method, ast, parameters);

// Resolve both the left and right sides of the expression.
const left = this.visit(ast.left, parameters);
const right = this.visit(ast.right, parameters);

// console.log('ast: ' + JSON.stringify(ast));
// console.log('left: ' + JSON.stringify(left));
// console.log('right: ' + JSON.stringify(right));
// Bypass the following validation if required. This will be set during
// the first pass of a CONTAINS when we are trying to figure out the name
// of the current scope so we can correctly validate model references.
if (parameters.validationDisabled) {
LOG.exit(method);
return null;
}

if (typeof left === 'string') {

if (!left.startsWith('_$')) {
const property = this.verifyProperty(left);
const property = this.verifyProperty(left, parameters);
this.verifyOperator(property, ast.operator);
}
if (right.type === 'Literal') {
const property = this.verifyProperty(left);
if (right && right.type === 'Literal') {
const property = this.verifyProperty(left, parameters);
this.verifyTypeCompatibility(property, right.value);
}
}

if (typeof right === 'string') {
if (!right.startsWith('_$')) {
const property = this.verifyProperty(right);
const property = this.verifyProperty(right, parameters);
this.verifyOperator(property, ast.operator);
}
if (left.type === 'Literal') {
const property = this.verifyProperty(right);
if (left && left.type === 'Literal') {
const property = this.verifyProperty(right, parameters);
this.verifyTypeCompatibility(property, left.value);
}
}
Expand All @@ -161,13 +219,21 @@ class WhereAstValidator {
/**
* Checks that a property exists on the class declaration
* @param {string} propertyName The property path
* @param {object} parameters The parameters
* @throws {IllegalModelException} if property path does not resolve to a property
* @returns {Property} the property
* @private
*/
verifyProperty(propertyName) {
verifyProperty(propertyName, parameters) {

const property = this.classDeclaration.getNestedProperty(propertyName);
// If we have entered a scope, for example a CONTAINS, then we need
// to prepend the current scope to the property name.
let actualPropertyName = propertyName;
if (parameters.scopes && parameters.scopes.length) {
actualPropertyName = parameters.scopes.concat(propertyName).join('.');
}

const property = this.classDeclaration.getNestedProperty(actualPropertyName);
if (!property) {
throw new IllegalModelException('Property ' + propertyName + ' not found on type ' + this.classDeclaration, this.classDeclaration.getModelFile(), this.classDeclaration. ast.location);
}
Expand Down Expand Up @@ -206,18 +272,15 @@ class WhereAstValidator {
*/
verifyTypeCompatibility(property, value) {

// console.log('property: ' + property);
// console.log('value: ' + value);

let dataType = typeof value;
// console.log('dataType: ' + dataType);

if (dataType === 'undefined' || dataType === 'symbol') {
WhereAstValidator.reportIncompatibleType(this.classDeclaration, property, value);
}

let invalid = false;

// Arrays of concepts can't currently be specified as literal values.
if (property.isArray()) {
WhereAstValidator.reportUnsupportedType(this.classDeclaration, property, value);
}
Expand Down Expand Up @@ -300,6 +363,23 @@ class WhereAstValidator {
return ast;
}

/**
* Visitor design pattern; handle an array expression.
* @param {Object} ast The abstract syntax tree being visited.
* @param {Object} parameters The parameters.
* @return {Object} The result of visiting, or null.
* @private
*/
visitArrayExpression(ast, parameters) {
const method = 'visitArrayExpression';
LOG.entry(method, ast, parameters);
const selector = ast.elements.map((element) => {
return this.visit(element, parameters);
});
LOG.exit(method, selector);
return selector;
}

/**
* Visitor design pattern; handle a member expression.
* @param {Object} ast The abstract syntax tree being visited.
Expand Down
12 changes: 12 additions & 0 deletions packages/composer-common/test/data/query/model.cto
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,15 @@ participant Regulator identified by email {
transaction MyTransaction {

}

concept TestConcept {
o String value
o String[] values
}

asset TestAsset identified by assetId {
o String assetId
o String[] stringValues
o TestConcept conceptValue
o TestConcept[] conceptValues
}
Loading

0 comments on commit 5931401

Please sign in to comment.