修复bug

This commit is contained in:
LIm1tZz7 committed 2026-10-11 17:32:12 +08:00
1 parent fc9bbc20e4
commit e87c4ee6a8
90 files changed
+3828 -452

No files matched your search

@@ -28,6 +28,8 @@ library
ChuSQL.Core.Engine.Runtime.Binding
ChuSQL.Core.Engine.Runtime.SQL
ChuSQL.Core.Engine.Runtime.Query
ChuSQL.Core.Engine.Runtime.Relation
ChuSQL.Core.Engine.Runtime.Dependencies
ChuSQL.Core.Engine.Semantic
ChuSQL.Core.Engine.Storage
ChuSQL.Core.Engine.Storage.FFI
@@ -53,6 +55,7 @@ library
if os(osx)
ld-options: -Wl,-rpath,@loader_path
default-language: Haskell2010
default-extensions: ImportQualifiedPost
ghc-options: -Wall
test-suite chusql-core-engine-test
+39 -25
View File
@@ -3,14 +3,14 @@ module ChuSQL.Core.Engine (runStatement, runStatementM, rowsOf) where
import ChuSQL.Core.Engine.Algebra.Eval (evalCondForRowM, evalExprM)
import ChuSQL.Core.Engine.Algebra.Expr (evalCondForRow, evalExpr)
import ChuSQL.Core.Engine.Runtime.Query (bindQuery, executeQueryM)
import ChuSQL.Core.Model
import ChuSQL.Core.Engine.Semantic (prepare, prepareRowExpression)
import ChuSQL.Core.Engine.Storage (MonadStorage (..))
import ChuSQL.Core.Engine.Storage.Memory (MemoryStorage (runMemoryStorage))
import ChuSQL.Core.Engine.Syntax.AST
import ChuSQL.Core.Engine.Syntax.Parser (parseExpression)
import ChuSQL.Core.Model
import Data.HashSet qualified as HS
import Data.Maybe (mapMaybe)
import qualified Data.HashSet as HS
-- 引擎入口:检查语义、按语句分发执行,另有内存实现与泛型版本。
@@ -39,6 +39,10 @@ runStatementM q = do
-- | 按语句类型分发(已检查过)
runStatementUncheckedM :: (MonadStorage m) => Database -> Statement -> m (Either String [Row])
runStatementUncheckedM db q@SelectDistinct{} =
case bindQuery db q of
Left e -> pure (Left e)
Right bound -> executeQueryM bound
runStatementUncheckedM db q@Select{} =
case bindQuery db q of
Left e -> pure (Left e)
@@ -74,7 +78,7 @@ runStatementUncheckedM db (Delete tbl mWhere) = do
result <- replaceAll tbl kept
pure (result >> Right [])
where
-- | 取行里的整数 id,取不到就报错
-- \| 取行里的整数 id,取不到就报错
rowId :: Row -> Either String Int
rowId r = case lookup "id" r of
Just (VInt k) -> Right k
@@ -104,9 +108,15 @@ runStatementUncheckedM _ (CreateDomain name base) = do
runStatementUncheckedM _ (DropDomain name) = do
result <- dropTable (domainTableName name)
pure (result >> Right [])
runStatementUncheckedM db ShowDomains = pure (Right
[[("domain", VStr (drop (length (domainTableName "")) name)), ("base_type", VStr (typeName (columnType base)))]
| (name, table) <- db, isDomainTable name, Just base <- [lookup "base" (tableCols table)]])
runStatementUncheckedM db ShowDomains =
pure
( Right
[ [("domain", VStr (drop (length (domainTableName "")) name)), ("base_type", VStr (typeName (columnType base)))]
| (name, table) <- db
, isDomainTable name
, Just base <- [lookup "base" (tableCols table)]
]
)
runStatementUncheckedM _ (DropTable name) = do
result <- dropTable name
pure (result >> Right [])
@@ -132,11 +142,13 @@ runStatementUncheckedM db (AlterColumnNull tbl col nullable) =
runStatementUncheckedM _ CreateUser{} = pure (Left "CREATE USER is executed by the account service")
runStatementUncheckedM _ AlterUser{} = pure (Left "ALTER USER is executed by the account service")
runStatementUncheckedM _ AlterIdentity{} = pure (Left "ALTER ROLE is executed by the account service")
runStatementUncheckedM _ ShowGrants = pure (Left "SHOW GRANTS is executed by the account service")
runStatementUncheckedM _ ShowRoles = pure (Left "SHOW ROLES is executed by the account service")
runStatementUncheckedM _ DropUser{} = pure (Left "DROP USER is executed by the account service")
runStatementUncheckedM _ CreateRole{} = pure (Left "CREATE ROLE is executed by the privilege service")
runStatementUncheckedM _ DropRole{} = pure (Left "DROP ROLE is executed by the privilege service")
runStatementUncheckedM _ GrantPrivileges{} = pure (Left "GRANT is executed by the privilege service")
runStatementUncheckedM _ RevokeGrantOption{} = pure (Left "REVOKE is executed by the privilege service")
runStatementUncheckedM _ RevokePrivileges{} = pure (Left "REVOKE is executed by the privilege service")
runStatementUncheckedM _ GrantRole{} = pure (Left "GRANT is executed by the privilege service")
runStatementUncheckedM _ RevokeRole{} = pure (Left "REVOKE is executed by the privilege service")
@@ -157,9 +169,9 @@ existingRows tbl table
| needsRows table = scan tbl
| otherwise = pure (Right [])
where
-- | 这一表有没有要判约束的列
-- \| 这一表有没有要判约束的列
needsRows t = any (constraintColumn . snd) (tableCols t)
-- | 这一列带不带要判的约束
-- \| 这一列带不带要判的约束
constraintColumn c = columnAutoIncrement c || columnUnique c || columnPrimaryKey c
-- | 插一行:补齐缺列、给自增值、收类型、查约束
@@ -172,7 +184,7 @@ buildRows table current cols rows = do
let auto = autoColumn table
go auto (autoStart auto current) values []
where
-- | 逐行构造,最后统一查一遍约束
-- \| 逐行构造,最后统一查一遍约束
go _ _ [] acc = do
let built = reverse acc
plan <- checkPlan table
@@ -189,25 +201,27 @@ buildRow :: Table -> Maybe (String, Column) -> [String] -> Int -> [Value] -> Eit
buildRow table auto cols counter vals = go (tableCols table) counter []
where
provided = zip cols vals
-- | 按列定义逐列补值
-- \| 按列定义逐列补值
go [] c acc = Right (reverse acc, c)
go ((name, col) : rest) c acc = do
(v, c') <- pick name col c
v' <- coerceValue (columnType col) v
go rest c' ((name, v') : acc)
-- | 决定这一列取什么值
-- \| 决定这一列取什么值
pick name col c
| Just (autoName, _) <- auto
, name == autoName
, maybe True (== VNull) (lookup name provided) = Right (VInt c, c + 1)
, maybe True (== VNull) (lookup name provided) =
Right (VInt c, c + 1)
| Just v <- lookup name provided = Right (v, bump name col c v)
| Just d <- columnDefault col = Right (d, c)
| columnNullable col = Right (VNull, c)
| otherwise = Left ("INSERT: column " ++ name ++ " is NOT NULL and has no default")
-- | 自增值跟着已有的值往前推
-- \| 自增值跟着已有的值往前推
bump name _ c v
| Just (autoName, _) <- auto
, name == autoName = max (c + 1) (valueInt v + 1)
, name == autoName =
max (c + 1) (valueInt v + 1)
| otherwise = c
-- | 取自增值,不是整数就当 0
@@ -235,9 +249,9 @@ type CheckPlan = [(String, Expr)]
checkPlan :: Table -> Either String CheckPlan
checkPlan table = mapM parse (mapMaybe checked (tableCols table))
where
-- | 只挑出带 CHECK 文本的列
-- \| 只挑出带 CHECK 文本的列
checked (name, col) = (,) name <$> columnCheck col
-- | 解析一列的 CHECK 文本
-- \| 解析一列的 CHECK 文本
parse (name, text) = case parseExpression text of
Left err -> Left ("CHECK on " ++ name ++ ": " ++ firstLine err)
Right e -> do
@@ -248,7 +262,7 @@ checkPlan table = mapM parse (mapMaybe checked (tableCols table))
runChecks :: CheckPlan -> Row -> Either String ()
runChecks plan row = mapM_ check plan
where
-- | 跑一条 CHECK
-- \| 跑一条 CHECK
check (name, e) = case evalCondForRow e row of
Left err -> Left ("CHECK on " ++ name ++ ": " ++ err)
Right True -> Right ()
@@ -262,7 +276,7 @@ firstLine = takeWhile (/= '\n')
enforceNotNull :: [(String, Column)] -> [Row] -> Either String ()
enforceNotNull cols rows = mapM_ check cols
where
-- | 查这一列的 NOT NULL
-- \| 查这一列的 NOT NULL
check (name, col)
| columnNullable col = Right ()
| any (\r -> lookup name r == Just VNull) rows = Left ("NOT NULL: column " ++ name ++ " cannot be null")
@@ -273,11 +287,11 @@ enforceUnique :: [(String, Column)] -> [Row] -> Either String ()
enforceUnique cols rows = mapM_ check targets
where
targets = [(n, c) | (n, c) <- cols, columnUnique c || columnPrimaryKey c]
-- | 查这一列有没有重复值
-- \| 查这一列有没有重复值
check (name, _) = go name HS.empty (mapMaybe (lookup name) rows)
-- | 逐个值比过去
-- \| 逐个值比过去
go _ _ [] = Right ()
-- | NULL 不算冲突,跳过
-- \| NULL 不算冲突,跳过
go name seen (VNull : rest) = go name seen rest
go name seen (v : rest)
| HS.member (canonicalValue v) seen = Left ("UNIQUE: duplicate value in column " ++ name)
@@ -317,7 +331,7 @@ setColumn col v = map (\(k, val) -> if k == col then (k, v) else (k, val))
splitByConditionM :: (MonadStorage m) => Database -> Maybe Expr -> [Row] -> m (Either String ([Row], [Row]))
splitByConditionM db cond = go [] []
where
-- | 逐行分流
-- \| 逐行分流
go doomed kept [] = pure (Right (reverse doomed, reverse kept))
go doomed kept (r : rs) = do
hit <- case cond of
@@ -346,9 +360,9 @@ alterTable db tbl change = case lookupTable db tbl of
result <- replaceSchema tbl cols newRows
pure (result >> Right [])
where
-- | 改完再统一查一遍约束
-- \| 改完再统一查一遍约束
checkAltered (cols, rows) = do
let table = Table tbl cols rows Nothing
let table = Table tbl cols rows Nothing Nothing Nothing
plan <- checkPlan table
mapM_ (runChecks plan) rows
enforceNotNull cols rows
@@ -387,7 +401,7 @@ retypeColumn col ty cols rows
rows' <- mapM convert rows
Right (map (\entry@(n, c) -> if n == col then (n, c{columnType = ty}) else entry) cols, rows')
where
-- | 把这一列的老值收进新类型
-- \| 把这一列的老值收进新类型
convert row = case lookup col row of
Nothing -> Right row
Just v -> do
@@ -5,7 +5,8 @@ import ChuSQL.Core.Engine.Algebra.Expr (evalCondForRow, evalExpr, hasSubquery, q
import ChuSQL.Core.Engine.Algebra.Optimize (optimize)
import ChuSQL.Core.Engine.Algebra.Op
import ChuSQL.Core.Engine.Algebra.Planner (translate)
import ChuSQL.Core.Engine.Algebra.Sort (sortRows)
import ChuSQL.Core.Engine.Algebra.Sort (sortRows, sortBoundRows)
import ChuSQL.Core.Engine.Runtime.Relation (bindRelOpColumns)
import ChuSQL.Core.Engine.Builtin (aggregateArg, builtinOfExpr, invokeBuiltin, unsupportedAggregate)
import ChuSQL.Core.Model
import ChuSQL.Core.Engine.Storage (IndexResult (..), MonadStorage (..))
@@ -14,6 +15,7 @@ import ChuSQL.Core.Engine.Syntax.AST (Expr (..), JoinKind (..), Subquery (..))
import Control.Monad (filterM)
import Data.List (nub)
import qualified Data.HashMap.Strict as HM
import qualified Data.HashSet as HS
-- 执行:按算子树算出结果行,外层行供相关子查询引用。
@@ -79,11 +81,30 @@ data EquiKeys = EquiKeys
-- | 认出 a = b 这种两列等值条件,并且能分清谁在左谁在右
equiKeys :: [String] -> [String] -> Expr -> Maybe EquiKeys
equiKeys lcols rcols (Eq (Col a) (Col b))
| a /= b, a `elem` lcols, b `elem` rcols = Just (EquiKeys a b)
| a /= b, b `elem` lcols, a `elem` rcols = Just (EquiKeys b a)
equiKeys lcols rcols (Eq left right)
| Just a <- columnName left, Just b <- columnName right
, a /= b, a `elem` lcols, b `elem` rcols = Just (EquiKeys a b)
| Just a <- columnName left, Just b <- columnName right
, a /= b, b `elem` lcols, a `elem` rcols = Just (EquiKeys b a)
equiKeys _ _ _ = Nothing
-- | 提取执行列的诊断名称
columnName :: Expr -> Maybe String
columnName (Col name) = Just name
columnName (ColumnSlot _ name) = Just name
columnName _ = Nothing
-- | 在存储边界按输入布局排列行
orderedRows :: [String] -> [Row] -> Either String [Row]
orderedRows columns = mapM order
where
-- | 按列布局排列一行
order row = mapM (column row) columns
-- | 缺少物理列时明确报错
column row name = case lookup name row of
Just value -> Right (name, value)
Nothing -> Left ("missing bound column: " ++ name)
-- | 连接两批行:等值键走哈希,否则嵌套循环
joinRowsIn ::
(MonadStorage m) =>
@@ -188,8 +209,9 @@ probeIndexJoin db outer kind mAlias t ks rcols cond l = case lookup (ekLeft ks)
Left e -> pure (Left e)
Right NoIndex -> pure (Right Nothing)
Right (IndexRows rows) -> do
pairs <- joinRowsIn db outer kind rcols cond [l] (prefixRows mAlias rows)
pure (fmap Just pairs)
case orderedRows rcols (prefixRows mAlias rows) of
Left err -> pure (Left err)
Right ordered -> fmap (fmap Just) (joinRowsIn db outer kind rcols cond [l] ordered)
-- | 列与键都不是浮点时才敢用索引
indexSafe :: Database -> String -> String -> Value -> Bool
@@ -213,20 +235,17 @@ floatValue (VFloat _) = True
floatValue _ = False
-- | 分组键的值
groupKey :: [String] -> Row -> Either String [Value]
groupKey keys row = mapM get keys
where
-- | 取这一列的分组值
get c = maybe (Left ("unknown column: " ++ c)) Right (lookup c row)
groupKey :: Row -> [Expr] -> Row -> Either String [Value]
groupKey outer keys row = mapM (\key -> evalExpr key (row ++ outer)) keys
-- | 按分组键把行分组,保留分组首次出现的顺序
groupRows :: [String] -> [Row] -> Either String [([Value], [Row])]
groupRows keys rows = go rows (HM.empty, [])
groupRows :: Row -> [Expr] -> [Row] -> Either String [([Value], [Row])]
groupRows outer keys rows = go rows (HM.empty, [])
where
-- | 逐行塞进对应分组,并记住分组首次出现的顺序
go [] (acc, order) = Right [(k, reverse (HM.lookupDefault [] k acc)) | k <- reverse order]
go (r : rs) (acc, order) = do
k <- groupKey keys r
k <- groupKey outer keys r
let order' = if HM.member k acc then order else k : order
go rs (HM.insertWith (++) k [r] acc, order')
@@ -249,11 +268,11 @@ oneGroup outer keys aggs (keyValues, rows) = do
Right (zip keys keyValues ++ zip (map fst aggs) values)
-- | 分组求值:没有分组列时,即使没有行也要出一行
evalAggregate :: Row -> [String] -> [(String, Expr)] -> [Row] -> Either String [Row]
evalAggregate :: Row -> [(String, Expr)] -> [(String, Expr)] -> [Row] -> Either String [Row]
evalAggregate outer keys aggs rows = do
groups <- groupRows keys rows
groups <- groupRows outer (map snd keys) rows
let top = if null keys && null groups then [([], [])] else groups
mapM (oneGroup outer keys aggs) top
mapM (oneGroup outer (map fst keys) aggs) top
-- | 绑定键:子查询引用的外层列在这一行上的取值
bindingKey :: [String] -> Row -> String
@@ -443,7 +462,9 @@ runSubqueryRows db sq outer = case translate (subqueryStatement sq) of
Left e -> pure (Left e)
Right op -> case mapM bind (subqueryRefs sq) of
Left err -> pure (Left err)
Right bindings -> evalRelOpIn db (bindings ++ outer) (optimize db op)
Right bindings -> case bindRelOpColumns db (map fst (bindings ++ outer)) (optimize db op) of
Left err -> pure (Left err)
Right bound -> evalRelOpIn db (bindings ++ outer) bound
where
-- | 给外层物理列补回已解析的限定名
bind column = case bindingValue column outer of
@@ -481,23 +502,26 @@ evalRelOpIn db outer (Compute items op) = do
es <- boundExprs refs (r ++ outer) table
mapM (\(label, e) -> (,) label <$> evalExpr e (r ++ outer)) (zip (map fst items) es)
evalRelOpIn db outer (Aggregate keys aggs op) = do
rows <- evalRelOpIn db outer op
pure (rows >>= evalAggregate outer [(name, Col name) | name <- keys] aggs)
evalRelOpIn db outer (BoundAggregate keys aggs op) = do
rows <- evalRelOpIn db outer op
pure (rows >>= evalAggregate outer keys aggs)
evalRelOpIn db _ (Scan mAlias tbl cols) = do
res <- scanTable db tbl cols
pure (fmap (prefixRows mAlias) res)
pure (res >>= orderedRows (relOpCols db (Scan mAlias tbl cols)) . prefixRows mAlias)
evalRelOpIn db outer (Lookup mAlias tbl col k) = do
res <- lookupByColumn tbl col k
case res of
Left e -> pure (Left e)
Right NoIndex -> scanFiltered db outer mAlias tbl (lookupCondition mAlias col k)
Right (IndexRows rows) -> pure (Right (prefixRows mAlias rows))
Right (IndexRows rows) -> pure (orderedRows (relOpCols db (Lookup mAlias tbl col k)) (prefixRows mAlias rows))
evalRelOpIn db outer (Range mAlias tbl col lo hi) = do
res <- scanRange tbl col lo hi
case res of
Left e -> pure (Left e)
Right Nothing -> scanFiltered db outer mAlias tbl (rangeCondition mAlias col lo hi)
Right (Just rows) -> pure (Right (prefixRows mAlias rows))
Right (Just rows) -> pure (orderedRows (relOpCols db (Range mAlias tbl col lo hi)) (prefixRows mAlias rows))
evalRelOpIn db outer (Filter e op) = do
rows <- evalRelOpIn db outer op
case rows of
@@ -516,9 +540,15 @@ evalRelOpIn db outer (Filter e op) = do
evalRelOpIn db outer (Project cols op) = do
rows <- evalRelOpIn db outer op
pure (fmap (map (project cols)) rows)
evalRelOpIn db outer (Distinct op) = do
rows <- evalRelOpIn db outer op
pure (distinctRows <$> rows)
evalRelOpIn db outer (Sort spec op) = do
rows <- evalRelOpIn db outer op
pure (fmap (sortRows spec) rows)
evalRelOpIn db outer (BoundSort spec op) = do
rows <- evalRelOpIn db outer op
pure (rows >>= sortBoundRows spec outer)
evalRelOpIn db outer (Limit n op) = do
rows <- evalRelOpIn db outer op
pure (fmap (take n) rows)
@@ -557,8 +587,8 @@ scanFiltered ::
scanFiltered db outer mAlias tbl cond = do
res <- scanTable db tbl Nothing
pure $ do
rows <- res
filterM (evalCondForRow cond . (++ outer)) (prefixRows mAlias rows)
rows <- res >>= orderedRows (relOpCols db (Scan mAlias tbl Nothing)) . prefixRows mAlias
filterM (evalCondForRow cond . (++ outer)) rows
-- | 行数到阈值才值得并行分片
parallelRowThreshold :: Int
@@ -574,3 +604,15 @@ scanTable db tbl cols = do
where
-- | 统计里的行数,没有统计算 0
rows = maybe 0 metaRowCount (tableMetaOf db tbl)
-- | 按输出值去重并保留首次行序
distinctRows :: [Row] -> [Row]
distinctRows = go HS.empty
where
-- | 只输出尚未遇到的值组合
go _ [] = []
go seen (row : rows)
| HS.member key seen = go seen rows
| otherwise = row : go (HS.insert key seen) rows
where
key = map (canonicalValue . snd) row
@@ -11,6 +11,13 @@ import ChuSQL.Core.Engine.Runtime.SQL (invokeSQLFunction, constructSQLValue)
evalExpr :: Expr -> Row -> Either String Value
evalExpr (Col name) row =
maybe (Left ("unknown column: " ++ name)) Right (lookup name row)
evalExpr (ColumnSlot slot name) row = case drop slot row of
(key, value) : _ | slot >= 0, key == name -> Right value
[] -> Left ("missing bound column: " ++ name)
_ -> Left ("bound column position mismatch: " ++ name)
evalExpr (OuterColumnSlot localWidth slot name) row
| localWidth < 0 || slot < 0 = Left ("bound column position mismatch: " ++ name)
| otherwise = evalExpr (ColumnSlot slot name) (drop localWidth row)
evalExpr (FunctionCall name _) _ = Left ("unbound function call: " ++ name)
evalExpr (Construct _ _) _ = Left "unbound runtime constructor"
evalExpr (BoundConstruct tid arguments) row = mapM (\argument -> evalExpr argument row) arguments >>= constructSQLValue tid
@@ -70,6 +77,8 @@ quantifiedCompare op lhs vs q = case vs of
-- | 表达式用到了哪些列
colsInExpr :: Expr -> [String]
colsInExpr (Col c) = [c]
colsInExpr (ColumnSlot _ c) = [c]
colsInExpr (OuterColumnSlot _ _ c) = [c]
colsInExpr (Construct _ arguments) = concatMap colsInExpr arguments
colsInExpr (BoundConstruct _ arguments) = concatMap colsInExpr arguments
colsInExpr (FunctionCall _ arguments) = concatMap colsInExpr arguments
@@ -247,6 +256,8 @@ bareColumns e = case e of
FunctionCall _ arguments -> concatMap bareColumns arguments
BoundFunction _ _ arguments -> concatMap bareColumns arguments
Col c -> [c]
ColumnSlot _ c -> [c]
OuterColumnSlot _ _ c -> [c]
Add a b -> both a b
Sub a b -> both a b
Mul a b -> both a b
@@ -15,6 +15,7 @@ reorderJoins db = pass db False
-- | 往下带“行序是否不可观察”,可观察时不动内连接
pass :: Database -> Bool -> RelOp -> RelOp
pass db free (Distinct x) = Distinct (pass db free x)
pass db _ (Sort spec x) = Sort spec (pass db True x)
pass db _ (Limit n x) = Limit n (pass db False x)
pass db _ (Aggregate keys aggs x) = Aggregate keys aggs (pass db False x)
@@ -13,8 +13,11 @@ data RelOp
| Project [String] RelOp
| Compute [(String, Expr)] RelOp
| Aggregate [String] [(String, Expr)] RelOp
| BoundAggregate [(String, Expr)] [(String, Expr)] RelOp
| Distinct RelOp
| Unit
| Sort [(String, SortDir)] RelOp
| BoundSort [(Expr, SortDir)] RelOp
| Limit Int RelOp
| Join JoinKind RelOp RelOp Expr
| Derived (Maybe String) RelOp
@@ -25,6 +28,7 @@ relOpCols :: Database -> RelOp -> [String]
relOpCols _ Unit = []
relOpCols _ (Compute items _) = map fst items
relOpCols _ (Aggregate keys aggs _) = keys ++ map fst aggs
relOpCols _ (BoundAggregate keys aggs _) = map fst keys ++ map fst aggs
relOpCols _ (Scan mAlias _ (Just cols)) = map (qualify mAlias) cols
relOpCols db (Scan mAlias tbl Nothing) =
case lookup tbl db of
@@ -37,7 +41,9 @@ relOpCols db (Range mAlias tbl _ _ _) = relOpCols db (Scan mAlias tbl Nothing)
relOpCols db (Filter _ x) = relOpCols db x
relOpCols _ (Project ["*"] _) = ["*"]
relOpCols _ (Project cols _) = cols
relOpCols db (Distinct x) = relOpCols db x
relOpCols db (Sort _ x) = relOpCols db x
relOpCols db (BoundSort _ x) = relOpCols db x
relOpCols db (Limit _ x) = relOpCols db x
relOpCols db (Join _ l r _) = relOpCols db l ++ relOpCols db r
relOpCols db (Derived mAlias x) = map (qualify mAlias) (relOpCols db x)
@@ -67,8 +73,12 @@ renderPlan = go 0
++ show [label ++ " <- " ++ show e | (label, e) <- aggs]
++ "\n"
++ go (ind + 1) x
Distinct x -> pad ++ "Distinct\n" ++ go (ind + 1) x
BoundAggregate keys aggs x -> pad ++ "BoundAggregate keys=" ++ show keys
++ " aggs=" ++ show aggs ++ "\n" ++ go (ind + 1) x
Unit -> pad ++ "Unit"
Sort s x -> pad ++ "Sort " ++ show s ++ "\n" ++ go (ind + 1) x
BoundSort s x -> pad ++ "BoundSort " ++ show s ++ "\n" ++ go (ind + 1) x
Limit n x -> pad ++ "Limit " ++ show n ++ "\n" ++ go (ind + 1) x
Join k l r c -> pad ++ kindLabel k ++ " on " ++ show c ++ "\n" ++ go (ind + 1) l ++ "\n" ++ go (ind + 1) r
Derived a x -> pad ++ "Derived " ++ show a ++ "\n" ++ go (ind + 1) x
@@ -77,6 +77,7 @@ planHasDivision (Filter p x) = hasDivision p || planHasDivision x
planHasDivision (Join _ l r c) = hasDivision c || planHasDivision l || planHasDivision r
planHasDivision (Compute items x) = any (hasDivision . snd) items || planHasDivision x
planHasDivision (Project _ x) = planHasDivision x
planHasDivision (Distinct x) = planHasDivision x
planHasDivision (Sort _ x) = planHasDivision x
planHasDivision (Limit _ x) = planHasDivision x
planHasDivision (Derived _ x) = planHasDivision x
@@ -96,6 +97,8 @@ pushProjectLeaf db need leaf
-- | 把需要哪些列往下传
pushProject :: Database -> [String] -> RelOp -> RelOp
pushProject _ _ Unit = Unit
pushProject _ _ op@BoundSort{} = op
pushProject _ _ op@BoundAggregate{} = op
pushProject db _ (Compute items x) = Compute items (pushProject db (concatMap (colsInExpr . snd) items) x)
pushProject db _ (Aggregate keys aggs x) =
Aggregate keys aggs (pushProject db (keys ++ concatMap (colsInExpr . snd) aggs) x)
@@ -103,6 +106,7 @@ pushProject db _ (Project cols x) = case pushProject db cols x of
Project cols' y | cols' == cols -> Project cols y
other -> Project cols other
pushProject db need (Filter p x) = Filter p (pushProject db (need ++ colsInExpr p) x)
pushProject db _ (Distinct x) = Distinct (pushProject db (relOpCols db x) x)
pushProject db need (Sort spec x) = Sort spec (pushProject db (need ++ map fst spec) x)
pushProject db need (Limit n x) = Limit n (pushProject db need x)
pushProject db need (Join kind l r c)
@@ -250,6 +254,7 @@ optimizeRelOp db = rewriteNode db . descend
descend (Project cols x) = Project cols (optimizeRelOp db x)
descend (Compute items x) = Compute items (optimizeRelOp db x)
descend (Aggregate keys aggs x) = Aggregate keys aggs (optimizeRelOp db x)
descend (Distinct x) = Distinct (optimizeRelOp db x)
descend (Sort spec x) = Sort spec (optimizeRelOp db x)
descend (Limit n x) = Limit n (optimizeRelOp db x)
descend (Join kind l r c) = Join kind (optimizeRelOp db l) (optimizeRelOp db r) c
@@ -82,6 +82,13 @@ limited orderBy mLimit op = case mLimit of
-- | 翻成:过滤 -> 分组 -> 排序 -> 截断 -> 取列
translate :: Statement -> Either String RelOp
translate q = case q of
SelectDistinct inner -> do
ordering <- distinctOrdering inner
plan <- case inner of
s@Select{} -> translate s { selectOrderBy = [], selectLimit = Nothing }
s@SelectExpr{} -> translate s { selectOrderBy = [], selectLimit = Nothing }
_ -> Left "DISTINCT requires SELECT"
Right (limited ordering (selectLimit inner) (Distinct plan))
Select
{ selectCols = cols
, selectFrom = fromC
@@ -1,7 +1,8 @@
module ChuSQL.Core.Engine.Algebra.Sort (sortRows, compareValue) where
module ChuSQL.Core.Engine.Algebra.Sort (sortRows, sortBoundRows, compareValue) where
import ChuSQL.Core.Model
import ChuSQL.Core.Engine.Syntax.AST
import ChuSQL.Core.Engine.Algebra.Expr (evalExpr)
import Data.List (sortBy)
-- 排序:按多列规格排出结果行,每列可升可降。
@@ -17,9 +18,9 @@ sortKey :: [(String, SortDir)] -> Row -> [Maybe Value]
sortKey spec row = [lookup col row | (col, _) <- spec]
-- | 比较两个排序键
compareKeys :: [(String, SortDir)] -> [Maybe Value] -> [Maybe Value] -> Ordering
compareKeys :: [SortDir] -> [Maybe Value] -> [Maybe Value] -> Ordering
compareKeys [] _ _ = EQ
compareKeys ((_, dir) : rest) (a : as) (b : bs) =
compareKeys (dir : rest) (a : as) (b : bs) =
case (a, b) of
(Just v1, Just v2) ->
case compareValue v1 v2 of
@@ -36,4 +37,19 @@ sortRows spec rows = map snd (sortBy cmp (map decorate rows))
-- | 取键并带上原行
decorate row = (sortKey spec row, row)
-- | 比较两个带键的行
cmp (k1, _) (k2, _) = compareKeys spec k1 k2
cmp (k1, _) (k2, _) = compareKeys (map snd spec) k1 k2
-- | 用绑定槽位取键并稳定排序
sortBoundRows :: [(Expr, SortDir)] -> Row -> [Row] -> Either String [Row]
sortBoundRows [] _ rows = Right rows
sortBoundRows spec outer rows = do
decorated <- mapM decorate rows
Right (map snd (sortBy compareRows decorated))
where
directions = map snd spec
-- | 每行只计算一次绑定排序键
decorate row = do
keys <- mapM (\(expression, _) -> evalExpr expression (row ++ outer)) spec
Right (map Just keys, row)
-- | 比较排序键并保留同键行序
compareRows (left, _) (right, _) = compareKeys directions left right
@@ -1,25 +1,25 @@
module ChuSQL.Core.Engine.Builtin
( Builtin (..)
, builtinNames
, resolveBuiltin
, builtinName
, invokeBuiltin
, builtinOfExpr
, builtinNode
, aggregateArg
, unsupportedAggregate
, Operator (..)
, operatorOfExpr
, operatorSymbol
, operatorOfCompare
, compareNode
, executeOperator
, threeValuedNot
, inValues
) where
module ChuSQL.Core.Engine.Builtin (
Builtin (..),
builtinNames,
resolveBuiltin,
builtinName,
invokeBuiltin,
builtinOfExpr,
builtinNode,
aggregateArg,
unsupportedAggregate,
Operator (..),
operatorOfExpr,
operatorSymbol,
operatorOfCompare,
compareNode,
executeOperator,
threeValuedNot,
inValues,
) where
import ChuSQL.Core.Model (Value (..), compareValue)
import ChuSQL.Core.Engine.Syntax.AST (CompareOp (..), Expr (..))
import ChuSQL.Core.Model (Value (..), compareValue)
import Control.Monad (foldM)
import Data.Char (toLower)
@@ -246,12 +246,12 @@ evalArith ops x y = case (asDouble x, asDouble y) of
divArith :: Arith
divArith = Arith intDiv floatDiv
where
-- | 整数除法:除零与溢出都报错
-- \| 整数除法:除零与溢出都报错
intDiv _ 0 = Left "division by zero"
intDiv x y
| x == minBound && y == -1 = Left "integer division overflow"
| otherwise = Right (x `quot` y)
-- | 浮点除法:不能除以零
-- \| 浮点除法:不能除以零
floatDiv _ 0 = Left "division by zero"
floatDiv x y = Right (x / y)
@@ -1,12 +1,12 @@
module ChuSQL.Core.Engine.Error
( ErrorCategory (..)
, ErrorRule (..)
, errorRules
, errorCategory
, errorCode
, errorLike
, accountErrorCode
) where
module ChuSQL.Core.Engine.Error (
ErrorCategory (..),
ErrorRule (..),
errorRules,
errorCategory,
errorCode,
errorLike,
accountErrorCode,
) where
import Data.List (isInfixOf)
@@ -35,6 +35,8 @@ errorRules :: [ErrorRule]
errorRules =
[ ErrorRule "no database selected" NoDatabaseError "no_database"
, ErrorRule "administrator required" PermissionError "forbidden"
, ErrorRule "bound query object changed" CatalogError "serialization_failure"
, ErrorRule "bound query schema changed" CatalogError "serialization_failure"
, ErrorRule "unknown table" CatalogError "not_found"
, ErrorRule "unknown domain" CatalogError "not_found"
, ErrorRule "domain is still used" CatalogError "query_error"
@@ -14,23 +14,27 @@ data Declaration = Declaration
, dependencies :: [Dependency]
, dependenciesComplete :: Bool
, atomicAttributes :: Maybe Bool
} deriving (Eq, Show)
}
deriving (Eq, Show)
data Finding = Finding
{ normalForm :: NormalForm
, verdict :: Verdict
, witnesses :: [Dependency]
, condition :: String
} deriving (Eq, Show)
}
deriving (Eq, Show)
data Decomposition = Decomposition
{ relations :: [Attributes]
, losslessJoin :: Verdict
, dependencyPreserving :: Verdict
} deriving (Eq, Show)
}
deriving (Eq, Show)
data Analysis = Analysis
{ candidateKeys :: [Attributes]
, findings :: [Finding]
, suggestions :: [Decomposition]
} deriving (Eq, Show)
}
deriving (Eq, Show)
-- 规范化属性集合。
canonical :: Attributes -> Attributes
@@ -45,8 +49,9 @@ attributeClosure :: [Dependency] -> Attributes -> Attributes
attributeClosure f seed = fixed (canonical seed)
where
-- 迭代加入已蕴含的属性。
fixed xs = let ys = canonical (xs ++ concat [b | Dependency a b <- f, subset a xs])
in if xs == ys then xs else fixed ys
fixed xs =
let ys = canonical (xs ++ concat [b | Dependency a b <- f, subset a xs])
in if xs == ys then xs else fixed ys
-- 判断声明是否蕴含依赖。
implies :: [Dependency] -> Dependency -> Bool
@@ -54,8 +59,12 @@ implies f (Dependency a b) = subset b (attributeClosure f a)
-- 枚举最小超键。
keysOf :: Attributes -> [Dependency] -> [Attributes]
keysOf attrs f = [x | x <- subsequences attrs, super x,
all (not . super) [x \\ [a] | a <- x]]
keysOf attrs f =
[ x
| x <- subsequences attrs
, super x
, all (not . super) [x \\ [a] | a <- x]
]
where
-- 判断闭包是否覆盖关系。
super x = subset attrs (attributeClosure f x)
@@ -63,8 +72,12 @@ keysOf attrs f = [x | x <- subsequences attrs, super x,
-- 投影关系上的全部非平凡依赖。
projectDependencies :: [Dependency] -> Attributes -> [Dependency]
projectDependencies f attrs =
[Dependency x [a] | x <- subsequences attrs,
a <- attributeClosure f x, a `elem` attrs, a `notElem` x]
[ Dependency x [a]
| x <- subsequences attrs
, a <- attributeClosure f x
, a `elem` attrs
, a `notElem` x
]
-- 验证声明并生成只读范式报告。
analyzeNormalization :: [(String, Column)] -> Declaration -> Either String Analysis
@@ -90,14 +103,26 @@ analyzeNormalization schema declaration
Just False -> Violated
Nothing -> Insufficient
allFDs = projectDependencies f attrs
partial = [d | d@(Dependency x [a]) <- allFDs, a `notElem` prime,
any (\k -> x /= k && subset x k) keys]
third = [d | d@(Dependency x [a]) <- allFDs, a `notElem` prime,
not (subset attrs (attributeClosure f x))]
boyce = [d | d@(Dependency x _) <- allFDs,
not (subset attrs (attributeClosure f x))]
results = Finding NF1 atomic [] "attributes must have atomic domains; requires an explicit declaration" :
[classify NF2 partial, classify NF3 third, classify BCNF boyce]
partial =
[ d
| d@(Dependency x [a]) <- allFDs
, a `notElem` prime
, any (\k -> x /= k && subset x k) keys
]
third =
[ d
| d@(Dependency x [a]) <- allFDs
, a `notElem` prime
, not (subset attrs (attributeClosure f x))
]
boyce =
[ d
| d@(Dependency x _) <- allFDs
, not (subset attrs (attributeClosure f x))
]
results =
Finding NF1 atomic [] "attributes must have atomic domains; requires an explicit declaration"
: [classify NF2 partial, classify NF3 third, classify BCNF boyce]
decompositions = [split d | d <- take 1 boyce, complete, atomic == Satisfied]
-- 校验字段集合。
valid xs = length xs == length (canonical xs) && subset xs attrs
@@ -108,10 +133,11 @@ analyzeNormalization schema declaration
-- 结合声明完整性判定范式。
classify nf ds = Finding nf status (if complete then ds else []) (rule nf)
where
status | atomic == Violated = Violated
| atomic == Insufficient || not complete = Insufficient
| null ds = Satisfied
| otherwise = Violated
status
| atomic == Violated = Violated
| atomic == Insufficient || not complete = Insufficient
| null ds = Satisfied
| otherwise = Violated
-- 说明各范式的必要条件。
rule NF1 = "attributes must have atomic domains"
rule NF2 = "no nonprime attribute depends on a proper subset of any candidate key"
@@ -22,6 +22,7 @@ parallelShardLimit = 8
newtype Pool = Pool (Chan (IO ()))
{-# NOINLINE poolRef #-}
-- | 进程全局的池,按容量复用
poolRef :: MVar (Maybe (Int, Pool))
poolRef = unsafePerformIO (newMVar Nothing)
@@ -30,7 +31,7 @@ poolRef = unsafePerformIO (newMVar Nothing)
workerPool :: Int -> IO Pool
workerPool size = modifyMVar poolRef reuse
where
-- | 复用队列并按需补充工作线程
-- \| 复用队列并按需补充工作线程
reuse current = case current of
Just (capacity, pool@(Pool jobs))
| capacity >= wanted -> pure (current, pool)
@@ -41,10 +42,10 @@ workerPool size = modifyMVar poolRef reuse
pool <- startPool wanted
pure (Just (wanted, pool), pool)
-- | 池的线程数至少一个
-- \| 池的线程数至少一个
wanted = max 1 size
-- | 起 wanted 个常驻线程吃队列
-- \| 起 wanted 个常驻线程吃队列
startPool n = do
jobs <- newChan
sequence_ [forkIO (worker jobs) | _ <- [1 .. n]]
@@ -16,7 +16,7 @@ data Expression = Literal TypeExpr RuntimeValue | Reference String | Apply Strin
| TupleLiteral [Expression] deriving (Show, Eq)
-- | 保存绑定后的表达式节点
data ExpressionNode = Constant RuntimeValue | BoundReference String | BoundApply FunctionId [TypedExpression]
data ExpressionNode = Constant RuntimeValue | BoundReference Int String | BoundApply FunctionId [TypedExpression]
| BoundList [TypedExpression] | BoundNothing | BoundJust TypedExpression | BoundTuple [TypedExpression]
deriving (Show, Eq)
@@ -34,8 +34,8 @@ bindExpression types functions env expression = case expression of
tid <- descriptorId <$> resolveType types ty
validateValue types tid value
Right (TypedExpression tid (Constant value))
Reference name -> case [tid | (key, tid) <- env, key == name] of
[tid] -> describeType types tid >> Right (TypedExpression tid (BoundReference name))
Reference name -> case [(slot, tid) | (slot, (key, tid)) <- zip [0 ..] env, key == name] of
[(slot, tid)] -> describeType types tid >> Right (TypedExpression tid (BoundReference slot name))
[] -> Left ("unknown column: " ++ name)
_ -> Left ("ambiguous column: " ++ name)
Apply name arguments -> do
@@ -68,10 +68,10 @@ evaluateExpression :: TypeRegistry -> FunctionRegistry -> [(String, RuntimeValue
evaluateExpression types functions row (TypedExpression tid node) = do
value <- case node of
Constant v -> Right v
BoundReference name -> case [v | (key, v) <- row, key == name] of
[v] -> Right v
BoundReference slot name -> case drop slot row of
(key, value) : _ | key == name -> Right value
[] -> Left ("missing bound column: " ++ name)
_ -> Left ("duplicate bound column: " ++ name)
_ -> Left ("bound column position mismatch: " ++ name)
BoundApply fid arguments -> mapM evaluate arguments >>= invokeFunction types functions fid
BoundList arguments -> RList <$> mapM evaluate arguments
BoundNothing -> Right (RMaybe Nothing)
@@ -0,0 +1,82 @@
module ChuSQL.Core.Engine.Runtime.Dependencies (queryTables, domainDependencies) where
import ChuSQL.Core.Engine.Syntax.AST
import ChuSQL.Core.Model (Database, ColumnType (..), columnType, tableCols, domainTableName, lastTablePart)
import Data.List (nub)
-- 收集查询及嵌套表达式的表依赖,供绑定执行前校验目录。
-- | 收集全部查询来源表
queryTables :: Statement -> [String]
queryTables statement = nub (case statement of
SelectDistinct child -> queryTables child
Select _ source condition _ _ _ -> sourceTables source ++ concatMap expressionTables condition
SelectExpr items source condition _ _ _ -> sourceTables source
++ concatMap (expressionTables . snd) items ++ concatMap expressionTables condition
_ -> [])
-- | 收集来源列依赖的领域定义
domainDependencies :: Database -> [String] -> [String]
domainDependencies catalog sources = collect []
[domain | (name, table) <- catalog, name `elem` sources
, (_, column) <- tableCols table, domain <- typeDomains (columnType column)]
where
-- | 递归展开定义并防止重复访问
collect seen [] = seen
collect seen (domain : remaining) =
let definitions = [(name, table) | (name, table) <- catalog
, lastTablePart name == domainTableName domain, name `notElem` seen]
names = map fst definitions
nested = [child | (_, table) <- definitions, (_, column) <- tableCols table
, child <- typeDomains (columnType column)]
in collect (seen ++ names) (remaining ++ nested)
-- | 保留嵌套领域的每层依赖
typeDomains (CDomain name base) = name : typeDomains base
typeDomains _ = []
-- | 收集来源树与连接条件依赖
sourceTables :: FromClause -> [String]
sourceTables source = case source of
FromTable _ name -> [name]
FromSubquery _ statement -> queryTables statement
FromJoin _ left right condition -> sourceTables left ++ sourceTables right ++ expressionTables condition
FromUnit -> []
-- | 收集表达式内嵌套子查询依赖
expressionTables :: Expr -> [String]
expressionTables expression = case expression of
ScalarSub query -> queryTables (subqueryStatement query)
ExistsSub query _ -> queryTables (subqueryStatement query)
InSub value query _ -> expressionTables value ++ queryTables (subqueryStatement query)
QuantCmp _ value query _ -> expressionTables value ++ queryTables (subqueryStatement query)
_ -> concatMap expressionTables (children expression)
-- | 列出当前表达式的直接子节点
children :: Expr -> [Expr]
children expression = case expression of
FunctionCall _ arguments -> arguments
BoundFunction _ _ arguments -> arguments
Construct _ arguments -> arguments
BoundConstruct _ arguments -> arguments
InList value arguments _ -> value : arguments
Add a b -> [a, b]
Sub a b -> [a, b]
Mul a b -> [a, b]
Div a b -> [a, b]
Gt a b -> [a, b]
Lt a b -> [a, b]
Eq a b -> [a, b]
GtE a b -> [a, b]
LtE a b -> [a, b]
NotEq a b -> [a, b]
And a b -> [a, b]
Or a b -> [a, b]
Neg a -> [a]
IsNull a -> [a]
IsNotNull a -> [a]
CountOf a -> [a]
SumOf a -> [a]
AvgOf a -> [a]
MinOf a -> [a]
MaxOf a -> [a]
_ -> []
@@ -8,22 +8,25 @@ import ChuSQL.Core.Engine.Algebra.Optimize (optimize)
import ChuSQL.Core.Engine.Algebra.Planner (translate)
import ChuSQL.Core.Engine.Semantic (prepare, querySchema)
import ChuSQL.Core.Engine.Syntax.AST (Statement)
import ChuSQL.Core.Engine.Storage (MonadStorage)
import ChuSQL.Core.Engine.Storage (MonadStorage (schema, withSchemaGuard))
import ChuSQL.Core.Engine.Runtime.SQL (sqlType)
import ChuSQL.Core.Engine.Runtime.Types (TypeId)
import ChuSQL.Core.Engine.Runtime.Relation (bindRelOpColumns)
import ChuSQL.Core.Engine.Runtime.Dependencies (queryTables, domainDependencies)
import ChuSQL.Core.Model
-- SQL 查询绑定边界:保存解析后的类型、函数身份及输出 schema。
-- | 保存运行时或兼容列类型
data QueryType = RuntimeType TypeId | CompatibilityType ColumnType | NullType deriving (Show, Eq)
data QueryType = RuntimeType TypeId | CompatibilityType ColumnType
| DomainType String QueryType | NullType deriving (Show, Eq)
-- | 保存仅由 schema 绑定的执行计划
data BoundQuery = BoundQuery Database [(String, Column)] [(String, QueryType)] RelOp
data BoundQuery = BoundQuery Database [String] [(String, Column)] [(String, QueryType)] RelOp
-- | 查询绑定计划的输出类型身份
queryTypes :: BoundQuery -> [(String, QueryType)]
queryTypes (BoundQuery _ _ types _) = types
queryTypes (BoundQuery _ _ _ types _) = types
-- | 绑定 SQL 查询并优化确定的计划
bindQuery :: Database -> Statement -> Either String BoundQuery
@@ -33,7 +36,10 @@ bindQuery catalog statement = do
columns <- querySchema schemaOnly resolved
plan <- translate resolved
types <- mapM typeOf columns
Right (BoundQuery schemaOnly columns types (optimize schemaOnly plan))
bound <- bindRelOpColumns schemaOnly [] (optimize schemaOnly plan)
let sources = queryTables resolved
dependencies = sources ++ domainDependencies schemaOnly sources
Right (BoundQuery schemaOnly dependencies columns types bound)
where
-- | 解析输出列的运行时身份
typeOf (name, column) = do
@@ -41,19 +47,32 @@ bindQuery catalog statement = do
Right (name, ty)
-- | 保留阶段外类型的兼容表示
bindingType CNull = Right NullType
bindingType (CDomain name base) = DomainType name <$> bindingType base
bindingType CDate = Right (CompatibilityType CDate)
bindingType CTimestamp = Right (CompatibilityType CTimestamp)
bindingType CBlob = Right (CompatibilityType CBlob)
bindingType ty@(CDomain _ base)
| typeClassOf base == TemporalClass || base == CBlob = Right (CompatibilityType ty)
bindingType ty = RuntimeType <$> sqlType ty
-- | 执行绑定查询并校验输出类型
executeQueryM :: MonadStorage m => BoundQuery -> m (Either String [Row])
executeQueryM (BoundQuery catalog columns _ plan) = do
result <- evalRelOpM catalog plan
pure (result >>= mapM validateRow)
executeQueryM (BoundQuery catalog dependencies columns _ plan) = do
current <- schema
case current >>= validateSchema of
Left err -> pure (Left err)
Right () -> do
result <- withSchemaGuard [(name, table) | (name, table) <- catalog, name `elem` dependencies]
(evalRelOpM catalog plan)
pure (result >>= mapM validateRow)
where
-- | 拒绝绑定目录中的结构变化
validateSchema current = mapM_ (validateTable current)
[(name, table) | (name, table) <- catalog, name `elem` dependencies]
-- | 比较列定义而不比较数据或统计
validateTable current (name, expected) = case lookup name current of
Just actual | tableIdentity actual /= tableIdentity expected -> Left ("bound query object changed: " ++ name)
Just actual | tableSchemaVersion actual /= tableSchemaVersion expected -> Left ("bound query schema changed: " ++ name)
Just actual | tableCols actual == tableCols expected -> Right ()
_ -> Left ("bound query schema changed: " ++ name)
-- | 校验完整输出行而不重新解析函数
validateRow row
| map fst row /= map fst columns = Left "bound query output schema mismatch"
@@ -0,0 +1,100 @@
module ChuSQL.Core.Engine.Runtime.Relation (bindRelOpColumns) where
import ChuSQL.Core.Engine.Algebra.Op
import ChuSQL.Core.Engine.Syntax.AST
import ChuSQL.Core.Model (Database)
-- 优化完成后按算子输入布局绑定列槽位,外层行作为独立尾部布局。
-- | 绑定关系算子的输入列位置
bindRelOpColumns :: Database -> [String] -> RelOp -> Either String RelOp
bindRelOpColumns db outer = bind
where
-- \| 递归绑定每个算子的输入布局
bind op = case op of
Filter expression child -> Filter <$> expressionIn child expression <*> bind child
Compute items child -> Compute <$> mapM (itemIn child) items <*> bind child
Aggregate keys items child ->
BoundAggregate
<$> mapM (itemIn child) [(name, Col name) | name <- keys]
<*> mapM (itemIn child) items
<*> bind child
BoundAggregate keys items child ->
BoundAggregate
<$> mapM (itemIn child) keys
<*> mapM (itemIn child) items
<*> bind child
Join kind left right condition ->
Join kind
<$> bind left
<*> bind right
<*> bindSlots (relOpCols db left ++ relOpCols db right) outer condition
Project columns child -> do
let names = if columns == ["*"] then relOpCols db child else columns
items <- mapM (itemIn child) [(name, Col name) | name <- names]
Compute items <$> bind child
Distinct child -> Distinct <$> bind child
Sort ordering child ->
BoundSort
<$> mapM (sortIn child) [(Col name, direction) | (name, direction) <- ordering]
<*> bind child
BoundSort ordering child -> BoundSort <$> mapM (sortIn child) ordering <*> bind child
Limit count child -> Limit count <$> bind child
Derived alias child -> Derived alias <$> bind child
_ -> Right op
-- \| 使用子计划输出布局绑定表达式
expressionIn child = bindSlots (relOpCols db child) outer
-- \| 保留输出标签并绑定计算项
itemIn child (name, expression) = (,) name <$> expressionIn child expression
-- \| 绑定排序键并保留方向
sortIn child (expression, direction) = (,) <$> expressionIn child expression <*> pure direction
-- | 绑定当前表达式的列引用
bindSlots :: [String] -> [String] -> Expr -> Either String Expr
bindSlots columns outer expression = case expression of
Col name -> case slots columns name of
slot : _ -> Right (ColumnSlot slot name)
[] -> outerReference name
ColumnSlot _ name -> case slots columns name of
slot : _ -> Right (ColumnSlot slot name)
[] -> Left ("unknown local column: " ++ name)
OuterColumnSlot _ _ name -> outerReference name
FunctionCall name arguments -> FunctionCall name <$> mapM bind arguments
BoundFunction fid tid arguments -> BoundFunction fid tid <$> mapM bind arguments
Construct ty arguments -> Construct ty <$> mapM bind arguments
BoundConstruct tid arguments -> BoundConstruct tid <$> mapM bind arguments
Add a b -> binary Add a b
Sub a b -> binary Sub a b
Mul a b -> binary Mul a b
Div a b -> binary Div a b
Neg a -> Neg <$> bind a
Gt a b -> binary Gt a b
Lt a b -> binary Lt a b
Eq a b -> binary Eq a b
GtE a b -> binary GtE a b
LtE a b -> binary LtE a b
NotEq a b -> binary NotEq a b
And a b -> binary And a b
Or a b -> binary Or a b
IsNull a -> IsNull <$> bind a
IsNotNull a -> IsNotNull <$> bind a
CountOf a -> CountOf <$> bind a
SumOf a -> SumOf <$> bind a
AvgOf a -> AvgOf <$> bind a
MinOf a -> MinOf <$> bind a
MaxOf a -> MaxOf <$> bind a
InSub a query negated -> InSub <$> bind a <*> pure query <*> pure negated
InList a values negated -> InList <$> bind a <*> mapM bind values <*> pure negated
QuantCmp op a query quantifier -> QuantCmp op <$> bind a <*> pure query <*> pure quantifier
_ -> Right expression
where
-- \| 绑定当前布局的子表达式
bind = bindSlots columns outer
-- \| 查找布局中首次出现的列
slots names name = [slot | (slot, key) <- zip [0 ..] names, key == name]
-- \| 在外层布局绑定并保留作用域
outerReference name = case slots outer name of
slot : _ -> Right (OuterColumnSlot (length columns) slot name)
[] -> Left ("unknown column: " ++ name)
-- \| 绑定两个操作数
binary constructor a b = constructor <$> bind a <*> bind b
@@ -1,7 +1,7 @@
module ChuSQL.Core.Engine.Runtime.SQL (resolveSQLFunction, sqlResultType, invokeSQLFunction, sqlType, constructSQLValue) where
import ChuSQL.Core.Engine.Runtime.Types
import ChuSQL.Core.Engine.Runtime.Functions
import ChuSQL.Core.Engine.Runtime.Types
import ChuSQL.Core.Model (ColumnType (..), Value (..))
-- SQL 现有标量表示与已解析运行时函数之间的类型边界。
@@ -46,16 +46,21 @@ invokeSQLFunction :: FunctionId -> TypeId -> [Value] -> Either String Value
invokeSQLFunction fid expected values = do
functions <- builtinFunctions builtinTypes
sig <- functionSignature functions fid
if signatureResult sig /= expected then Left "bound function result identity mismatch"
else if length values /= length (signatureArguments sig) then Left "function argument count mismatch"
else do
sequence_ (zipWith validateArgument (signatureArguments sig) values)
if VNull `elem` values then Right VNull else do
arguments <- mapM runtimeValue values
result <- invokeFunction builtinTypes functions fid arguments
sqlValue result
if signatureResult sig /= expected
then Left "bound function result identity mismatch"
else
if length values /= length (signatureArguments sig)
then Left "function argument count mismatch"
else do
sequence_ (zipWith validateArgument (signatureArguments sig) values)
if VNull `elem` values
then Right VNull
else do
arguments <- mapM runtimeValue values
result <- invokeFunction builtinTypes functions fid arguments
sqlValue result
where
-- | 校验具体参数并保留 SQL NULL
-- \| 校验具体参数并保留 SQL NULL
validateArgument _ VNull = Right ()
validateArgument tid value = runtimeValue value >>= validateValue builtinTypes tid
@@ -1,12 +1,12 @@
module ChuSQL.Core.Engine.Semantic (check, prepare, prepareRowExpression, querySchema) where
import ChuSQL.Core.Engine.Algebra.Expr (bareColumns, colsInExpr, hasAggregate)
import ChuSQL.Core.Model
import ChuSQL.Core.Engine.Syntax.AST
import ChuSQL.Core.Engine.Syntax.Parser (parseExpression)
import ChuSQL.Core.Engine.Runtime.Functions (FunctionSignature (..), builtinFunctions, functionSignature)
import ChuSQL.Core.Engine.Runtime.SQL (resolveSQLFunction, sqlResultType, sqlType)
import ChuSQL.Core.Engine.Runtime.Types
import ChuSQL.Core.Engine.Syntax.AST
import ChuSQL.Core.Engine.Syntax.Parser (parseExpression)
import ChuSQL.Core.Model
import Data.Char (isAlphaNum, isAscii, toLower)
import Data.List (intercalate, isInfixOf, isPrefixOf, isSuffixOf, nub)
@@ -115,7 +115,7 @@ checkGrouping env keys items
mapM_ (checkRuntimeColumn "GROUP BY" env) keys
mapM_ okCol (nub (concatMap (bareColumns . snd) items))
where
-- | 这一列能不能出现在分组查询里
-- \| 这一列能不能出现在分组查询里
okCol c
| c == allColumns = Left "SELECT: * cannot be selected together with GROUP BY"
| c `elem` keys = Right ()
@@ -129,6 +129,8 @@ renderType (InferType t) = typeLabel t
-- | 推导表达式的类型(子查询也在这里整条查一遍)
inferExpr :: Database -> String -> Scope -> Expr -> Either String InferredType
inferExpr _ place env (Col c) = inferredColumnType . columnType . snd <$> scopeAt place env c
inferExpr _ _ _ ColumnSlot{} = Left "execution column cannot be used in semantic input"
inferExpr _ _ _ OuterColumnSlot{} = Left "execution column cannot be used in semantic input"
inferExpr _ _ _ LitNull = Right InferNull
inferExpr _ _ _ (LitInt _) = Right (InferType CInt)
inferExpr _ _ _ (LitFloat _) = Right (InferType CFloat)
@@ -231,18 +233,23 @@ itemType place want got
-- | 推导子查询的输出列与类型
inferSubqueryOutput :: Database -> Scope -> Subquery -> Either String [(String, InferredType)]
inferSubqueryOutput db outerScope sq = case subqueryStatement sq of
q@Select { selectCols = cols } -> do
SelectDistinct q -> do
columns <- inferSubqueryOutput db outerScope sq{subqueryStatement = q}
mapM_ (requireOrdering "DISTINCT" . snd) columns
_ <- distinctOrdering q
Right columns
q@Select{selectCols = cols} -> do
env <- checkFrom db enclosing (selectFrom q)
checkResolvedWith db enclosing q
mapM (columnTypeOf env) cols
q@SelectExpr { selectItems = items } -> do
q@SelectExpr{selectItems = items} -> do
env <- checkFrom db enclosing (selectFrom q)
checkResolvedWith db enclosing q
mapM (\(l, e) -> (\t -> (l, t)) <$> inferExpr db "SELECT" (Scope env enclosing) e) items
_ -> Left "subquery must be a SELECT"
where
enclosing = scopeFlat outerScope
-- | 查这一列的推导类型
-- \| 查这一列的推导类型
columnTypeOf env c = do
(_, col) <- scopeAt "SELECT" (Scope env enclosing) c
Right (c, inferredColumnType (columnType col))
@@ -278,7 +285,7 @@ arithmetic db place env a b = do
mapM_ (numericOperand place (InferType want) (InferType want)) [ta, tb]
Right (arithResult ta tb)
where
-- | 是不是浮点类型
-- \| 是不是浮点类型
isFloatSide (InferType x) = numericType x && not (integerType x)
isFloatSide _ = False
@@ -312,7 +319,7 @@ boolean db place env a b = do
mapM_ (boolOperand place ta) [ta, tb]
Right (InferType CBool)
where
-- | 挨个查操作数是不是布尔
-- \| 挨个查操作数是不是布尔
boolOperand _ _ InferNull = Right ()
boolOperand _ _ (InferType ty) | typeClassOf ty == BooleanClass = Right ()
boolOperand p other t = Left (p ++ ": operator needs TBool on both sides, got " ++ renderType other ++ " and " ++ renderType t)
@@ -419,13 +426,13 @@ resolveDomainTypes db statement = case statement of
AlterColumnType name col ty -> AlterColumnType name col <$> resolveDomainType ty
_ -> Right statement
where
-- | 解析一列的类型
-- \| 解析一列的类型
resolveColumnType (name, col) = do
ty <- resolveDomainType (columnType col)
Right (name, col {columnType = ty})
-- | 从类型目录取基础类型
Right (name, col{columnType = ty})
-- \| 从类型目录取基础类型
resolveDomainType = resolveWith []
-- | 递归解析类型并拒绝循环定义
-- \| 递归解析类型并拒绝循环定义
resolveWith seen (CDomain name _)
| map toLower name `elem` seen = Left ("cyclic domain definition: " ++ name)
| otherwise = do
@@ -442,6 +449,7 @@ checkResolved db = checkResolvedWith db []
-- | 检查已解析的语句;outer 是相关子查询能看见的外层列
checkResolvedWith :: Database -> [(String, Column)] -> Statement -> Either String ()
checkResolvedWith db outer q = case q of
SelectDistinct inner -> () <$ inferSubqueryOutput db (Scope [] outer) (Subquery (SelectDistinct inner) [])
Select
{ selectCols = cols
, selectFrom = fromC
@@ -552,6 +560,7 @@ checkResolvedWith db outer q = case q of
if length (nub (map fst attributes)) == length attributes
then Right ()
else Left "duplicate identity attribute"
ShowGrants -> Right ()
ShowRoles -> Right ()
DropUser name -> checkUserName name
CreateRole name -> checkRoleName name
@@ -560,6 +569,7 @@ checkResolvedWith db outer q = case q of
mapM_ checkPrivilege privs
checkGrantObject obj
checkRoleName role
RevokeGrantOption privs obj role -> checkResolvedWith db outer (RevokePrivileges privs obj role)
RevokePrivileges privs obj role -> do
mapM_ checkPrivilege privs
checkGrantObject obj
@@ -577,11 +587,11 @@ checkResolvedWith db outer q = case q of
CreateDomain{} -> Right ()
DropDomain{} -> Right ()
ShowDomains -> Right ()
-- | 事务控制由会话层执行,语义层不做列检查
-- \| 事务控制由会话层执行,语义层不做列检查
BeginTransaction -> Right ()
CommitTransaction -> Right ()
RollbackTransaction -> Right ()
-- | 保存点名字只是会话层的记号,不做列检查
-- \| 保存点名字只是会话层的记号,不做列检查
Savepoint _ -> Right ()
RollbackToSavepoint _ -> Right ()
ReleaseSavepoint _ -> Right ()
@@ -615,10 +625,11 @@ checkColumnDefs names cols = do
else Right ()
checkChecks names cols
where
-- | 拒绝缺少能力实现的运行时键
-- \| 拒绝缺少能力实现的运行时键
checkRuntimeKey (_, column)
| typeClassOf (columnType column) == RuntimeClass
, columnUnique column || columnPrimaryKey column = Left "runtime type key capability is not registered"
, columnUnique column || columnPrimaryKey column =
Left "runtime type key capability is not registered"
| otherwise = Right ()
-- | 校验列类型的长度和精度参数
@@ -649,13 +660,13 @@ checkDefault c v
checkChecks :: [String] -> [(String, Column)] -> Either String ()
checkChecks names = mapM_ checkOne
where
-- | 查这一列的 CHECK
-- \| 查这一列的 CHECK
checkOne (_, c) = case columnCheck c of
Nothing -> Right ()
Just text -> case parseExpression text of
Left err -> Left ("CHECK: " ++ firstLine err)
Right e -> mapM_ (known e) (colsInExpr e)
-- | 这个列名在不在定义里
-- \| 这个列名在不在定义里
known _ col
| col `elem` names = Right ()
| otherwise = Left ("CHECK: unknown column: " ++ col)
@@ -683,7 +694,7 @@ checkUserName name
| not (all allowed name) = Left "USER: account name may only contain ASCII letters, digits, dot, dash or underscore"
| otherwise = Right ()
where
-- | 账号名字符规则
-- \| 账号名字符规则
allowed c = isAscii c && (isAlphaNum c || c `elem` ("_.-" :: String))
-- | 建号 / 改口令:名字合法且口令不空
@@ -706,7 +717,7 @@ checkRoleName name
| map toLower name `elem` privilegeNames = Left "ROLE: role name must not be a privilege keyword"
| otherwise = Right ()
where
-- | 角色名字符规则
-- \| 角色名字符规则
allowed c = isAscii c && (isAlphaNum c || c `elem` ("_.-" :: String))
-- | 权限关键字(小写,只用于比较)
@@ -728,12 +739,13 @@ checkGrantObject obj
| not (all allowed obj) = Left "GRANT: object name may only contain ASCII letters, digits, dot, dash or underscore"
| otherwise = Right ()
where
-- | 授权对象字符规则
-- \| 授权对象字符规则
allowed c = isAscii c && (isAlphaNum c || c `elem` ("_.-" :: String))
-- | 内置 id 列名
idColumn :: String
idColumn = "id"
-- | 把列引用统一为执行期名字(子查询一起解析)
resolveExpr ::
Database ->
@@ -783,7 +795,7 @@ resolveExpr db env outer f e = case e of
QuantCmp op a sq q -> QuantCmp op <$> resolveExpr db env outer f a <*> resolveSubquery db (env ++ outer) sq <*> pure q
_ -> Right e
where
-- | 递归处理两个子表达式
-- \| 递归处理两个子表达式
binary ctor a b = ctor <$> resolveExpr db env outer f a <*> resolveExpr db env outer f b
-- | 校验具体构造器和参数逻辑类型
@@ -800,7 +812,7 @@ inferConstruction db place env ty@(TypeApply cid parameters) arguments = do
required <- mapM (fmap descriptorId . resolveType builtinTypes) expected
if supplied == required then Right (descriptorId descriptor) else Left "composite constructor argument type mismatch"
where
-- | 解析非空参数的逻辑类型身份
-- \| 解析非空参数的逻辑类型身份
argumentIdentity InferNull = Left "SQL NULL needs Maybe construction inside a runtime value"
argumentIdentity (InferType column) = sqlType column
@@ -829,7 +841,7 @@ outerRefsOf db stmt = nub [c | c <- statementColumns stmt, not (inInner c)]
inner = case checkFrom db [] (statementFrom stmt) of
Right env -> env
Left _ -> []
-- | 这一列能不能在内层解析到
-- \| 这一列能不能在内层解析到
inInner c = case resolveColumn c inner of
Right _ -> True
Left _ -> False
@@ -837,13 +849,14 @@ outerRefsOf db stmt = nub [c | c <- statementColumns stmt, not (inInner c)]
-- | 一条语句里出现的所有列引用
statementColumns :: Statement -> [String]
statementColumns stmt = case stmt of
SelectDistinct q -> statementColumns q
Select cols fromC mWhere groupBy orderBy _ ->
cols ++ fromColumns fromC ++ concatMap colsInExpr mWhere ++ groupBy ++ map fst orderBy
SelectExpr items fromC mWhere groupBy orderBy _ ->
concatMap (colsInExpr . snd) items ++ fromColumns fromC ++ concatMap colsInExpr mWhere ++ groupBy ++ map fst orderBy
_ -> []
where
-- | 取 FROM 里出现的列
-- \| 取 FROM 里出现的列
fromColumns FromUnit = []
fromColumns (FromTable _ _) = []
fromColumns (FromSubquery _ _) = []
@@ -851,12 +864,14 @@ statementColumns stmt = case stmt of
-- | 取语句的 FROM 子句,没有就是 FromUnit
statementFrom :: Statement -> FromClause
statementFrom Select { selectFrom = fromC } = fromC
statementFrom SelectExpr { selectFrom = fromC } = fromC
statementFrom (SelectDistinct q) = statementFrom q
statementFrom Select{selectFrom = fromC} = fromC
statementFrom SelectExpr{selectFrom = fromC} = fromC
statementFrom _ = FromUnit
-- | 解析子查询里的 SELECT
resolveSubqueryStatement :: Database -> [(String, Column)] -> Statement -> Either String Statement
resolveSubqueryStatement db outer (SelectDistinct q) = SelectDistinct <$> resolveSubqueryStatement db outer q
resolveSubqueryStatement db outer q@Select{} = resolveQuery db outer q (map (\c -> (c, Col c)) (selectCols q))
resolveSubqueryStatement db outer q@SelectExpr{} = resolveQuery db outer q (selectItems q)
resolveSubqueryStatement _ _ _ = Left "subquery must be a SELECT"
@@ -864,6 +879,7 @@ resolveSubqueryStatement _ _ _ = Left "subquery must be a SELECT"
-- | 列名解析入口:查询走连接解析,改数据的语句按本表列解析
resolveStatement :: Database -> Statement -> Either String Statement
resolveStatement db q = case q of
SelectDistinct inner -> SelectDistinct <$> resolveStatement db inner
Select{} -> resolveSelect db q
SelectExpr{} -> resolveSelect db q
Update tbl assigns mWhere -> do
@@ -923,37 +939,45 @@ resolveQuery :: Database -> [(String, Column)] -> Statement -> [(String, Expr)]
resolveQuery db outer q items = do
source <- resolveSource db outer (selectFrom q)
env <- checkFrom db outer source
-- | 单表无别名时把裸列名换成物理列名;带别名时表头保留别名限定名
-- \| 单表无别名时把裸列名换成物理列名;带别名时表头保留别名限定名
let physical c = case selectFrom q of
FromTable Nothing tbl -> unqualify (Just (deriveQualifier Nothing tbl)) c
_ -> c
-- | 查一列并换算成物理名
-- \| 查一列并换算成物理名
name place c
| length (filter (== '.') c) == 2, not (qualifiedReference c) = Left (missingColumn place c env)
| otherwise = physical . fst <$> scopedColumnAt place env outer c
-- | 库限定列只引用来源中的真实表名
qualifiedReference c = qualifiedReferenceIn db (selectFrom q) c
|| (any (\(key, _) -> length (filter (== '.') key) == 1 && ("." ++ key) `isSuffixOf` c) outer
&& (not (any (elem '.' . fst) db) || any (\(table, _) -> (table ++ ".") `isPrefixOf` c) db))
-- | 换算一个投影项:带别名时保留别名做输出列名
-- \| 库限定列只引用来源中的真实表名
qualifiedReference c =
qualifiedReferenceIn db (selectFrom q) c
|| ( any (\(key, _) -> length (filter (== '.') key) == 1 && ("." ++ key) `isSuffixOf` c) outer
&& (not (any (elem '.' . fst) db) || any (\(table, _) -> (table ++ ".") `isPrefixOf` c) db)
)
-- \| 换算一个投影项:带别名时保留别名做输出列名
output (label, Col c) = do
k <- name "SELECT" c
pure (if label == c then (k, Col k) else (label, Col k))
output (label, e) = do
e' <- resolveExpr db env outer (name "SELECT") e
pure (label, e')
projected <- if items == [("*", Col "*")]
then if null env && selectFrom q == FromUnit
then Left "SELECT: * requires FROM"
else Right [(physical c, Col (physical c)) | (c, _) <- env]
else mapM output items
projected <-
if items == [("*", Col "*")]
then
if null env && selectFrom q == FromUnit
then Left "SELECT: * requires FROM"
else Right [(physical c, Col (physical c)) | (c, _) <- env]
else mapM output items
condition <- mapM (resolveExpr db env outer (name "WHERE")) (selectWhere q)
grouping <- mapM (name "GROUP BY") (selectGroupBy q)
-- | 优先解析唯一投影别名,再查来源列
let orderName c = case [label | (label, _) <- projected, label == c, qIsExpression] of
[] -> name "ORDER BY" c
[label] -> Right label
_ -> Left ("ORDER BY: ambiguous projection alias: " ++ c)
-- \| 优先解析唯一投影别名,再查来源列
let orderName c
| "$position" `isPrefixOf` c = case reads (drop 9 c) of
[(n, "")] | n > 0, n <= length projected -> Right (fst (projected !! (n - 1)))
_ -> Left "ORDER BY: position is outside the select list"
| otherwise = case [label | (label, _) <- projected, label == c, qIsExpression] of
[] -> name "ORDER BY" c
[label] -> Right label
_ -> Left ("ORDER BY: ambiguous projection alias: " ++ c)
qIsExpression = case q of SelectExpr{} -> True; _ -> False
ordering <- mapM (\(c, d) -> (\k -> (k, d)) <$> orderName c) (selectOrderBy q)
pure $ case q of
@@ -994,7 +1018,7 @@ resolveFrom db outer (FromJoin kind left right cond) = do
l <- resolveFrom db outer left
r <- resolveFrom db outer right
env <- checkFrom db outer (FromJoin kind l r cond)
-- | 连接条件也拒绝不存在的库前缀
-- \| 连接条件也拒绝不存在的库前缀
let joinName column
| length (filter (== '.') column) == 2, not (qualifiedReferenceIn db (FromJoin kind left right cond) column) = Left (missingColumn "ON" column env)
| otherwise = fst <$> columnAt "ON" env column
@@ -1003,7 +1027,8 @@ resolveFrom db outer (FromJoin kind left right cond) = do
-- | 派生表的输出列名:列取裸名,表达式保留原标签
nameDerivedColumns :: Statement -> Either String Statement
nameDerivedColumns q@Select { selectCols = cols, selectFrom = fromC } = do
nameDerivedColumns (SelectDistinct q) = SelectDistinct <$> nameDerivedColumns q
nameDerivedColumns q@Select{selectCols = cols, selectFrom = fromC} = do
let names = map lastTablePart cols
checkDerivedNames names
Right
@@ -1015,10 +1040,10 @@ nameDerivedColumns q@Select { selectCols = cols, selectFrom = fromC } = do
(selectOrderBy q)
(selectLimit q)
)
nameDerivedColumns q@SelectExpr { selectItems = items } = do
nameDerivedColumns q@SelectExpr{selectItems = items} = do
let names = map derivedItemName items
checkDerivedNames names
Right q { selectItems = zip names (map snd items) }
Right q{selectItems = zip names (map snd items)}
nameDerivedColumns _ = Left "FROM: subquery must be a SELECT"
-- | 派生表里一个投影项的输出名:没写别名的列取裸名
@@ -1032,7 +1057,7 @@ derivedItemName (label, _) = label
checkDerivedNames :: [String] -> Either String ()
checkDerivedNames = go []
where
-- | 逐个查有没有见过
-- \| 逐个查有没有见过
go _ [] = Right ()
go seen (n : ns)
| n `elem` seen = Left ("FROM: duplicate column name in subquery: " ++ n)
@@ -13,15 +13,22 @@ data IndexResult
deriving (Show, Eq)
class (Monad m) => MonadStorage m where
-- \| 在稳定存储视图中执行绑定读取
withSchemaGuard :: Database -> m (Either String a) -> m (Either String a)
withSchemaGuard _ _ = pure (Left "bound schema guards are unavailable in this storage")
-- \| 建一个库
createDatabase :: String -> m (Either String ())
createDatabase _ = pure (Left "database management is unavailable in this storage")
-- \| 删一个库
dropDatabase :: String -> m (Either String ())
dropDatabase _ = pure (Left "database management is unavailable in this storage")
-- \| 切当前库
useDatabase :: String -> m (Either String ())
useDatabase _ = pure (Left "database selection is unavailable in this storage")
-- \| 列出现有库
listDatabases :: m (Either String [String])
listDatabases = pure (Right [])
@@ -50,7 +57,7 @@ class (Monad m) => MonadStorage m where
insertMany :: String -> [Row] -> m (Either String ())
insertMany t = go
where
-- | 逐行插入,出错就停
-- \| 逐行插入,出错就停
go [] = pure (Right ())
go (r : rs) = do
x <- insert t r
@@ -66,7 +73,7 @@ class (Monad m) => MonadStorage m where
Left e -> pure (Left e)
Right rs -> replaceAll t [r | r <- rs, not (doomed r)]
where
-- | 这一行要不要删
-- \| 这一行要不要删
doomed r = case lookup "id" r of
Just (VInt k) -> k `elem` ks
_ -> False
@@ -114,5 +121,5 @@ class (Monad m) => MonadStorage m where
schema :: m (Either String Database)
schema = fmap (fmap (map withoutRows)) snapshot
where
-- | 表定义去掉所有行
withoutRows (name, t) = (name, t {tableRows = []})
-- \| 表定义去掉所有行
withoutRows (name, t) = (name, t{tableRows = []})
@@ -65,7 +65,7 @@ openStorage path = onStorageThread $ mask_ $ do
| ptr == nullPtr -> Left <$> lastError
| otherwise -> pure (Right (StorageHandle ptr))
where
-- | 没有路径就传空指针,否则转成 C 字符串
-- \| 没有路径就传空指针,否则转成 C 字符串
withCStringOrNull Nothing f = f nullPtr
withCStringOrNull (Just s) f = BS.useAsCString (TE.encodeUtf8 (T.pack s)) f
@@ -78,18 +78,24 @@ storageRequest :: StorageHandle -> BS.ByteString -> IO (Either String BS.ByteStr
storageRequest (StorageHandle handle) = callStorage (cStorageRequest handle)
-- | 调用存储并复制和归还响应缓冲
callStorage :: (Ptr CChar -> CSize -> Ptr (Ptr CChar) -> Ptr CSize -> IO CInt)
-> BS.ByteString -> IO (Either String BS.ByteString)
callStorage call payload = onStorageThread $ mask_ $
BS.useAsCStringLen payload $ \(input, size) ->
alloca $ \outRef -> alloca $ \lenRef -> do
code <- call input (fromIntegral size) outRef lenRef
if code /= 0 then Left <$> lastError else do
output <- peek outRef
len <- peek lenRef
bytes <- BS.packCStringLen (output, fromIntegral len)
`finally` cStorageFree output len
pure (Right bytes)
callStorage ::
(Ptr CChar -> CSize -> Ptr (Ptr CChar) -> Ptr CSize -> IO CInt) ->
BS.ByteString ->
IO (Either String BS.ByteString)
callStorage call payload = onStorageThread $
mask_ $
BS.useAsCStringLen payload $ \(input, size) ->
alloca $ \outRef -> alloca $ \lenRef -> do
code <- call input (fromIntegral size) outRef lenRef
if code /= 0
then Left <$> lastError
else do
output <- peek outRef
len <- peek lenRef
bytes <-
BS.packCStringLen (output, fromIntegral len)
`finally` cStorageFree output len
pure (Right bytes)
-- | 多线程运行时绑定存储调用线程
onStorageThread :: IO a -> IO a
@@ -30,15 +30,17 @@ module ChuSQL.Core.Engine.Storage.IPC (
doListCatalog,
) where
import ChuSQL.Core.Model
import ChuSQL.Core.Protocol (Account (..), Request (..), Response (..), SchemaColumn (..), TableInfo (..))
import ChuSQL.Core.Engine.Parallel (parallelShardLimit, poolRun, workerPool)
import ChuSQL.Core.Engine.Storage
import ChuSQL.Core.Engine.Storage.FFI (closeStorage, openStorage, storageRequest)
import ChuSQL.Core.Model
import ChuSQL.Core.Protocol (Account (..), Request (..), Response (..), SchemaColumn (..), TableInfo (..))
import Control.Concurrent (getNumCapabilities)
import Control.Concurrent.MVar (MVar, modifyMVar_, newMVar, putMVar, readMVar, takeMVar, withMVar)
import Control.Exception (bracket, mask_)
import Data.Aeson (eitherDecodeStrict, encode)
import Data.Aeson qualified as A
import Data.Aeson.KeyMap qualified as KM
import Data.ByteString qualified as BS
import Data.ByteString.Lazy qualified as BL
import Data.Maybe (fromMaybe)
@@ -49,6 +51,7 @@ import System.IO.Unsafe (unsafePerformIO)
-- | 一条会话的存储环境:当前库名随会话走,不共享
data Env = Env
{ envDatabase :: Maybe String
, envSchemaGuard :: Maybe Database
}
newtype IPCStorage a = IPCStorage
@@ -57,11 +60,11 @@ newtype IPCStorage a = IPCStorage
-- | 按库名造一个会话环境
envForDatabase :: Maybe String -> Env
envForDatabase = Env
envForDatabase name = Env name Nothing
-- | 进程默认环境:读全局库名,给测试与 CLI 用
defaultEnv :: IO Env
defaultEnv = Env <$> readMVar databaseRef
defaultEnv = envForDatabase <$> readMVar databaseRef
-- | 用进程默认环境跑(兼容既有调用点)
runIPCStorage :: IPCStorage a -> IO a
@@ -86,11 +89,13 @@ data StorageLink = StorageLink
}
{-# NOINLINE linkRef #-}
-- | 进程全局的存储链路
linkRef :: MVar (Maybe StorageLink)
linkRef = unsafePerformIO (newMVar Nothing)
{-# NOINLINE linkUsers #-}
-- | 在途请求计数与空闲信号
linkUsers :: (MVar Int, MVar ())
linkUsers = unsafePerformIO $ (,) <$> newMVar 0 <*> newMVar ()
@@ -100,7 +105,7 @@ withStorageLink :: (Maybe StorageLink -> IO a) -> IO a
withStorageLink = bracket enter leave
where
(users, idle) = linkUsers
-- | 登记当前链路的在途请求
-- \| 登记当前链路的在途请求
enter = withMVar linkRef $ \current -> do
case current of
Nothing -> pure ()
@@ -108,7 +113,7 @@ withStorageLink = bracket enter leave
if count == 0 then takeMVar idle else pure ()
pure (count + 1)
pure current
-- | 最后一个请求结束后发出空闲信号
-- \| 最后一个请求结束后发出空闲信号
leave Nothing = pure ()
leave (Just _) = mask_ $ modifyMVar_ users $ \count -> do
if count == 1 then putMVar idle () else pure ()
@@ -122,7 +127,7 @@ setStorageLink link = modifyMVar_ linkRef $ \_ -> withMVar (snd linkUsers) (cons
localStorageLink :: Maybe FilePath -> IO (Either String StorageLink)
localStorageLink path = fmap (fmap toLink) (openStorage path)
where
-- | 把句柄包成一条链路
-- \| 把句柄包成一条链路
toLink handle =
StorageLink
{ slRequest = storageRequest handle
@@ -157,6 +162,7 @@ sendRawRequest payload = withStorageLink $ \current -> do
Just link -> slRequest link payload
{-# NOINLINE databaseRef #-}
-- | 进程全局的当前库名
databaseRef :: MVar (Maybe String)
databaseRef = unsafePerformIO (newMVar Nothing)
@@ -169,7 +175,6 @@ setDatabaseName name = modifyMVar_ databaseRef (const (pure name))
getDatabaseName :: IO (Maybe String)
getDatabaseName = readMVar databaseRef
-- | 给语句类请求盖上当前库名
stampDatabase :: Maybe String -> Request -> Request
stampDatabase Nothing req = req
@@ -192,13 +197,43 @@ globalRequest _ = False
-- | 发请求并把响应翻成 Either
ask :: Env -> String -> Request -> (Response -> Maybe a) -> IO (Either String a)
ask env what req recognize = do
resp <- sendRequest (stampDatabase (envDatabase env) req)
resp <- case envSchemaGuard env of
Nothing -> sendRequest (stampDatabase (envDatabase env) req)
Just dependencies -> guardedRequest env dependencies req
pure $ case resp of
RespError e -> Left e
other -> case recognize other of
Just a -> Right a
Nothing -> Left ("unexpected response to " ++ what)
-- | 发送依赖守卫并要求存储确认执行
guardedRequest :: Env -> Database -> Request -> IO Response
guardedRequest env dependencies req = do
let payload = case A.toJSON (stampDatabase (envDatabase env) req) of
A.Object fields -> A.Object (KM.insert "schema_guard" (A.toJSON (map guardOf dependencies)) fields)
other -> other
result <- sendRawRequest (BL.toStrict (encode payload))
pure $ case result >>= eitherDecodeStrict of
Left err -> RespError err
Right (A.Object fields)
| KM.lookup "status" fields == Just (A.String "guarded")
, Just response <- KM.lookup "response" fields -> case A.fromJSON response of
A.Success decoded -> decoded
A.Error err -> RespError ("decode error: " ++ err)
Right value -> case A.fromJSON value of
A.Success (RespError err) -> RespError err
_ -> RespError "storage did not confirm bound schema guard"
where
-- \| 编码依赖身份、版本与完整列定义
guardOf (name, table) =
A.object
[ "table" A..= name
, "database_id" A..= fmap (\(TableIdentity database _) -> show database) (tableIdentity table)
, "object_id" A..= fmap (\(TableIdentity _ object) -> show object) (tableIdentity table)
, "schema_version" A..= fmap show (tableSchemaVersion table)
, "columns" A..= map toWire (tableCols table)
]
-- | 只认"成了"(多数写操作都这样)
okOnly :: Response -> Maybe ()
okOnly RespOk = Just ()
@@ -310,18 +345,27 @@ toWire (n, c) =
}
instance MonadStorage IPCStorage where
-- \| 将依赖守卫限定于本次执行环境
withSchemaGuard dependencies action = IPCStorage $ \env ->
runIPCStorageIn action env{envSchemaGuard = Just dependencies}
-- \| 建一个库
createDatabase name = IPCStorage $ \env -> ask env "create_database" (ReqDatabase "create_database" name) okOnly
-- \| 删一个库
dropDatabase name = IPCStorage $ \env -> ask env "drop_database" (ReqDatabase "drop_database" name) okOnly
-- \| 切当前库
useDatabase name = IPCStorage $ \env -> ask env "use_database" (ReqDatabase "use_database" name) okOnly
-- \| 列出现有库
listDatabases = IPCStorage $ \env -> ask env "list_databases" (ReqDatabase "list_databases" "") $ \resp -> case resp of
RespTables names -> Just names
_ -> Nothing
-- \| 发 Scan
scan t = IPCStorage (\env -> doScan env t)
-- \| 只取指定列
scanColumns t cols = IPCStorage (\env -> doScanColumns env t cols)
@@ -381,13 +425,17 @@ instance MonadStorage IPCStorage where
Left err -> pure (Left err)
Right xs -> fmap sequence (mapM (loadEntry env) xs)
where
-- | 读取表数据并保留扫描错误
-- \| 读取表数据并保留扫描错误
loadEntry env info = do
rows <- doScan env (tiTable info)
pure $ fmap (\rs ->
( tiTable info
, Table (tiTable info) (schemaToColumns (tiColumns info)) rs (Just (metaOf info))
)) rows
pure $
fmap
( \rs ->
( tiTable info
, Table (tiTable info) (schemaToColumns (tiColumns info)) rs (Just (metaOf info)) (tiIdentity info) (tiSchemaVersion info)
)
)
rows
-- \| 只问数据字典要结构,设了当前库只留它的表
schema = IPCStorage $ \env -> do
@@ -409,6 +457,6 @@ visibleTables current info
else []
| otherwise = [bare info]
where
named = info {tiTable = unqualify current (tiTable info)}
-- | 组装成不带行的表
bare i = (tiTable i, Table (tiTable i) (schemaToColumns (tiColumns i)) [] (Just (metaOf i)))
named = info{tiTable = unqualify current (tiTable info)}
-- \| 组装成不带行的表
bare i = (tiTable i, Table (tiTable i) (schemaToColumns (tiColumns i)) [] (Just (metaOf i)) (tiIdentity i) (tiSchemaVersion i))
@@ -1,8 +1,9 @@
module ChuSQL.Core.Engine.Storage.Memory (MemoryStorage (runMemoryStorage)) where
import ChuSQL.Core.Model
import ChuSQL.Core.Engine.Storage
import qualified Data.HashSet as HS
import ChuSQL.Core.Model
import Data.HashSet qualified as HS
import Data.Word (Word64)
-- 内存实现:Database 上的状态,错误通道是 Either String。
@@ -26,6 +27,9 @@ instance Monad MemoryStorage where
runMemoryStorage (k a) db'
instance MonadStorage MemoryStorage where
-- \| 纯状态视图在求值期间不受外部修改
withSchemaGuard _ action = action
-- \| 全表扫描
scan t = MemoryStorage $ \db -> do
tbl <- lookupTable db t
@@ -45,7 +49,7 @@ instance MonadStorage MemoryStorage where
createTable name cols = MemoryStorage $ \db ->
if any ((== name) . fst) db
then Right (Left ("table already exists: " ++ name), db)
else Right (Right (), db ++ [(name, Table name cols [] Nothing)])
else Right (Right (), db ++ [(name, Table name cols [] Nothing Nothing (Just 1))])
-- \| 删表;不存在报错
dropTable name = MemoryStorage $ \db ->
@@ -60,7 +64,8 @@ instance MonadStorage MemoryStorage where
Just tbl ->
if col `notElem` map fst (tableCols tbl)
then Right (Left ("unknown column: " ++ col), db)
else
else do
version <- nextSchemaVersion tbl
Right
( Right ()
, replaceTable
@@ -68,6 +73,7 @@ instance MonadStorage MemoryStorage where
tbl
{ tableCols = filter ((/= col) . fst) (tableCols tbl)
, tableRows = map (filter ((/= col) . fst)) (tableRows tbl)
, tableSchemaVersion = Just version
}
db
)
@@ -78,9 +84,9 @@ instance MonadStorage MemoryStorage where
-- \| 返回实时行数与去重统计的无数据结构
schema = MemoryStorage $ \db -> Right (Right (map describe db), db)
where
-- | 将存储数据汇总为结构统计
-- \| 将存储数据汇总为结构统计
describe (name, table) =
(name, table {tableRows = [], tableMeta = Just metadata})
(name, table{tableRows = [], tableMeta = Just metadata})
where
metadata = TableMeta (length (tableRows table)) distinct [] []
distinct = [columnStatistics column (tableRows table) | (column, _) <- tableCols table]
@@ -88,13 +94,25 @@ instance MonadStorage MemoryStorage where
-- \| 改表结构:列定义与行一起换
replaceSchema name cols rows = MemoryStorage $ \db -> case lookup name db of
Nothing -> Right (Left ("unknown table: " ++ name), db)
Just tbl -> Right (Right (), replaceTable name tbl{tableCols = cols, tableRows = rows} db)
Just tbl -> do
version <-
if tableCols tbl == cols
then Right (tableSchemaVersion tbl)
else Just <$> nextSchemaVersion tbl
Right (Right (), replaceTable name tbl{tableCols = cols, tableRows = rows, tableSchemaVersion = version} db)
-- | 递增结构版本并拒绝耗尽
nextSchemaVersion :: Table -> Either String Word64
nextSchemaVersion table = case tableSchemaVersion table of
Just version | version == maxBound -> Left "catalog schema version exhausted"
Just version -> Right (version + 1)
Nothing -> Right 1
-- | 对列值去重并限制统计内存
columnStatistics :: String -> [Row] -> (String, Int, Bool)
columnStatistics column rows = go rows HS.empty
where
-- | 累积规范化值,达到上限后停止
-- \| 累积规范化值,达到上限后停止
go remaining values
| HS.size values >= 4096 = (column, 4096, True)
| otherwise = case remaining of
@@ -105,4 +123,3 @@ columnStatistics column rows = go rows HS.empty
replaceTable :: String -> Table -> Database -> Database
replaceTable name table =
map (\(n, t) -> if n == name then (n, table) else (n, t))
@@ -1,4 +1,4 @@
module ChuSQL.Core.Engine.Syntax.AST (Statement (..), FromClause (..), JoinKind (..), Expr (..), Subquery (..), Quantifier (..), CompareOp (..), SortDir (..), makeSelect) where
module ChuSQL.Core.Engine.Syntax.AST (Statement (..), FromClause (..), JoinKind (..), Expr (..), Subquery (..), Quantifier (..), CompareOp (..), SortDir (..), makeSelect, distinctOrdering) where
import ChuSQL.Core.Model (Column (..), ColumnType, Value (..))
import ChuSQL.Core.Engine.Runtime.Types (TypeId, TypeExpr, RuntimeValue)
@@ -23,6 +23,7 @@ data Statement
, selectOrderBy :: [(String, SortDir)]
, selectLimit :: Maybe Int
}
| SelectDistinct Statement
| Insert String [String] [[Expr]]
| Delete String (Maybe Expr)
| Update String [(String, Expr)] (Maybe Expr)
@@ -42,6 +43,7 @@ data Statement
| CreateUser String String
| AlterUser String String
| AlterIdentity String [(String, Bool)]
| ShowGrants
| ShowRoles
| DropUser String
-- | 新角色:权限的载体,用户靠成员资格继承它的权限
@@ -52,6 +54,7 @@ data Statement
| GrantPrivileges [String] String String Bool
-- | REVOKE 权限 ON 表 FROM 角色
| RevokePrivileges [String] String String
| RevokeGrantOption [String] String String
-- | GRANT 角色 TO 用户:给用户加角色
| GrantRole String [String]
-- | REVOKE <角色> FROM <用户>[, ...]
@@ -114,6 +117,8 @@ data CompareOp
-- | 条件表达式:列、字面量、算术、比较、逻辑、聚合与子查询
data Expr
= Col String
| ColumnSlot Int String
| OuterColumnSlot Int Int String
| FunctionCall String [Expr]
| BoundFunction FunctionId TypeId [Expr]
| Construct TypeExpr [Expr]
@@ -172,3 +177,23 @@ makeSelect cols tbl w =
, selectOrderBy = []
, selectLimit = Nothing
}
-- | 将去重排序映射到输出列
distinctOrdering :: Statement -> Either String [(String, SortDir)]
distinctOrdering q@Select{} = resolveDistinctOrdering q
distinctOrdering q@SelectExpr{} = resolveDistinctOrdering q
distinctOrdering _ = Left "DISTINCT requires SELECT"
-- | 解析去重输出的排序列
resolveDistinctOrdering :: Statement -> Either String [(String, SortDir)]
resolveDistinctOrdering q = mapM resolve (selectOrderBy q)
where
items = case q of
Select { selectCols = cols } -> [(c, Col c) | c <- cols]
SelectExpr { selectItems = projected } -> projected
_ -> []
-- | 排序键必须出现在去重结果中
resolve (column, direction) = case [label | (label, e) <- items, label == column || e == Col column] of
[label] -> Right (label, direction)
[] -> Left "DISTINCT: ORDER BY column must appear in the select list"
_ -> Left "DISTINCT: ambiguous ORDER BY column"
@@ -262,7 +262,7 @@ literalValue =
-- | 一个排序列
orderItem :: Parser (String, SortDir)
orderItem = do
col <- qualifiedName
col <- qualifiedName <|> (("$position" ++) . show <$> integer)
dir <- sortDir
return (col, dir)
@@ -590,14 +590,15 @@ grantStatement = do
revokeStatement :: Parser Statement
revokeStatement = do
keyword "revoke"
optionOnly <- fromMaybe False <$> optional (True <$ (keyword "grant" *> keyword "option" *> keyword "for"))
choice
[ try $ do
privs <- sepBy1 privilegeName (symbol ",")
keyword "on"
obj <- grantObject
keyword "from"
RevokePrivileges privs obj <$> userName
, RevokeRole <$> userName <*> (keyword "from" *> sepBy1 userName (symbol ","))
(if optionOnly then RevokeGrantOption else RevokePrivileges) privs obj <$> userName
, if optionOnly then fail "GRANT OPTION FOR requires object privileges" else RevokeRole <$> userName <*> (keyword "from" *> sepBy1 userName (symbol ","))
]
-- | 建表时的一列:类型后面跟若干约束
@@ -689,13 +690,14 @@ decimalArgs = do
selectStatement :: Parser Statement
selectStatement = do
keyword "select"
distinct <- fromMaybe False <$> optional ((True <$ keyword "distinct") <|> (False <$ keyword "all"))
cols <- selectList
fromC <- fromMaybe FromUnit <$> optional (keyword "from" *> fromClause)
mWhere <- optional (keyword "where" *> expr)
groupBy <- fromMaybe [] <$> optional groupByClause
orderBy <- fromMaybe [] <$> optional orderByClause
mLimit <- optional limitClause
pure $ if all (\(name, e) -> e == Col name) cols
pure $ (if distinct then SelectDistinct else id) $ if all (\(name, e) -> e == Col name) cols
then Select (map fst cols) fromC mWhere groupBy orderBy mLimit
else SelectExpr cols fromC mWhere groupBy orderBy mLimit
@@ -738,6 +740,7 @@ cteKey = map toLower
-- | CTE 带列清单时改本体的输出名
renameColumns :: String -> [String] -> Statement -> Either String Statement
renameColumns _ [] q = Right q
renameColumns name cols (SelectDistinct q) = SelectDistinct <$> renameColumns name cols q
renameColumns name cols q@Select { selectCols = names } = do
checkCteArity name cols names
Right
@@ -770,6 +773,7 @@ checkCteArity name cols out
-- | 把语句里对 CTE 的引用换成派生表
expandStatement :: [(String, Statement)] -> Statement -> Either String Statement
expandStatement env stmt = case stmt of
SelectDistinct q -> SelectDistinct <$> expandStatement env q
q@Select { selectFrom = fromC, selectWhere = mWhere } -> do
fromC' <- expandClause env fromC
w <- traverse (expandExpr env) mWhere
@@ -909,6 +913,7 @@ parseStatement input =
<|> try createUserStatement
<|> try alterUserStatement
<|> try alterRoleStatement
<|> try (ShowGrants <$ (keyword "show" *> keyword "grants"))
<|> try (ShowRoles <$ (keyword "show" *> keyword "roles"))
<|> try dropUserStatement
<|> try createRoleStatement
+258 -4
View File
@@ -3,20 +3,38 @@ module RuntimeSpec (spec) where
import ChuSQL.Core.Engine.Runtime.Types
import ChuSQL.Core.Engine.Runtime.Functions
import ChuSQL.Core.Engine.Runtime.Binding
import ChuSQL.Core.Engine.Runtime.Relation (bindRelOpColumns)
import qualified ChuSQL.Core.Engine.Algebra.Op as Rel
import ChuSQL.Core.Engine.Algebra.Expr (evalExpr)
import ChuSQL.Core.Engine.Algebra.Eval (evalRelOp)
import ChuSQL.Core.Engine.Algebra.Sort (sortBoundRows)
import ChuSQL.Core.Engine.Storage.Memory (runMemoryStorage)
import qualified ChuSQL.Core.Engine.Runtime.Query as Query
import ChuSQL.Core.Engine (rowsOf, runStatement)
import ChuSQL.Core.Engine.Semantic (prepare)
import ChuSQL.Core.Engine.Semantic (prepare, prepareRowExpression)
import ChuSQL.Core.Engine.Syntax.Parser (parseStatement)
import ChuSQL.Core.Engine.Syntax.AST (Expr (..), Statement (..))
import ChuSQL.Core.Engine.Syntax.AST (Expr (..), Statement (..), SortDir (..))
import qualified ChuSQL.Core.Model as SQL
import ChuSQL.Core.Protocol (valueToJSON, valueFromJSON)
import ChuSQL.Core.Protocol (valueToJSON, valueFromJSON, TableInfo (..))
import qualified Data.Aeson as A
import Data.Word (Word64)
import Data.Aeson.Types (parseEither)
import Data.Hashable (hash)
import Control.Monad (foldM)
import qualified Data.ByteString.Lazy.Char8 as BS
import Test.Hspec
-- 运行时类型、函数绑定、物理编码和类型化查询测试。
-- | 执行语句建立测试目录
sqlCatalog :: [String] -> Either String SQL.Database
sqlCatalog = foldM apply []
where
-- | 更新当前测试数据库
apply db source = do
statement <- parseStatement source
fst <$> runStatement db statement
-- | 检查运行时类型基础契约
spec :: Spec
spec = do
@@ -72,6 +90,35 @@ spec = do
let expression = Apply "not" [Apply "not" [Literal boolType (RBool True)]]
(bindExpression builtinTypes functions [] expression >>= evaluateExpression builtinTypes functions [])
`shouldBe` Right (RBool True)
it "uses the bound column slot and rejects reordered expression input" $ withFunctions $ \functions ->
withType intType $ \int -> withType stringType $ \string -> do
let bound = bindExpression builtinTypes functions [("count", int), ("name", string)]
(Apply "length" [Reference "name"])
(bound >>= evaluateExpression builtinTypes functions [("count", RInt 1), ("name", RString "abc")])
`shouldBe` Right (RInt 3)
(bound >>= evaluateExpression builtinTypes functions [("name", RString "abc"), ("count", RInt 1)])
`shouldBe` Left "bound column position mismatch: name"
(bound >>= evaluateExpression builtinTypes functions [("count", RInt 1)])
`shouldBe` Left "missing bound column: name"
it "rejects ambiguous references before binding a slot" $ withFunctions $ \functions ->
withType intType $ \int ->
bindExpression builtinTypes functions [("id", int), ("id", int)] (Reference "id")
`shouldSatisfy` isFailure
it "normalizes storage order at scan boundaries before evaluating slots" $ withFunctions $ \functions -> do
let catalog = [("items", [("name", stringType), ("enabled", boolType)])]
query = Projection [("size", Apply "length" [Reference "name"])]
(Selection (Reference "enabled") (TableScan "items"))
scan _ = Right [[("enabled", RBool True), ("name", RString "abc")]]
(bindPlan builtinTypes functions catalog query >>= executePlan builtinTypes functions scan)
`shouldBe` Right [[("size", RInt 3)]]
it "rebinds slots to each projection output schema" $ withFunctions $ \functions -> do
let catalog = [("items", [("name", stringType), ("enabled", boolType)])]
source = Projection [("enabled", Reference "enabled"), ("label", Reference "name")]
(TableScan "items")
query = Projection [("size", Apply "length" [Reference "label"])] source
scan _ = Right [[("name", RString "abc"), ("enabled", RBool True)]]
(bindPlan builtinTypes functions catalog query >>= executePlan builtinTypes functions scan)
`shouldBe` Right [[("size", RInt 3)]]
it "rejects a wrong function argument type during binding" $ withFunctions $ \functions ->
bindExpression builtinTypes functions [] (Apply "length" [Literal intType (RInt 1)]) `shouldSatisfy` isFailure
it "rejects unknown function identities" $ withFunctions $ \functions ->
@@ -104,12 +151,219 @@ spec = do
(bindPlan builtinTypes functions [("t", [])] (TableScan "t")
>>= executePlan builtinTypes functions (const (Left "storage unavailable"))) `shouldBe` Left "storage unavailable"
describe "SQL runtime functions" $ do
it "validates transitive DOMAIN dependencies and ignores unrelated definitions" $ do
let result = do
db <- sqlCatalog ["CREATE DOMAIN amount AS INT", "CREATE DOMAIN total AS amount"
, "CREATE DOMAIN unrelated AS TEXT", "CREATE TABLE amounts (value total)"]
statement <- parseStatement "SELECT value FROM amounts"
bound <- Query.bindQuery db statement
let change target = [(name, if name == target then table {SQL.tableCols = [("base", SQL.plainColumn SQL.CBool)]}
else table) | (name, table) <- db]
accepted <- fst <$> runMemoryStorage (Query.executeQueryM bound) (change "__system_domain_unrelated")
rejected <- fst <$> runMemoryStorage (Query.executeQueryM bound) (change "__system_domain_amount")
Right (accepted, rejected)
result `shouldBe` Right (Right [], Left "bound query schema changed: __system_domain_amount")
it "preserves exact catalog identities and rejects malformed identifiers" $ do
let identity = SQL.TableIdentity (maxBound :: Word64) (maxBound :: Word64)
info = TableInfo "items" [] 0 [] [] [] (Just identity) Nothing
decode text = A.eitherDecode (BS.pack text) :: Either String TableInfo
A.fromJSON (A.toJSON info) `shouldBe` A.Success info
fmap tiIdentity (decode "{\"table\":\"items\",\"columns\":[],\"row_count\":0}") `shouldBe` Right Nothing
fmap tiIdentity (decode "{\"table\":\"items\",\"columns\":[],\"row_count\":0,\"database_id\":0,\"object_id\":1}")
`shouldBe` Right (Just (SQL.TableIdentity 0 1))
mapM_ (\fields -> decode ("{\"table\":\"items\",\"columns\":[],\"row_count\":0," ++ fields ++ "}")
`shouldSatisfy` isFailure)
["\"database_id\":1", "\"database_id\":1,\"object_id\":-1"
, "\"database_id\":1,\"object_id\":1.5", "\"database_id\":1,\"object_id\":\"18446744073709551616\""
, "\"database_id\":1,\"object_id\":1e99999999"]
it "rejects a same-schema replacement or loss of object identity" $ do
let result = do
db <- sqlCatalog ["CREATE TABLE bound_items (id INT)"]
let identified = [(name, table {SQL.tableIdentity = Just (SQL.TableIdentity 1 1)}) | (name, table) <- db]
statement <- parseStatement "SELECT id FROM bound_items"
bound <- Query.bindQuery identified statement
let replaced = [(name, table {SQL.tableIdentity = Just (SQL.TableIdentity 1 2)}) | (name, table) <- db]
changed <- fst <$> runMemoryStorage (Query.executeQueryM bound) replaced
lost <- fst <$> runMemoryStorage (Query.executeQueryM bound) db
Right (changed, lost)
result `shouldBe` Right (Left "bound query object changed: bound_items", Left "bound query object changed: bound_items")
it "preserves exact schema versions and rejects malformed versions" $ do
let info = TableInfo "items" [] 0 [] [] [] Nothing (Just maxBound)
decode version = A.eitherDecode (BS.pack ("{\"table\":\"items\",\"columns\":[],\"row_count\":0,\"schema_version\":" ++ version ++ "}")) :: Either String TableInfo
A.fromJSON (A.toJSON info) `shouldBe` A.Success info
fmap tiSchemaVersion (decode "0") `shouldBe` Right Nothing
fmap tiSchemaVersion (decode "7") `shouldBe` Right (Just 7)
mapM_ (\version -> decode version `shouldSatisfy` isFailure)
["-1", "1.5", "\"18446744073709551616\"", "\"+1\"", "1e99999999"]
it "invalidates a bound query after restoring its original columns" $ do
let result = do
db <- sqlCatalog ["CREATE TABLE versioned (id INT)"]
statement <- parseStatement "SELECT id FROM versioned"
bound <- Query.bindQuery db statement
added <- parseStatement "ALTER TABLE versioned ADD COLUMN extra TEXT" >>= runStatement db
restored <- parseStatement "ALTER TABLE versioned DROP COLUMN extra" >>= runStatement (fst added)
fst <$> runMemoryStorage (Query.executeQueryM bound) (fst restored)
result `shouldBe` Right (Left "bound query schema changed: versioned")
it "rejects loss of a bound schema version" $ do
let result = do
db <- sqlCatalog ["CREATE TABLE versioned (id INT)"]
statement <- parseStatement "SELECT id FROM versioned"
bound <- Query.bindQuery db statement
fst <$> runMemoryStorage (Query.executeQueryM bound)
[(name, table {SQL.tableSchemaVersion = Nothing}) | (name, table) <- db]
result `shouldBe` Right (Left "bound query schema changed: versioned")
it "ignores unrelated table changes but validates nested query dependencies" $ do
let result = do
db <- sqlCatalog ["CREATE TABLE referenced (id INT)", "CREATE TABLE unrelated (id INT)"]
statement <- parseStatement "SELECT length((SELECT MIN(name) FROM (SELECT 'abc' AS name FROM referenced) d)) AS n"
bound <- Query.bindQuery db statement
unrelated <- parseStatement "ALTER TABLE unrelated ADD COLUMN extra TEXT" >>= runStatement db
accepted <- fst <$> runMemoryStorage (Query.executeQueryM bound) (fst unrelated)
referenced <- parseStatement "ALTER TABLE referenced ADD COLUMN extra TEXT" >>= runStatement db
rejected <- fst <$> runMemoryStorage (Query.executeQueryM bound) (fst referenced)
Right (accepted, rejected)
result `shouldBe` Right (Right [[("n", SQL.VNull)]], Left "bound query schema changed: referenced")
it "rejects schema drift before executing an empty bound query" $ do
let result = do
db <- sqlCatalog ["CREATE TABLE bound_items (id INT)"]
statement <- parseStatement "SELECT id FROM bound_items"
bound <- Query.bindQuery db statement
changed <- parseStatement "ALTER TABLE bound_items ADD COLUMN extra TEXT" >>= runStatement db
fst <$> runMemoryStorage (Query.executeQueryM bound) (fst changed)
result `shouldBe` Right (Left "bound query schema changed: bound_items")
it "keeps binding valid across row and statistics changes" $ do
let result = do
db <- sqlCatalog ["CREATE TABLE bound_items (id INT)"]
statement <- parseStatement "SELECT id FROM bound_items"
bound <- Query.bindQuery db statement
changed <- parseStatement "INSERT INTO bound_items (id) VALUES (7)" >>= runStatement db
let current = [(name, table {SQL.tableMeta = Just (SQL.TableMeta 100 [] [] [])})
| (name, table) <- fst changed]
fst <$> runMemoryStorage (Query.executeQueryM bound) current
result `shouldBe` Right (Right [[("id", SQL.VInt 7)]])
it "binds grouping keys and then sorting to the grouped output" $ do
let source = Rel.Compute [("first", LitInt 1), ("key", LitInt 2)] Rel.Unit
plan = Rel.Sort [("key", Desc)] (Rel.Aggregate ["key"] [("n", CountAll)] source)
bound = Rel.BoundSort [(ColumnSlot 0 "key", Desc)]
(Rel.BoundAggregate [("key", ColumnSlot 1 "key")] [("n", CountAll)] source)
bindRelOpColumns [] [] plan `shouldBe` Right bound
evalRelOp [] bound `shouldBe` Right [[("key", SQL.VInt 2), ("n", SQL.VInt 1)]]
evalRelOp [] (Rel.BoundAggregate [("key", ColumnSlot 0 "key")] [] source)
`shouldBe` Left "bound column position mismatch: key"
it "sorts bound NULL keys stably and rejects wrong slots" $ do
let a = [("id", SQL.VInt 1), ("key", SQL.VInt 2)]
b = [("id", SQL.VInt 2), ("key", SQL.VNull)]
c = [("id", SQL.VInt 3), ("key", SQL.VInt 2)]
sortBoundRows [(ColumnSlot 1 "key", Asc)] [] [a, b, c] `shouldBe` Right [b, a, c]
sortBoundRows [(ColumnSlot 1 "key", Desc)] [] [a, b, c] `shouldBe` Right [a, c, b]
sortBoundRows [(ColumnSlot 0 "key", Asc)] [] [a]
`shouldBe` Left "bound column position mismatch: key"
it "preserves NULL group order and empty-input global aggregates" $ do
let result = do
db <- sqlCatalog ["CREATE TABLE grouped_slots (id INT, tag TEXT)"
, "INSERT INTO grouped_slots (id, tag) VALUES (1, 'b'), (2, NULL), (3, 'b')"]
grouped <- parseStatement "SELECT tag, COUNT(*) AS n FROM grouped_slots GROUP BY tag"
groups <- rowsOf (runStatement db grouped)
empty <- parseStatement "SELECT COUNT(*) AS n, SUM(id) AS total FROM grouped_slots WHERE id < 0"
aggregates <- rowsOf (runStatement db empty)
Right (map (map snd) groups, map (map snd) aggregates)
result `shouldBe` Right ([[SQL.VStr "b", SQL.VInt 2], [SQL.VNull, SQL.VInt 1]]
, [[SQL.VInt 0, SQL.VNull]])
it "binds SQL relation expressions to the optimized input slots" $ do
let plan = Rel.Compute [("value", Col "second")]
(Rel.Compute [("first", LitInt 1), ("second", LitInt 2)] Rel.Unit)
bindRelOpColumns [] [] plan `shouldBe` Right (Rel.Compute [("value", ColumnSlot 1 "second")]
(Rel.Compute [("first", LitInt 1), ("second", LitInt 2)] Rel.Unit))
bindRelOpColumns [] [] (Rel.Filter (Col "missing") Rel.Unit)
`shouldBe` Left "unknown column: missing"
it "checks SQL slot position instead of searching another column" $ do
evalExpr (ColumnSlot 1 "name") [("name", SQL.VStr "abc"), ("id", SQL.VInt 1)]
`shouldBe` Left "bound column position mismatch: name"
evalExpr (ColumnSlot (-1) "name") [("name", SQL.VStr "abc")]
`shouldBe` Left "bound column position mismatch: name"
it "binds projections without losing repeated outputs or empty rows" $ do
let source = Rel.Compute [("first", LitInt 1), ("second", LitInt 2)] Rel.Unit
bindRelOpColumns [] [] (Rel.Project ["second", "first", "second"] source)
`shouldBe` Right (Rel.Compute [("second", ColumnSlot 1 "second")
, ("first", ColumnSlot 0 "first"), ("second", ColumnSlot 1 "second")] source)
bindRelOpColumns [] [] (Rel.Project [] source)
`shouldBe` Right (Rel.Compute [] source)
bindRelOpColumns [] [] (Rel.Project ["*"] source)
`shouldBe` Right (Rel.Compute [("first", ColumnSlot 0 "first"), ("second", ColumnSlot 1 "second")] source)
it "preserves repeated SQL outputs and correlated projection values" $ do
let result = do
db <- sqlCatalog ["CREATE TABLE slot_items (id INT)"
, "INSERT INTO slot_items (id) VALUES (1), (2)"]
repeated <- parseStatement "SELECT id, id FROM slot_items"
repeatedRows <- rowsOf (runStatement db repeated)
correlated <- parseStatement "SELECT o.id, (SELECT o.id FROM slot_items i WHERE i.id = o.id) AS echoed FROM slot_items o"
correlatedRows <- rowsOf (runStatement db correlated)
Right (map (map snd) repeatedRows, map (map snd) correlatedRows)
result `shouldBe` Right ([[SQL.VInt 1, SQL.VInt 1], [SQL.VInt 2, SQL.VInt 2]]
, [[SQL.VInt 1, SQL.VInt 1], [SQL.VInt 2, SQL.VInt 2]])
it "binds outer references after the local relation layout" $ do
bindRelOpColumns [] ["outer.id"] (Rel.Compute [("id", Col "outer.id")] Rel.Unit)
`shouldBe` Right (Rel.Compute [("id", OuterColumnSlot 0 0 "outer.id")] Rel.Unit)
it "preserves outer scope identity when rebinding beside a local name" $ do
let source = Rel.Compute [("id", LitInt 1)] Rel.Unit
expression = OuterColumnSlot 0 0 "id"
bound = bindRelOpColumns [] ["id"] (Rel.Compute [("outer", expression)] source)
bound `shouldBe` Right (Rel.Compute [("outer", OuterColumnSlot 1 0 "id")] source)
evalExpr (OuterColumnSlot 1 0 "id") [("id", SQL.VInt 1), ("id", SQL.VInt 9)]
`shouldBe` Right (SQL.VInt 9)
bindRelOpColumns [] [] (Rel.Compute [("outer", expression)] source)
`shouldBe` Left "unknown column: id"
prepareRowExpression [] expression `shouldBe` Left "execution column cannot be used in semantic input"
prepareRowExpression [] (ColumnSlot 0 "id") `shouldBe` Left "execution column cannot be used in semantic input"
it "keeps inner shadowing and multi-level correlated SQL references" $ do
let result = do
db <- sqlCatalog ["CREATE TABLE scoped_items (id INT)"
, "INSERT INTO scoped_items (id) VALUES (1), (2)"]
statement <- parseStatement "SELECT o.id, (SELECT id FROM scoped_items i WHERE i.id = o.id) AS inner_id, (SELECT (SELECT o.id FROM scoped_items j WHERE j.id = i.id) FROM scoped_items i WHERE i.id = o.id) AS nested_id FROM scoped_items o"
rows <- rowsOf (runStatement db statement)
Right (map (map snd) rows)
result `shouldBe` Right [[SQL.VInt 1, SQL.VInt 1, SQL.VInt 1], [SQL.VInt 2, SQL.VInt 2, SQL.VInt 2]]
it "executes SQL against reversed physical column order" $ do
let result = do
db <- sqlCatalog ["CREATE TABLE slot_items (id INT, name TEXT)"
, "INSERT INTO slot_items (id, name) VALUES (1, 'abc'), (2, 'xy')"]
let reordered = [(name, table {SQL.tableRows = map reverse (SQL.tableRows table)}) | (name, table) <- db]
statement <- parseStatement "SELECT length(name) AS size, id + 1 AS next FROM slot_items WHERE id > 0 ORDER BY size"
rowsOf (runStatement reordered statement)
fmap (map (map snd)) result `shouldBe` Right [[SQL.VInt 2, SQL.VInt 3], [SQL.VInt 3, SQL.VInt 2]]
it "binds the SQL entry point to output TypeIds" $ withType intType $ \tid -> do
let result = parseStatement "SELECT length('abc') AS size" >>= Query.bindQuery []
fmap Query.queryTypes result `shouldBe` Right [("size", Query.RuntimeType tid)]
it "retains compatibility for existing temporal SQL types" $ do
let result = parseStatement "SELECT DATE '2026-10-05' AS today" >>= Query.bindQuery []
fmap Query.queryTypes result `shouldBe` Right [("today", Query.CompatibilityType SQL.CDate)]
it "retains nested DOMAIN identities through derived aliases and DISTINCT" $ withType intType $ \tid -> do
let result = do
db <- sqlCatalog ["CREATE DOMAIN quantity AS INT", "CREATE DOMAIN stock AS quantity"
, "CREATE TABLE inventory (qty stock)", "INSERT INTO inventory (qty) VALUES (2), (NULL)"]
statement <- parseStatement "SELECT DISTINCT d.qty AS available FROM (SELECT qty FROM inventory) d"
bound <- Query.bindQuery db statement
rows <- rowsOf (runStatement db statement)
Right (Query.queryTypes bound, map (map snd) rows)
result `shouldBe` Right ([("available", Query.DomainType "stock"
(Query.DomainType "quantity" (Query.RuntimeType tid)))], [[SQL.VInt 2], [SQL.VNull]])
it "preserves temporal and binary DOMAIN wrappers" $ do
let result = do
db <- sqlCatalog ["CREATE DOMAIN day AS DATE", "CREATE DOMAIN payload AS BLOB"
, "CREATE TABLE documents (created day, contents payload)"]
statement <- parseStatement "SELECT created, contents FROM documents"
Query.queryTypes <$> Query.bindQuery db statement
result `shouldBe` Right [("created", Query.DomainType "day" (Query.CompatibilityType SQL.CDate))
, ("contents", Query.DomainType "payload" (Query.CompatibilityType SQL.CBlob))]
it "uses base function semantics without labeling function results as DOMAIN" $ withType intType $ \tid -> do
let result = do
db <- sqlCatalog ["CREATE DOMAIN label AS VARCHAR(3)", "CREATE TABLE names (name label)"
, "INSERT INTO names (name) VALUES ('abc')"]
statement <- parseStatement "SELECT length(name) AS size FROM names"
bound <- Query.bindQuery db statement
rows <- rowsOf (runStatement db statement)
Right (Query.queryTypes bound, map (map snd) rows)
result `shouldBe` Right ([("size", Query.RuntimeType tid)], [[SQL.VInt 3]])
it "rejects composite ordering without an Ord implementation" $
sql "SELECT LIST<Int>(1) AS xs ORDER BY xs" `shouldSatisfy` isFailure
it "rejects composite extrema without an Ord implementation" $
@@ -201,7 +455,7 @@ sql source = do
-- | 提供函数调用测试表
database = [("t", SQL.Table "t" [("name", SQL.plainColumn SQL.CStr), ("enabled", SQL.plainColumn SQL.CBool)]
[[("name", SQL.VStr "ab"), ("enabled", SQL.VBool True)]
,[("name", SQL.VStr "xyz"), ("enabled", SQL.VBool False)]] Nothing)]
,[("name", SQL.VStr "xyz"), ("enabled", SQL.VBool False)]] Nothing Nothing Nothing)]
-- | 判断结果是否明确失败
isFailure :: Either a b -> Bool
+142 -10
View File
@@ -12,6 +12,7 @@ import ChuSQL.Core.Engine.Algebra.Sort (sortRows)
import ChuSQL.Core.Engine.Builtin
import ChuSQL.Core.Engine.Error
import ChuSQL.Core.Engine
import qualified ChuSQL.Core.Engine.Runtime.Query as Bound
import ChuSQL.Core.Engine.Parallel (poolRun, workerPool)
import ChuSQL.Core.Model
import ChuSQL.Core.Engine.Semantic (prepare)
@@ -46,7 +47,9 @@ import qualified RuntimeSpec
users :: Table
users =
Table
{ tableName = "users"
{ tableSchemaVersion = Nothing
, tableIdentity = Nothing
, tableName = "users"
, tableCols = [("id", TInt), ("name", TStr), ("age", TInt)]
, tableRows =
[ [("id", VInt 1), ("name", VStr "Alice"), ("age", VInt 25)]
@@ -60,7 +63,9 @@ users =
orders :: Table
orders =
Table
{ tableName = "orders"
{ tableSchemaVersion = Nothing
, tableIdentity = Nothing
, tableName = "orders"
, tableCols = [("id", TInt), ("user_id", TInt), ("product", TStr)]
, tableRows =
[ [("id", VInt 1), ("user_id", VInt 1), ("product", VStr "Book")]
@@ -78,7 +83,9 @@ testDB = [("users", users), ("orders", orders)]
chainA :: Table
chainA =
Table
{ tableName = "a"
{ tableSchemaVersion = Nothing
, tableIdentity = Nothing
, tableName = "a"
, tableCols = [("id", TInt), ("name", TStr)]
, tableRows = [[("id", VInt n), ("name", VStr ("a" ++ show n))] | n <- [1 .. 6]]
, tableMeta = Just (TableMeta 6 [("id", 6, False)] [] ["id"])
@@ -88,7 +95,9 @@ chainA =
chainB :: Table
chainB =
Table
{ tableName = "b"
{ tableSchemaVersion = Nothing
, tableIdentity = Nothing
, tableName = "b"
, tableCols = [("id", TInt), ("a_id", TInt)]
, tableRows = [[("id", VInt n), ("a_id", VInt n)] | n <- [1 .. 4]]
, tableMeta = Just (TableMeta 4 [("id", 4, False), ("a_id", 4, False)] [] ["id"])
@@ -98,7 +107,9 @@ chainB =
chainC :: Table
chainC =
Table
{ tableName = "c"
{ tableSchemaVersion = Nothing
, tableIdentity = Nothing
, tableName = "c"
, tableCols = [("id", TInt), ("b_id", TInt)]
, tableRows = [[("id", VInt n), ("b_id", VInt n)] | n <- [1, 2]]
, tableMeta = Just (TableMeta 2 [("id", 2, False), ("b_id", 2, False)] [] ["id"])
@@ -124,7 +135,64 @@ domainSql db (sql : rest) = do
-- | 跑引擎层全部 hspec 用例
main :: IO ()
main = hspec $ do
describe "REVOKE GRANT OPTION FOR" $ do
it "parses object privileges and rejects membership options" $ do
parseStatement "REVOKE GRANT OPTION FOR SELECT, UPDATE ON users FROM alice"
`shouldBe` Right (RevokeGrantOption ["SELECT", "UPDATE"] "users" "alice")
parseStatement "REVOKE GRANT OPTION FOR reader FROM alice" `shouldSatisfy` either (const True) (const False)
describe "ORDER BY positions" $ do
it "resolves positions after wildcard expansion and before limiting" $ do
rowsOf (parseStatement "SELECT * FROM orders ORDER BY 2 DESC, 1 DESC LIMIT 2" >>= runStatement testDB)
`shouldBe` Right [tableRows orders !! 1, tableRows orders !! 2]
rowsOf (parseStatement "SELECT DISTINCT user_id + 1 AS k FROM orders ORDER BY 1 DESC" >>= runStatement testDB)
`shouldBe` Right [[("k", VInt 3)], [("k", VInt 2)]]
it "rejects zero and positions beyond the output" $ do
mapM_ (\sql -> (parseStatement sql >>= runStatement testDB) `shouldSatisfy` either (isInfixOf "outside the select list") (const False))
["SELECT id FROM users ORDER BY 0", "SELECT id FROM users ORDER BY 2"]
describe "SELECT DISTINCT" $ do
it "deduplicates projected values before ordering and limiting" $ do
let db = [("orders", orders { tableRows = tableRows orders ++ tableRows orders })]
rowsOf (parseStatement "SELECT DISTINCT user_id FROM orders ORDER BY user_id DESC LIMIT 2" >>= runStatement db)
`shouldBe` Right [[("user_id", VInt 2)], [("user_id", VInt 1)]]
rowsOf (parseStatement "SELECT DISTINCT user_id + 1 AS k FROM orders ORDER BY k LIMIT 2" >>= runStatement db)
`shouldBe` Right [[("k", VInt 2)], [("k", VInt 3)]]
rowsOf (parseStatement "SELECT ALL user_id FROM orders" >>= runStatement db)
`shouldBe` Right (map (filter ((== "user_id") . fst)) (tableRows orders ++ tableRows orders))
it "merges NULL and equal numeric values while retaining first rows" $ do
evalRelOp [] (Distinct Unit) `shouldBe` Right [[]]
let t = users { tableRows = [[("id", v), ("name", VStr "a"), ("age", VInt 1)] | v <- [VNull, VNull, VInt 1, VFloat 1, VInt 2]] }
evalRelOp [("users", t)] (Distinct (Project ["id"] (Scan Nothing "users" Nothing)))
`shouldBe` Right [[("id", VNull)], [("id", VInt 1)], [("id", VInt 2)]]
it "keeps all distinct columns when an outer query projects fewer" $ do
rowsOf (parseStatement "SELECT d.user_id FROM (SELECT DISTINCT user_id, product FROM orders) AS d" >>= runStatement testDB)
`shouldBe` Right [[("d.user_id", VInt 1)], [("d.user_id", VInt 2)], [("d.user_id", VInt 1)]]
rowsOf (parseStatement "WITH d(k) AS (SELECT DISTINCT user_id FROM orders) SELECT k FROM d" >>= runStatement testDB)
`shouldBe` Right [[("d.k", VInt 1)], [("d.k", VInt 2)]]
it "supports aggregates, empty inputs and output aliases" $ do
rowsOf (parseStatement "SELECT DISTINCT COUNT(*) AS n FROM orders" >>= runStatement testDB)
`shouldBe` Right [[("n", VInt 3)]]
rowsOf (parseStatement "SELECT DISTINCT user_id FROM orders WHERE user_id = 99" >>= runStatement testDB)
`shouldBe` Right []
rowsOf (parseStatement "SELECT DISTINCT user_id AS k FROM orders ORDER BY user_id" >>= runStatement testDB)
`shouldBe` Right [[("k", VInt 1)], [("k", VInt 2)]]
it "rejects composite values without equality capability" $ do
(parseStatement "SELECT DISTINCT LIST<Int>(1) AS xs" >>= runStatement [])
`shouldSatisfy` either (isInfixOf "capability is not registered") (const False)
it "evaluates correlated DISTINCT subqueries with the outer binding" $ do
rowsOf (parseStatement "SELECT u.id FROM users u WHERE u.id IN (SELECT DISTINCT o.user_id FROM orders o WHERE o.user_id = u.id)" >>= runStatement testDB)
`shouldBe` Right [[("u.id", VInt 1)], [("u.id", VInt 2)]]
it "rejects sorting by values excluded from the distinct output" $ do
let result = parseStatement "SELECT DISTINCT user_id FROM orders ORDER BY product" >>= runStatement testDB
result `shouldSatisfy` either (isInfixOf "must appear in the select list") (const False)
describe "Storage read failures" $ do
it "rejects storage that silently ignores a bound read guard" $ do
setStorageLink StorageLink
{ slRequest = \_ -> pure (Right (BSC.pack "{\"status\":\"rows\",\"rows\":[]}"))
, slClose = pure ()
}
(runIPCStorage (withSchemaGuard [] (scan "ignored"))
`shouldReturn` Left "storage did not confirm bound schema guard") `finally` closeConnection
it "propagates catalog failures through snapshot, schema and SQL" $ do
setStorageLink StorageLink
{ slRequest = \_ -> pure (Right (BSC.pack "{\"status\":\"error\",\"message\":\"catalog unavailable\"}"))
@@ -729,7 +797,7 @@ main = hspec $ do
it "rewrites >= and <= into an inclusive index range" $ do
let stat rows distinct indexes = TableMeta rows [(c, d, False) | (c, d) <- distinct] [] indexes
dbWith m = [("users", Table "users" [("id", TInt), ("name", TStr)] [] (Just m))]
dbWith m = [("users", Table "users" [("id", TInt), ("name", TStr)] [] (Just m) Nothing Nothing)]
indexed = dbWith (stat 10000 [("id", 10000)] ["id"])
low = Filter (GtE (Col "id") (LitInt 2)) (Scan Nothing "users" Nothing)
both = Filter (And (GtE (Col "id") (LitInt 1)) (LtE (Col "id") (LitInt 3))) (Scan Nothing "users" Nothing)
@@ -1798,7 +1866,7 @@ main = hspec $ do
let buckets = replicate 8 63 ++ replicate 8 62
withHist = TableMeta 1000 [("id", 1000, False)] [("id", Histogram 0 1600 buckets)] ["id"]
plain = TableMeta 1000 [("id", 1000, False)] [] ["id"]
dbWith m = [("users", Table "users" [("id", TInt), ("name", TStr)] [] (Just m))]
dbWith m = [("users", Table "users" [("id", TInt), ("name", TStr)] [] (Just m) Nothing Nothing)]
narrow = Filter (And (GtE (Col "id") (LitInt 10)) (LtE (Col "id") (LitInt 20))) (Scan Nothing "users" Nothing)
wide = Filter (Gt (Col "id") (LitInt 0)) (Scan Nothing "users" Nothing)
-- 直方图说这个窄范围只命中十几行:改写
@@ -1884,8 +1952,8 @@ main = hspec $ do
it "picks the index only when the statistics make it cheaper" $ do
let stat rows distinct indexes = TableMeta rows [(c, d, False) | (c, d) <- distinct] [] indexes
dbWith m = [("users", Table "users" [("id", TInt), ("name", TStr)] [] (Just m))]
unknown = [("users", Table "users" [("id", TInt), ("name", TStr)] [] Nothing)]
dbWith m = [("users", Table "users" [("id", TInt), ("name", TStr)] [] (Just m) Nothing Nothing)]
unknown = [("users", Table "users" [("id", TInt), ("name", TStr)] [] Nothing Nothing Nothing)]
point = Filter (Eq (Col "id") (LitInt 1)) (Scan Nothing "users" Nothing)
range = Filter (Gt (Col "id") (LitInt 5)) (Scan Nothing "users" Nothing)
-- 索引在、点查命中一行:改写
@@ -2332,6 +2400,51 @@ main = hspec $ do
map fst db `shouldBe` ["a", "b"]
map (length . tableRows . snd) db `shouldBe` [1, 1]
map (map (lookup "id") . tableRows . snd) db `shouldBe` [[Just (VInt 1)], [Just (VInt 2)]]
it "rejects a bound query after a same-schema persistent table recreation" $ do
withTestSession $ \srv -> withServerEnv srv $ do
runIPCStorage (createTable "rebuilt" [("id", TInt)]) `shouldReturn` Right ()
Right catalog <- runIPCStorage schema
let bound = parseStatement "SELECT id FROM rebuilt" >>= Bound.bindQuery catalog
case bound of
Left err -> expectationFailure err
Right query -> do
runIPCStorage (Bound.executeQueryM query) `shouldReturn` Right []
runIPCStorage (dropTable "rebuilt") `shouldReturn` Right ()
runIPCStorage (createTable "rebuilt" [("id", TInt)]) `shouldReturn` Right ()
changed <- runIPCStorage (Bound.executeQueryM query)
changed `shouldSatisfy` either (isInfixOf "bound query object changed") (const False)
it "preserves persistent versions in snapshots and rejects restored schema" $ do
withTestSession $ \srv -> withServerEnv srv $ do
runIPCStorage (createTable "versioned" [("id", TInt)]) `shouldReturn` Right ()
Right catalog <- runIPCStorage schema
Right contents <- runIPCStorage snapshot
fmap tableSchemaVersion (lookup "versioned" catalog) `shouldBe` Just (Just 1)
fmap tableSchemaVersion (lookup "versioned" contents) `shouldBe` Just (Just 1)
case parseStatement "SELECT id FROM versioned" >>= Bound.bindQuery catalog of
Left err -> expectationFailure err
Right query -> do
runIPCStorage (replaceSchema "versioned" [("id", TInt), ("extra", TStr)] []) `shouldReturn` Right ()
runIPCStorage (replaceSchema "versioned" [("id", TInt)] []) `shouldReturn` Right ()
changed <- runIPCStorage (Bound.executeQueryM query)
changed `shouldSatisfy` either (isInfixOf "bound query schema changed") (const False)
it "rejects a DDL race between schema validation and the actual read" $ do
withTestSession $ \srv -> withServerEnv srv $ do
runIPCStorage (createTable "raced" [("id", TInt)]) `shouldReturn` Right ()
Right catalog <- runIPCStorage schema
closeConnection
Right link <- localStorageLink (Just (dataDir srv ++ ".toml"))
setStorageLink link {slRequest = \payload -> do
if BSC.pack "schema_guard" `BS.isInfixOf` payload
then do
changed <- slRequest link (BSC.pack "{\"method\":\"replace_schema\",\"database\":\"ipctest\",\"table\":\"raced\",\"columns\":[{\"name\":\"id\",\"ty\":\"int\"},{\"name\":\"extra\",\"ty\":\"str\"}],\"rows\":[]}")
changed `shouldSatisfy` either (const False) (BS.isInfixOf (BSC.pack "ok"))
else pure ()
slRequest link payload}
case parseStatement "SELECT id FROM raced" >>= Bound.bindQuery catalog of
Left err -> expectationFailure err
Right query -> do
result <- runIPCStorage (Bound.executeQueryM query)
result `shouldSatisfy` either (isInfixOf "bound query schema changed") (const False)
it "doListTables returns the tables that exist" $ do
withTestSession $ \srv -> withServerEnv srv $ do
@@ -2529,7 +2642,7 @@ main = hspec $ do
`shouldBe` Right (makeSelect ["*"] "t" (Just (Eq (Col "flag") (LitBool True))))
it "inserts and reads back a boolean column end-to-end" $ do
let db = [("t", Table "t" [("id", TInt), ("flag", TBool)] [] Nothing)]
let db = [("t", Table "t" [("id", TInt), ("flag", TBool)] [] Nothing Nothing Nothing)]
case runStatement db (Insert "t" ["id", "flag"] [[LitInt 1, LitBool True]]) of
Left e -> expectationFailure e
Right (db', _) ->
@@ -3154,6 +3267,8 @@ instance Monad ProbeStorage where
ProbeStorage (\probe -> act probe >>= \value -> runProbeStorage (next value) probe)
instance MonadStorage ProbeStorage where
-- \| 测试视图不支持并发结构变更
withSchemaGuard _ action = action
scan table = ProbeStorage $ \probe -> do
recordCall probe ("scan:" ++ table)
pure (Right (tableRows (probeTable probe)))
@@ -3227,7 +3342,10 @@ planTables op = case op of
Project _ x -> planTables x
Compute _ x -> planTables x
Aggregate _ _ x -> planTables x
BoundAggregate _ _ x -> planTables x
Distinct x -> planTables x
Sort _ x -> planTables x
BoundSort _ x -> planTables x
Limit _ x -> planTables x
Join _ l r _ -> planTables l ++ planTables r
Derived _ x -> planTables x
@@ -3239,8 +3357,11 @@ leftJoinKinds (Join kind l r _) = (kind == LeftJoin) : (leftJoinKinds l ++ leftJ
leftJoinKinds (Project _ x) = leftJoinKinds x
leftJoinKinds (Compute _ x) = leftJoinKinds x
leftJoinKinds (Aggregate _ _ x) = leftJoinKinds x
leftJoinKinds (BoundAggregate _ _ x) = leftJoinKinds x
leftJoinKinds (Filter _ x) = leftJoinKinds x
leftJoinKinds (Distinct x) = leftJoinKinds x
leftJoinKinds (Sort _ x) = leftJoinKinds x
leftJoinKinds (BoundSort _ x) = leftJoinKinds x
leftJoinKinds (Limit _ x) = leftJoinKinds x
leftJoinKinds (Derived _ x) = leftJoinKinds x
leftJoinKinds _ = []
@@ -3251,8 +3372,11 @@ firstFilterCond (Filter p _) = Just p
firstFilterCond (Project _ x) = firstFilterCond x
firstFilterCond (Compute _ x) = firstFilterCond x
firstFilterCond (Aggregate _ _ x) = firstFilterCond x
firstFilterCond (BoundAggregate _ _ x) = firstFilterCond x
firstFilterCond Unit = Nothing
firstFilterCond (Distinct x) = firstFilterCond x
firstFilterCond (Sort _ x) = firstFilterCond x
firstFilterCond (BoundSort _ x) = firstFilterCond x
firstFilterCond (Limit _ x) = firstFilterCond x
firstFilterCond (Join _ l r _) = case firstFilterCond l of
Just p -> Just p
@@ -3268,8 +3392,11 @@ anyFilter (Filter _ _) = True
anyFilter (Project _ x) = anyFilter x
anyFilter (Compute _ x) = anyFilter x
anyFilter (Aggregate _ _ x) = anyFilter x
anyFilter (BoundAggregate _ _ x) = anyFilter x
anyFilter Unit = False
anyFilter (Distinct x) = anyFilter x
anyFilter (Sort _ x) = anyFilter x
anyFilter (BoundSort _ x) = anyFilter x
anyFilter (Limit _ x) = anyFilter x
anyFilter (Join _ l r _) = anyFilter l || anyFilter r
anyFilter (Scan _ _ _) = False
@@ -3282,9 +3409,12 @@ stripProjects :: RelOp -> RelOp
stripProjects (Project _ x) = stripProjects x
stripProjects (Compute items x) = Compute items (stripProjects x)
stripProjects (Aggregate keys aggs x) = Aggregate keys aggs (stripProjects x)
stripProjects (BoundAggregate keys aggs x) = BoundAggregate keys aggs (stripProjects x)
stripProjects Unit = Unit
stripProjects (Filter p x) = Filter p (stripProjects x)
stripProjects (Distinct x) = Distinct (stripProjects x)
stripProjects (Sort spec x) = Sort spec (stripProjects x)
stripProjects (BoundSort spec x) = BoundSort spec (stripProjects x)
stripProjects (Limit n x) = Limit n (stripProjects x)
stripProjects (Join k l r c) = Join k (stripProjects l) (stripProjects r) c
stripProjects (Scan a t _) = Scan a t Nothing
@@ -3379,6 +3509,8 @@ memReplaceSchema :: String -> [(String, Column)] -> [Row] -> MemoryStorage (Eith
memReplaceSchema = replaceSchema
instance MonadStorage Counting where
-- \| 纯测试状态在求值期间保持稳定
withSchemaGuard _ action = action
-- \| 扫描:记一笔表名,其余走内存实现
scan t = Counting $ \db -> case runMemoryStorage (memScan t) db of
Left e -> Left e