完成语义检查,整理项目

This commit is contained in:
LIm1tZz7 committed 2026-09-22 21:26:54 +08:00
1 parent 0791fe0446
commit 91ce26d7f0
17 files changed
+447 -301

No files matched your search

+171 -22
View File
@@ -1,13 +1,13 @@
module Main where
import ChuSQL.Algebra.Eval (evalRelOp)
import ChuSQL.Algebra.Op (RelOp (..))
import ChuSQL.Algebra.Op (RelOp (..), renderPlan)
import ChuSQL.Algebra.Optimize (optimize, pushProject)
import ChuSQL.Algebra.Planner (translate)
import ChuSQL.Model
import ChuSQL.SQLSyntax.AST
import ChuSQL.SQLSyntax.Executor
import ChuSQL.SQLSyntax.Parser
import ChuSQL.Syntax.AST
import ChuSQL.Engine
import ChuSQL.Syntax.Parser
import Test.Hspec
-- ============================================================
@@ -17,7 +17,7 @@ users :: Table
users =
Table
{ tableName = "users"
, tableCols = ["id", "name", "age"]
, tableCols = [("id", TInt), ("name", TStr), ("age", TInt)]
, tableRows =
[ [("id", VInt 1), ("name", VStr "Alice"), ("age", VInt 25)]
, [("id", VInt 2), ("name", VStr "Bob"), ("age", VInt 17)]
@@ -29,7 +29,7 @@ orders :: Table
orders =
Table
{ tableName = "orders"
, tableCols = ["id", "user_id", "product"]
, tableCols = [("id", TInt), ("user_id", TInt), ("product", TStr)]
, tableRows =
[ [("id", VInt 1), ("user_id", VInt 1), ("product", VStr "Book")]
, [("id", VInt 2), ("user_id", VInt 2), ("product", VStr "Pen")]
@@ -48,7 +48,7 @@ main = hspec $ do
-- ============================================================
-- AST
-- ============================================================
describe "ChuSQL.Syntax.Ast" $ do
describe "ChuSQL.Syntax.AST" $ do
it "equal Selects are equal" $ do
makeSelect ["name"] "users" Nothing
`shouldBe` makeSelect ["name"] "users" Nothing
@@ -247,7 +247,7 @@ main = hspec $ do
-- ============================================================
-- Engine: SELECT
-- ============================================================
describe "ChuSQL.Eval.Engine (SELECT)" $ do
describe "ChuSQL.Engine (SELECT)" $ do
it "returns all rows without WHERE" $ do
rowsOf (runQuery testDB (makeSelect ["*"] "users" Nothing))
`shouldBe` Right
@@ -304,7 +304,7 @@ main = hspec $ do
-- ============================================================
-- Engine: INSERT
-- ============================================================
describe "ChuSQL.Eval.Engine (INSERT)" $ do
describe "ChuSQL.Engine (INSERT)" $ do
it "parses a simple INSERT" $ do
parseQuery "INSERT INTO users (name, age) VALUES ('Dave', 22)"
`shouldBe` Right (Insert "users" ["name", "age"] [LitStr "Dave", LitInt 22])
@@ -353,7 +353,7 @@ main = hspec $ do
-- ============================================================
-- Engine: DELETE
-- ============================================================
describe "ChuSQL.Eval.Engine (DELETE)" $ do
describe "ChuSQL.Engine (DELETE)" $ do
it "parses DELETE with WHERE" $ do
parseQuery "DELETE FROM users WHERE age < 18"
`shouldBe` Right (Delete "users" (Just (Lt (Col "age") (LitInt 18))))
@@ -403,7 +403,7 @@ main = hspec $ do
-- ============================================================
-- Engine: UPDATE
-- ============================================================
describe "ChuSQL.Eval.Engine (UPDATE)" $ do
describe "ChuSQL.Engine (UPDATE)" $ do
it "parses UPDATE with WHERE" $ do
parseQuery "UPDATE users SET age = 26 WHERE name = 'Alice'"
`shouldBe` Right
@@ -495,7 +495,7 @@ main = hspec $ do
-- ============================================================
-- Engine: ORDER BY
-- ============================================================
describe "ChuSQL.Eval.Engine (ORDER BY)" $ do
describe "ChuSQL.Engine (ORDER BY)" $ do
it "parses ORDER BY ASC" $ do
case parseQuery "SELECT name FROM users ORDER BY age ASC" of
Right q -> selectOrderBy q `shouldBe` [("age", Asc)]
@@ -583,7 +583,7 @@ main = hspec $ do
-- ============================================================
-- Engine: LIMIT
-- ============================================================
describe "ChuSQL.Eval.Engine (LIMIT)" $ do
describe "ChuSQL.Engine (LIMIT)" $ do
it "parses LIMIT" $ do
case parseQuery "SELECT * FROM users LIMIT 2" of
Right q -> selectLimit q `shouldBe` Just 2
@@ -666,7 +666,7 @@ main = hspec $ do
-- ============================================================
-- Engine: JOIN
-- ============================================================
describe "ChuSQL.Eval.Engine (JOIN)" $ do
describe "ChuSQL.Engine (JOIN)" $ do
it "executes a two-table JOIN with aliases" $ do
rowsOf (parseQuery "SELECT u.name, o.product FROM users u JOIN orders o ON u.id = o.user_id" >>= runQuery testDB)
`shouldBe` Right
@@ -834,7 +834,7 @@ main = hspec $ do
case parseQuery "SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id WHERE u.age > 20" of
Left err -> expectationFailure err
Right q ->
case translate testDB q of
case translate q of
Left err -> expectationFailure err
Right relOp ->
filterPushedIntoLeft (optimize testDB relOp) `shouldBe` True
@@ -843,7 +843,7 @@ main = hspec $ do
case parseQuery "SELECT * FROM users" of
Left err -> expectationFailure err
Right q ->
case translate testDB q of
case translate q of
Left err -> expectationFailure err
Right relOp ->
optimize testDB relOp `shouldBe` Scan Nothing "users"
@@ -857,7 +857,7 @@ main = hspec $ do
case parseQuery "SELECT u.name, o.product FROM users u JOIN orders o ON u.id = o.user_id WHERE u.age > 20" of
Left err -> expectationFailure err
Right q ->
case translate testDB q of
case translate q of
Left err -> expectationFailure err
Right relOp ->
optimize testDB (optimize testDB relOp) `shouldBe` optimize testDB relOp
@@ -980,6 +980,154 @@ main = hspec $ do
it "keeps projection with ORDER BY and LIMIT results identical" $ do
sameResultAsUnoptimized "SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id ORDER BY o.product DESC LIMIT 2"
-- ============================================================
-- Semantic
-- ============================================================
describe "ChuSQL.Semantic" $ do
it "rejects an unknown column in WHERE" $ do
(parseQuery "SELECT name FROM users WHERE nope > 18" >>= runQuery testDB)
`shouldSatisfy` isLeft
it "rejects an unknown column in WHERE even when the table is empty" $ do
(parseQuery "SELECT name FROM users WHERE nope > 18" >>= runQuery emptyDB)
`shouldSatisfy` isLeft
it "rejects an unknown column in an ON condition" $ do
(parseQuery "SELECT u.name FROM users u JOIN orders o ON u.nope = o.user_id" >>= runQuery testDB)
`shouldSatisfy` isLeft
it "rejects a type mismatch inside a comparison" $ do
(parseQuery "SELECT name FROM users WHERE name > 18" >>= runQuery testDB)
`shouldSatisfy` isLeft
it "rejects a type mismatch even when the table is empty" $ do
(parseQuery "SELECT name FROM users WHERE name > 18" >>= runQuery emptyDB)
`shouldSatisfy` isLeft
it "rejects a WHERE clause that is not a condition" $ do
(parseQuery "SELECT name FROM users WHERE age" >>= runQuery testDB)
`shouldSatisfy` isLeft
it "rejects a non-boolean WHERE even when the table is empty" $ do
(parseQuery "SELECT name FROM users WHERE age" >>= runQuery emptyDB)
`shouldSatisfy` isLeft
it "rejects comparing two different types with =" $ do
(parseQuery "SELECT name FROM users WHERE name = 18" >>= runQuery testDB)
`shouldSatisfy` isLeft
it "rejects an unknown column in INSERT" $ do
(parseQuery "INSERT INTO users (nickname) VALUES (1)" >>= runQuery testDB)
`shouldSatisfy` isLeft
it "rejects a value whose type does not match the column" $ do
(parseQuery "INSERT INTO users (name, age) VALUES ('Dave', 'abc')" >>= runQuery testDB)
`shouldSatisfy` isLeft
it "rejects an unknown column in UPDATE SET" $ do
(parseQuery "UPDATE users SET nope = 1" >>= runQuery testDB)
`shouldSatisfy` isLeft
it "rejects an unknown column in DELETE WHERE" $ do
(parseQuery "DELETE FROM users WHERE nope > 1" >>= runQuery testDB)
`shouldSatisfy` isLeft
it "still accepts a valid query" $ do
rowsOf (parseQuery "SELECT name FROM users WHERE age > 18" >>= runQuery testDB)
`shouldBe` Right
[ [("name", VStr "Alice")]
, [("name", VStr "Carol")]
]
it "rejects an UPDATE whose value has the wrong type" $ do
(parseQuery "UPDATE users SET age = 'abc'" >>= runQuery testDB)
`shouldSatisfy` isLeft
it "rejects a type mismatch in an ON condition" $ do
(parseQuery "SELECT u.name FROM users u JOIN orders o ON u.name = o.user_id" >>= runQuery testDB)
`shouldSatisfy` isLeft
it "rejects an unknown column in DELETE WHERE even when the table is empty" $ do
(parseQuery "DELETE FROM users WHERE nope > 1" >>= runQuery emptyDB)
`shouldSatisfy` isLeft
it "rejects an unknown column in UPDATE SET even when the table is empty" $ do
(parseQuery "UPDATE users SET nope = 1" >>= runQuery emptyDB)
`shouldSatisfy` isLeft
it "currently allows an INSERT that leaves a column out (known gap)" $ do
(parseQuery "INSERT INTO users (name) VALUES ('Eve')" >>= runQuery testDB)
`shouldSatisfy` isRight
it "says which clause is wrong and which columns are available" $ do
case parseQuery "SELECT name FROM users WHERE nope > 18" >>= runQuery testDB of
Right _ -> expectationFailure "expected a Left"
Left err -> do
err `shouldContain` "unknown column in WHERE"
err `shouldContain` "available: id, name, age"
it "names INSERT when a target column does not exist" $ do
case parseQuery "INSERT INTO users (nickname) VALUES (1)" >>= runQuery testDB of
Right _ -> expectationFailure "expected a Left"
Left err -> err `shouldContain` "unknown column in INSERT"
it "names ORDER BY when a sort key does not exist" $ do
case parseQuery "SELECT name FROM users ORDER BY nope" >>= runQuery testDB of
Right _ -> expectationFailure "expected a Left"
Left err -> err `shouldContain` "unknown column in ORDER BY"
it "names ON and both types when a join condition mismatches" $ do
case parseQuery "SELECT u.name FROM users u JOIN orders o ON u.name = o.user_id" >>= runQuery testDB of
Right _ -> expectationFailure "expected a Left"
Left err -> do
err `shouldContain` "ON: both sides of ="
err `shouldContain` "TStr and TInt"
it "names the column and the expected type when an assignment has the wrong type" $ do
case parseQuery "UPDATE users SET age = 'abc'" >>= runQuery testDB of
Right _ -> expectationFailure "expected a Left"
Left err -> err `shouldContain` "UPDATE: column age needs TInt, got TStr"
it "accepts SELECT * across a join (the sentinel is not a column)" $ do
(parseQuery "SELECT * FROM users u JOIN orders o ON u.id = o.user_id" >>= runQuery testDB)
`shouldSatisfy` isRight
-- ============================================================
-- Op
-- ============================================================
describe "ChuSQL.Algebra.Op" $ do
it "renders a plan as indented text" $ do
case parseQuery "SELECT name FROM users WHERE age > 18" >>= translate of
Left err -> expectationFailure err
Right plan ->
renderPlan plan
`shouldBe` "Project [\"name\"]\n Filter Gt (Col \"age\") (LitInt 18)\n Scan Nothing \"users\""
it "renders a join plan with indentation" $ do
case parseQuery "SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id" >>= translate of
Left err -> expectationFailure err
Right plan ->
lines (renderPlan plan)
`shouldBe` [ "Project [\"u.name\"]"
, " Join on Eq (Col \"u.id\") (Col \"o.user_id\")"
, " Scan Just \"u\" \"users\""
, " Scan Just \"o\" \"orders\""
]
it "renders a sort and limit plan" $ do
case parseQuery "SELECT name FROM users ORDER BY age DESC LIMIT 2" >>= translate of
Left err -> expectationFailure err
Right plan ->
lines (renderPlan plan)
`shouldBe` [ "Project [\"name\"]"
, " Limit 2"
, " Sort [(\"age\",Desc)]"
, " Scan Nothing \"users\""
]
emptyDB :: Database
emptyDB = [(n, t {tableRows = []}) | (n, t) <- testDB]
-- ============================================================
-- Helpers
-- ============================================================
@@ -987,15 +1135,16 @@ isLeft :: Either a b -> Bool
isLeft (Left _) = True
isLeft (Right _) = False
rowsOf :: Either String (Database, [Row]) -> Either String [Row]
rowsOf = fmap snd
isRight :: Either a b -> Bool
isRight (Right _) = True
isRight (Left _) = False
sameResultAsUnoptimized :: String -> Expectation
sameResultAsUnoptimized sql =
case parseQuery sql of
Left err -> expectationFailure err
Right q ->
case translate testDB q of
case translate q of
Left err -> expectationFailure err
Right relOp ->
evalRelOp testDB (optimize testDB relOp) `shouldBe` evalRelOp testDB relOp
@@ -1007,7 +1156,7 @@ filterPushedIntoLeft _ = False
optimizedPlan :: String -> Either String RelOp
optimizedPlan sql = do
q <- parseQuery sql
optimize testDB <$> translate testDB q
optimize testDB <$> translate q
firstFilterCond :: RelOp -> Maybe Expr
firstFilterCond (Filter p _) = Just p
@@ -1038,7 +1187,7 @@ stripProjects (Scan a t) = Scan a t
projectedPlan :: String -> Either String RelOp
projectedPlan sql = do
q <- parseQuery sql
plan <- translate testDB q
plan <- translate q
Right (pushProject testDB ["*"] plan)
sideCols :: RelOp -> Maybe [String]