diff --git a/.gitmodules b/.gitmodules
index d2ffc0c9c6..465e70960e 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -4,3 +4,7 @@
[submodule "extern/log"]
path = extern/log
url = ../../lsst/log
+[submodule "extern/hyrise-sql-parser"]
+ path = extern/hyrise-sql-parser
+ url = https://github.com/lsst/hyrise-sql-parser
+ branch = qserv-compat
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9b1ee2c3db..d9d3bc0d6a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -7,12 +7,18 @@ project(Qserv
enable_testing()
+option(QSERV_USE_HYRISE_SQL_PARSER
+ "Use the Hyrise SQL parser to build query IR for SELECT statements, instead of the ANTLR-based parser"
+ ON
+)
+
include(GNUInstallDirs)
set(CMAKE_INSTALL_PREFIX ${PROJECT_BINARY_DIR}/install)
add_subdirectory(bin)
add_subdirectory(doc)
add_subdirectory(etc)
+add_subdirectory(extern/hyrise-sql-parser)
add_subdirectory(extern/log)
add_subdirectory(extern/sphgeom)
add_subdirectory(python)
diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt
index 0b88a81811..f6995f74b0 100644
--- a/doc/CMakeLists.txt
+++ b/doc/CMakeLists.txt
@@ -19,11 +19,11 @@ add_custom_target(docs-linkcheck
COMMENT "Checking documentation links with Sphinx"
COMMAND ${SPHINX_EXECUTABLE}
-b linkcheck
- -d ${CMAKE_CURRENT_BINARY_DIR}/doctrees
+ -d ${CMAKE_CURRENT_BINARY_DIR}/doctrees-linkcheck
-n -W
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR}/linkcheck
BYPRODUCTS
- ${CMAKE_CURRENT_BINARY_DIR}/doctrees
+ ${CMAKE_CURRENT_BINARY_DIR}/doctrees-linkcheck
${CMAKE_CURRENT_BINARY_DIR}/linkcheck
)
diff --git a/extern/hyrise-sql-parser b/extern/hyrise-sql-parser
new file mode 160000
index 0000000000..17e8d126e6
--- /dev/null
+++ b/extern/hyrise-sql-parser
@@ -0,0 +1 @@
+Subproject commit 17e8d126e6774cfdc1a30f1195fa755e590474e1
diff --git a/src/ccontrol/CMakeLists.txt b/src/ccontrol/CMakeLists.txt
index c1cb26ddb3..4b8bc681e2 100644
--- a/src/ccontrol/CMakeLists.txt
+++ b/src/ccontrol/CMakeLists.txt
@@ -5,6 +5,7 @@ target_include_directories(ccontrol PRIVATE
)
target_sources(ccontrol PRIVATE
+ HyriseAdapter.cc
MergingHandler.cc
ParseAdapters.cc
ParseListener.cc
@@ -27,11 +28,16 @@ target_link_libraries(ccontrol PUBLIC
cconfig
css
global
+ hyrise_sql_parser::sqlparser
log
parser
sphgeom
)
+if(QSERV_USE_HYRISE_SQL_PARSER)
+ target_compile_definitions(ccontrol PUBLIC QSERV_USE_HYRISE_SQL_PARSER)
+endif()
+
install(TARGETS ccontrol)
FUNCTION(ccontrol_tests)
@@ -58,7 +64,21 @@ FUNCTION(ccontrol_tests)
ENDFUNCTION()
ccontrol_tests(
- testAntlr4GeneratedIR
testCControl
+ testParserCorpus
testUserQueryType
)
+
+target_compile_definitions(testParserCorpus PRIVATE
+ QSERV_PARSER_CORPUS_DIR="${CMAKE_CURRENT_SOURCE_DIR}/testdata/parser-corpus"
+)
+
+if(QSERV_USE_HYRISE_SQL_PARSER)
+ ccontrol_tests(
+ testHyriseGeneratedIR
+ )
+else()
+ ccontrol_tests(
+ testAntlr4GeneratedIR
+ )
+endif()
diff --git a/src/ccontrol/HyriseAdapter.cc b/src/ccontrol/HyriseAdapter.cc
new file mode 100644
index 0000000000..66565f8600
--- /dev/null
+++ b/src/ccontrol/HyriseAdapter.cc
@@ -0,0 +1,935 @@
+// -*- LSST-C++ -*-
+/*
+ * LSST Data Management System
+ * Copyright 2026 LSST.
+ *
+ * This product includes software developed by the
+ * LSST Project (http://www.lsst.org/).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the LSST License Statement and
+ * the GNU General Public License along with this program. If not,
+ * see .
+ */
+
+#include "ccontrol/HyriseAdapter.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+#include "SQLParser.h"
+#include "SQLParserResult.h"
+#include "sql/Expr.h"
+#include "sql/SelectStatement.h"
+#include "sql/Table.h"
+
+#include "global/constants.h"
+#include "parser/ParseException.h"
+#include "query/AndTerm.h"
+#include "query/AreaRestrictor.h"
+#include "query/BetweenPredicate.h"
+#include "query/BoolFactor.h"
+#include "query/BoolTermFactor.h"
+#include "query/ColumnRef.h"
+#include "query/CompPredicate.h"
+#include "query/FromList.h"
+#include "query/FuncExpr.h"
+#include "query/GroupByClause.h"
+#include "query/HavingClause.h"
+#include "query/InPredicate.h"
+#include "query/JoinRef.h"
+#include "query/JoinSpec.h"
+#include "query/LikePredicate.h"
+#include "query/NullPredicate.h"
+#include "query/OrderByClause.h"
+#include "query/OrTerm.h"
+#include "query/SelectList.h"
+#include "query/SelectStmt.h"
+#include "query/TableRef.h"
+#include "query/ValueExpr.h"
+#include "query/ValueExprPredicate.h"
+#include "query/ValueFactor.h"
+#include "query/WhereClause.h"
+
+namespace {
+
+using namespace lsst::qserv;
+
+// Forward declarations
+void validateStatement(hsql::SelectStatement const* stmt);
+void validateExpression(hsql::Expr const* expr);
+std::shared_ptr buildValueExpr(hsql::Expr const* expr);
+std::shared_ptr buildBoolFactorTerm(hsql::Expr const* expr);
+std::shared_ptr buildBoolTerm(hsql::Expr const* expr);
+
+std::string nullToEmpty(char const* value) { return value == nullptr ? std::string() : std::string(value); }
+
+[[noreturn]] void unsupported(std::string const& what) {
+ throw parser::ParseException("HyriseAdapter unsupported SQL construct: " + what);
+}
+
+std::string unparseableQueryError(std::string const& sql) {
+ return std::string("Failed to instantiate query: \"") + sql + '"';
+}
+
+std::string formatFloat(double value) {
+ // Note using: std::to_chars because it is locale-independent
+ // and emits the shortest representation that round-trips back to the same double
+ if (!std::isfinite(value)) unsupported("infinite floating-point literal");
+ std::array buf;
+ auto const [ptr, ec] = std::to_chars(buf.data(), buf.data() + buf.size(), value);
+ if (ec != std::errc()) unsupported("unrepresentable floating-point literal");
+ return std::string(buf.data(), ptr);
+}
+
+// Human-readable names for the Hyrise enum values to help produce better error messages.
+std::string exprTypeName(hsql::ExprType type) {
+ switch (type) {
+ case hsql::kExprCast:
+ return "CAST expression";
+ case hsql::kExprExtract:
+ return "EXTRACT expression";
+ case hsql::kExprSelect:
+ return "scalar subquery";
+ case hsql::kExprParameter:
+ return "parameter placeholder";
+ case hsql::kExprArray:
+ return "array literal";
+ case hsql::kExprArrayIndex:
+ return "array subscript";
+ case hsql::kExprLiteralDate:
+ return "date literal";
+ case hsql::kExprLiteralInterval:
+ return "interval literal";
+ case hsql::kExprHint:
+ return "optimizer hint";
+ default:
+ return "expression type " + std::to_string(type);
+ }
+}
+
+std::string operatorName(hsql::OperatorType op) {
+ switch (op) {
+ case hsql::kOpILike:
+ return "ILIKE";
+ case hsql::kOpExists:
+ return "EXISTS";
+ case hsql::kOpCase:
+ return "CASE";
+ case hsql::kOpNot:
+ return "NOT";
+ case hsql::kOpLike:
+ return "LIKE";
+ case hsql::kOpAnd:
+ return "AND";
+ case hsql::kOpOr:
+ return "OR";
+ case hsql::kOpIn:
+ return "IN";
+ case hsql::kOpBetween:
+ return "BETWEEN";
+ case hsql::kOpIsNull:
+ return "IS NULL";
+ default:
+ return "operator type " + std::to_string(op);
+ }
+}
+
+void validateIdentifier(char const* identifier) {
+ if (identifier != nullptr && identifier[0] == '_') {
+ throw parser::ParseException("Error parsing query, near \"" + nullToEmpty(identifier) +
+ "\", Identifiers in Qserv may not start with an underscore.");
+ }
+}
+
+void validateAlias(hsql::Alias const* alias) {
+ if (alias == nullptr) return;
+ validateIdentifier(alias->name);
+ if (alias->columns != nullptr) {
+ for (auto const* column : *alias->columns) {
+ validateIdentifier(column);
+ }
+ }
+}
+
+void validateOrder(hsql::OrderDescription const* order) {
+ if (order != nullptr) validateExpression(order->expr);
+}
+
+void validateLimit(hsql::LimitDescription const* limit) {
+ if (limit == nullptr) return;
+ validateExpression(limit->limit);
+ validateExpression(limit->offset);
+}
+
+void validateExpression(hsql::Expr const* expr) {
+ if (expr == nullptr) return;
+ if (expr->type == hsql::kExprOperator && expr->opType == hsql::kOpCase) {
+ throw parser::ParseException("qserv can not parse query: CASE expressions are not supported.");
+ }
+ if (expr->type == hsql::kExprColumnRef || expr->type == hsql::kExprStar) {
+ validateIdentifier(expr->schema);
+ validateIdentifier(expr->table);
+ validateIdentifier(expr->name);
+ } else if (expr->type == hsql::kExprFunctionRef) {
+ validateIdentifier(expr->schema);
+ validateIdentifier(expr->name);
+ }
+ validateIdentifier(expr->alias);
+ validateExpression(expr->expr);
+ validateExpression(expr->expr2);
+ if (expr->exprList != nullptr) {
+ for (auto const* child : *expr->exprList) {
+ validateExpression(child);
+ }
+ }
+ validateStatement(expr->select);
+ if (expr->windowDescription != nullptr) {
+ if (expr->windowDescription->partitionList != nullptr) {
+ for (auto const* partition : *expr->windowDescription->partitionList) {
+ validateExpression(partition);
+ }
+ }
+ if (expr->windowDescription->orderList != nullptr) {
+ for (auto const* order : *expr->windowDescription->orderList) {
+ validateOrder(order);
+ }
+ }
+ }
+}
+
+void validateTable(hsql::TableRef const* table) {
+ if (table == nullptr) return;
+ validateIdentifier(table->schema);
+ validateIdentifier(table->name);
+ validateAlias(table->alias);
+ validateStatement(table->select);
+ if (table->list != nullptr) {
+ for (auto const* child : *table->list) {
+ validateTable(child);
+ }
+ }
+ if (table->join != nullptr) {
+ validateTable(table->join->left);
+ validateTable(table->join->right);
+ validateExpression(table->join->condition);
+ if (table->join->namedColumns != nullptr) {
+ for (auto const* column : *table->join->namedColumns) {
+ validateIdentifier(column);
+ }
+ }
+ }
+}
+
+void validateStatement(hsql::SelectStatement const* stmt) {
+ if (stmt == nullptr) return;
+ if (stmt->selectList != nullptr) {
+ for (auto const* expr : *stmt->selectList) {
+ validateExpression(expr);
+ }
+ }
+ validateTable(stmt->fromTable);
+ validateExpression(stmt->whereClause);
+ if (stmt->groupBy != nullptr) {
+ if (stmt->groupBy->columns != nullptr) {
+ for (auto const* expr : *stmt->groupBy->columns) {
+ validateExpression(expr);
+ }
+ }
+ validateExpression(stmt->groupBy->having);
+ }
+ validateExpression(stmt->having);
+ if (stmt->order != nullptr) {
+ for (auto const* order : *stmt->order) {
+ validateOrder(order);
+ }
+ }
+ validateLimit(stmt->limit);
+ if (stmt->withDescriptions != nullptr) {
+ for (auto const* with : *stmt->withDescriptions) {
+ validateIdentifier(with->alias);
+ validateStatement(with->select);
+ }
+ }
+ if (stmt->setOperations != nullptr) {
+ for (auto const* setOp : *stmt->setOperations) {
+ validateStatement(setOp->nestedSelectStatement);
+ if (setOp->resultOrder != nullptr) {
+ for (auto const* order : *setOp->resultOrder) {
+ validateOrder(order);
+ }
+ }
+ validateLimit(setOp->resultLimit);
+ }
+ }
+ if (stmt->lockings != nullptr) {
+ for (auto const* locking : *stmt->lockings) {
+ if (locking->tables != nullptr) {
+ for (auto const* table : *locking->tables) {
+ validateIdentifier(table);
+ }
+ }
+ }
+ }
+}
+
+query::ValueExpr::Op valueOp(hsql::OperatorType op) {
+ switch (op) {
+ case hsql::kOpPlus:
+ return query::ValueExpr::PLUS;
+ case hsql::kOpMinus:
+ return query::ValueExpr::MINUS;
+ case hsql::kOpAsterisk:
+ return query::ValueExpr::MULTIPLY;
+ case hsql::kOpSlash:
+ return query::ValueExpr::DIVIDE;
+ case hsql::kOpPercentage:
+ return query::ValueExpr::MODULO;
+ case hsql::kOpMod:
+ return query::ValueExpr::MOD;
+ case hsql::kOpDiv:
+ return query::ValueExpr::DIV;
+ case hsql::kOpCaret:
+ case hsql::kOpBitXor:
+ return query::ValueExpr::BIT_XOR;
+ case hsql::kOpBitAnd:
+ return query::ValueExpr::BIT_AND;
+ case hsql::kOpBitOr:
+ return query::ValueExpr::BIT_OR;
+ case hsql::kOpBitShiftLeft:
+ return query::ValueExpr::BIT_SHIFT_LEFT;
+ case hsql::kOpBitShiftRight:
+ return query::ValueExpr::BIT_SHIFT_RIGHT;
+ default:
+ unsupported("value operator " + operatorName(op));
+ }
+}
+
+query::CompPredicate::OpType compOp(hsql::OperatorType op) {
+ switch (op) {
+ case hsql::kOpEquals:
+ return query::CompPredicate::EQUALS_OP;
+ case hsql::kOpNullSafeEquals:
+ return query::CompPredicate::NULL_SAFE_EQUALS_OP;
+ case hsql::kOpNotEquals:
+ return query::CompPredicate::NOT_EQUALS_OP;
+ case hsql::kOpLess:
+ return query::CompPredicate::LESS_THAN_OP;
+ case hsql::kOpLessEq:
+ return query::CompPredicate::LESS_THAN_OR_EQUALS_OP;
+ case hsql::kOpGreater:
+ return query::CompPredicate::GREATER_THAN_OP;
+ case hsql::kOpGreaterEq:
+ return query::CompPredicate::GREATER_THAN_OR_EQUALS_OP;
+ default:
+ unsupported("comparison operator " + operatorName(op));
+ }
+}
+
+std::shared_ptr buildColumnRef(hsql::Expr const* expr) {
+ if (expr->type != hsql::kExprColumnRef) unsupported("non-column column reference");
+ std::string schema = nullToEmpty(expr->schema);
+ std::string table = nullToEmpty(expr->table);
+ std::string name = nullToEmpty(expr->name);
+ if (name.empty()) unsupported("empty column reference");
+ if (!schema.empty() || !table.empty()) {
+ return std::make_shared(schema, table, name);
+ }
+ return std::make_shared(name);
+}
+
+std::shared_ptr buildSimpleBoolTerm(std::shared_ptr const& term,
+ bool hasNot = false) {
+ return std::make_shared(
+ query::BoolTerm::PtrVector{std::make_shared(term, hasNot)});
+}
+
+bool isQservRestrictorName(std::string const& name) {
+ return boost::algorithm::iequals(name, "qserv_areaspec_box") ||
+ boost::algorithm::iequals(name, "qserv_areaspec_circle") ||
+ boost::algorithm::iequals(name, "qserv_areaspec_ellipse") ||
+ boost::algorithm::iequals(name, "qserv_areaspec_poly");
+}
+
+bool isIntegerLiteral(hsql::Expr const* expr) {
+ return expr != nullptr &&
+ (expr->type == hsql::kExprLiteralInt || expr->type == hsql::kExprLiteralIntString);
+}
+
+std::shared_ptr buildValueFactor(hsql::Expr const* expr) {
+ if (expr == nullptr) unsupported("null expression");
+ switch (expr->type) {
+ case hsql::kExprColumnRef:
+ return query::ValueFactor::newColumnRefFactor(buildColumnRef(expr));
+
+ case hsql::kExprStar:
+ return query::ValueFactor::newStarFactor(nullToEmpty(expr->table));
+
+ case hsql::kExprLiteralInt:
+ return query::ValueFactor::newConstFactor(std::to_string(expr->ival));
+
+ case hsql::kExprLiteralIntString:
+ return query::ValueFactor::newConstFactor(nullToEmpty(expr->name));
+
+ case hsql::kExprLiteralFloat:
+ return query::ValueFactor::newConstFactor(formatFloat(expr->fval));
+
+ case hsql::kExprLiteralString: {
+ std::string s = nullToEmpty(expr->name);
+ std::string escaped;
+ escaped.reserve(s.size());
+ for (char c : s) {
+ if (c == '\'') escaped += '\'';
+ escaped += c;
+ }
+ return query::ValueFactor::newConstFactor("'" + escaped + "'");
+ }
+
+ case hsql::kExprLiteralNull:
+ return query::ValueFactor::newConstFactor("NULL");
+
+ case hsql::kExprFunctionRef: {
+ // Filter out disallowed function modifiers
+ if (expr->distinct) unsupported("DISTINCT in function arguments");
+ if (expr->windowDescription != nullptr) unsupported("window function");
+
+ std::string name = nullToEmpty(expr->name);
+ if (isQservRestrictorName(name)) unsupported("qserv area restrictor function in this position");
+
+ // Populate our function and its arguments.
+ query::ValueExprPtrVector args;
+ if (expr->exprList != nullptr) {
+ for (auto const* arg : *expr->exprList) {
+ args.push_back(buildValueExpr(arg));
+ }
+ }
+ auto func = query::FuncExpr::newWithArgs(name, args);
+
+ // Check for supported / unsupported aggregation functions
+ static char const* const supportedAggregation[] = {
+ "COUNT", "MIN", "MAX", "SUM", "AVG",
+ };
+ for (auto const* agg : supportedAggregation) {
+ if (boost::algorithm::iequals(name, agg)) {
+ return query::ValueFactor::newAggFactor(func);
+ }
+ }
+
+ static char const* const unsupportedAggregation[] = {
+ "STD", "STDDEV", "STDDEV_POP", "STDDEV_SAMP", "VAR_POP", "VAR_SAMP",
+ "VARIANCE", "GROUP_CONCAT", "BIT_AND", "BIT_OR", "BIT_XOR"};
+ for (auto const* agg : unsupportedAggregation) {
+ if (boost::algorithm::iequals(name, agg)) unsupported("aggregate function " + name);
+ }
+
+ return query::ValueFactor::newFuncFactor(func);
+ }
+
+ case hsql::kExprOperator:
+ return query::ValueFactor::newExprFactor(buildValueExpr(expr));
+
+ default:
+ unsupported(exprTypeName(expr->type));
+ }
+}
+
+std::shared_ptr buildBoolFactorTerm(hsql::Expr const* expr) {
+ if (expr == nullptr) unsupported("null boolean factor");
+ if (expr->type != hsql::kExprOperator) {
+ return std::make_shared(buildValueExpr(expr));
+ }
+
+ std::shared_ptr predicate;
+ if (expr->opType == hsql::kOpIn) {
+ if (expr->select != nullptr) unsupported("IN subquery");
+ if (expr->exprList == nullptr) unsupported("empty IN list");
+ query::ValueExprPtrVector values;
+ for (auto const* value : *expr->exprList) {
+ values.push_back(buildValueExpr(value));
+ }
+ predicate = std::make_shared(buildValueExpr(expr->expr), values, false);
+ } else if (expr->opType == hsql::kOpBetween) {
+ if (expr->exprList == nullptr || expr->exprList->size() != 2) unsupported("BETWEEN bounds");
+ predicate = std::make_shared(buildValueExpr(expr->expr),
+ buildValueExpr(expr->exprList->at(0)),
+ buildValueExpr(expr->exprList->at(1)), false);
+ } else if (expr->opType == hsql::kOpLike || expr->opType == hsql::kOpNotLike) {
+ predicate = std::make_shared(
+ buildValueExpr(expr->expr), buildValueExpr(expr->expr2), expr->opType == hsql::kOpNotLike);
+ } else if (expr->opType == hsql::kOpIsNull) {
+ predicate = std::make_shared(buildValueExpr(expr->expr), false);
+ } else {
+ predicate = std::make_shared(buildValueExpr(expr->expr), compOp(expr->opType),
+ buildValueExpr(expr->expr2));
+ }
+ return predicate;
+}
+
+std::string restrictorArg(hsql::Expr const* expr) {
+ if (expr == nullptr) unsupported("null qserv area restrictor argument");
+ switch (expr->type) {
+ case hsql::kExprLiteralInt:
+ return std::to_string(expr->ival);
+ case hsql::kExprLiteralIntString:
+ return nullToEmpty(expr->name);
+ case hsql::kExprLiteralFloat:
+ return formatFloat(expr->fval);
+ case hsql::kExprOperator:
+ if (expr->opType == hsql::kOpUnaryMinus) {
+ return "-" + restrictorArg(expr->expr);
+ }
+ break;
+ default:
+ break;
+ }
+ unsupported("non-constant qserv area restrictor argument");
+}
+
+std::shared_ptr buildAreaRestrictor(hsql::Expr const* expr) {
+ if (expr == nullptr) return nullptr;
+
+ // The following is intended to match the legacy ANTLR-based parser. It used a dedicated
+ // QservFunctionSpec rule for qserv_areaspec_* functions, but only when it was bare or
+ // compared against an integer literal ("qserv_areaspec_box(...) = 1", "1 = ...").
+ if (expr->type == hsql::kExprOperator && expr->opType == hsql::kOpEquals) {
+ if (isIntegerLiteral(expr->expr2)) {
+ if (auto r = buildAreaRestrictor(expr->expr)) return r;
+ }
+ if (isIntegerLiteral(expr->expr)) {
+ if (auto r = buildAreaRestrictor(expr->expr2)) return r;
+ }
+ }
+
+ // Ensure this is a function ref.
+ if (expr->type != hsql::kExprFunctionRef) return nullptr;
+
+ // Only qserv_areaspec_* are restrictors; for any other function return nullptr so normal value /
+ // predicate handling takes over
+ std::string name = nullToEmpty(expr->name);
+ if (!isQservRestrictorName(name)) return nullptr;
+
+ // Set up args
+ if (expr->exprList == nullptr) return nullptr;
+ std::vector args;
+ for (auto const* arg : *expr->exprList) {
+ args.push_back(restrictorArg(arg));
+ }
+
+ // Create the appropriate AreaRestrictor*
+ try {
+ if (boost::algorithm::iequals(name, "qserv_areaspec_box")) {
+ return std::make_shared(args);
+ } else if (boost::algorithm::iequals(name, "qserv_areaspec_circle")) {
+ return std::make_shared(args);
+ } else if (boost::algorithm::iequals(name, "qserv_areaspec_ellipse")) {
+ return std::make_shared(args);
+ } else if (boost::algorithm::iequals(name, "qserv_areaspec_poly")) {
+ return std::make_shared(args);
+ }
+ } catch (std::logic_error const& err) {
+ throw parser::ParseException(err.what());
+ }
+
+ return nullptr;
+}
+
+std::shared_ptr buildConstValueExpr(std::string const& value) {
+ return query::ValueExpr::newSimple(query::ValueFactor::newConstFactor(value));
+}
+
+std::shared_ptr buildUnaryMinusValueExpr(hsql::Expr const* expr) {
+ auto const* operand = expr->expr;
+ if (operand != nullptr) {
+ switch (operand->type) {
+ case hsql::kExprLiteralInt:
+ return buildConstValueExpr("-" + std::to_string(operand->ival));
+ case hsql::kExprLiteralIntString:
+ return buildConstValueExpr("-" + nullToEmpty(operand->name));
+ case hsql::kExprLiteralFloat:
+ return buildConstValueExpr("-" + formatFloat(operand->fval));
+ default:
+ break;
+ }
+ }
+
+ auto value = std::make_shared();
+ value->addValueFactor(query::ValueFactor::newConstFactor("0"));
+ value->addOp(query::ValueExpr::MINUS);
+ value->addValueFactor(buildValueFactor(operand));
+ return value;
+}
+
+std::shared_ptr buildValueExpr(hsql::Expr const* expr) {
+ if (expr == nullptr) unsupported("null value expression");
+ if (expr->type != hsql::kExprOperator) {
+ return query::ValueExpr::newSimple(buildValueFactor(expr));
+ }
+ if (expr->opType == hsql::kOpUnaryMinus) {
+ return buildUnaryMinusValueExpr(expr);
+ }
+
+ auto value = std::make_shared();
+ value->addValueFactor(buildValueFactor(expr->expr));
+ value->addOp(valueOp(expr->opType));
+ value->addValueFactor(buildValueFactor(expr->expr2));
+ return value;
+}
+
+void validateAreaRestrictorPlacement(hsql::Expr const* expr, bool allowAreaRestrictorExtraction = true) {
+ if (expr == nullptr) unsupported("null boolean expression");
+
+ if (buildAreaRestrictor(expr) != nullptr) {
+ if (!allowAreaRestrictorExtraction) {
+ unsupported("qserv area restrictors are only supported in conjunctive (AND) contexts");
+ }
+ return;
+ }
+
+ if (expr->type == hsql::kExprOperator && (expr->opType == hsql::kOpAnd || expr->opType == hsql::kOpOr)) {
+ // Area restrictors are extracted into WhereClause and handled as a special case, effectively making
+ // them conjunctive. We explicitly disallow area restrictors combined with OR / NOT.
+ auto const childAllowsAreaRestrictorExtraction =
+ allowAreaRestrictorExtraction && expr->opType != hsql::kOpOr;
+ validateAreaRestrictorPlacement(expr->expr, childAllowsAreaRestrictorExtraction);
+ validateAreaRestrictorPlacement(expr->expr2, childAllowsAreaRestrictorExtraction);
+ return;
+ }
+
+ if (expr->type == hsql::kExprOperator && expr->opType == hsql::kOpNot) {
+ if (expr->expr == nullptr) unsupported("NOT expression");
+ validateAreaRestrictorPlacement(expr->expr, false);
+ }
+}
+
+std::shared_ptr buildWhereTerm(hsql::Expr const* expr, query::WhereClause& where) {
+ if (expr == nullptr) unsupported("null boolean expression");
+ if (auto restrictor = buildAreaRestrictor(expr)) {
+ where.addAreaRestrictor(restrictor);
+ return nullptr;
+ }
+
+ if (expr->type == hsql::kExprOperator && (expr->opType == hsql::kOpAnd || expr->opType == hsql::kOpOr)) {
+ auto left = buildWhereTerm(expr->expr, where);
+ auto right = buildWhereTerm(expr->expr2, where);
+
+ query::BoolTerm::PtrVector terms;
+ if (left != nullptr) terms.push_back(left);
+ if (right != nullptr) terms.push_back(right);
+ if (terms.empty()) return nullptr;
+ if (terms.size() == 1) {
+ auto term = std::dynamic_pointer_cast(terms.front());
+ if (term == nullptr) {
+ term = std::make_shared(terms);
+ }
+ return term;
+ }
+ if (expr->opType == hsql::kOpAnd) {
+ auto andTerm = std::make_shared();
+ for (auto const& term : terms) {
+ if (!andTerm->merge(*term)) {
+ andTerm->addBoolTerm(term);
+ }
+ }
+ return andTerm;
+ }
+
+ auto orTerm = std::make_shared();
+ for (auto const& term : terms) {
+ if (!orTerm->merge(*term)) {
+ orTerm->addBoolTerm(std::make_shared(term));
+ }
+ }
+ return orTerm;
+ }
+ return buildBoolTerm(expr);
+}
+
+std::shared_ptr buildBoolTerm(hsql::Expr const* expr) {
+ if (expr == nullptr) unsupported("null boolean expression");
+ if (expr->type != hsql::kExprOperator) {
+ return buildSimpleBoolTerm(buildBoolFactorTerm(expr));
+ }
+ if (expr->opType == hsql::kOpAnd || expr->opType == hsql::kOpOr) {
+ std::shared_ptr left = buildBoolTerm(expr->expr);
+ if (auto reduced = left->getReduced()) left = reduced;
+ std::shared_ptr right = buildBoolTerm(expr->expr2);
+ if (auto reduced = right->getReduced()) right = reduced;
+ query::BoolTerm::PtrVector terms{left, right};
+ if (expr->opType == hsql::kOpAnd) {
+ return std::make_shared(terms);
+ }
+ return std::make_shared(terms);
+ }
+ if (expr->opType == hsql::kOpNot) {
+ if (expr->expr == nullptr) unsupported("NOT expression");
+ if (expr->expr->type == hsql::kExprOperator) {
+ if (expr->expr->opType == hsql::kOpAnd || expr->expr->opType == hsql::kOpOr) {
+ auto inner = buildBoolTerm(expr->expr);
+ auto wrapped = std::make_shared(
+ std::make_shared(inner), true);
+ wrapped->addParenthesis();
+ return std::make_shared(query::BoolTerm::PtrVector{wrapped});
+ }
+ if (expr->expr->opType == hsql::kOpIn) {
+ if (expr->expr->select != nullptr) unsupported("NOT IN subquery");
+ if (expr->expr->exprList == nullptr) unsupported("empty NOT IN list");
+ query::ValueExprPtrVector values;
+ for (auto const* value : *expr->expr->exprList) {
+ values.push_back(buildValueExpr(value));
+ }
+ return buildSimpleBoolTerm(
+ std::make_shared(buildValueExpr(expr->expr->expr), values, true));
+ }
+ if (expr->expr->opType == hsql::kOpIsNull) {
+ return buildSimpleBoolTerm(
+ std::make_shared(buildValueExpr(expr->expr->expr), true));
+ }
+ if (expr->expr->opType == hsql::kOpBetween) {
+ if (expr->expr->exprList == nullptr || expr->expr->exprList->size() != 2) {
+ unsupported("NOT BETWEEN bounds");
+ }
+ return buildSimpleBoolTerm(std::make_shared(
+ buildValueExpr(expr->expr->expr), buildValueExpr(expr->expr->exprList->at(0)),
+ buildValueExpr(expr->expr->exprList->at(1)), true));
+ }
+ }
+ return buildSimpleBoolTerm(buildBoolFactorTerm(expr->expr), true);
+ }
+ return buildSimpleBoolTerm(buildBoolFactorTerm(expr));
+}
+
+std::shared_ptr buildSelectList(hsql::SelectStatement const& stmt) {
+ auto selectList = std::make_shared();
+ if (stmt.selectList == nullptr) unsupported("missing select list");
+ for (auto const* expr : *stmt.selectList) {
+ auto value = buildValueExpr(expr);
+ if (expr->alias != nullptr) {
+ value->setAlias(nullToEmpty(expr->alias));
+ value->setAliasIsUserDefined(true);
+ }
+ selectList->addValueExpr(value);
+ }
+ return selectList;
+}
+
+query::JoinRef::Type buildJoinType(hsql::JoinType type) {
+ switch (type) {
+ case hsql::kJoinInner:
+ return query::JoinRef::DEFAULT;
+ case hsql::kJoinLeft:
+ return query::JoinRef::LEFT;
+ case hsql::kJoinRight:
+ return query::JoinRef::RIGHT;
+ case hsql::kJoinFull:
+ return query::JoinRef::FULL;
+ case hsql::kJoinCross:
+ return query::JoinRef::CROSS;
+ case hsql::kJoinNatural:
+ return query::JoinRef::DEFAULT;
+ default:
+ unsupported("join type");
+ }
+}
+
+std::shared_ptr buildJoinSpec(hsql::JoinDefinition const* join) {
+ if (join == nullptr) unsupported("missing join definition");
+ if (join->namedColumns != nullptr) {
+ if (join->namedColumns->size() != 1) unsupported("multi-column USING");
+ return std::make_shared(
+ std::make_shared(nullToEmpty(join->namedColumns->front())));
+ }
+ if (join->condition != nullptr) {
+ return std::make_shared(buildBoolTerm(join->condition));
+ }
+ if (join->natural || join->type == hsql::kJoinNatural || join->type == hsql::kJoinCross) {
+ return nullptr;
+ }
+ unsupported("join without ON or USING");
+}
+
+query::TableRef::Ptr buildNamedTableRef(hsql::TableRef const* table) {
+ if (table == nullptr) unsupported("missing from table");
+ if (table->type != hsql::kTableName) unsupported("non-simple table reference");
+
+ std::string alias;
+ if (table->alias != nullptr) alias = nullToEmpty(table->alias->name);
+
+ return std::make_shared(nullToEmpty(table->schema), nullToEmpty(table->name), alias);
+}
+
+query::TableRef::Ptr buildTableRef(hsql::TableRef const* table) {
+ if (table == nullptr) unsupported("missing from table");
+
+ if (table->type == hsql::kTableName) return buildNamedTableRef(table);
+
+ if (table->type != hsql::kTableJoin || table->join == nullptr) {
+ unsupported("non-simple table reference");
+ }
+
+ // Hyrise represents joins as a tree, e.g.:
+ // FROM A
+ // JOIN B ON A.id = B.id
+ // JOIN C ON B.id = C.id
+ // JOIN D ON C.id = D.id
+ //
+ // Is represented as:
+ // kTableJoin
+ // |- kTableJoin (left)
+ // | |- kTableJoin (left)
+ // | | |- kTableName: 'A' (left)
+ // | | |- kTableName: 'B' (right)
+ // | |- kTableName: 'C' (right)
+ // |- kTableName: 'D' (right)
+ //
+ // So we traverse down the left side recursively and build the right
+ // side on our way up to convert this to a Qserv join list.
+
+ auto left = buildTableRef(table->join->left);
+
+ // Qserv supports only named tables on the right side of an explicit join.
+ auto right = buildNamedTableRef(table->join->right);
+
+ left->addJoin(std::make_shared(
+ right, buildJoinType(table->join->type),
+ table->join->natural || table->join->type == hsql::kJoinNatural, buildJoinSpec(table->join)));
+ return left;
+}
+
+std::shared_ptr buildFromList(hsql::SelectStatement const& stmt) {
+ auto tables = std::make_shared();
+ if (stmt.fromTable == nullptr) unsupported("missing FROM");
+ if (stmt.fromTable->type == hsql::kTableCrossProduct) {
+ // We are selecting from multiple tables e.g., SELECT * FROM Object, Source, Filter
+ // (cross-product / CROSS JOIN)
+ if (stmt.fromTable->list == nullptr) unsupported("empty table list");
+ for (auto const* table : *stmt.fromTable->list) {
+ tables->push_back(buildTableRef(table));
+ }
+ } else {
+ // single FROM
+ tables->push_back(buildTableRef(stmt.fromTable));
+ }
+ return std::make_shared(tables);
+}
+
+std::shared_ptr buildWhereClause(hsql::SelectStatement const& stmt) {
+ if (stmt.whereClause == nullptr) return nullptr;
+
+ // Qserv area restrictors have specific placment requirements:
+ validateAreaRestrictorPlacement(stmt.whereClause);
+
+ // The parse tree for WHERE works similarly to table refs, see buildTableRefs
+ // for a brief explanation.
+ auto where = std::make_shared();
+ auto rootTerm = buildWhereTerm(stmt.whereClause, *where);
+ if (rootTerm != nullptr) {
+ where->setRootTerm(rootTerm);
+ }
+ return where;
+}
+
+std::shared_ptr buildOrderBy(hsql::SelectStatement const& stmt) {
+ if (stmt.order == nullptr || stmt.order->empty()) return nullptr;
+
+ auto orderBy = std::make_shared();
+ for (auto const* term : *stmt.order) {
+ if (term->null_ordering != hsql::NullOrdering::Undefined) unsupported("NULLS FIRST/LAST in ORDER BY");
+
+ auto valueExpr = buildValueExpr(term->expr);
+ if (valueExpr->isFunction()) {
+ throw parser::ParseException("qserv does not support functions in ORDER BY.");
+ }
+
+ query::OrderByTerm::Order order = query::OrderByTerm::DEFAULT;
+ if (term->type == hsql::kOrderAsc) order = query::OrderByTerm::ASC;
+ if (term->type == hsql::kOrderDesc) order = query::OrderByTerm::DESC;
+
+ orderBy->addTerm(query::OrderByTerm(valueExpr, order));
+ }
+ return orderBy;
+}
+
+std::shared_ptr buildGroupBy(hsql::SelectStatement const& stmt) {
+ if (stmt.groupBy == nullptr || stmt.groupBy->columns == nullptr || stmt.groupBy->columns->empty()) {
+ return nullptr;
+ }
+
+ auto groupBy = std::make_shared();
+ for (auto const* expr : *stmt.groupBy->columns) {
+ groupBy->addTerm(query::GroupByTerm(buildValueExpr(expr), std::string()));
+ }
+ return groupBy;
+}
+
+std::shared_ptr buildHaving(hsql::SelectStatement const& stmt) {
+ if (stmt.groupBy != nullptr && stmt.groupBy->having != nullptr) {
+ return std::make_shared(buildBoolTerm(stmt.groupBy->having));
+ }
+
+ if (stmt.having != nullptr) {
+ return std::make_shared(buildBoolTerm(stmt.having));
+ }
+ return nullptr;
+}
+
+int buildLimit(hsql::SelectStatement const& stmt) {
+ if (stmt.limit == nullptr || stmt.limit->limit == nullptr) return lsst::qserv::NOTSET;
+ if (stmt.limit->offset != nullptr) unsupported("OFFSET");
+
+ auto const* limit = stmt.limit->limit;
+ if (limit->type != hsql::kExprLiteralInt) unsupported("non-integer LIMIT");
+ if (limit->ival > static_cast(std::numeric_limits::max())) unsupported("LIMIT overflow");
+
+ return static_cast(limit->ival);
+}
+
+} // namespace
+
+namespace lsst::qserv::ccontrol {
+
+std::shared_ptr HyriseAdapter::makeSelectStmt(std::string const& sql) {
+ hsql::SQLParserResult result;
+ hsql::SQLParser::parse(sql, &result);
+
+ if (!result.isValid() || result.size() == 0) {
+ throw parser::ParseException(unparseableQueryError(sql));
+ }
+
+ if (result.size() > 1) {
+ unsupported("multiple statements");
+ }
+
+ auto const* stmt = dynamic_cast(result.getStatement(0));
+
+ if (stmt == nullptr) unsupported("non-SELECT statement");
+ if (stmt->setOperations != nullptr && !stmt->setOperations->empty()) unsupported("set operations");
+ if (stmt->withDescriptions != nullptr && !stmt->withDescriptions->empty()) unsupported("WITH");
+ if (stmt->lockings != nullptr && !stmt->lockings->empty()) unsupported("row locking");
+
+ validateStatement(stmt);
+
+ return std::make_shared(
+ buildSelectList(*stmt), buildFromList(*stmt), buildWhereClause(*stmt), buildOrderBy(*stmt),
+ buildGroupBy(*stmt), buildHaving(*stmt), stmt->selectDistinct, buildLimit(*stmt));
+}
+
+} // namespace lsst::qserv::ccontrol
diff --git a/src/ccontrol/HyriseAdapter.h b/src/ccontrol/HyriseAdapter.h
new file mode 100644
index 0000000000..0e462eec99
--- /dev/null
+++ b/src/ccontrol/HyriseAdapter.h
@@ -0,0 +1,44 @@
+// -*- LSST-C++ -*-
+/*
+ * LSST Data Management System
+ * Copyright 2026 LSST.
+ *
+ * This product includes software developed by the
+ * LSST Project (http://www.lsst.org/).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the LSST License Statement and
+ * the GNU General Public License along with this program. If not,
+ * see .
+ */
+
+#ifndef LSST_QSERV_CCONTROL_HYRISEADAPTER_H
+#define LSST_QSERV_CCONTROL_HYRISEADAPTER_H
+
+#include
+#include
+
+namespace lsst::qserv::query {
+class SelectStmt;
+} // namespace lsst::qserv::query
+
+namespace lsst::qserv::ccontrol {
+
+/// Build Qserv query IR from Hyrise parser output.
+class HyriseAdapter {
+public:
+ static std::shared_ptr makeSelectStmt(std::string const& sql);
+};
+
+} // namespace lsst::qserv::ccontrol
+
+#endif // LSST_QSERV_CCONTROL_HYRISEADAPTER_H
diff --git a/src/ccontrol/ParseRunner.cc b/src/ccontrol/ParseRunner.cc
index 6b7fbfa5bb..d6410251fd 100644
--- a/src/ccontrol/ParseRunner.cc
+++ b/src/ccontrol/ParseRunner.cc
@@ -25,6 +25,7 @@
#include "ccontrol/ParseRunner.h"
// Qserv headers
+#include "ccontrol/HyriseAdapter.h"
#include "ccontrol/UserQuery.h"
#include "parser/ParseException.h"
#include "query/SelectStmt.h"
@@ -97,8 +98,12 @@ class NonRecoveringQSMySqlLexer : public QSMySqlLexer {
namespace lsst::qserv::ccontrol {
std::shared_ptr ParseRunner::makeSelectStmt(std::string const& statement) {
+#ifdef QSERV_USE_HYRISE_SQL_PARSER
+ return HyriseAdapter::makeSelectStmt(statement);
+#else
auto parser = std::make_shared(statement);
return parser->getSelectStmt();
+#endif
}
ParseRunner::ParseRunner(std::string const& statement) : _statement(statement) { run(); }
@@ -126,7 +131,13 @@ void ParseRunner::run() {
walker.walk(listener, tree);
}
-std::shared_ptr ParseRunner::getSelectStmt() { return _listener->getSelectStatement(); }
+std::shared_ptr ParseRunner::getSelectStmt() {
+#ifdef QSERV_USE_HYRISE_SQL_PARSER
+ return HyriseAdapter::makeSelectStmt(_statement);
+#else
+ return _listener->getSelectStatement();
+#endif
+}
std::shared_ptr ParseRunner::getUserQuery() { return _listener->getUserQuery(); }
diff --git a/src/ccontrol/UserQueryFactory.cc b/src/ccontrol/UserQueryFactory.cc
index 3ede5d752c..ec085f99af 100644
--- a/src/ccontrol/UserQueryFactory.cc
+++ b/src/ccontrol/UserQueryFactory.cc
@@ -45,6 +45,7 @@
#include "ccontrol/UserQueryProcessList.h"
#include "ccontrol/UserQueryQueries.h"
#include "ccontrol/UserQueryResources.h"
+#include "ccontrol/UserQueryResultDelete.h"
#include "ccontrol/UserQuerySelect.h"
#include "ccontrol/UserQuerySelectCountStar.h"
#include "ccontrol/UserQuerySet.h"
@@ -78,6 +79,19 @@ namespace lsst::qserv::ccontrol {
using userQuerySharedResourcesPtr = std::shared_ptr;
+UserQuery::Ptr setBooleanSessionVariable(std::string const& varName, std::string const& varValue,
+ bool& sessionVariable) {
+ if (varValue == "0") {
+ sessionVariable = false;
+ } else if (varValue == "1") {
+ sessionVariable = true;
+ } else {
+ return std::make_shared("Unsupported value for " + varName + ": " + varValue);
+ }
+ LOGS(_log, LOG_LVL_WARN, varName << "=" << (sessionVariable ? "1" : "0"));
+ return std::make_shared(varName, varValue);
+}
+
/**
* @brief Determine if the table name in the FROM statement refers to PROCESSLIST table.
*
@@ -289,15 +303,14 @@ UserQuery::Ptr UserQueryFactory::newUserQuery(std::string const& aQuery, std::st
// Parse SELECT
- ParseRunner::Ptr parser;
+ query::SelectStmt::Ptr stmt;
try {
- parser = std::make_shared(query);
+ stmt = ParseRunner::makeSelectStmt(query);
} catch (parser::ParseException& e) {
return std::make_shared(std::string("ParseException:") + e.what());
}
- auto stmt = parser->getSelectStmt();
- std::lock_guard focatoryLock(_factoryMtx);
+ std::lock_guard factoryLock(_factoryMtx);
// handle special database/table names
if (_stmtRefersToProcessListTable(stmt, defaultDb)) {
return _makeUserQueryProcessList(stmt, _userQuerySharedResources, userQueryId, resultDb, aQuery,
@@ -399,10 +412,26 @@ UserQuery::Ptr UserQueryFactory::newUserQuery(std::string const& aQuery, std::st
}
} else if (UserQueryType::isCall(query)) {
std::lock_guard factoryLock(_factoryMtx);
+#ifdef QSERV_USE_HYRISE_SQL_PARSER
+ std::string resultDeleteQueryId;
+ if (!UserQueryType::isResultDelete(query, resultDeleteQueryId)) {
+ return std::make_shared("Only CALL QSERV_RESULT_DELETE is supported: " + query);
+ }
+ return std::make_shared(
+ _userQuerySharedResources->makeUserQueryResources(userQueryId, resultDb),
+ resultDeleteQueryId);
+#else
auto parser = std::make_shared(
query, _userQuerySharedResources->makeUserQueryResources(userQueryId, resultDb));
return parser->getUserQuery();
+#endif
} else if (UserQueryType::isSet(query)) {
+ std::string varName, varValue;
+#ifdef QSERV_USE_HYRISE_SQL_PARSER
+ if (!UserQueryType::isSet(query, varName, varValue)) {
+ return std::make_shared("Unsupported SET statement: " + query);
+ }
+#else
ParseRunner::Ptr parser;
try {
parser = std::make_shared(query);
@@ -410,17 +439,17 @@ UserQuery::Ptr UserQueryFactory::newUserQuery(std::string const& aQuery, std::st
return std::make_shared(std::string("ParseException:") + e.what());
}
auto uq = parser->getUserQuery();
- std::lock_guard factoryLock(_factoryMtx);
auto setQuery = std::static_pointer_cast(uq);
- if (setQuery->varName() == "QSERV_ROW_COUNTER_OPTIMIZATION") {
- _useQservRowCounterOptimization = setQuery->varValue() != "0";
- LOGS(_log, LOG_LVL_WARN,
- "QSERV_ROW_COUNTER_OPTIMIZATION=" << (_useQservRowCounterOptimization ? "1" : "0"));
- } else if (setQuery->varName() == "QSERV_DEBUG_CZAR_NO_MERGE") {
- _debugNoMerge = setQuery->varValue() != "0";
- LOGS(_log, LOG_LVL_WARN, "QSERV_DEBUG_CZAR_NO_MERGE=" << (_debugNoMerge ? "1" : "0"));
+ varName = setQuery->varName();
+ varValue = setQuery->varValue();
+#endif
+ std::lock_guard factoryLock(_factoryMtx);
+ if (varName == "QSERV_ROW_COUNTER_OPTIMIZATION") {
+ return setBooleanSessionVariable(varName, varValue, _useQservRowCounterOptimization);
+ } else if (varName == "QSERV_DEBUG_CZAR_NO_MERGE") {
+ return setBooleanSessionVariable(varName, varValue, _debugNoMerge);
}
- return uq;
+ return std::make_shared("Unsupported SET variable: " + varName);
} else {
std::lock_guard factoryLock(_factoryMtx);
// something that we don't recognize
diff --git a/src/ccontrol/UserQueryType.cc b/src/ccontrol/UserQueryType.cc
index e49c2c312b..4ff53b87e8 100644
--- a/src/ccontrol/UserQueryType.cc
+++ b/src/ccontrol/UserQueryType.cc
@@ -85,6 +85,14 @@ boost::regex _callRe(R"(^call\s+.+$)",
// Note that parens around whole string are not part of the regex but raw string literal
boost::regex _setRe(R"(^set\s+.+$)", boost::regex::ECMAScript | boost::regex::icase | boost::regex::optimize);
+// regex extracting `SET GLOBAL = ` (GLOBAL required; integer value only)
+boost::regex _setGlobalRe(R"(^set\s+global\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([-+]?\d+)\s*;?\s*$)",
+ boost::regex::ECMAScript | boost::regex::icase | boost::regex::optimize);
+
+// regex extracting the argument of `CALL QSERV_RESULT_DELETE()`
+boost::regex _resultDeleteRe(R"(^call\s+qserv_result_delete\s*\(\s*(\d+)\s*\)\s*;?\s*$)",
+ boost::regex::ECMAScript | boost::regex::icase | boost::regex::optimize);
+
} // namespace
namespace lsst::qserv::ccontrol {
@@ -180,6 +188,17 @@ bool UserQueryType::isCall(std::string const& query) {
return boost::regex_match(query, _callRe);
}
+bool UserQueryType::isResultDelete(std::string const& query, std::string& queryId) {
+ LOGS(_log, LOG_LVL_TRACE, "isResultDelete: " << query);
+ boost::smatch sm;
+ bool match = boost::regex_match(query, sm, _resultDeleteRe);
+ if (match) {
+ queryId = sm.str(1);
+ LOGS(_log, LOG_LVL_TRACE, "isResultDelete: queryId: " << queryId);
+ }
+ return match;
+}
+
bool UserQueryType::isSimpleCountStar(std::shared_ptr const& stmt, std::string& spelling) {
if (stmt->hasWhereClause() || stmt->hasOrderBy() || stmt->hasGroupBy() || stmt->hasHaving()) {
return false;
@@ -209,4 +228,16 @@ bool UserQueryType::isSet(std::string const& query) {
return match;
}
+bool UserQueryType::isSet(std::string const& query, std::string& varName, std::string& varValue) {
+ LOGS(_log, LOG_LVL_TRACE, "isSet (extract): " << query);
+ boost::smatch sm;
+ bool match = boost::regex_match(query, sm, _setGlobalRe);
+ if (match) {
+ varName = sm.str(1);
+ varValue = sm.str(2);
+ LOGS(_log, LOG_LVL_TRACE, "isSet: " << varName << "=" << varValue);
+ }
+ return match;
+}
+
} // namespace lsst::qserv::ccontrol
diff --git a/src/ccontrol/UserQueryType.h b/src/ccontrol/UserQueryType.h
index a4105e43b7..94205e9216 100644
--- a/src/ccontrol/UserQueryType.h
+++ b/src/ccontrol/UserQueryType.h
@@ -101,6 +101,12 @@ class UserQueryType {
*/
static bool isCall(std::string const& query);
+ /**
+ * Returns true if query is `CALL QSERV_RESULT_DELETE()`
+ * On a match, sets @p queryId to the numeric query id string (digits only).
+ */
+ static bool isResultDelete(std::string const& query, std::string& queryId);
+
/**
*
* @param stmt the statement to check
@@ -116,6 +122,12 @@ class UserQueryType {
/// Returns true if query is SET (for variable assignment)
static bool isSet(std::string const& query);
+
+ /**
+ * Returns true if query is `SET GLOBAL = `. On a match, sets @p varName and
+ * @p varValue. The value must be an integer literal.
+ */
+ static bool isSet(std::string const& query, std::string& varName, std::string& varValue);
};
} // namespace lsst::qserv::ccontrol
diff --git a/src/ccontrol/testAntlr4GeneratedIR.cc b/src/ccontrol/testAntlr4GeneratedIR.cc
index 88e6d79c4b..6d42a3d52f 100644
--- a/src/ccontrol/testAntlr4GeneratedIR.cc
+++ b/src/ccontrol/testAntlr4GeneratedIR.cc
@@ -69,6 +69,18 @@ using namespace std;
namespace test = boost::test_tools;
using namespace lsst::qserv;
+// -------------
+// !!! NOTE: !!!
+// -------------
+// The ANTLR adapter currently rejects some valid AND placements and accepts some invalid OR placements.
+// This behavior was corrected in the Hyrise adapter. See Hyrise tests:
+// - area_restrictor_in_conjunction_supported
+// - area_restrictor_under_or_rejected
+// - area_restrictor_under_not_rejected
+//
+// The ANTLR adapter was left unchanged to preserve legacy behavior. In the event that we decide to retain the
+// legacy ANTLR parser, we should investigate fixing the bug here as well.
+
BOOST_AUTO_TEST_SUITE(Suite)
/// Negation is used in class constructors where the class may be negated by 'NOT', where IS_NOT == "NOT",
@@ -1139,6 +1151,29 @@ static const vector ANTLR4_TEST_QUERIES = {
nullptr, nullptr, 0, -1);
},
"SELECT `objectId` FROM `Object` WHERE qserv_areaspec_box(0,0,3,10) ORDER BY `objectId`"),
+ // test qserv_areaspec_box wrapped in an equality comparison (form used in some integration tests)
+ Antlr4TestQueries(
+ "SELECT objectId FROM Object WHERE qserv_areaspec_box(0, 0, 3, 10) = 1",
+ []() -> shared_ptr {
+ return SelectStmt(
+ SelectList(ValueExpr("", FactorOp(ValueFactor(ColumnRef("", "", "objectId")),
+ query::ValueExpr::NONE))),
+ FromList(TableRef("", "Object", "")),
+ WhereClause(nullptr, AreaRestrictorBox("0", "0", "3", "10")), nullptr, nullptr,
+ nullptr, 0, -1);
+ },
+ "SELECT `objectId` FROM `Object` WHERE qserv_areaspec_box(0,0,3,10)"),
+ Antlr4TestQueries(
+ "SELECT objectId FROM Object WHERE 1 = qserv_areaspec_box(0, 0, 3, 10)",
+ []() -> shared_ptr {
+ return SelectStmt(
+ SelectList(ValueExpr("", FactorOp(ValueFactor(ColumnRef("", "", "objectId")),
+ query::ValueExpr::NONE))),
+ FromList(TableRef("", "Object", "")),
+ WhereClause(nullptr, AreaRestrictorBox("0", "0", "3", "10")), nullptr, nullptr,
+ nullptr, 0, -1);
+ },
+ "SELECT `objectId` FROM `Object` WHERE qserv_areaspec_box(0,0,3,10)"),
// test null-safe equals operator <>
Antlr4TestQueries(
"SELECT o1.objectId AS objId1, o2.objectId AS objId2, scisql_angSep(o1.ra_PS, o1.decl_PS, "
@@ -2242,6 +2277,89 @@ static const vector ANTLR4_TEST_QUERIES = {
FromList(TableRef("", "Object", "")), nullptr, nullptr, nullptr, nullptr, 0, -1);
},
"SELECT (`objectId`-1) AS `o` FROM `Object`"),
+
+ // test HAVING with an aggregate
+ Antlr4TestQueries(
+ "SELECT objectId, COUNT(*) FROM Object GROUP BY objectId HAVING COUNT(*) > 1",
+ []() -> shared_ptr {
+ return SelectStmt(
+ SelectList(
+ ValueExpr("", FactorOp(ValueFactor(ColumnRef("", "", "objectId")),
+ query::ValueExpr::NONE)),
+ ValueExpr(
+ "",
+ FactorOp(
+ ValueFactor(
+ query::ValueFactor::AGGFUNC,
+ FuncExpr("COUNT",
+ ValueExpr("",
+ FactorOp(ValueFactor(STAR, ""),
+ query::ValueExpr::
+ NONE)))),
+ query::ValueExpr::NONE))),
+ FromList(TableRef("", "Object", "")),
+ nullptr, // WhereClause
+ nullptr, // OrderByClause
+ GroupByClause(GroupByTerm(
+ ValueExpr("", FactorOp(ValueFactor(ColumnRef("", "", "objectId")),
+ query::ValueExpr::NONE)),
+ "")),
+ HavingClause(OrTerm(AndTerm(BoolFactor(
+ IS,
+ CompPredicate(
+ ValueExpr(
+ "",
+ FactorOp(
+ ValueFactor(
+ query::ValueFactor::AGGFUNC,
+ FuncExpr(
+ "COUNT",
+ ValueExpr(
+ "",
+ FactorOp(
+ ValueFactor(STAR,
+ ""),
+ query::ValueExpr::
+ NONE)))),
+ query::ValueExpr::NONE)),
+ query::CompPredicate::GREATER_THAN_OP,
+ ValueExpr("", FactorOp(ValueFactor("1"),
+ query::ValueExpr::NONE))))))),
+ 0, -1);
+ },
+ "SELECT `objectId`,COUNT(*) FROM `Object` GROUP BY `objectId` HAVING COUNT(*)>1"),
+
+ // test a function nested inside an ORDER BY expression. A bare "ORDER BY f(x)" is rejected, but a
+ // function within a larger expression is allowed.
+ Antlr4TestQueries(
+ "SELECT objectId FROM Object ORDER BY ra_PS + scisql_fluxToAbMag(gFlux_PS)",
+ []() -> shared_ptr {
+ return SelectStmt(
+ SelectList(ValueExpr("", FactorOp(ValueFactor(ColumnRef("", "", "objectId")),
+ query::ValueExpr::NONE))),
+ FromList(TableRef("", "Object", "")),
+ nullptr, // WhereClause
+ OrderByClause(OrderByTerm(
+ ValueExpr(
+ "",
+ FactorOp(ValueFactor(ColumnRef("", "", "ra_PS")),
+ query::ValueExpr::PLUS),
+ FactorOp(
+ ValueFactor(
+ query::ValueFactor::FUNCTION,
+ FuncExpr("scisql_fluxToAbMag",
+ ValueExpr("",
+ FactorOp(ValueFactor(ColumnRef(
+ "", "",
+ "gFlux_PS")),
+ query::ValueExpr::
+ NONE)))),
+ query::ValueExpr::NONE)),
+ query::OrderByTerm::DEFAULT, "")),
+ nullptr, // GroupByClause
+ nullptr, 0, -1);
+ },
+ "SELECT `objectId` FROM `Object` ORDER BY(`ra_PS`+scisql_fluxToAbMag(`gFlux_PS`))"),
};
BOOST_DATA_TEST_CASE(antlr4_test, ANTLR4_TEST_QUERIES, queryInfo) {
@@ -2283,4 +2401,50 @@ BOOST_AUTO_TEST_CASE(set_session_var_test) {
parser::adapter_order_error);
}
+// OFFSET is not supported by qserv.
+BOOST_AUTO_TEST_CASE(offset_not_supported) {
+ BOOST_CHECK_THROW(ccontrol::ParseRunner::makeSelectStmt("SELECT objectId FROM Object LIMIT 10 OFFSET 5"),
+ parser::ParseException);
+}
+
+// DISTINCT inside an aggregate function is not supported by qserv.
+BOOST_AUTO_TEST_CASE(aggregate_distinct_not_supported) {
+ BOOST_CHECK_THROW(ccontrol::ParseRunner::makeSelectStmt("SELECT COUNT(DISTINCT objectId) FROM Object"),
+ parser::ParseException);
+}
+
+// Window functions (OVER clause) are not supported by qserv.
+BOOST_AUTO_TEST_CASE(window_function_not_supported) {
+ BOOST_CHECK_THROW(ccontrol::ParseRunner::makeSelectStmt(
+ "SELECT SUM(ra_PS) OVER (PARTITION BY objectId) FROM Object"),
+ parser::ParseException);
+}
+
+// Aggregate functions outside the supported set (COUNT/MIN/MAX/SUM/AVG) are not supported by qserv.
+BOOST_AUTO_TEST_CASE(unsupported_aggregate_not_supported) {
+ BOOST_CHECK_THROW(ccontrol::ParseRunner::makeSelectStmt("SELECT STDDEV(ra_PS) FROM Object"),
+ parser::ParseException);
+}
+
+// Row locking clauses (FOR UPDATE / LOCK IN SHARE MODE) are not supported by qserv.
+BOOST_AUTO_TEST_CASE(row_locking_not_supported) {
+ BOOST_CHECK_THROW(ccontrol::ParseRunner::makeSelectStmt("SELECT objectId FROM Object FOR UPDATE"),
+ parser::ParseException);
+}
+
+// NULLS FIRST/LAST ordering is not supported by qserv.
+BOOST_AUTO_TEST_CASE(nulls_ordering_not_supported) {
+ BOOST_CHECK_THROW(
+ ccontrol::ParseRunner::makeSelectStmt("SELECT objectId FROM Object ORDER BY objectId NULLS LAST"),
+ parser::ParseException);
+}
+
+// A qserv area restrictor may appear bare or compared against an integer literal ("= 1"). Comparing it
+// against a column should be rejected
+BOOST_AUTO_TEST_CASE(area_restrictor_against_column_not_supported) {
+ BOOST_CHECK_THROW(ccontrol::ParseRunner::makeSelectStmt(
+ "SELECT objectId FROM Object WHERE qserv_areaspec_box(0, 0, 3, 10) = objectId"),
+ parser::ParseException);
+}
+
BOOST_AUTO_TEST_SUITE_END()
diff --git a/src/ccontrol/testCControl.cc b/src/ccontrol/testCControl.cc
index 8d46b69ffc..d73ddc2657 100644
--- a/src/ccontrol/testCControl.cc
+++ b/src/ccontrol/testCControl.cc
@@ -34,6 +34,7 @@
// Qserv headers
#include "ccontrol/UserQueryType.h"
+#include "tests/ParserExpected.h"
#include "parser/ParseException.h"
#include "qproc/QuerySession.h"
#include "query/AndTerm.h"
@@ -78,10 +79,12 @@ static const std::vector PARSE_ERROR_QUERIES = {
// "Expressions/functions in ORDER BY clauses are not allowed
// In SQL92 ORDER BY is limited to actual table columns, thus expressions or functions in ORDER BY are
// rejected. This is true for Qserv too.
- ParseErrorQueryInfo("SELECT objectId, iE1_SG, ABS(iE1_SG) FROM Object WHERE iE1_SG between -0.1 and "
- "0.1 ORDER BY ABS(iE1_SG)",
- "ParseException:Error parsing query, near \"ABS(iE1_SG)\", qserv does not "
- "support functions in ORDER BY."),
+ ParseErrorQueryInfo(
+ "SELECT objectId, iE1_SG, ABS(iE1_SG) FROM Object WHERE iE1_SG between -0.1 and "
+ "0.1 ORDER BY ABS(iE1_SG)",
+ PARSER_EXPECTED("ParseException:qserv does not support functions in ORDER BY.",
+ "ParseException:Error parsing query, near \"ABS(iE1_SG)\", qserv does not "
+ "support functions in ORDER BY.")),
ParseErrorQueryInfo("SELECT foo from Filter f limit 5 garbage query !#$%!#$",
"ParseException:Failed to instantiate query: \"SELECT foo from Filter f limit 5 "
@@ -100,6 +103,30 @@ static const std::vector PARSE_ERROR_QUERIES = {
"ParseException:Error parsing query, near \"_chunkId\", Identifiers in Qserv may not start "
"with an underscore."),
+ ParseErrorQueryInfo(
+ "SELECT objectId AS _objectId FROM Object;",
+ "ParseException:Error parsing query, near \"_objectId\", Identifiers in Qserv may not "
+ "start with an underscore."),
+
+ ParseErrorQueryInfo("SELECT _ra FROM Object;",
+ "ParseException:Error parsing query, near \"_ra\", Identifiers in Qserv may not "
+ "start with an underscore."),
+
+ ParseErrorQueryInfo(
+ "SELECT `_ra` FROM Object;",
+ PARSER_EXPECTED("ParseException:Error parsing query, near \"_ra\", Identifiers in Qserv "
+ "may not start with an underscore.",
+ "ParseException:Error parsing query, near \"`_ra`\", Identifiers in Qserv "
+ "may not start with an underscore.")),
+
+ ParseErrorQueryInfo(
+ "SELECT objectId AS `_objectId` FROM Object;",
+ PARSER_EXPECTED(
+ "ParseException:Error parsing query, near \"_objectId\", Identifiers in Qserv "
+ "may not start with an underscore.",
+ "ParseException:Error parsing query, near \"`_objectId`\", Identifiers in Qserv "
+ "may not start with an underscore.")),
+
ParseErrorQueryInfo(
"LECT sce.filterName,sce.field "
"FROM LSST.Science_Ccd_Exposure AS sce "
@@ -112,7 +139,10 @@ static const std::vector PARSE_ERROR_QUERIES = {
"SELECT COUNT(*) AS totalCount, "
"SUM(CASE WHEN (typeId=3) THEN 1 ELSE 0 END) AS galaxyCount "
"FROM Object WHERE rFlux_PS > 10;",
- "ParseException:qserv can not parse query, near \"CASE WHEN (typeId=3) THEN 1 ELSE 0 END\""),
+ PARSER_EXPECTED("ParseException:qserv can not parse query: CASE expressions are not "
+ "supported.",
+ "ParseException:qserv can not parse query, near \"CASE WHEN (typeId=3) "
+ "THEN 1 ELSE 0 END\"")),
};
BOOST_DATA_TEST_CASE(expected_parse_error, PARSE_ERROR_QUERIES, queryInfo) {
@@ -122,6 +152,31 @@ BOOST_DATA_TEST_CASE(expected_parse_error, PARSE_ERROR_QUERIES, queryInfo) {
BOOST_REQUIRE_EQUAL(querySession.getError(), queryInfo.errorMessage);
}
+// While underscores are not allowed in identifiers, we need to ensure the parser/adapter layers
+// do not simply reject the presence of an underscore in the SQL string.
+BOOST_AUTO_TEST_CASE(underscoreInStringLiteralIsAllowed) {
+ auto querySession = qproc::QuerySession();
+ auto selectStmt = querySession.parseQuery("SELECT objectId FROM Object WHERE description = '_chunkId'");
+ BOOST_REQUIRE(selectStmt != nullptr);
+ BOOST_REQUIRE(querySession.getError().empty());
+}
+
+// The parser/adapter should reject CASE WHEN, but this test ensures that it can still appear in a string.
+BOOST_AUTO_TEST_CASE(caseWhenInStringLiteralIsAllowed) {
+ auto querySession = qproc::QuerySession();
+ auto selectStmt = querySession.parseQuery(
+ "SELECT objectId FROM Object WHERE description = 'CASE WHEN this is text'");
+ BOOST_REQUIRE(selectStmt != nullptr);
+ BOOST_REQUIRE(querySession.getError().empty());
+}
+
+BOOST_AUTO_TEST_CASE(repeatedTrailingSemicolonsAreAllowed) {
+ auto querySession = qproc::QuerySession();
+ auto selectStmt = querySession.parseQuery("SELECT objectId FROM Object;;; ");
+ BOOST_REQUIRE(selectStmt != nullptr);
+ BOOST_REQUIRE(querySession.getError().empty());
+}
+
BOOST_AUTO_TEST_CASE(testSimpleCountStar) {
using lsst::qserv::ccontrol::UserQueryType;
auto querySession = qproc::QuerySession();
diff --git a/src/ccontrol/testHyriseGeneratedIR.cc b/src/ccontrol/testHyriseGeneratedIR.cc
new file mode 100644
index 0000000000..201ab4a821
--- /dev/null
+++ b/src/ccontrol/testHyriseGeneratedIR.cc
@@ -0,0 +1,2515 @@
+// -*- LSST-C++ -*-
+/*
+ * LSST Data Management System
+ * Copyright 2019 AURA/LSST.
+ *
+ * This product includes software developed by the
+ * LSST Project (http://www.lsst.org/).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the LSST License Statement and
+ * the GNU General Public License along with this program. If not,
+ * see .
+ */
+
+// System headers
+#include
+#include
+#include
+
+#define BOOST_TEST_MODULE HyriseGeneratedIR
+
+// Third-party headers
+#include
+#include
+
+// Qserv headers
+#include "ccontrol/ParseRunner.h"
+#include "ccontrol/UserQueryType.h"
+#include "parser/ParseException.h"
+#include "qproc/QuerySession.h"
+#include "query/AndTerm.h"
+#include "query/BetweenPredicate.h"
+#include "query/BoolFactor.h"
+#include "query/BoolFactorTerm.h"
+#include "query/BoolTerm.h"
+#include "query/BoolTermFactor.h"
+#include "query/ColumnRef.h"
+#include "query/CompPredicate.h"
+#include "query/FromList.h"
+#include "query/FuncExpr.h"
+#include "query/GroupByClause.h"
+#include "query/HavingClause.h"
+#include "query/InPredicate.h"
+#include "query/JoinRef.h"
+#include "query/JoinSpec.h"
+#include "query/LikePredicate.h"
+#include "query/NullPredicate.h"
+#include "query/OrTerm.h"
+#include "query/PassTerm.h"
+#include "query/AreaRestrictor.h"
+#include "query/QueryTemplate.h"
+#include "query/SelectList.h"
+#include "query/SelectStmt.h"
+#include "query/TableRef.h"
+#include "query/ValueExpr.h"
+#include "query/ValueFactor.h"
+#include "query/WhereClause.h"
+
+using namespace std;
+namespace test = boost::test_tools;
+using namespace lsst::qserv;
+
+BOOST_AUTO_TEST_SUITE(Suite)
+
+/// Negation is used in class constructors where the class may be negated by 'NOT', where IS_NOT == "NOT",
+/// and IS is the explicit absence of "NOT".
+enum Negation { IS, IS_NOT };
+
+/// InNotIn is used in the case where something may be specified as 'in' or 'not in' another thing (i.e. for
+/// the query::InPredicate.)
+enum InNotIn { IN, NOT_IN };
+
+/// Star is used to indicate a star value, i.e. "*" as in "SELECT *"
+enum Star { STAR };
+
+/// Natural is used to indicate if a join is natural or not natural, in a JoinRef.
+enum Natural { NATURAL, NOT_NATURAL };
+
+/// Between is used to indicate if something is between, or not between.
+enum Between { BETWEEN, NOT_BETWEEN };
+
+/// Like is used to indicate is something is like, or not like.
+enum Like { LIKE, NOT_LIKE };
+
+/// Null is used to indicate IS_NULL or IS_NOT_NULL
+enum IsNull { IS_NULL, IS_NOT_NULL };
+
+/**
+ * @brief Pusher is a set of recursive variadic functions to receive a variable number of arguments and push
+ * them into a container that has a push_back function.
+ *
+ * This form of `pusher` is the last to get called, for the single remaining element.
+ *
+ * @tparam Container The type of container
+ * @tparam Types The template parameters the container accepts (e.g. for a vector it will be the element type
+ * and the allocator type)
+ * @tparam T The type of the object to push into the container.
+ * @tparam TArgs The type of the remaining objects to push onto the vector (this should match T)
+ * @param container The container to push objects into.
+ * @param first The object to push into the container.
+ */
+template
+void pusher(Container& container, T&& first) {
+ container.push_back(forward(first));
+}
+
+/**
+ * @brief Pusher is a set of recursive variadic functions to receive a variable number of arguments and push
+ * them into a container that has a push_back function.
+ *
+ * @tparam Container The type of container
+ * @tparam Types The template parameters the container accepts (e.g. for a vector it will be the element type
+ * and the allocator type)
+ * @tparam T The type of the object to push into the container.
+ * @tparam TArgs The type of the remaining objects to push onto the vector (this should match T)
+ * @param container The container to push objects into.
+ * @param first The object to push into the container.
+ * @param args The rest of the objects to push into the container.
+ */
+template
+void pusher(Container& container, T&& first, TArgs&&... args) {
+ container.push_back(forward(first));
+ pusher(container, forward(args)...);
+}
+
+/// Create a new AndTerm, with terms. Args should be a comma separated list of BoolTermPtr.
+template
+shared_ptr AndTerm(Targs... args) {
+ vector> terms;
+ pusher(terms, args...);
+ return make_shared(terms);
+}
+
+/// Create a new BetweenPredicate.
+shared_ptr BetweenPredicate(shared_ptr const& iValue,
+ Between between,
+ shared_ptr const& iMinValue,
+ shared_ptr const& iMaxValue) {
+ return make_shared(iValue, iMinValue, iMaxValue, (between == NOT_BETWEEN));
+}
+
+/// Create a new AndTerm, with terms. Args should be a comma separated list of BoolFactorPtr.
+template
+shared_ptr BoolFactor(Negation negation, Targs... args) {
+ vector> terms;
+ pusher(terms, args...);
+ return make_shared(terms, negation);
+}
+
+/// Create a new BoolTermFactor with a BoolTerm member term.
+shared_ptr BoolTermFactor(shared_ptr const& term) {
+ return make_shared(term);
+}
+
+/// Create a new ColumnRef with given database, table, and column names.
+shared_ptr ColumnRef(string const& db, string const& table, string const& column) {
+ return make_shared(db, table, column);
+}
+
+/// Create a new ColumnRef with given TableRef and column name.
+shared_ptr ColumnRef(shared_ptr const& tableRef, string const& column) {
+ return make_shared(tableRef, column);
+}
+
+/// Create a new CompPredicate, comparising the `left` and `right` ValueExprPtrs, with an operator
+shared_ptr CompPredicate(shared_ptr const& left,
+ query::CompPredicate::OpType op,
+ shared_ptr const& right) {
+ return make_shared(left, op, right);
+}
+
+/// Create a FactorOp with a ValueFactor
+query::ValueExpr::FactorOp FactorOp(shared_ptr const& factor, query::ValueExpr::Op op) {
+ return query::ValueExpr::FactorOp(factor, op);
+}
+
+/// Create a FuncExpr
+/// args should be instance of shared_ptr to query::ValueExpr.
+template
+shared_ptr FuncExpr(string const& name, Targs const&... args) {
+ vector> valueExprVec;
+ pusher(valueExprVec, args...);
+ return make_shared(name, valueExprVec);
+}
+
+/// Create a new FromList. Args should be a comma separated list of TableRefPtr.
+template
+shared_ptr FromList(Targs... args) {
+ auto tableRefs = make_shared>>();
+ pusher(*tableRefs, args...);
+ return make_shared(tableRefs);
+}
+
+// No need to write a factory function for GroupByTerm; its cosntructor is named consistently with the
+// factory functions here, and its ultimate owner wants an instance, not a shared_ptr.
+// The 'using' statement is placed here to put the function (that is, class constructor) in alphabetical
+// order with the other factory functions to make it as obvious as possible where the GroupByTerm function
+// is coming from.
+using query::GroupByTerm;
+
+/// Create a new GroupByClause. Args should be a comma separated list of GroupByTerm.
+template
+shared_ptr GroupByClause(Targs... args) {
+ auto terms = make_shared>();
+ pusher(*terms, args...);
+ return make_shared(terms);
+}
+
+/// Create a new HavingClause
+shared_ptr HavingClause(shared_ptr const& term) {
+ return make_shared(term);
+}
+
+/// Create a new InPredicate. Args should be a comma separated list of ValueExpr.
+template
+shared_ptr InPredicate(shared_ptr const& left, InNotIn in,
+ Targs const&... args) {
+ auto valueExprVec = vector>();
+ pusher(valueExprVec, args...);
+ return make_shared(left, valueExprVec, in == NOT_IN);
+}
+
+/// Create a new JoinRef
+shared_ptr JoinRef(shared_ptr right, query::JoinRef::Type joinType,
+ Natural natural, shared_ptr joinSpec) {
+ bool isNatural = (NATURAL == natural);
+ return make_shared(right, joinType, isNatural, joinSpec);
+}
+
+/// Create a new JoinSpec
+shared_ptr JoinSpec(shared_ptr ref,
+ shared_ptr const& onTerm) {
+ return make_shared(ref, onTerm);
+}
+
+/// Create a new LikePredicate with ValueExprPtrs, where `left LIKE right`.
+shared_ptr LikePredicate(shared_ptr const& left, Like like,
+ shared_ptr const& right) {
+ return make_shared(left, right, NOT_LIKE == like);
+}
+
+/// Create a new NullPredicate
+shared_ptr NullPredicate(shared_ptr const& valueExpr, IsNull isNull) {
+ return make_shared(valueExpr, IS_NOT_NULL == isNull);
+}
+
+/// Create a new OrderByClause. Args should be a comma separated list of OrderByTerm object instances (not
+/// shared_ptr)
+template
+shared_ptr OrderByClause(Targs... args) {
+ auto orderByTerms = make_shared>();
+ pusher(*orderByTerms, args...);
+ return make_shared(orderByTerms);
+}
+
+/// Create an OrderByTerm with a ValueExprPtr term.
+/// Note this does not new an object or create a shared_ptr, as dictated by the OrderByClause interface.
+query::OrderByTerm OrderByTerm(shared_ptr const& term, query::OrderByTerm::Order order,
+ string collate) {
+ return query::OrderByTerm(term, order, collate);
+}
+
+/// Create a new OrTerm. Args can be a shared_ptr to any kind of object that inherits from BoolTerm.
+template
+shared_ptr OrTerm(Targs... args) {
+ vector> terms;
+ pusher(terms, args...);
+ return make_shared(terms);
+}
+
+/// Create a new PassTerm with given text.
+shared_ptr PassTerm(string const& text) { return make_shared(text); }
+
+shared_ptr AreaRestrictorBox(std::string const& lonMinDegree,
+ std::string const& latMinDegree,
+ std::string const& lonMaxDegree,
+ std::string const& latMaxDegree) {
+ return make_shared(lonMinDegree, latMinDegree, lonMaxDegree, latMaxDegree);
+}
+
+shared_ptr AreaRestrictorCircle(std::string const& centerLonDegree,
+ std::string const& centerLatDegree,
+ std::string const& radiusDegree) {
+ return make_shared(centerLonDegree, centerLatDegree, radiusDegree);
+}
+
+shared_ptr AreaRestrictorEllipse(std::string const& centerLonDegree,
+ std::string const& centerLatDegree,
+ std::string const& semiMajorAxisAngleArcsec,
+ std::string const& semiMinorAxisAngleArcsec,
+ std::string const& positionAngleDegree) {
+ return make_shared(centerLonDegree, centerLatDegree,
+ semiMajorAxisAngleArcsec, semiMinorAxisAngleArcsec,
+ positionAngleDegree);
+}
+
+shared_ptr AreaRestrictorPoly(std::vector const& parameters) {
+ return make_shared(parameters);
+}
+
+/// Create a new SelectList. Args should be a comma separated list of shared_ptr to ValueExpr.
+template
+shared_ptr SelectList(Targs... args) {
+ auto ptr = make_shared>>();
+ pusher(*ptr, args...);
+ return make_shared(ptr);
+}
+
+/// Create a new SelectList with the given members.
+shared_ptr SelectStmt(shared_ptr const& selectList,
+ shared_ptr const& fromList,
+ shared_ptr const& whereClause,
+ shared_ptr const& orderByClause,
+ shared_ptr const& groupByClause,
+ shared_ptr const& havingClause,
+ bool hasDistinct, int limit) {
+ return make_shared(selectList, fromList, whereClause, orderByClause, groupByClause,
+ havingClause, hasDistinct, limit);
+}
+
+/// Create a new TableRef with the given database, table, alias name, and JoinRefs. Args should
+/// be a comma separated list of shared_ptr to JoinRef.
+template
+shared_ptr TableRef(string const& db, string const& table, const string& alias,
+ Targs const&... args) {
+ vector> joinRefs;
+ pusher(joinRefs, args...);
+ auto tableRef = make_shared(db, table, alias);
+ tableRef->addJoins(joinRefs);
+ return tableRef;
+}
+
+/// Create a new TableRef with the given database, table, and alias name.
+shared_ptr TableRef(string const& db, string const& table, const string& alias) {
+ return make_shared(db, table, alias);
+}
+
+/// Create a new ValueExpr with a ValueFactorPtr.
+template
+shared_ptr ValueExpr(string alias, Targs const&... factorOps) {
+ vector factorOpVec;
+ pusher(factorOpVec, factorOps...);
+ auto valueExpr = make_shared(factorOpVec);
+ if (!alias.empty()) {
+ valueExpr->setAlias(alias);
+ }
+ return valueExpr;
+}
+
+/// Create a ValueFactor with a COLUMNREF value.
+shared_ptr ValueFactor(shared_ptr const& columnRef) {
+ return make_shared(columnRef);
+}
+
+/// Create a ValueFactor with a CONST value.
+shared_ptr ValueFactor(string const& constVal) {
+ return make_shared(constVal);
+}
+
+/// Create a ValueFactor with a FUNCTION value.
+shared_ptr ValueFactor(query::ValueFactor::Type type,
+ shared_ptr const& funcExpr) {
+ if (query::ValueFactor::AGGFUNC == type) {
+ return query::ValueFactor::newAggFactor(funcExpr);
+ } else if (query::ValueFactor::FUNCTION == type) {
+ return query::ValueFactor::newFuncFactor(funcExpr);
+ }
+ BOOST_REQUIRE_MESSAGE(false, "ValueFactor with a FuncExpr may only be of type FUNCTION or AGGFUNC");
+ return nullptr;
+}
+
+/// Create a ValueFactor with a STAR value.
+shared_ptr ValueFactor(Star star, string const& table) {
+ return query::ValueFactor::newStarFactor(table);
+}
+
+/// Create a ValueFactor with a ValueExpr value.
+shared_ptr ValueFactor(shared_ptr valueExpr) {
+ return query::ValueFactor::newExprFactor(valueExpr);
+}
+
+/// Create a new WhereClause with a given OrTerm for its root term.
+shared_ptr WhereClause(
+ shared_ptr const& orTerm,
+ shared_ptr const& areaRestrictor = nullptr) {
+ auto restrictorVec = make_shared();
+ if (nullptr != areaRestrictor) {
+ restrictorVec->push_back(areaRestrictor);
+ }
+ return make_shared(orTerm, restrictorVec);
+}
+
+/**
+ * @brief holds related test data.
+ *
+ */
+struct Antlr4TestQueries {
+ /**
+ * @brief Construct a new Antlr 4 Test Queries object
+ *
+ * @param iQuery The sql to parse and get generated IR.
+ * @param iCompareStmt a function that creates IR that should be equivalent to the parser-generated IR.
+ * @param iSerializedQuery The SQL string that should exactly match the string generated by serializing
+ * the IR.
+ */
+ Antlr4TestQueries(string const& iQuery, function()> const& iCompareStmt,
+ string const& iSerializedQuery)
+ : query(iQuery), compareStmt(iCompareStmt), serializedQuery(iSerializedQuery) {}
+
+ /// query to test, that will be turned into a SelectStmt by the andlr4-based parser.
+ string query;
+
+ /// comparison query, that will be turned into a SelectStmt by the andlr4-based parser and then that will
+ /// be modified by modFunc
+ function()> compareStmt;
+
+ /// the query as it should appear after serialization.
+ string serializedQuery;
+};
+
+ostream& operator<<(ostream& os, Antlr4TestQueries const& i) {
+ os << "Antlr4TestQueries(" << i.query << "...)";
+ return os;
+}
+
+static const vector ANTLR4_TEST_QUERIES = {
+
+ // tests NOT LIKE (which is 'NOT LIKE', different than 'NOT' and 'LIKE' operators separately)
+ Antlr4TestQueries(
+ "SELECT sce.filterId, sce.filterName "
+ "FROM Science_Ccd_Exposure AS sce "
+ "WHERE (sce.visit = 887404831) AND (sce.raftName = '3,3') AND (sce.ccdName LIKE '%') "
+ "ORDER BY filterId", // case01/queries/0012.1_raftAndCcd.sql
+ []() -> shared_ptr {
+ return SelectStmt(
+ SelectList(ValueExpr("", FactorOp(ValueFactor(ColumnRef("", "sce", "filterId")),
+ query::ValueExpr::NONE)),
+ ValueExpr("", FactorOp(ValueFactor(ColumnRef("", "sce", "filterName")),
+ query::ValueExpr::NONE))),
+ FromList(TableRef("", "Science_Ccd_Exposure", "sce")),
+ WhereClause(OrTerm(AndTerm(
+ BoolFactor(
+ IS, PassTerm("("),
+ BoolTermFactor(OrTerm(AndTerm(BoolFactor(
+ IS,
+ CompPredicate(
+ ValueExpr("",
+ FactorOp(ValueFactor(ColumnRef(
+ "", "sce", "visit")),
+ query::ValueExpr::NONE)),
+ query::CompPredicate::EQUALS_OP,
+ ValueExpr("",
+ FactorOp(ValueFactor("887404831"),
+ query::ValueExpr::NONE))))))),
+ PassTerm(")")),
+ BoolFactor(
+ IS, PassTerm("("),
+ BoolTermFactor(OrTerm(AndTerm(BoolFactor(
+ IS,
+ CompPredicate(
+ ValueExpr("", FactorOp(ValueFactor(ColumnRef(
+ "", "sce",
+ "raftName")),
+ query::ValueExpr::NONE)),
+ query::CompPredicate::EQUALS_OP,
+ ValueExpr("",
+ FactorOp(ValueFactor("'3,3'"),
+ query::ValueExpr::NONE))))))),
+ PassTerm(")")),
+ BoolFactor(
+ IS, PassTerm("("),
+ BoolTermFactor(OrTerm(AndTerm(BoolFactor(
+ IS,
+ LikePredicate(
+ ValueExpr("",
+ FactorOp(ValueFactor(ColumnRef(
+ "", "sce", "ccdName")),
+ query::ValueExpr::NONE)),
+ LIKE,
+ ValueExpr("",
+ FactorOp(ValueFactor("'%'"),
+ query::ValueExpr::NONE))))))),
+ PassTerm(")"))))),
+ OrderByClause(OrderByTerm(
+ ValueExpr("", FactorOp(ValueFactor(ColumnRef("", "", "filterId")),
+ query::ValueExpr::NONE)),
+ query::OrderByTerm::DEFAULT, "")),
+ nullptr, // Group By Clause
+ nullptr, 0, -1);
+ },
+ "SELECT `sce`.`filterId`,`sce`.`filterName` "
+ "FROM `Science_Ccd_Exposure` AS `sce` "
+ "WHERE `sce`.`visit`=887404831 AND `sce`.`raftName`='3,3' AND `sce`.`ccdName` LIKE '%' "
+ "ORDER BY `filterId` ASC"),
+
+ // tests a query with 2 items in the GROUP BY expression
+ Antlr4TestQueries(
+ "SELECT objectId, filterId FROM Source GROUP BY objectId, filterId;",
+ []() -> shared_ptr