修复bug
This commit is contained in:
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user