/*
* Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of Business Objects nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/*
* Sql.cal
* Creation date: Feb 5, 2004.
* By: Richard Webster
*/
/**
* This module provides a combinator library for creating abstract SQL queries.
* A query is represented by the type {@link typeConstructor = Query@}.
*
* A new, empty query can be created by calling {@link newQuery@}.
*
* {@code
* qry0 = newQuery;
* @}
*
* To project a column or a SQL expression, use the {@link project@} function.
* There are several variations on this function, such as {@link projectColumn@} and {@link projectWithAliases@}.
*
* {@code
* qry1 = project qry0 [toUntypedExpr countryField, toUntypedExpr orderDateField];
* @}
*
* To add a restriction to the query, use the {@link restrict@} function.
*
* {@code
* qry2 = restrict qry1 (eqExpr countryField (stringConstant "Canada"));
* @}
*
* To add sorting to the query, use the {@link order@} function.
*
* {@code
* qry3 = order qry2 orderDateField True;
* @}
*
* To add a join to the query, use the {@link join@} function.
*
* {@code
* joinInfo = makeJoinInfo (intField custTable "Customer ID") (intField ordersTable "Customer ID") InnerJoin;
* qry4 = join qry3 joinInfo;
* @}
*
* An abstract {@link typeConstructor = Query@} can be converted to a concrete SQL query with the {@link queryText@} function.
* This function requires a {@link typeConstructor = SqlBuilder@} for a specific RDBMS as found in the module {@link module = "Cal.Data.SqlBuilder"@}.
*
* See the module {@link module = "Cal.Test.Data.Sql_Tests"@} for examples of query construction.
*
* There is some support for other types of SQL statements, such as ones to create/delete tables and inserting rows.
* See the {@link Statement@} type.
*
* @author Richard Webster
*/
module Cal.Data.Sql;
import Cal.Core.Prelude using
typeConstructor = Byte, Int, Double, String, Boolean, Char, Integer, JObject, JList, Maybe, Ordering;
dataConstructor = False, True, LT, EQ, GT, Nothing, Just;
typeClass = Eq, Ord, Num, Inputable, Outputable;
function =
append, compare, concat, const, doubleToString, equals, error, fromJust, fst, input,
intToString, isNothing, isEmpty, max, mod, not, output, round, seq, snd, toDouble,
field1, field2, field3, upFrom, upFromTo;
typeConstructor = Long;
function = compose, fromMaybe, isJust, listToMaybe, maybeToList, longToString;
;
import Cal.Collections.List using
function = all, chop, filter, foldLeft, foldLeftStrict, foldRight, head, intersperse,
last, length, list2, map, outputList, outputListWith, reverse, subscript, sum, tail, take,
zip, zip3, zipWith;
function = any, concatMap, deleteFirsts, drop, elemIndex, isElem, mapIndexed, repeat, removeDuplicates,
removeDuplicatesBy, lookupWithDefault, find, groupBy, intersect, isSingletonList, sort;
;
import Cal.Core.String using
function = toLowerCase;
;
import Cal.Core.Debug using
typeClass = Show;
function = show;
;
import Cal.Utilities.TimeZone using
typeConstructor = TimeZone;
;
import Cal.Utilities.Time using
typeConstructor = Time;
;
import Cal.Utilities.PrettyPrinter using
typeConstructor = Document;
function = line, multilineText, nest, text;
;
import Cal.Data.SqlType using
typeConstructor = SqlType;
;
import Cal.Collections.Set using
typeConstructor = Set;
;
import Cal.Collections.Array using
typeConstructor = Array;
;
import Cal.Data.DatabaseMetadata using
typeConstructor = DatabaseReference, TableReference, TableDescription, FieldDescription,
TableConstraint;
dataConstructor = PrimaryKeyConstraint, UniqueConstraint, ForeignKeyConstraint;
function = getTableNameFromReference, makeTableReference;
;
import Cal.Utilities.UniqueIdentifier using
typeConstructor = UniqueIdentifier;
typeClass = UniquelyNamedItem;
function = makeUniqueIdentifierByName;
;
import Cal.Utilities.Range using
typeConstructor = Range;
function =
hasLeftEndpoint, hasRightEndpoint, includesLeftEndpoint,
includesRightEndpoint, leftEndpoint, rightEndpoint;
;
friend Cal.Data.SqlBuilder;
/**
* Associates a table resource with its alias for the query.
*/
data public QueryTable =
/**
* A reference to an actual database table with a given alias for the query.
* @arg tableRef a reference to a database table by name
* @arg tableAlias the alias for the table in the query
*/
private BaseTable
tableRef :: !TableReference
tableAlias :: !String
|
/**
* A nested SELECT query to be treated as a query table with a given alias.
* @arg subquery a database query to be treated as a query table
* @arg tableAlias the alias for the nested SELECT 'table' in the query
*/
private SubQueryTable
subquery :: Query
tableAlias :: !String
|
/**
* A nested SELECT statement to be treated as a query table with a given alias.
* @arg subqueryText the SQL for a SELECT statement to be treated as a query table
* @arg tableAlias the alias for the nested SELECT 'table' in the query
*/
private OpaqueSubQueryTable
subqueryText :: String
tableAlias :: !String
;
/**
* Constructs a query table using the specified table name.
* The table alias will be based on the table name.
* @arg tableName the name of the table to be used in the query
* @return a query table for the specified table using the table name as the alias
*/
makeQueryTable :: String -> QueryTable;
public makeQueryTable !tableName = makeQueryTableWithAlias tableName "";
/**
* Constructs a query table using the specified table name and base alias name.
* If no table alias is specified, the table name will be used as the alias.
* @arg tableName the name of the table to be used in the query
* @arg tableAlias the alias to use for the table in the query;
* An empty alias name indicates that the table name should be used as the alias
* @return a query table for the specified table and alias
*/
makeQueryTableWithAlias :: String -> String -> QueryTable;
public makeQueryTableWithAlias !tableName !tableAlias =
let
fixedAlias :: String;
fixedAlias = if (isEmpty tableAlias) then tableName else tableAlias;
in
BaseTable (makeTableReference tableName) fixedAlias;
/**
* Constructs a subquery table using the specified query and alias.
* This function will remove any ordering from the subquery (unless it is a {@link TopN@}
* query).
* @arg subquery a database query to be treated as a query table
* @arg tableAlias the alias to use for the table in the query;
* If an empty alias is specified, then a default one will be generated.
* @return a query table for the nested SELECT query with the given alias
*/
makeSubQueryTable :: Query -> String -> QueryTable;
public makeSubQueryTable subquery !tableAlias =
let
fixedAlias :: String;
fixedAlias = if (isEmpty tableAlias) then "Q" else tableAlias;
in
SubQueryTable (fixSubqueryOrdering subquery) fixedAlias;
/**
* Constructs a subquery table using the specified query text and alias.
* The query text must be valid when used as a subquery.
* In most cases, this means that it cannot contain an ORDER BY clause.
* @arg subqueryText a SELECT statement text to be treated as a query table
* @arg tableAlias the alias to use for the table in the query;
* If an empty alias is specified, then a default one will be generated.
* @return a query table for the nested SELECT query with the given alias
*/
makeOpaqueSubQueryTable :: String -> String -> QueryTable;
public makeOpaqueSubQueryTable subqueryText !tableAlias =
let
fixedAlias :: String;
fixedAlias = if (isEmpty tableAlias) then "Q" else tableAlias;
in
OpaqueSubQueryTable subqueryText fixedAlias;
/**
* Returns whether the specified table is a subquery table.
* @arg queryTable a query table
* @return True if the table is a subquery (or opaque subquery) table;
* False if the table is a base database table
*/
isSubqueryTable :: QueryTable -> Boolean;
public isSubqueryTable !queryTable =
case queryTable of
(SubQueryTable | OpaqueSubQueryTable) {} -> True;
_ -> False;
;
/**
* Returns the name of the table (without quotes).
* An empty string is returned for the name of a subquery table.
* @arg queryTable a query table
* @return the name of the base database table,
* or an empty string if the query table is based on a subquery
*/
getQueryTableName :: QueryTable -> String;
public getQueryTableName !queryTable =
case queryTable of
BaseTable {tableRef} -> getTableNameFromReference tableRef;
_ -> "";
;
/**
* Returns the table alias for a query table.
* This is a composition of the base alias and the table ID to give a distinct alias for the table.
* @arg queryTable a query table
* @return the table alias for the query table
*/
getQueryTableAlias :: QueryTable -> String;
public getQueryTableAlias !queryTable =
case queryTable of
(BaseTable | SubQueryTable | OpaqueSubQueryTable) {tableAlias} -> tableAlias;
;
/**
* Query tables can be compared for equality.
* This is based only on the table aliases, not the actual table information.
*/
instance Eq QueryTable where
equals = equalsQueryTable;
notEquals = notEqualsQueryTable;
;
equalsQueryTable :: QueryTable -> QueryTable -> Boolean;
private equalsQueryTable table1 table2 =
case table1 of
BaseTable {tableAlias} ->
case table2 of
// TODO: should this compare the tables as well? (This breaks the
// SqlParser code currently.)
BaseTable {tableAlias = tableAlias2} ->
tableAlias == tableAlias2;
// && table == table2;
_ -> False;
;
SubQueryTable {tableAlias} ->
case table2 of
// TODO: should this compare the subqueries as well? (This breaks the
// SqlParser code currently.)
SubQueryTable {tableAlias = tableAlias2} ->
tableAlias == tableAlias2;
// && subQuery == subQuery2;
_ -> False;
;
OpaqueSubQueryTable {tableAlias} ->
case table2 of
OpaqueSubQueryTable {tableAlias = tableAlias2} ->
tableAlias == tableAlias2;
// && subqueryText == subqueryText2;
_ -> False;
;
;
notEqualsQueryTable :: QueryTable -> QueryTable -> Boolean;
private notEqualsQueryTable table1 table2 = not (equalsQueryTable table1 table2);
/**
* Query tables can be shown.
* For subquery tables, the subquery text will not be shown since this may be too verbose.
*/
instance Debug.Show QueryTable where
show = showQueryTable;
;
showQueryTable :: QueryTable -> String;
private showQueryTable table =
case table of
BaseTable {tableRef, tableAlias} ->
if (getTableNameFromReference tableRef == tableAlias) then tableAlias
else (show tableRef) ++ " AS " ++ tableAlias;
(SubQueryTable |
OpaqueSubQueryTable) {tableAlias} -> "<subquery> AS "++ tableAlias;
;
/**
* Options for the query.
*/
data public QueryOption =
/**
* This option indicates that only distinct rows should be returned for the query.
*/
public Distinct
|
/**
* This option indicates that the first N rows should be returned.
* @arg n the number of rows to be returned
* @arg percent if True then the number with be interpretted as a percentage of the full set of rows,
* if False then the number will be interpretted as a number of rows
* @arg withTies if True then any rows that are considered equivalent order-wise to the Nth row will also be included in the results;
* if False then only the first N rows will be returned, even if there are ties
*/
public TopN
n :: !Int
percent :: !Boolean
withTies :: !Boolean
deriving Eq, Show
;
/**
* Returns whether the query option specifies TopN criteria.
* @arg option a query option
* @return True if the query option specifies TopN criteria
*/
isTopNOption :: QueryOption -> Boolean;
public isTopNOption !option =
case option of
TopN {} -> True;
_ -> False;
;
/**
* Information about a join between 2 tables.
*/
data public JoinInfo =
/**
* Information about a join between 2 tables.
* @arg leftTable one of the query tables to be joined
* @arg rightTable the other query table to be joined
* @arg fieldJoinExprs Boolean expressions joining fields from the tables
* @arg joinType the type of join being performed (inner, left outer, right outer, or full outer)
*/
private JoinInfo
leftTable :: !QueryTable
rightTable :: !QueryTable
fieldJoinExprs :: [TypedExpr Boolean]
joinType :: !JoinType
deriving Eq
;
/**
* Creates a join info using the query field info specified.
* The expressions provided must both be {@link QueryField@}s.
* The link expressions will be created to compare each pair of fields using
* {@code '='@}.
* If other link comparison are required, the {@link typeConstructor = JoinInfo@} can be constructed with
* these explicitly.
* @arg leftField an expression for a field in the left table to be joined
* @arg rightField an expression for a field in the right table to be joined
* @arg joinType the type of join being performed (inner, left outer, right outer, or full outer)
* @return join info between the specified table fields
*/
makeJoinInfo :: Eq a => TypedExpr a -> TypedExpr a -> JoinType -> JoinInfo;
public makeJoinInfo !leftField !rightField !joinType =
let
tableFromQueryField fieldExpr =
case (toUntypedExpr fieldExpr) of
QueryField {queryTable} -> queryTable;
_ -> error ("The join fields must be QueryFields");
;
leftTable :: QueryTable;
leftTable = tableFromQueryField leftField;
rightTable :: QueryTable;
rightTable = tableFromQueryField rightField;
fieldJoinExpr :: TypedExpr Boolean;
fieldJoinExpr = eqExpr leftField rightField;
in
JoinInfo leftTable rightTable [fieldJoinExpr] joinType;
/**
* Creates a join info between 2 tables using the specified linking expression.
* @arg leftTable one of the query tables to be joined
* @arg rightTable the other query table to be joined
* @arg linkingExpr a Boolean expression joining fields from the tables
* @arg joinType the type of join being performed (inner, left outer, right outer, or full outer)
*/
makeJoinInfo2 :: QueryTable -> QueryTable -> TypedExpr Boolean -> JoinType -> JoinInfo;
public makeJoinInfo2 !leftTable !rightTable linkingExpr !joinType =
JoinInfo leftTable rightTable [linkingExpr] joinType;
/**
* The types of joins that can be performed between two tables.
*/
data public JoinType =
/**
* The inner join option indicates that rows should be returned where
* the join condition between the table is satisfied.
*/
public InnerJoin
|
/**
* The left outer join option indicates that all rows from the left table should
* be included along with values from the right table where the join condition is satisfied.
*/
public LeftOuterJoin
|
/**
* The right outer join option indicates that all rows from the right table should
* be included along with values from the left table where the join condition is satisfied.
*/
public RightOuterJoin
|
/**
* The full outer join option indicates that all rows satisfying the join condition should be returned
* as well as any unmatched rows from both the left and right tables.
*/
public FullOuterJoin
deriving Eq, Show
;
/**
* A binary tree structure for representing joins.
*/
data public JoinNode =
/**
* A node representing a single query table.
* @arg table a query table
*/
public JoinTable
table :: QueryTable
|
/**
* A node specifying a join between tables in two join trees.
* @arg leftNode one of the join trees to be joined
* @arg rightNode the other join tree to be joined
* @arg linkingExpr a Boolean expression joining tables in the join trees
* @arg joinType the type of join to be performed
*/
public JoinSubtree
leftNode :: JoinNode
rightNode :: JoinNode
linkingExpr :: (TypedExpr Boolean)
joinType :: JoinType
deriving Eq
;
/**
* Join nodes can be shown.
*/
instance Debug.Show JoinNode where
show = showJoinNode;
;
showJoinNode :: JoinNode -> String;
private showJoinNode !node =
case node of
JoinTable joinTable -> show joinTable;
JoinSubtree {leftNode, rightNode} -> "{" ++ (showJoinNode leftNode) ++ " -> " ++ (showJoinNode rightNode) ++ "}";
;
/**
* A named set of joins.
*/
data public JoinSet =
/**
* A named set of joins.
* @arg name the name of the join set
* @arg joins the root join of the join set
*/
private JoinSet
name :: !String
joins :: JoinNode
deriving Show
;
/**
* Construct a join set from a list of JoinInfo values.
* @arg name the name of the join set
* @arg joins the joins to be part of the set
* @return a join set with the specified name
*/
makeJoinSet :: String -> [JoinInfo] -> JoinSet;
public makeJoinSet !name joins =
makeJoinSet2 name (buildJoinTree joins);
/**
* Construct a join set from a JoinNode tree
* @arg name the name of the join set
* @arg joinTree the root join of the join set
* @return a join set with the specified name
*/
makeJoinSet2 :: String -> JoinNode -> JoinSet;
public makeJoinSet2 !name joinTree =
JoinSet name joinTree;
/**
* Returns the join info from the join set.
* @arg joinSet a join set
* @return the root join node of the set
*/
joinSetJoinTree :: JoinSet -> JoinNode;
public joinSetJoinTree !joinSet =
joinSet.JoinSet.joins;
// Implement the UniquelyNamedItem class.
instance UniquelyNamedItem JoinSet where
getDisplayName = joinSetName;
getUniqueIdentifier = joinSetUniqueIdentifier;
;
joinSetName !joinSet = joinSet.JoinSet.name;
joinSetUniqueIdentifier !joinSet = makeUniqueIdentifierByName (joinSetName joinSet);
/**
* A database expression.
*/
data public Expr =
/**
* A reference to a field in a query table.
* @arg fieldName the name of the field
* @arg queryTable the query table
*/
private QueryField
fieldName :: String
queryTable :: QueryTable
|
/**
* A literal value expression.
* @arg constantValue the constant value
*/
private ConstExpr
constantValue :: ConstValue
|
/**
* A parameter placeholder expression.
* @arg parameter the database parameter
*/
private ParameterExpr
parameter :: Parameter
|
/**
* A list of database expressions.
* @arg listValues the list of expressions
*/
private ListExpr
listValues :: [Expr]
|
/**
* A function expression.
* @arg func an identifier for the database function
* @arg arguments the function argument expressions
*/
private FunctionExpr
func :: DbFunction
arguments :: [Expr]
|
/**
* A subquery expression.
* The subquery must have a single projected column.
* @arg subquery the subquery to be treated as a database expression
*/
private SubQueryExpr
subquery :: Query
deriving Eq, Show
;
/**
* Returns {@link True@} if the expression represents a list.
* @arg expr a database expression
* @return {@link True@} if the expression represents a list; {@link False@} otherwise
*/
isListExpr :: Expr -> Boolean;
public isListExpr !expr =
case expr of
ListExpr {} -> True;
_ -> False;
;
/**
* Returns the value expressions from a list expression.
* An exception will be thrown if the expression is not a list expression.
* @arg expr a database expression
* @return the list of value expressions form a list expression
*/
getListFromListExpr :: Expr -> [Expr];
public getListFromListExpr !expr =
case expr of
ListExpr {listValues} -> listValues;
_ -> error "Not a list expression";
;
/**
* Returns {@link True@} if the expression represents a constant containing a string value.
* @arg expr a database expression
* @return {@link True@} if the expression represents a string literal; {@link False@} otherwise
*/
isStringConstExpr :: Expr -> Boolean;
public isStringConstExpr !expr =
case expr of
ConstExpr {constantValue} ->
case constantValue of
StringValue {} -> True;
_ -> False;
;
_ -> False;
;
/**
* Returns the string value from a constant expression.
* An exception will be thrown if the expression is not a string constant expression.
* @arg expr a database expression
* @return the string value from the constant database