diff --git a/src/bin/SQL/Interpreter/Base.hs b/src/bin/SQL/Interpreter/Base.hs index 8888c5b7..7b921088 100644 --- a/src/bin/SQL/Interpreter/Base.hs +++ b/src/bin/SQL/Interpreter/Base.hs @@ -27,7 +27,7 @@ reserveds words' = do -- does not consume trailing spaces qualifiedNameSegment :: Text -> Parser Text -qualifiedNameSegment sym = T.toLower <$> string' sym +qualifiedNameSegment sym = T.toLower <$> string' sym reservedOp :: Text -> Parser () reservedOp op = try (spaceConsumer *> string op *> notFollowedBy opChar *> spaceConsumer) diff --git a/src/bin/SQL/Interpreter/Convert.hs b/src/bin/SQL/Interpreter/Convert.hs index 6819c8a8..573c7c13 100644 --- a/src/bin/SQL/Interpreter/Convert.hs +++ b/src/bin/SQL/Interpreter/Convert.hs @@ -19,32 +19,41 @@ import qualified Data.Functor.Foldable as Fold import qualified Data.List.NonEmpty as NE import Control.Monad (when) import ProjectM36.DataTypes.Maybe +import Control.Monad (void) +import Data.Maybe (fromMaybe) import Debug.Trace data SQLError = NotSupportedError T.Text | TypeMismatchError AtomType AtomType | - NoSuchSQLFunctionError QualifiedName | - DuplicateTableReferenceError QualifiedName | - MissingTableReferenceError QualifiedName | - UnexpectedQualifiedNameError QualifiedName | - ColumnResolutionError QualifiedName | + NoSuchSQLFunctionError FuncName | + DuplicateTableReferenceError TableAlias | + MissingTableReferenceError TableAlias | + UnexpectedTableNameError TableName | + UnexpectedColumnNameError ColumnName | + ColumnResolutionError ColumnName | UnexpectedRelationalExprError RelationalExpr | - AmbiguousColumnResolutionError QualifiedName | + UnexpectedAsteriskError ColumnProjectionName | + AmbiguousColumnResolutionError ColumnName | + DuplicateColumnAliasError ColumnAlias | SQLRelationalError RelationalError deriving (Show, Eq) type TypeForRelExprF = RelationalExpr -> Either RelationalError Relation -data SelectItemsConvertTask = SelectItemsConvertTask { taskProjections :: S.Set QualifiedProjectionName, - taskRenames :: [(QualifiedProjectionName, AliasName)], +--type ConvertM = State + +data SelectItemsConvertTask = SelectItemsConvertTask { taskProjections :: S.Set ColumnProjectionName, + taskRenames :: [(ColumnProjectionName, ColumnAlias)], taskExtenders :: [ExtendTupleExpr] } deriving (Show, Eq) --over the course of conversion, we collect all the table aliases we encounter, including non-aliased table references, including the type of the table -newtype TableContext = TableContext (M.Map QualifiedName (RelationalExpr, Attributes)) +newtype TableContext = TableContext (M.Map TableAlias (RelationalExpr, Attributes, ColumnAliasMap)) deriving (Semigroup, Monoid, Show, Eq) - + +-- key: alias value: real column attribute name +type ColumnAliasMap = M.Map ColumnAlias AttributeName tableAliasesAsWithNameAssocs :: TableContext -> Either SQLError WithNamesAssocs tableAliasesAsWithNameAssocs (TableContext tmap) = @@ -53,48 +62,96 @@ tableAliasesAsWithNameAssocs (TableContext tmap) = notSelfRef (WithNameExpr nam (), RelationVariable nam' ()) | nam == nam' = False | otherwise = True notSelfRef _ = True - mapper :: (QualifiedName, (RelationalExpr, Attributes)) -> Either SQLError (WithNameExpr, RelationalExpr) - mapper (QualifiedName [nam], (rvExpr, _)) = pure (WithNameExpr nam (), rvExpr) +-- mapper :: (QualifiedName, (RelationalExpr, Attributes, _)) -> Either SQLError (WithNameExpr, RelationalExpr) + mapper (TableAlias nam, (rvExpr, _, _)) = pure (WithNameExpr nam (), rvExpr) mapper (qn, _) = Left (NotSupportedError ("schema qualified table names: " <> T.pack (show qn))) -- | Insert another table into the TableContext. -insertTable :: QualifiedName -> RelationalExpr -> Attributes -> TableContext -> Either SQLError TableContext -insertTable qn expr rtype (TableContext map') = - case M.lookup qn map' of - Nothing -> pure $ TableContext $ M.insert qn (expr, rtype) map' - Just _ -> Left (DuplicateTableReferenceError qn) - -lookupTable :: QualifiedName -> TableContext -> Either SQLError (RelationalExpr, Attributes) -lookupTable qn (TableContext map') = - case M.lookup qn map' of - Nothing -> Left (MissingTableReferenceError qn) +insertTable :: TableAlias -> RelationalExpr -> Attributes -> TableContext -> Either SQLError TableContext +insertTable tAlias expr rtype (TableContext map') = + case M.lookup tAlias map' of + Nothing -> pure $ TableContext $ M.insert tAlias (expr, rtype, mempty) map' + Just _ -> Left (DuplicateTableReferenceError tAlias) + +-- | When a column is mentioned, it may need to be aliased. The table name must already be in the table context so that we can identify that the attribute exists. Without a table name, we must look for a uniquely named column amongst all tables. +insertColumn :: Maybe TableAlias -> ColumnName -> Maybe ColumnAlias -> TableContext -> Either SQLError (TableContext, ColumnAlias) +insertColumn mTblAlias colName mColAlias tcontext@(TableContext tmap) = do + -- find the relevant table for the key to the right table + tblAlias' <- case mTblAlias of + Just tblAlias -> do + void $ lookupTable tblAlias tcontext + pure tblAlias + Nothing -> + -- scan column names for match- if there are multiple matches, return a column ambiguity error + findOneColumn colName tcontext + -- insert into the column alias map + let newAlias = case mColAlias of + Nothing -> case colName of + ColumnName [c] -> ColumnAlias c + ColumnName [_,c] -> ColumnAlias c + Just al -> al + origColName = case colName of + ColumnName [c] -> c + ColumnName [_,c] -> c + + when (newAlias `elem` allColumnAliases tcontext) $ Left (DuplicateColumnAliasError newAlias) + --verify that the alias is not duplicated + let tcontext' = M.adjust insertCol tblAlias' tmap + insertCol (rvexpr, attrs, colMap) = + (rvexpr, attrs, M.insert newAlias origColName colMap) + pure (TableContext tcontext', newAlias) + + +allColumnAliases :: TableContext -> [ColumnAlias] +allColumnAliases (TableContext tmap) = foldl' folder [] tmap + where + folder acc (_,_,colmap) = M.keys colmap <> acc + +lookupTable :: TableAlias -> TableContext -> Either SQLError (RelationalExpr, Attributes, ColumnAliasMap) +lookupTable ta (TableContext map') = + case M.lookup ta map' of + Nothing -> Left (MissingTableReferenceError ta) Just res -> pure res +-- | Merge table contexts (used in subselects) +insertTables :: TableContext -> TableContext -> Either SQLError TableContext +insertTables (TableContext tMapA) ctxB = + foldM folder ctxB (M.toList tMapA) + where + folder acc (qn, (re,attrs, _)) = insertTable qn re attrs acc +{- replaceTableName :: QualifiedName -> QualifiedName -> TableContext -> Either SQLError TableContext replaceTableName oldName newName (TableContext tctx) = case M.lookup oldName tctx of Nothing -> Left (MissingTableReferenceError oldName) Just match -> pure $ TableContext $ M.insert newName match (M.delete oldName tctx) - --- | Find a column name or column alias in the underlying table context. -findColumn :: QualifiedName -> TableContext -> [QualifiedName] -findColumn colName (TableContext tMap) = +-} +-- | Find a column name or column alias in the underlying table context. Returns key into table context. +findColumn :: ColumnName -> TableContext -> [TableAlias] +findColumn targetCol (TableContext tMap) = M.foldrWithKey folder [] tMap where - folder (QualifiedName [tAlias]) (rvExpr, rtype) acc = - case colName of - QualifiedName [colName'] -> + folder tAlias@(TableAlias tat) (rvExpr, rtype, _) acc = + case targetCol of + ColumnName [colName'] -> if S.member colName' (attributeNameSet rtype) then - QualifiedName [tAlias] : acc + tAlias : acc else acc - QualifiedName [tPrefix, colName'] -> - if tAlias == tPrefix && colName' == colName' then - QualifiedName [tAlias] : acc + ColumnName [tPrefix, colName'] -> + if tat == tPrefix && S.member colName' (attributeNameSet rtype) then + tAlias : acc else acc _ -> acc +findOneColumn :: ColumnName -> TableContext -> Either SQLError TableAlias +findOneColumn colName tcontext = + case findColumn colName tcontext of + [] -> Left (ColumnResolutionError colName) + [match] -> pure match + _matches -> Left (AmbiguousColumnResolutionError colName) + wrapTypeF :: TypeForRelExprF -> RelationalExpr -> Either SQLError Relation wrapTypeF typeF relExpr = case typeF relExpr of @@ -103,35 +160,38 @@ wrapTypeF typeF relExpr = -- | Return the table alias for the column name iff the attribute is unique. Used for attribute resolution. -tableAliasForColumnName :: TypeForRelExprF -> QualifiedName -> TableContext -> Either SQLError QualifiedName +{- +tableAliasForColumnName :: TypeForRelExprF -> ColumnName -> TableContext -> Either SQLError TableAlias -- the table alias is included -tableAliasForColumnName typeF qn@(QualifiedName [tAlias, _]) (TableContext tMap) = do - if M.member qn tMap then - pure (QualifiedName [tAlias]) +tableAliasForColumnName typeF cn@(ColumnName [tAlias, _]) (TableContext tMap) = do + if M.member (TableAlias tAlias) tMap then + pure (TableAlias tAlias) else - Left (ColumnResolutionError qn) -tableAliasForColumnName typeF qn@(QualifiedName [colName]) (TableContext tMap) = do + Left (ColumnResolutionError cn) +tableAliasForColumnName typeF qn@(ColumnName [colName]) (TableContext tMap) = do --look up the column name in all possible tables res <- foldM folder Nothing (M.toList tMap) case res of Just res -> pure res Nothing -> Left (ColumnResolutionError qn) where --- folder :: Maybe QualifiedName -> (QualifiedName, RelationalExpr) -> + folder :: Maybe ColumnName -> (TableAlias, RelationalExpr) -> _ folder Just{} _ = Left (AmbiguousColumnResolutionError qn) - folder Nothing (qn'@(QualifiedName [tableAlias]), (rvExpr,_)) = do + folder Nothing (TableAlias tableAlias, (rvExpr,_)) = do tRel <- wrapTypeF typeF rvExpr -- we could cache this in the table alias map ADT --traceShowM ("findColName", rvExpr, tRel) if colName `S.member` attributeNameSet (attributes tRel) then - pure (Just (QualifiedName [tableAlias, colName])) + pure (Just (ColumnName [tableAlias, colName])) else pure Nothing - +-} +baseDFExpr :: DataFrameExpr +baseDFExpr = DataFrameExpr { convertExpr = MakeRelationFromExprs (Just []) (TupleExprs () []), + orderExprs = [], + offset = Nothing, + limit = Nothing } + convertSelect :: TypeForRelExprF -> Select -> Either SQLError DataFrameExpr convertSelect typeF sel = do - let baseDFExpr = DataFrameExpr { convertExpr = MakeRelationFromExprs (Just []) (TupleExprs () []), - orderExprs = [], - offset = Nothing, - limit = Nothing } -- extract all mentioned tables into the table alias map for (dfExpr, tAliasMap, colRemap) <- case tableExpr sel of Nothing -> pure (baseDFExpr, mempty, mempty) @@ -153,64 +213,87 @@ convertSelect typeF sel = do -- if we have only one table alias or the columns are all unambiguous, remove table aliasing of attributes pure (dfExpr { convertExpr = explicitWithF (withF (projF (convertExpr dfExpr))) }) +-- | Slightly different processing for subselects. +convertSubSelect :: TypeForRelExprF -> TableContext -> Select -> Either SQLError (RelationalExpr, TableContext) +convertSubSelect typeF tctx sel = do + (dfExpr, subTContext, colRemap) <- case tableExpr sel of + Nothing -> pure (baseDFExpr, mempty, mempty) + Just tExpr -> convertTableExpr typeF tExpr + when (usesDataFrameFeatures dfExpr) $ Left (NotSupportedError "ORDER BY/LIMIT/OFFSET in subquery") + tableContext' <- insertTables tctx subTContext + explicitWithF <- case withClause sel of + Nothing -> pure id + Just wClause -> do + wExprs <- convertWithClause typeF wClause + pure (With wExprs) + -- convert projection using table alias map to resolve column names + projF <- convertProjection typeF subTContext (projectionClause sel) -- the projection can only project on attributes from the subselect table expression + -- add with clauses + withAssocs <- tableAliasesAsWithNameAssocs subTContext + let withF = case withAssocs of + [] -> id + _ -> With withAssocs + -- if we have only one table alias or the columns are all unambiguous, remove table aliasing of attributes + pure (explicitWithF (withF (projF (convertExpr dfExpr))), tableContext') + + + + convertSelectItem :: TypeForRelExprF -> TableContext -> SelectItemsConvertTask -> (Int,SelectItem) -> Either SQLError SelectItemsConvertTask -convertSelectItem typeF tAliasMap acc (c,selItem) = +convertSelectItem typeF tableContext acc (c,selItem) = case selItem of -- select * from x - (Identifier (QualifiedProjectionName [Asterisk]), Nothing) -> + (Identifier (ColumnProjectionName [Asterisk]), Nothing) -> pure acc -- select sup.* from s as sup - (Identifier qpn@(QualifiedProjectionName [ProjectionName _, Asterisk]), Nothing) -> + (Identifier qpn@(ColumnProjectionName [ProjectionName _, Asterisk]), Nothing) -> pure $ acc { taskProjections = S.insert qpn (taskProjections acc) } -- select a from x - (Identifier qpn@(QualifiedProjectionName [ProjectionName col]), Nothing) -> do + (Identifier qpn@(ColumnProjectionName [ProjectionName col]), Nothing) -> do --look up unaliased column name _ <- colinfo qpn pure $ acc { taskProjections = S.insert qpn (taskProjections acc) } -- select city as x from s - (Identifier qpn@(QualifiedProjectionName [ProjectionName _]), Just newName@(AliasName newNameTxt)) -> do - pure $ acc { taskProjections = S.insert (QualifiedProjectionName [ProjectionName newNameTxt]) (taskProjections acc), + (Identifier qpn@(ColumnProjectionName [ProjectionName _]), Just newName@(ColumnAlias newNameTxt)) -> do + pure $ acc { taskProjections = S.insert (ColumnProjectionName [ProjectionName newNameTxt]) (taskProjections acc), taskRenames = taskRenames acc <> [(qpn, newName)] } -- select s.city from s - (Identifier qpn@(QualifiedProjectionName [ProjectionName tname, ProjectionName colname]), Nothing) -> do + (Identifier qpn@(ColumnProjectionName [ProjectionName tname, ProjectionName colname]), Nothing) -> do --lookup column renaming, if applicable pure $ acc { taskProjections = S.insert qpn (taskProjections acc), - taskRenames = taskRenames acc <> [(QualifiedProjectionName [ProjectionName colname], AliasName (T.intercalate "." [tname,colname]))] } + taskRenames = taskRenames acc <> [(ColumnProjectionName [ProjectionName colname], ColumnAlias (T.intercalate "." [tname,colname]))] } -- other exprs (scalarExpr, mAlias) -> do - let attrName' (Just (AliasName nam)) _ = nam + let attrName' (Just (ColumnAlias nam)) _ = nam attrName' Nothing c = "attr_" <> T.pack (show c) - atomExpr <- convertProjectionScalarExpr typeF scalarExpr + atomExpr <- convertProjectionScalarExpr typeF tableContext scalarExpr let newAttrName = attrName' mAlias c -- we need to apply the projections after the extension! pure $ acc { taskExtenders = AttributeExtendTupleExpr newAttrName atomExpr : taskExtenders acc, - taskProjections = S.insert (QualifiedProjectionName [ProjectionName newAttrName]) (taskProjections acc) + taskProjections = S.insert (ColumnProjectionName [ProjectionName newAttrName]) (taskProjections acc) } where - colinfo (QualifiedProjectionName [ProjectionName name]) = - case tableAliasForColumnName typeF (QualifiedName [name]) tAliasMap of - Left err -> Left err - Right (QualifiedName names') -> pure $ AliasName (T.intercalate "." names') - + colinfo (ColumnProjectionName [ProjectionName name]) = + findOneColumn (ColumnName [name]) tableContext convertProjection :: TypeForRelExprF -> TableContext -> [SelectItem] -> Either SQLError (RelationalExpr -> RelationalExpr) -convertProjection typeF tAliasMap selItems = do +convertProjection typeF tContext selItems = do let emptyTask = SelectItemsConvertTask { taskProjections = S.empty, taskRenames = mempty, taskExtenders = mempty } - attrName' (Just (AliasName nam)) _ = nam + attrName' (Just (ColumnAlias nam)) _ = nam attrName' Nothing c = "attr_" <> T.pack (show c) - task <- foldM (convertSelectItem typeF tAliasMap) emptyTask (zip [1::Int ..] selItems) + task <- foldM (convertSelectItem typeF tContext) emptyTask (zip [1::Int ..] selItems) --apply projections fProjection <- if S.null (taskProjections task) then pure id else do - let projFolder (attrNames, b) (QualifiedProjectionName [ProjectionName nam]) = + let projFolder (attrNames, b) (ColumnProjectionName [ProjectionName nam]) = pure (S.insert nam attrNames, b) - projFolder (attrNames, b) (QualifiedProjectionName [ProjectionName nameA, ProjectionName nameB]) = + projFolder (attrNames, b) (ColumnProjectionName [ProjectionName nameA, ProjectionName nameB]) = pure $ (S.insert (T.concat [nameA, ".", nameB]) attrNames, b) - projFolder (attrNames, relExprAttributes) (QualifiedProjectionName [ProjectionName tname, Asterisk]) = + projFolder (attrNames, relExprAttributes) (ColumnProjectionName [ProjectionName tname, Asterisk]) = pure $ (attrNames, relExprAttributes <> [tname]) (attrNames, relExprRvs) <- foldM projFolder mempty (S.toList (taskProjections task)) let attrsProj = A.some (map (\rv -> RelationalExprAttributeNames (RelationVariable rv ())) relExprRvs <> [AttributeNames attrNames]) @@ -218,33 +301,42 @@ convertProjection typeF tAliasMap selItems = do -- apply extensions let fExtended = foldr (\ext acc -> (Extend ext) . acc) id (taskExtenders task) -- apply rename - renamesSet <- foldM (\acc (qProjName, (AliasName newName)) -> do - oldName <- convertProjectionName qProjName + renamesSet <- foldM (\acc (qProjName, (ColumnAlias newName)) -> do + oldName <- convertColumnProjectionName qProjName tContext pure $ S.insert (oldName, newName) acc) S.empty (taskRenames task) let fRenames = if S.null renamesSet then id else Rename renamesSet pure (fProjection . fExtended . fRenames) -convertProjectionName :: QualifiedProjectionName -> Either SQLError AttributeName -convertProjectionName (QualifiedProjectionName names) = do +{- +convertColumnProjectionName :: ColumnProjectionName -> Either SQLError AttributeName +convertColumnProjectionName (ColumnProjectionName names) = do let namer (ProjectionName t) = pure t namer Asterisk = Left (NotSupportedError "asterisk in projection conversion") names' <- mapM namer names pure (T.concat names') +-} -convertQualifiedName :: QualifiedName -> Either SQLError AttributeName -convertQualifiedName (QualifiedName ts) = pure $ T.intercalate "." ts -convertQualifiedProjectionName :: QualifiedProjectionName -> Either SQLError AttributeName -convertQualifiedProjectionName (QualifiedProjectionName names) = do +convertUnqualifiedColumnName :: UnqualifiedColumnName -> AttributeName +convertUnqualifiedColumnName (UnqualifiedColumnName nam) = nam + +convertColumnName :: ColumnName -> TableContext -> Either SQLError AttributeName +convertColumnName colName tcontext = do + --(_, <- insertColumn Nothing colName Nothing tcontext + (TableAlias ts) <- findOneColumn colName tcontext -- wrong! why convert to a tablealias? + pure ts + +convertColumnProjectionName :: ColumnProjectionName -> TableContext -> Either SQLError AttributeName +convertColumnProjectionName qpn@(ColumnProjectionName names) tableContext = do let namer (ProjectionName t) = pure t - namer Asterisk = error "wrong asterisk" + namer Asterisk = Left $ UnexpectedAsteriskError qpn names' <- mapM namer names - pure (T.concat names') + convertColumnName (ColumnName names') tableContext convertTableExpr :: TypeForRelExprF -> TableExpr -> Either SQLError (DataFrameExpr, TableContext, ColumnRemap) convertTableExpr typeF tExpr = do - (fromExpr, tableAliasMap, columnRemap) <- convertFromClause typeF (fromClause tExpr) + (fromExpr, tContext, columnRemap) <- convertFromClause typeF (fromClause tExpr) {- let tableAliasMap' = M.filterWithKey filterRedundantAlias tableAliasMap filterRedundantAlias (QualifiedName [nam]) (RelationVariable nam' ()) | nam == nam' = False @@ -256,55 +348,55 @@ convertTableExpr typeF tExpr = do expr' <- case whereClause tExpr of Just whereExpr -> do - restrictPredExpr <- convertWhereClause typeF whereExpr + restrictPredExpr <- convertWhereClause typeF tContext whereExpr pure $ Restrict restrictPredExpr fromExpr Nothing -> pure fromExpr - orderExprs <- convertOrderByClause typeF (orderByClause tExpr) + orderExprs <- convertOrderByClause typeF tContext (orderByClause tExpr) let dfExpr = DataFrameExpr { convertExpr = expr', orderExprs = orderExprs, offset = offsetClause tExpr, limit = limitClause tExpr } - pure (dfExpr, tableAliasMap, columnRemap) + pure (dfExpr, tContext, columnRemap) -convertWhereClause :: TypeForRelExprF -> RestrictionExpr -> Either SQLError RestrictionPredicateExpr -convertWhereClause typeF (RestrictionExpr rexpr) = do +convertWhereClause :: TypeForRelExprF -> TableContext -> RestrictionExpr -> Either SQLError RestrictionPredicateExpr +convertWhereClause typeF tableContext (RestrictionExpr rexpr) = do let wrongType t = Left $ TypeMismatchError t BoolAtomType --must be boolean expression - attrName' (QualifiedName ts) = T.intercalate "." ts + attrName' (ColumnName ts) = T.intercalate "." ts case rexpr of IntegerLiteral{} -> wrongType IntegerAtomType DoubleLiteral{} -> wrongType DoubleAtomType StringLiteral{} -> wrongType TextAtomType Identifier i -> wrongType TextAtomType -- could be a better error here - BinaryOperator (Identifier a) (QualifiedName ["="]) exprMatch -> --we don't know here if this results in a boolean expression, so we pass it down - AttributeEqualityPredicate (attrName' a) <$> convertScalarExpr typeF exprMatch - BinaryOperator exprA qn exprB -> do - a <- convertScalarExpr typeF exprA - b <- convertScalarExpr typeF exprB - f <- lookupFunc qn + BinaryOperator i@(Identifier colName) (OperatorName ["="]) exprMatch -> do --we don't know here if this results in a boolean expression, so we pass it down + (tctx', colAlias) <- insertColumn Nothing colName Nothing tableContext + AttributeEqualityPredicate (unColumnAlias colAlias) <$> convertScalarExpr typeF tableContext exprMatch + BinaryOperator exprA op exprB -> do + a <- convertScalarExpr typeF tableContext exprA + b <- convertScalarExpr typeF tableContext exprB + f <- lookupOperator op pure (AtomExprPredicate (f [a,b])) InExpr inOrNotIn sexpr (InList matches') -> do - eqExpr <- convertScalarExpr typeF sexpr + eqExpr <- convertScalarExpr typeF tableContext sexpr let (match:matches) = reverse matches' - firstItem <- convertScalarExpr typeF match + firstItem <- convertScalarExpr typeF tableContext match let inFunc a b = AtomExprPredicate (FunctionAtomExpr "eq" [a,b] ()) predExpr' = inFunc eqExpr firstItem folder predExpr'' sexprItem = do - item <- convertScalarExpr typeF sexprItem + item <- convertScalarExpr typeF tableContext sexprItem pure $ OrPredicate (inFunc eqExpr item) predExpr'' res <- foldM folder predExpr' matches --be careful here once we introduce NULLs case inOrNotIn of In -> pure res NotIn -> pure (NotPredicate res) ExistsExpr subQ -> do - dfExpr <- convertSelect typeF subQ + (relExpr, tcontext') <- convertSubSelect typeF tableContext subQ --pretty sure I have to rename attributes in both the top-level query and in this one to prevent attribute conflicts- we can't rename all the attributes in the subquery, because the renamer won't know which attributes actually refer to the top-level attributes- should we just prefix all attributes unconditionally or send a signal upstream to rename attributes? - when (usesDataFrameFeatures dfExpr) $ Left (NotSupportedError "ORDER BY/LIMIT/OFFSET in EXISTS subquery") - let rexpr = Equals (Project A.empty (convertExpr dfExpr)) (RelationVariable "true" ()) + let rexpr = Equals (Project A.empty relExpr) (RelationVariable "true" ()) pure (RelationalExprPredicate rexpr) -convertScalarExpr :: TypeForRelExprF -> ScalarExpr -> Either SQLError AtomExpr -convertScalarExpr typeF expr = do +convertScalarExpr :: TypeForRelExprF -> TableContext -> ScalarExpr -> Either SQLError AtomExpr +convertScalarExpr typeF tableContext expr = do let naked = pure . NakedAtomExpr case expr of IntegerLiteral i -> naked (IntegerAtom i) @@ -313,15 +405,15 @@ convertScalarExpr typeF expr = do -- we don't have enough type context with a cast, so we default to text NullLiteral -> naked (ConstructedAtom "Nothing" (maybeAtomType TextAtomType) []) Identifier i -> - AttributeAtomExpr <$> convertQualifiedName i - BinaryOperator exprA qn exprB -> do - a <- convertScalarExpr typeF exprA - b <- convertScalarExpr typeF exprB - f <- lookupFunc qn + AttributeAtomExpr <$> convertColumnName i tableContext + BinaryOperator exprA op exprB -> do + a <- convertScalarExpr typeF tableContext exprA + b <- convertScalarExpr typeF tableContext exprB + f <- lookupOperator op pure $ f [a,b] -convertProjectionScalarExpr :: TypeForRelExprF -> ProjectionScalarExpr -> Either SQLError AtomExpr -convertProjectionScalarExpr typeF expr = do +convertProjectionScalarExpr :: TypeForRelExprF -> TableContext -> ProjectionScalarExpr -> Either SQLError AtomExpr +convertProjectionScalarExpr typeF tableContext expr = do let naked = pure . NakedAtomExpr case expr of IntegerLiteral i -> naked (IntegerAtom i) @@ -329,19 +421,19 @@ convertProjectionScalarExpr typeF expr = do StringLiteral s -> naked (TextAtom s) NullLiteral -> naked (ConstructedAtom "Nothing" (maybeAtomType TextAtomType) []) Identifier i -> - AttributeAtomExpr <$> convertQualifiedProjectionName i - BinaryOperator exprA qn exprB -> do - a <- convertProjectionScalarExpr typeF exprA - b <- convertProjectionScalarExpr typeF exprB - f <- lookupFunc qn + AttributeAtomExpr <$> convertColumnProjectionName i tableContext + BinaryOperator exprA op exprB -> do + a <- convertProjectionScalarExpr typeF tableContext exprA + b <- convertProjectionScalarExpr typeF tableContext exprB + f <- lookupOperator op pure $ f [a,b] -convertOrderByClause :: TypeForRelExprF -> [SortExpr] -> Either SQLError [AttributeOrderExpr] -convertOrderByClause typeF exprs = +convertOrderByClause :: TypeForRelExprF -> TableContext -> [SortExpr] -> Either SQLError [AttributeOrderExpr] +convertOrderByClause typeF tableContext exprs = mapM converter exprs where converter (SortExpr sexpr mDirection mNullsOrder) = do - atomExpr <- convertScalarExpr typeF sexpr + atomExpr <- convertScalarExpr typeF tableContext sexpr attrn <- case atomExpr of AttributeAtomExpr aname -> pure aname x -> Left (NotSupportedError (T.pack (show x))) @@ -358,24 +450,24 @@ convertOrderByClause typeF exprs = convertWithClause :: TypeForRelExprF -> WithClause -> Either SQLError WithNamesAssocs convertWithClause = undefined -type ColumnRemap = M.Map QualifiedName QualifiedName +type ColumnRemap = M.Map ColumnName ColumnName convertFromClause :: TypeForRelExprF -> [TableRef] -> Either SQLError (RelationalExpr, TableContext, ColumnRemap) convertFromClause typeF (firstRef:trefs) = do --the first table ref must be a straight RelationVariable - let convertFirstTableRef (SimpleTableRef qn@(QualifiedName [nam])) = do + let convertFirstTableRef (SimpleTableRef qn@(TableName [nam])) = do let rv = RelationVariable nam () typeR <- wrapTypeF typeF rv - let tContext = TableContext (M.singleton qn (rv, attributes typeR)) + let tContext = TableContext (M.singleton (TableAlias nam) (rv, attributes typeR, mempty)) pure (rv, tContext) -- include with clause even for simple cases because we use this mapping to columns to tables - convertFirstTableRef (AliasedTableRef tref (AliasName alias)) = do + convertFirstTableRef (AliasedTableRef tref al@(TableAlias alias)) = do (rvExpr, TableContext tContext) <- convertFirstTableRef tref (rvExpr', tContext') <- case rvExpr of RelationVariable oldName () -> - let origQn = QualifiedName [oldName] in + let origQn = TableAlias oldName in case M.lookup origQn tContext of Just res -> pure $ (RelationVariable alias (), - M.delete origQn (M.insert (QualifiedName [alias]) res tContext)) + M.delete origQn (M.insert al res tContext)) Nothing -> Left (MissingTableReferenceError origQn) other -> Left (UnexpectedRelationalExprError other) pure (rvExpr', TableContext tContext') @@ -384,20 +476,20 @@ convertFromClause typeF (firstRef:trefs) = do pure (expr', tContext'', mempty {- FIXME add column remapping-}) -- | Convert TableRefs after the first one (assumes all additional TableRefs are for joins). Returns the qualified name key that was added to the map, the underlying relexpr (not aliased so that it can used for extracting type information), and the new table context map -convertTableRef :: TypeForRelExprF -> TableContext -> TableRef -> Either SQLError (QualifiedName, RelationalExpr, TableContext) +convertTableRef :: TypeForRelExprF -> TableContext -> TableRef -> Either SQLError (TableAlias, RelationalExpr, TableContext) convertTableRef typeF tableContext tref = case tref of - SimpleTableRef qn@(QualifiedName [nam]) -> do + SimpleTableRef qn@(TableName [nam]) -> do let rv = RelationVariable nam () + ta = TableAlias nam typeRel <- wrapTypeF typeF rv - tContext' <- insertTable qn rv (attributes typeRel) tableContext - pure (qn, rv, tContext') -- include with clause even for simple cases because we use this mapping to - AliasedTableRef (SimpleTableRef qn@(QualifiedName [nam])) (AliasName newName) -> do + tContext' <- insertTable ta rv (attributes typeRel) tableContext + pure (ta, rv, tContext') -- include with clause even for simple cases because we use this mapping to + AliasedTableRef (SimpleTableRef qn@(TableName [nam])) tAlias -> do typeRel <- wrapTypeF typeF (RelationVariable nam ()) let rv = RelationVariable nam () - newKey = QualifiedName [newName] - tContext' <- insertTable newKey rv (attributes typeRel) tableContext - pure $ (newKey, RelationVariable nam (), tContext') + tContext' <- insertTable tAlias rv (attributes typeRel) tableContext + pure $ (tAlias, RelationVariable nam (), tContext') x -> Left $ NotSupportedError (T.pack (show x)) @@ -406,7 +498,6 @@ joinTableRef typeF (rvA, tcontext) (c,tref) = do -- optionally prefix attributes unelss the expr is a RelationVariable let attrRenamer x expr attrs = do renamed <- mapM (renameOneAttr x expr) attrs - traceShowM ("attrRenamer", renamed) pure (Rename (S.fromList renamed) expr) -- prefix all attributes prefixRenamer prefix expr attrs = do @@ -443,14 +534,17 @@ joinTableRef typeF (rvA, tcontext) (c,tref) = do exprA <- attrRenamer "a" rvA (S.toList attrsIntersection) pure (Join exprA rvB, tcontext') InnerJoinTableRef jtref (JoinUsing qnames) -> do - (_, rvB, tcontext') <- convertTableRef typeF tcontext jtref - let jCondAttrs = S.fromList $ map convertUnqualifiedName qnames + (tKey, rvB, tcontext') <- convertTableRef typeF tcontext jtref + let jCondAttrs = S.fromList $ map convertUnqualifiedColumnName qnames (attrsIntersection, attrsA, attrsB) <- commonAttributeNames typeF rvA rvB --rename attributes used in the join condition - let attrsToRename = S.difference attrsIntersection jCondAttrs + let attrsToRename = S.difference attrsIntersection jCondAttrs -- traceShowM ("inner", attrsToRename, attrsIntersection, jCondAttrs) + let rvNameB = case tKey of + TableAlias ta -> ta exprA <- attrRenamer "a" rvA (S.toList attrsToRename) - pure (Join exprA rvB, tcontext') + exprB <- prefixRenamer rvNameB (RelationVariable rvNameB ()) (S.toList attrsToRename) + pure (Join exprA exprB, tcontext') InnerJoinTableRef jtref (JoinOn (JoinOnCondition joinExpr)) -> do --create a cross join but extend with the boolean sexpr @@ -469,23 +563,23 @@ joinTableRef typeF (rvA, tcontext) (c,tref) = do case rvExpr of RelationVariable nam () -> pure nam x -> Left $ NotSupportedError ("cannot derived name for relational expression " <> T.pack (show x)) - rvNameB <- case tKey of -- could be original relvar name or an alias whereas rvB is the unaliased name - QualifiedName [nam] -> pure nam - other -> Left (UnexpectedQualifiedNameError other) + + rvNameB = case tKey of + TableAlias ta -> ta rvNameA <- rvPrefix rvA -- rvPrefixB <- rvPrefix rvB exprA <- prefixRenamer rvNameA rvA (S.toList attrsA) exprB <- prefixRenamer rvNameB (RelationVariable rvNameB ()) (S.toList attrsB) -- for the join condition, we can potentially extend to include all the join criteria columns, then project them away after constructing the join condition let joinExpr' = renameIdentifier renamer joinExpr - renamer n@(QualifiedName [tableAlias,attr]) = --lookup prefixed with table alias - case traceShow ("renamer", n) $ M.lookup n allAliases of + renamer n@(ColumnName [tAlias,attr]) = --lookup prefixed with table alias + case M.lookup (TableAlias tAlias) allAliases of -- the table was not renamed, but the attribute may have been renamed -- find the source of the attribute Nothing -> n - Just found -> error (show (tableAlias, found)) - renamer n@(QualifiedName [attr]) = error (show n) - joinRe <- convertScalarExpr typeF joinExpr' + Just found -> error (show (tAlias, found)) + renamer n@(ColumnName [attr]) = error (show n) + joinRe <- convertScalarExpr typeF tContext' joinExpr' --let joinCommonAttrRenamer (RelationVariable rvName ()) old_name = --rename all common attrs and use the new names in the join condition let allAttrs = S.union attrsA attrsB @@ -501,14 +595,14 @@ joinTableRef typeF (rvA, tcontext) (c,tref) = do projectAwayJoinMatch = Project (InvertedAttributeNames (S.fromList [joinName])) pure (projectAwayJoinMatch (joinMatchRestriction (Extend extender (Join exprB exprA))), tContext') -convertUnqualifiedName :: UnqualifiedName -> AttributeName -convertUnqualifiedName (UnqualifiedName t) = t +lookupOperator :: OperatorName -> Either SQLError ([AtomExpr] -> AtomExpr) +lookupOperator (OperatorName nam) = lookupFunc (FuncName nam) -- this could be amended to support more complex expressions such as coalesce by returning an [AtomExpr] -> AtomExpr function -lookupFunc :: QualifiedName -> Either SQLError ([AtomExpr] -> AtomExpr) +lookupFunc :: FuncName -> Either SQLError ([AtomExpr] -> AtomExpr) lookupFunc qname = case qname of - QualifiedName [nam] -> + FuncName [nam] -> case lookup nam sqlFuncs of Nothing -> Left $ NoSuchSQLFunctionError qname Just match -> pure match @@ -539,399 +633,11 @@ commonAttributeNames typeF rvA rvB = attrsB = A.attributeNameSet (attributes typeB) pure $ (S.intersection attrsA attrsB, attrsA, attrsB) - ------------------------------------------------ -{- -class SQLConvert sqlexpr where - type ConverterF sqlexpr :: Type - convert :: TypeForRelExprF -> sqlexpr -> Either SQLError (ConverterF sqlexpr) - -instance SQLConvert Select where - type ConverterF Select = DataFrameExpr - convert typeF sel = do - --new strategy- rename all attributes by default and keep a mapping of discovered attributes. At the end of conversion, if there is no overlap in base attribute names, remove the table alias prefixes. - projF <- convert typeF (projectionClause sel) - -- we have explicit with clauses written by the user, but also our own implementation-specific with expressions - explicitWithF <- case withClause sel of - Nothing -> pure id - Just wClause -> do - wExprs <- convert typeF wClause - pure (With wExprs) - - let baseDFExpr = DataFrameExpr { convertExpr = MakeRelationFromExprs (Just []) (TupleExprs () []), - orderExprs = [], - offset = Nothing, - limit = Nothing } - case tableExpr sel of - Nothing -> - Just tExpr -> do - (dfExpr, withNames) <- convert typeF tExpr - let withF = case withNames of - [] -> id - _ -> With withNames - pure (dfExpr { convertExpr = explicitWithF (withF (projF (convertExpr dfExpr))) }) - - -instance SQLConvert [SelectItem] where - type ConverterF [SelectItem] = (RelationalExpr -> RelationalExpr) - convert typeF selItems = do - --SQL projections conflate static values to appear in a table with attribute names to include in the resultant relation - --split the projections and extensions -{- let (projections, extensions) = partition isProjection selItems - isProjection (Identifier{},_) = True - isProjection _ = False-} - let emptyTask = SelectItemsConvertTask { taskProjections = S.empty, - taskRenames = mempty, - taskExtenders = mempty } - attrName' (Just (AliasName nam)) _ = nam - attrName' Nothing c = "attr_" <> T.pack (show c) - - let selItemFolder :: SelectItemsConvertTask -> (Int, SelectItem) -> Either SQLError SelectItemsConvertTask - selItemFolder acc (_, (Identifier (QualifiedProjectionName [Asterisk]), Nothing)) = pure acc - --select a from s - selItemFolder acc (_, (Identifier qpn@(QualifiedProjectionName [ProjectionName _]), Nothing)) = - pure $ acc { taskProjections = S.insert qpn (taskProjections acc) - } - --select t.a from test as t -- we don't support schemas yet- that would require matching three name components - selItemFolder acc (_, (Identifier qpn@(QualifiedProjectionName [ProjectionName tname, ProjectionName colname]), Nothing)) = - pure $ acc { taskProjections = S.insert qpn (taskProjections acc), - taskRenames = taskRenames acc <> [(QualifiedProjectionName [ProjectionName colname], AliasName (T.intercalate "." [tname,colname]))] } - -- select city as x from s - selItemFolder acc (_, (Identifier qn, Just newName@(AliasName newNameTxt))) = do - pure $ acc { taskProjections = S.insert (QualifiedProjectionName [ProjectionName newNameTxt]) (taskProjections acc), - taskRenames = taskRenames acc <> [(qn, newName)] } - -- select sup.* from s as sup - selItemFolder acc (_, (Identifier qpn@(QualifiedProjectionName [ProjectionName _, Asterisk]), Nothing)) = - pure $ acc { taskProjections = S.insert qpn (taskProjections acc) } - - selItemFolder acc (c, (scalarExpr, mAlias)) = do - atomExpr <- convert typeF scalarExpr - let newAttrName = attrName' mAlias c - -- we need to apply the projections after the extension! - pure $ acc { taskExtenders = AttributeExtendTupleExpr newAttrName atomExpr : taskExtenders acc, - taskProjections = S.insert (QualifiedProjectionName [ProjectionName newAttrName]) (taskProjections acc) - } - task <- foldM selItemFolder emptyTask (zip [1::Int ..] selItems) - --apply projections - fProjection <- if S.null (taskProjections task) then - pure id - else do - let projFolder (attrNames, b) (QualifiedProjectionName [ProjectionName nam]) = - pure (S.insert nam attrNames, b) - projFolder (attrNames, b) (QualifiedProjectionName [ProjectionName nameA, ProjectionName nameB]) = - pure $ (S.insert (T.concat [nameA, ".", nameB]) attrNames, b) - projFolder (attrNames, relExprAttributes) (QualifiedProjectionName [ProjectionName tname, Asterisk]) = - pure $ (attrNames, relExprAttributes <> [tname]) - (attrNames, relExprRvs) <- foldM projFolder mempty (S.toList (taskProjections task)) - let attrsProj = A.some (map (\rv -> RelationalExprAttributeNames (RelationVariable rv ())) relExprRvs <> [AttributeNames attrNames]) - pure $ Project attrsProj - -- apply extensions - let fExtended = foldr (\ext acc -> (Extend ext) . acc) id (taskExtenders task) - -- apply rename - renamesSet <- foldM (\acc (qProjName, (AliasName newName)) -> do - oldName <- convert typeF qProjName - pure $ S.insert (oldName, newName) acc) S.empty (taskRenames task) - let fRenames = if S.null renamesSet then id else Rename renamesSet - pure (fProjection . fExtended . fRenames) - -instance SQLConvert TableExpr where - --pass with exprs up because they must be applied after applying projections - type ConverterF TableExpr = (DataFrameExpr, WithNamesAssocs) - convert typeF tExpr = do - let renameAllAttrs = case whereClause tExpr of - Nothing -> False - Just wClause -> needsToRenameAllAttributes wClause - - (fromExpr, tableAliasMap) <- convert typeF (fromClause tExpr) - let tableAliasMap' = M.filterWithKey filterRedundantAlias tableAliasMap - filterRedundantAlias (QualifiedName [nam]) (RelationVariable nam' ()) - | nam == nam' = False - filterRedundantAlias _ _ = True - withExprs <- mapM (\(qnam, expr) -> do - nam <- convert typeF qnam - pure (WithNameExpr nam (), expr)) (M.toList tableAliasMap') - - - expr' <- case whereClause tExpr of - Just whereExpr -> do - restrictPredExpr <- convert typeF whereExpr - pure $ Restrict restrictPredExpr fromExpr - Nothing -> pure fromExpr - orderExprs <- convert typeF (orderByClause tExpr) - let dfExpr = DataFrameExpr { convertExpr = expr', - orderExprs = orderExprs, - offset = offsetClause tExpr, - limit = limitClause tExpr } - pure (dfExpr, withExprs) - --group by - --having - -instance SQLConvert [SortExpr] where - type ConverterF [SortExpr] = [AttributeOrderExpr] - convert typeF exprs = mapM converter exprs - where - converter (SortExpr sexpr mDirection mNullsOrder) = do - atomExpr <- convert typeF sexpr - attrn <- case atomExpr of - AttributeAtomExpr aname -> pure aname - x -> Left (NotSupportedError (T.pack (show x))) - let ordering = case mDirection of - Nothing -> AscendingOrder - Just Ascending -> AscendingOrder - Just Descending -> DescendingOrder - case mNullsOrder of - Nothing -> pure () - Just x -> Left (NotSupportedError (T.pack (show x))) - pure (AttributeOrderExpr attrn ordering) - -instance SQLConvert [TableRef] where - -- returns base relation expressions plus top-level renames required - type ConverterF [TableRef] = (RelationalExpr, TableAliasMap) - convert _ [] = pure (ExistingRelation relationFalse, M.empty) - convert typeF (firstRef:trefs) = do - --the first table ref must be a straight RelationVariable - (firstRel, tableAliases) <- convert typeF firstRef - (expr', tableAliases') <- foldM joinTRef (firstRel, tableAliases) (zip [1..] trefs) - pure (expr', tableAliases') - where - --TODO: if any of the previous relations have overlap in their attribute names, we must change it to prevent a natural join! - joinTRef (rvA,tAliasesA) (c,tref) = do - let attrRenamer x expr attrs = do - renamed <- mapM (renameOneAttr x expr) attrs - pure (Rename (S.fromList renamed) expr) - renameOneAttr x expr old_name = pure (old_name, new_name) - where - new_name = T.concat [prefix, ".", old_name] - prefix = case expr of - RelationVariable rvName () -> rvName - _ -> x -- probably need to return errors for some expressions - - case tref of - NaturalJoinTableRef jtref -> do - -- then natural join is the only type of join which the relational algebra supports natively - (rvB, tAliasesB) <- convert typeF jtref - pure $ (Join rvA rvB, M.union tAliasesA tAliasesB) - CrossJoinTableRef jtref -> do - --rename all columns to prefix them with a generated alias to prevent any natural join occurring, then perform normal join - -- we need the type to get all the attribute names for both relexprs - (rvB, tAliasesB) <- convert typeF jtref - case typeF rvA of - Left err -> Left (SQLRelationalError err) - Right typeA -> - case typeF rvB of - Left err -> Left (SQLRelationalError err) - Right typeB -> do - let attrsA = A.attributeNameSet (attributes typeA) - attrsB = A.attributeNameSet (attributes typeB) - attrsIntersection = S.intersection attrsA attrsB - --find intersection of attributes and rename all of them with prefix 'expr'+c+'.' - exprA <- attrRenamer "a" rvA (S.toList attrsIntersection) - pure (Join exprA rvB, M.union tAliasesA tAliasesB) - InnerJoinTableRef jtref (JoinUsing qnames) -> do - (rvB, tAliasesB) <- convert typeF jtref - jCondAttrs <- S.fromList <$> mapM (convert typeF) qnames - (attrsIntersection, attrsA, attrsB) <- commonAttributeNames typeF rvA rvB - --rename attributes used in the join condition - let attrsToRename = S.difference attrsIntersection jCondAttrs --- traceShowM ("inner", attrsToRename, attrsIntersection, jCondAttrs) - exprA <- attrRenamer "a" rvA (S.toList attrsToRename) - pure (Join exprA rvB, M.union tAliasesA tAliasesB) - - InnerJoinTableRef jtref (JoinOn (JoinOnCondition joinExpr)) -> do - --create a cross join but extend with the boolean sexpr - --extend the table with the join conditions, then join on those - --exception: for simple attribute equality, use regular join renames using JoinOn logic - - (rvB, tAliasesB) <- convert typeF jtref - --rvA and rvB now reference potentially aliased relation variables (needs with clause to execute), but this is useful for making attributes rv-prefixed --- traceShowM ("converted", rvA, rvB, tAliases) - --extract all table aliases to create a remapping for SQL names discovered in the sexpr - let allAliases = M.union tAliasesA tAliasesB - withExpr <- With <$> tableAliasesAsWithNameAssocs allAliases - (commonAttrs, attrsA, attrsB) <- commonAttributeNames typeF (withExpr rvA) (withExpr rvB) - -- first, execute the rename, renaming all attributes according to their table aliases - let rvPrefix rvExpr = - case rvExpr of - RelationVariable nam () -> pure nam - x -> Left $ NotSupportedError ("cannot derived name for relational expression " <> T.pack (show x)) - rvPrefixA <- rvPrefix rvA - rvPrefixB <- rvPrefix rvB - exprA <- attrRenamer rvPrefixA rvA (S.toList attrsA) - exprB <- attrRenamer rvPrefixB rvB (S.toList attrsB) - -- for the join condition, we can potentially extend to include all the join criteria columns, then project them away after constructing the join condition - let joinExpr' = renameIdentifier renamer joinExpr - renamer n@(QualifiedName [tableAlias,attr]) = --lookup prefixed with table alias - case M.lookup n allAliases of - -- the table was not renamed, but the attribute may have been renamed - -- find the source of the attribute - Nothing -> n - Just found -> error (show (tableAlias, found)) - renamer n@(QualifiedName [attr]) = error (show n) - joinRe <- convert typeF joinExpr' - --let joinCommonAttrRenamer (RelationVariable rvName ()) old_name = - --rename all common attrs and use the new names in the join condition - let allAttrs = S.union attrsA attrsB - firstAvailableName c allAttrs' = - let new_name = T.pack ("join_" <> show c) in - if S.member new_name allAttrs' then - firstAvailableName (c + 1) allAttrs' - else - new_name - joinName = firstAvailableName 1 allAttrs - extender = AttributeExtendTupleExpr joinName joinRe - joinMatchRestriction = Restrict (AttributeEqualityPredicate joinName (ConstructedAtomExpr "True" [] ())) - projectAwayJoinMatch = Project (InvertedAttributeNames (S.fromList [joinName])) - pure (projectAwayJoinMatch (joinMatchRestriction (Extend extender (Join exprB exprA))), allAliases) - - ---type AttributeNameRemap = M.Map RelVarName AttributeName - --- | Used in join condition detection necessary for renames to enable natural joins. -commonAttributeNames :: TypeForRelExprF -> RelationalExpr -> RelationalExpr -> Either SQLError (S.Set AttributeName, S.Set AttributeName, S.Set AttributeName) -commonAttributeNames typeF rvA rvB = - case typeF rvA of - Left err -> Left (SQLRelationalError err) - Right typeA -> - case typeF rvB of - Left err -> Left (SQLRelationalError err) - Right typeB -> do - let attrsA = A.attributeNameSet (attributes typeA) - attrsB = A.attributeNameSet (attributes typeB) - pure $ (S.intersection attrsA attrsB, attrsA, attrsB) - - - --- convert a TableRef in isolation- to be used with the first TableRef only -instance SQLConvert TableRef where - -- return base relation variable expression plus a function to apply top-level rv renames using WithNameExpr - type ConverterF TableRef = (RelationalExpr, TableAliasMap) - --SELECT x FROM a,_b_ creates a cross join - convert _ (SimpleTableRef qn@(QualifiedName [nam])) = do - let rv = RelationVariable nam () - pure (rv, M.singleton qn rv) -- include with clause even for simple cases because we use this mapping to - convert typeF (AliasedTableRef tnam (AliasName newName)) = do - (rv, _) <- convert typeF tnam - pure $ (RelationVariable newName (), M.singleton (QualifiedName [newName]) rv) - convert _ x = Left $ NotSupportedError (T.pack (show x)) - - -instance SQLConvert RestrictionExpr where - type ConverterF RestrictionExpr = RestrictionPredicateExpr - convert typeF (RestrictionExpr rexpr) = do - let wrongType t = Left $ TypeMismatchError t BoolAtomType --must be boolean expression - attrName' (QualifiedName ts) = T.intercalate "." ts - case rexpr of - IntegerLiteral{} -> wrongType IntegerAtomType - DoubleLiteral{} -> wrongType DoubleAtomType - StringLiteral{} -> wrongType TextAtomType - Identifier i -> wrongType TextAtomType -- could be a better error here - BinaryOperator (Identifier a) (QualifiedName ["="]) exprMatch -> --we don't know here if this results in a boolean expression, so we pass it down - AttributeEqualityPredicate (attrName' a) <$> convert typeF exprMatch - BinaryOperator exprA qn exprB -> do - a <- convert typeF exprA - b <- convert typeF exprB - f <- lookupFunc qn - pure (AtomExprPredicate (f [a,b])) - InExpr inOrNotIn sexpr (InList matches') -> do - eqExpr <- convert typeF sexpr - let (match:matches) = reverse matches' - firstItem <- convert typeF match - let inFunc a b = AtomExprPredicate (FunctionAtomExpr "eq" [a,b] ()) - predExpr' = inFunc eqExpr firstItem - folder predExpr'' sexprItem = do - item <- convert typeF sexprItem - pure $ OrPredicate (inFunc eqExpr item) predExpr'' - res <- foldM folder predExpr' matches --be careful here once we introduce NULLs - case inOrNotIn of - In -> pure res - NotIn -> pure (NotPredicate res) - ExistsExpr subQ -> do - dfExpr <- convert typeF subQ - --pretty sure I have to rename attributes in both the top-level query and in this one to prevent attribute conflicts- we can't rename all the attributes in the subquery, because the renamer won't know which attributes actually refer to the top-level attributes- should we just prefix all attributes unconditionally or send a signal upstream to rename attributes? - when (usesDataFrameFeatures dfExpr) $ Left (NotSupportedError "ORDER BY/LIMIT/OFFSET in EXISTS subquery") - let rexpr = Equals (Project A.empty (convertExpr dfExpr)) (RelationVariable "true" ()) - pure (RelationalExprPredicate rexpr) - - --} -{- -instance SQLConvert ScalarExpr where - type ConverterF ScalarExpr = AtomExpr - convert typeF expr = do - let naked = pure . NakedAtomExpr - case expr of - IntegerLiteral i -> naked (IntegerAtom i) - DoubleLiteral d -> naked (DoubleAtom d) - StringLiteral s -> naked (TextAtom s) - -- we don't have enough type context with a cast, so we default to text - NullLiteral -> naked (ConstructedAtom "Nothing" (maybeAtomType TextAtomType) []) - Identifier i -> - AttributeAtomExpr <$> convert typeF i - BinaryOperator exprA qn exprB -> do - a <- convert typeF exprA - b <- convert typeF exprB - f <- lookupFunc qn - pure $ f [a,b] - --- PrefixOperator qn expr -> do - - -instance SQLConvert JoinOnCondition where - type ConverterF JoinOnCondition = (RelationalExpr -> RelationalExpr) - convert typeF (JoinOnCondition expr) = do - case expr of - Identifier (QualifiedName [tAlias, colName]) -> undefined - -instance SQLConvert ProjectionScalarExpr where - type ConverterF ProjectionScalarExpr = AtomExpr - convert typeF expr = do - let naked = pure . NakedAtomExpr - case expr of - IntegerLiteral i -> naked (IntegerAtom i) - DoubleLiteral d -> naked (DoubleAtom d) - StringLiteral s -> naked (TextAtom s) - NullLiteral -> naked (ConstructedAtom "Nothing" (maybeAtomType TextAtomType) []) - Identifier i -> - AttributeAtomExpr <$> convert typeF i - BinaryOperator exprA qn exprB -> do - a <- convert typeF exprA - b <- convert typeF exprB - f <- lookupFunc qn - pure $ f [a,b] - -instance SQLConvert QualifiedName where - type ConverterF QualifiedName = AttributeName - convert _ (QualifiedName ts) = pure $ T.intercalate "." ts - -instance SQLConvert UnqualifiedName where - type ConverterF UnqualifiedName = AttributeName - convert _ (UnqualifiedName t) = pure t - -instance SQLConvert QualifiedProjectionName where - type ConverterF QualifiedProjectionName = AttributeName - convert _ (QualifiedProjectionName names) = do - let namer (ProjectionName t) = pure t - namer Asterisk = error "wrong asterisk" - names' <- mapM namer names - pure (T.concat names') - -instance SQLConvert WithClause where - type ConverterF WithClause = WithNamesAssocs - convert typeF (WithClause True _) = Left (NotSupportedError "recursive CTEs") - convert typeF (WithClause False ctes) = do - let mapper (WithExpr (UnqualifiedName nam) subquery) = do - dfExpr <- convert typeF subquery - -- we don't support dataframe features in the cte query - when (usesDataFrameFeatures dfExpr) $ Left (NotSupportedError "ORDER BY/LIMIT/OFFSET in CTE subexpression") - pure (WithNameExpr nam (), (convertExpr dfExpr)) - -- if the subquery is a Select, how do I get a rvexpr out of it rather than a data frame- perhaps a different conversion function? - mapM mapper (NE.toList ctes) --} - -- | Used to remap SQL qualified names to new names to prevent conflicts in join conditions. -renameIdentifier :: (QualifiedName -> QualifiedName) -> ScalarExpr -> ScalarExpr +renameIdentifier :: (ColumnName -> ColumnName) -> ScalarExpr -> ScalarExpr renameIdentifier renamer sexpr = Fold.cata renamer' sexpr where - renamer' :: ScalarExprBaseF QualifiedName ScalarExpr -> ScalarExpr + renamer' :: ScalarExprBaseF ColumnName ScalarExpr -> ScalarExpr renamer' (IdentifierF n) = Identifier (renamer n) renamer' x = Fold.embed x diff --git a/src/bin/SQL/Interpreter/Select.hs b/src/bin/SQL/Interpreter/Select.hs index f1c752e5..fe2a8579 100644 --- a/src/bin/SQL/Interpreter/Select.hs +++ b/src/bin/SQL/Interpreter/Select.hs @@ -1,4 +1,4 @@ -{-# LANGUAGE TemplateHaskell, KindSignatures, TypeFamilies, DeriveTraversable #-} +{-# LANGUAGE TemplateHaskell, KindSignatures, TypeFamilies, DeriveTraversable, GeneralizedNewtypeDeriving #-} module SQL.Interpreter.Select where import Text.Megaparsec import Text.Megaparsec.Char @@ -29,7 +29,10 @@ data WithClause = WithClause { isRecursive :: Bool, withExprs :: NE.NonEmpty WithExpr } deriving (Show, Eq) -data WithExpr = WithExpr UnqualifiedName Select +data WithExpr = WithExpr WithExprAlias Select + deriving (Show, Eq) + +newtype WithExprAlias = WithExprAlias Text deriving (Show, Eq) data InFlag = In | NotIn @@ -41,20 +44,20 @@ data ComparisonOperator = OpLT | OpGT | OpGTE | OpEQ | OpNE | OpLTE data QuantifiedComparisonPredicate = QCAny | QCSome | QCAll deriving (Show,Eq) -data TableRef = SimpleTableRef QualifiedName +data TableRef = SimpleTableRef TableName | InnerJoinTableRef TableRef JoinCondition | RightOuterJoinTableRef TableRef JoinCondition | LeftOuterJoinTableRef TableRef JoinCondition | FullOuterJoinTableRef TableRef JoinCondition | CrossJoinTableRef TableRef | NaturalJoinTableRef TableRef - | AliasedTableRef TableRef AliasName + | AliasedTableRef TableRef TableAlias | QueryTableRef Select deriving (Show, Eq) -- distinguish between projection attributes which may include an asterisk and scalar expressions (such as in a where clause) where an asterisk is invalid -type ProjectionScalarExpr = ScalarExprBase QualifiedProjectionName -type ScalarExpr = ScalarExprBase QualifiedName +type ProjectionScalarExpr = ScalarExprBase ColumnProjectionName +type ScalarExpr = ScalarExprBase ColumnName data ScalarExprBase n = IntegerLiteral Integer @@ -63,11 +66,11 @@ data ScalarExprBase n = | NullLiteral -- | Interval | Identifier n - | BinaryOperator (ScalarExprBase n) QualifiedName (ScalarExprBase n) - | PrefixOperator QualifiedName (ScalarExprBase n) - | PostfixOperator (ScalarExprBase n) QualifiedName + | BinaryOperator (ScalarExprBase n) OperatorName (ScalarExprBase n) + | PrefixOperator OperatorName (ScalarExprBase n) + | PostfixOperator (ScalarExprBase n) ColumnName | BetweenOperator (ScalarExprBase n) (ScalarExprBase n) (ScalarExprBase n) - | FunctionApplication QualifiedName (ScalarExprBase n) + | FunctionApplication FuncName (ScalarExprBase n) | CaseExpr { caseWhens :: [([ScalarExprBase n],ScalarExprBase n)], caseElse :: Maybe (ScalarExprBase n) } | QuantifiedComparison { qcExpr :: ScalarExprBase n, @@ -107,30 +110,39 @@ data NullsOrder = NullsFirst | NullsLast data JoinType = InnerJoin | RightOuterJoin | LeftOuterJoin | FullOuterJoin | CrossJoin | NaturalJoin deriving (Show, Eq) -data JoinCondition = JoinOn JoinOnCondition | JoinUsing [UnqualifiedName] +data JoinCondition = JoinOn JoinOnCondition | JoinUsing [UnqualifiedColumnName] deriving (Show, Eq) newtype JoinOnCondition = JoinOnCondition ScalarExpr deriving (Show, Eq) -data Alias = Alias QualifiedName (Maybe AliasName) - deriving (Show, Eq) - -data QualifiedProjectionName = QualifiedProjectionName [ProjectionName] --dot-delimited reference +data ColumnProjectionName = ColumnProjectionName [ProjectionName] --dot-delimited reference deriving (Show, Eq, Ord) data ProjectionName = ProjectionName Text | Asterisk deriving (Show, Eq, Ord) -data QualifiedName = QualifiedName [Text] +data ColumnName = ColumnName [Text] deriving (Show, Eq, Ord) -data UnqualifiedName = UnqualifiedName Text - deriving (Show, Eq) +data UnqualifiedColumnName = UnqualifiedColumnName Text + deriving (Show, Eq, Ord) + +data TableName = TableName [Text] + deriving (Show, Eq, Ord) + +data OperatorName = OperatorName [Text] + deriving (Show, Eq, Ord) + +newtype ColumnAlias = ColumnAlias { unColumnAlias :: Text } + deriving (Show, Eq, Ord) -newtype AliasName = AliasName Text +newtype TableAlias = TableAlias Text + deriving (Show, Eq, Ord, Monoid, Semigroup) + +newtype FuncName = FuncName [Text] deriving (Show, Eq) - + data Distinctness = Distinct | All deriving (Show, Eq) queryExprP :: Parser Select @@ -139,8 +151,11 @@ queryExprP = tableP <|> selectP tableP :: Parser Select tableP = do reserved "table" - tname <- qualifiedNameP + tname <- tableNameP pure $ emptySelect { tableExpr = Just $ emptyTableExpr { fromClause = [SimpleTableRef tname] } } + +tableNameP :: Parser TableName +tableNameP = TableName <$> qualifiedNameP' selectP :: Parser Select selectP = do @@ -155,13 +170,13 @@ selectP = do withClause = withClause' }) -type SelectItem = (ProjectionScalarExpr, Maybe AliasName) +type SelectItem = (ProjectionScalarExpr, Maybe ColumnAlias) selectItemListP :: Parser [SelectItem] selectItemListP = sepBy1 selectItemP comma selectItemP :: Parser SelectItem -selectItemP = (,) <$> scalarExprP <*> optional (reserved "as" *> aliasNameP) +selectItemP = (,) <$> scalarExprP <*> optional (reserved "as" *> columnAliasP) newtype RestrictionExpr = RestrictionExpr ScalarExpr deriving (Show, Eq) @@ -194,9 +209,9 @@ fromP :: Parser [TableRef] fromP = reserved "from" *> ((:) <$> nonJoinTref <*> sepByComma joinP) where nonJoinTref = choice [parens $ QueryTableRef <$> selectP, - try (AliasedTableRef <$> simpleRef <*> (reserved "as" *> aliasNameP)), + try (AliasedTableRef <$> simpleRef <*> (reserved "as" *> tableAliasP)), simpleRef] - simpleRef = SimpleTableRef <$> qualifiedNameP + simpleRef = SimpleTableRef <$> tableNameP joinP = do joinType <- joinTypeP tref <- nonJoinTref @@ -211,7 +226,7 @@ fromP = reserved "from" *> ((:) <$> nonJoinTref <*> sepByComma joinP) joinConditionP :: Parser JoinCondition joinConditionP = do (JoinOn <$> (reserved "on" *> (JoinOnCondition <$> scalarExprP))) <|> - JoinUsing <$> (reserved "using" *> parens (sepBy1 unqualifiedNameP comma)) + JoinUsing <$> (reserved "using" *> parens (sepBy1 unqualifiedColumnNameP comma)) joinTypeP :: Parser JoinType joinTypeP = choice [reserveds "cross join" $> CrossJoin, @@ -248,8 +263,17 @@ orderByP = nameP :: Parser Text nameP = quotedIdentifier <|> identifier -aliasNameP :: Parser AliasName -aliasNameP = AliasName <$> (quotedIdentifier <|> identifier) +qualifiedNameP' :: Parser [Text] +qualifiedNameP' = sepBy1 nameP (symbol ".") + +columnAliasP :: Parser ColumnAlias +columnAliasP = ColumnAlias <$> (quotedIdentifier <|> identifier) + +tableAliasP :: Parser TableAlias +tableAliasP = TableAlias <$> (quotedIdentifier <|> identifier) + +unqualifiedColumnNameP :: Parser UnqualifiedColumnName +unqualifiedColumnNameP = UnqualifiedColumnName <$> nameP scalarExprP :: QualifiedNameP a => Parser (ScalarExprBase a) scalarExprP = E.makeExprParser scalarTermP scalarExprOp @@ -289,9 +313,9 @@ scalarExprOp = binarySymbolN s = E.InfixN $ binary s qComparisonOp = E.Postfix $ try quantifiedComparisonSuffixP -qualifiedOperatorP :: Text -> Parser QualifiedName +qualifiedOperatorP :: Text -> Parser OperatorName qualifiedOperatorP sym = - QualifiedName <$> segmentsP (splitOn "." sym) <* spaceConsumer + OperatorName <$> segmentsP (splitOn "." sym) <* spaceConsumer where segmentsP :: [Text] -> Parser [Text] segmentsP segments = case segments of @@ -404,16 +428,15 @@ class QualifiedNameP a where qualifiedNameP :: Parser a -- | col, table.col, table.*, * -instance QualifiedNameP QualifiedProjectionName where +instance QualifiedNameP ColumnProjectionName where qualifiedNameP = - QualifiedProjectionName <$> sepBy1 ((ProjectionName <$> nameP) <|> (char '*' $> Asterisk)) (char '.') <* spaceConsumer + ColumnProjectionName <$> sepBy1 ((ProjectionName <$> nameP) <|> (char '*' $> Asterisk)) (char '.') <* spaceConsumer -instance QualifiedNameP QualifiedName where - qualifiedNameP = QualifiedName <$> sepBy1 nameP (char '.') +instance QualifiedNameP ColumnName where + qualifiedNameP = ColumnName <$> sepBy1 nameP (char '.') --- | For use where qualified names need not apply (such as in USING (...) clause) -unqualifiedNameP :: Parser UnqualifiedName -unqualifiedNameP = UnqualifiedName <$> nameP +withExprAliasP :: Parser WithExprAlias +withExprAliasP = WithExprAlias <$> nameP limitP :: Parser (Maybe Integer) limitP = optional (reserved "limit" *> integer) @@ -426,7 +449,7 @@ withP = do reserved "with" recursive <- try (reserved "recursive" *> pure True) <|> pure False wExprs <- sepByComma1 $ do - wName <- unqualifiedNameP + wName <- withExprAliasP reserved "as" wSelect <- parens selectP pure (WithExpr wName wSelect) diff --git a/test/SQL/InterpreterTest.hs b/test/SQL/InterpreterTest.hs index 1efd5318..07faf78c 100644 --- a/test/SQL/InterpreterTest.hs +++ b/test/SQL/InterpreterTest.hs @@ -6,31 +6,50 @@ import TutorialD.Interpreter.RODatabaseContextOperator import ProjectM36.RelationalExpression import ProjectM36.TransactionGraph import ProjectM36.DateExamples +import ProjectM36.DatabaseContext import ProjectM36.NormalizeExpr +import ProjectM36.Client import ProjectM36.Base import System.Exit import Test.HUnit import Text.Megaparsec import qualified Data.Text as T +import qualified Data.Map as M main :: IO () main = do tcounts <- runTestTT (TestList tests) if errors tcounts + failures tcounts > 0 then exitFailure else exitSuccess where - tests = [testSelect] + tests = [testFindColumn, testSelect] + + +testFindColumn :: Test +testFindColumn = TestCase $ do + let tctx = TableContext $ M.fromList [(TableAlias "s", + (RelationVariable "s" (), + attributesFromList [Attribute "city" TextAtomType, Attribute "status" IntegerAtomType], + mempty + ) + )] + assertEqual "findColumn city" [TableAlias "s"] (findColumn (ColumnName ["city"]) tctx) + assertEqual "findColumn s.city" [TableAlias "s"] (findColumn (ColumnName ["s", "city"]) tctx) testSelect :: Test testSelect = TestCase $ do -- check that SQL and tutd compile to same thing - (tgraph,transId) <- freshTransactionGraph dateExamples - let readTests = [{- + (tgraph,transId) <- freshTransactionGraph dateExamples + (sess, conn) <- dateExamplesConnection emptyNotificationCallback + + let readTests = [ -- simple relvar ("SELECT * FROM s", "(s)"), -- simple projection ("SELECT city FROM s", "(s{city})"), -- restriction ("SELECT city FROM s where status=20","((s where status=20){city})"), + -- restriction with asterisk and qualified name + ("SELECT * FROM s WHERE \"s\".\"status\"=20","(s where status=20)"), -- restriction ("SELECT status,city FROM s where status>20","((s where gt(@status,20)){status,city})"), -- extension mixed with projection @@ -50,12 +69,12 @@ testSelect = TestCase $ do ("SELECT * FROM s CROSS JOIN sp", "((s rename {s# as `s.s#`}) join sp)"), -- unaliased join using ("SELECT * FROM sp INNER JOIN sp AS sp2 USING (\"s#\")", - "((sp rename {p# as `sp.p#`, qty as `sp.qty`}) join sp)"), + "(with (sp2 as sp) ((sp rename {p# as `sp.p#`, qty as `sp.qty`}) join (sp2 rename {p# as `sp2.p#`, qty as `sp2.qty`})))"), -- unaliased join - ("SELECT * FROM sp JOIN s ON s.s# = sp.s#","(((((s rename {s# as `s.s#`,sname as `s.sname`,city as `s.city`,status as `s.status`}) join (sp rename {s# as `sp.s#`,p# as `sp.p#`,qty as `sp.qty`})):{join_1:=eq(@`s.s#`,@`sp.s#`)}) where join_1=True) {all but join_1})"),-} + ("SELECT * FROM sp JOIN s ON s.s# = sp.s#","(((((s rename {s# as `s.s#`,sname as `s.sname`,city as `s.city`,status as `s.status`}) join (sp rename {s# as `sp.s#`,p# as `sp.p#`,qty as `sp.qty`})):{join_1:=eq(@`s.s#`,@`sp.s#`)}) where join_1=True) {all but join_1})"), -- aliased join on ("SELECT * FROM sp AS sp2 JOIN s AS s2 ON s2.s# = sp2.s#", - "(with (s2 as s, sp2 as sp) ((((s2 rename {s# as `s2.s#`,sname as `s2.sname`,city as `s2.city`,status as `s2.status`}) join (sp2 rename {s# as `sp2.s#`,p# as `sp2.p#`,qty as `sp2.qty`})):{join_1:=eq(@`s2.s#`,@`sp2.s#`)}) where join_1=True) {all but join_1})"){-, + "(with (s2 as s, sp2 as sp) ((((s2 rename {s# as `s2.s#`,sname as `s2.sname`,city as `s2.city`,status as `s2.status`}) join (sp2 rename {s# as `sp2.s#`,p# as `sp2.p#`,qty as `sp2.qty`})):{join_1:=eq(@`s2.s#`,@`sp2.s#`)}) where join_1=True) {all but join_1})"), -- formula extension ("SELECT status+10 FROM s", "((s : {attr_1:=add(@status,10)}) { attr_1 })"), -- extension and formula @@ -95,7 +114,7 @@ testSelect = TestCase $ do -- SELECT with no table expression ("SELECT 1,2,3","((relation{}{}:{attr_1:=1,attr_2:=2,attr_3:=3}){attr_1,attr_2,attr_3})"), -- basic NULL - ("SELECT NULL", "((relation{}{}:{attr_1:=Nothing}){attr_1})")-} + ("SELECT NULL", "((relation{}{}:{attr_1:=Nothing}){attr_1})") ] gfEnv = GraphRefRelationalExprEnv { gre_context = Just dateExamples, @@ -113,20 +132,43 @@ testSelect = TestCase $ do --print x pure x --parse tutd - relExpr <- case parse (dataFrameP <* eof) "test" tutd of + tutdAsDFExpr <- case parse (dataFrameP <* eof) "test" tutd of Left err -> error (errorBundlePretty err) Right x -> do --print x pure x - selectAsRelExpr <- case convertSelect typeF select of + selectAsDFExpr <- case convertSelect typeF select of Left err -> error (show err) Right x -> do print x pure x --print ("selectAsRelExpr"::String, selectAsRelExpr) - assertEqual (T.unpack sql) relExpr selectAsRelExpr + assertEqual (T.unpack sql) tutdAsDFExpr selectAsDFExpr + --check that the expression can actually be executed + eEvald <- executeDataFrameExpr sess conn tutdAsDFExpr + case eEvald of + Left err -> assertFailure (show err <> ": " <> show tutdAsDFExpr) + Right _ -> pure () mapM_ check readTests -- assertEqual "SELECT * FROM test" (Right (Select {distinctness = Nothing, projectionClause = [(Identifier (QualifiedProjectionName [Asterisk]),Nothing)], tableExpr = Just (TableExpr {fromClause = [SimpleTableRef (QualifiedName ["test"])], whereClause = Nothing, groupByClause = [], havingClause = Nothing, orderByClause = [], limitClause = Nothing, offsetClause = Nothing})})) (p "SELECT * FROM test") +dateExamplesConnection :: NotificationCallback -> IO (SessionId, Connection) +dateExamplesConnection callback = do + dbconn <- connectProjectM36 (InProcessConnectionInfo NoPersistence callback []) + case dbconn of + Left err -> error (show err) + Right conn -> do + eSessionId <- createSessionAtHead conn "master" + case eSessionId of + Left err -> error (show err) + Right sessionId -> do + executeDatabaseContextExpr sessionId conn (databaseContextAsDatabaseContextExpr dateExamples) >>= eitherFail + --skipping atom functions for now- there are no atom function manipulation operators yet + commit sessionId conn >>= eitherFail + pure (sessionId, conn) + +eitherFail :: Either RelationalError a -> IO () +eitherFail (Left err) = assertFailure (show err) +eitherFail (Right _) = pure ()