diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index e20cc39..af3e10d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -14,7 +14,7 @@
# sh scripts/install.sh --component web --from-source
#
# 说明:macOS 两个 runner 会让总时长变长(每个 job 都要 cargo + stack 各编一遍);
-# 不需要 Intel mac 的包就把 macos-13 那两行删掉。
+# 不需要 Intel mac 的包就把 macos-15-intel 那两行删掉。
name: release
on:
@@ -43,10 +43,12 @@ jobs:
include:
- { os: ubuntu-latest, platform: linux-x86_64 }
- { os: windows-latest, platform: windows-x86_64 }
- - { os: macos-13, platform: macos-x86_64 }
- - { os: macos-14, platform: macos-arm64 }
+ - { os: macos-15-intel, platform: macos-x86_64 }
+ - { os: macos-15, platform: macos-arm64 }
steps:
- uses: actions/checkout@v4
+ with:
+ ref: ${{ inputs.tag || github.ref }}
# ---------- 工具链 ----------
- name: Rust toolchain
@@ -101,113 +103,15 @@ jobs:
working-directory: chusql-bootstrap
run: stack build --fast chusql-bootstrap:exe:csql-bootstrap
- # ---------- 装配 + 打归档(CI 里直接写在这里;本机打同一个包用 scripts/package.sh)----------
- # 归档里是包内容本身(bin/、scripts/、static/、install.sh 就在根上),安装脚本按这个布局找文件。
- - name: package (Unix)
+ - name: package and verify (Unix)
if: runner.os != 'Windows'
shell: bash
- run: |
- set -eu
- name="chusql-${{ matrix.platform }}"
- stage="dist/$name"
- rm -rf "$stage"
- mkdir -p "$stage/bin" "$stage/resources"
+ run: sh scripts/package.sh --out-dir "$PWD/dist" --platform '${{ matrix.platform }}' --version '${{ inputs.tag || github.ref_name }}' --no-build
- cp chusql-core/storage/target/release/libchusql_core_storage.* "$stage/bin/"
-
- stack_root="$(cd chusql-cli && stack path --local-install-root | tail -n 1 | tr -d '\r')"
- for front_name in chusql-web chusql-server csql; do
- front="$stack_root/bin/$front_name"
- if [ ! -f "$front" ]; then
- front="$(find chusql-web/.stack-work chusql-cli/.stack-work -name "$front_name" -type f 2>/dev/null | head -n 1 || true)"
- fi
- [ -n "$front" ] && [ -f "$front" ] || { echo "front end binary not found: $front_name" >&2; exit 1; }
- cp "$front" "$stage/bin/"
- done
-
- # 引导程序住在自己的工程里,安装根也单独问它要
- bootstrap_root="$(cd chusql-bootstrap && stack path --local-install-root | tail -n 1 | tr -d '\r')"
- bootstrap_bin="$bootstrap_root/bin/csql-bootstrap"
- if [ ! -f "$bootstrap_bin" ]; then
- bootstrap_bin="$(find chusql-bootstrap/.stack-work -name csql-bootstrap -type f 2>/dev/null | head -n 1 || true)"
- fi
- [ -n "$bootstrap_bin" ] && [ -f "$bootstrap_bin" ] || { echo "csql-bootstrap not found" >&2; exit 1; }
- cp "$bootstrap_bin" "$stage/bin/"
-
- # Haskell 一般静态链接,靶目录里真有共享库就一起带上;改名前的旧库(libchusql_storage.*)不能进包
- for lib in chusql-core/storage/target/release/*.so chusql-core/storage/target/release/*.dylib; do
- [ -f "$lib" ] || continue
- case "$(basename "$lib")" in
- libchusql_storage.*|libchusql_storage-*) continue ;;
- esac
- cp "$lib" "$stage/bin/"
- done
-
- cp resources/settings.toml.* "$stage/resources/"
- cp scripts/install.ps1 scripts/install.sh "$stage/"
- cp scripts/uninstall.ps1 scripts/uninstall.sh "$stage/"
- mkdir -p "$stage/static"
- cp -R chusql-web/static/. "$stage/static/"
- cp scripts/csql-web.sh "$stage/"
- if [ -f scripts/csql-web.ps1 ]; then cp scripts/csql-web.ps1 "$stage/"; fi
-
- chmod +x "$stage/install.sh" "$stage/uninstall.sh" "$stage/csql-web.sh"
-
- mkdir -p dist
- tar -czf "dist/$name.tar.gz" -C "$stage" .
- rm -rf "$stage"
- ls -l "dist/$name.tar.gz"
-
- - name: package (Windows)
+ - name: package and verify (Windows)
if: runner.os == 'Windows'
shell: pwsh
- run: |
- $ErrorActionPreference = 'Stop'
- $name = "chusql-${{ matrix.platform }}"
- $stage = "dist/$name"
- if (Test-Path $stage) { Remove-Item -Recurse -Force $stage }
- New-Item -ItemType Directory -Force -Path "$stage/bin", "$stage/resources" | Out-Null
-
- Copy-Item 'chusql-core/storage/target/release/chusql_core_storage.dll' "$stage/bin/"
-
- $stackRoot = (& stack path --local-install-root | Select-Object -Last 1).Trim()
- foreach ($frontName in @('chusql-web.exe', 'chusql-server.exe', 'csql.exe')) {
- $front = Join-Path $stackRoot "bin/$frontName"
- if (-not (Test-Path $front)) {
- $front = Get-ChildItem -Path 'chusql-web/.stack-work', 'chusql-cli/.stack-work' -Recurse -Filter $frontName -File -ErrorAction SilentlyContinue |
- Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName
- }
- if (-not $front -or -not (Test-Path $front)) { throw "front end binary not found: $frontName" }
- Copy-Item $front "$stage/bin/"
- }
-
- $bootstrapRoot = (& stack path --local-install-root | Select-Object -Last 1).Trim()
- $bootstrapExe = Join-Path $bootstrapRoot 'bin/csql-bootstrap.exe'
- if (-not (Test-Path $bootstrapExe)) {
- $bootstrapExe = Get-ChildItem -Path 'chusql-bootstrap/.stack-work' -Recurse -Filter 'csql-bootstrap.exe' -File -ErrorAction SilentlyContinue |
- Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName
- }
- if (-not $bootstrapExe -or -not (Test-Path $bootstrapExe)) { throw 'csql-bootstrap.exe not found' }
- Copy-Item $bootstrapExe "$stage/bin/"
-
- # 第三方 dll 才要连带上:名字带 chusql 的是本 crate 产物或改名前的残留,上面已按名拷过
- Get-ChildItem 'chusql-core/storage/target/release' -Filter '*.dll' -File -ErrorAction SilentlyContinue |
- Where-Object { $_.Name -notlike 'chusql*' } |
- ForEach-Object { Copy-Item $_.FullName "$stage/bin/" }
-
- Copy-Item 'resources/settings.toml.*' "$stage/resources/"
- Copy-Item 'scripts/install.ps1', 'scripts/install.sh' $stage
- Copy-Item 'scripts/uninstall.ps1', 'scripts/uninstall.sh' $stage
- New-Item -ItemType Directory -Force -Path "$stage/static" | Out-Null
- Copy-Item 'chusql-web/static/*' "$stage/static" -Recurse
- Copy-Item 'scripts/csql-web.sh' $stage
- if (Test-Path 'scripts/csql-web.ps1') { Copy-Item 'scripts/csql-web.ps1' $stage }
-
- New-Item -ItemType Directory -Force -Path dist | Out-Null
- Compress-Archive -Path "$stage/*" -DestinationPath "dist/$name.zip" -Force
- Remove-Item -Recurse -Force $stage
- Get-Item "dist/$name.zip" | Select-Object Name, Length | Format-Table
-
+ run: ./scripts/package.ps1 -OutDir dist -Platform '${{ matrix.platform }}' -Version '${{ inputs.tag || github.ref_name }}' -NoBuild
- name: checksum
shell: pwsh
run: |
diff --git a/README.md b/README.md
index e2bad18..83b1ad5 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,13 @@
# ChuSQL
-[](https://github.com/TheBadRoger/ChuSQL/releases) [](https://github.com/TheBadRoger/ChuSQL/commits/main)  [](scripts/update-code-lines.ps1)
-
-[](https://github.com/TheBadRoger/ChuSQL/actions/workflows/build.yml) [](https://github.com/TheBadRoger/ChuSQL/actions/workflows/linux.yml) [](https://github.com/TheBadRoger/ChuSQL/actions/workflows/gate.yml)
+
+
+
+
+
+
+
+
ChuSQL 是采用 Haskell SQL 引擎与 Rust 存储层的数据库项目,提供 TCP 服务、CLI 和 Web 管理端。支持查询优化、快照事务、WAL 崩溃恢复、DOMAIN 自定义类型及用户与角色管理。
diff --git a/benchmark/src/Main.hs b/benchmark/src/Main.hs
index 00be0ef..48eb9e0 100644
--- a/benchmark/src/Main.hs
+++ b/benchmark/src/Main.hs
@@ -32,7 +32,7 @@ import System.Environment (getArgs)
import System.Exit (ExitCode (..), exitFailure)
import System.FilePath ((>))
import System.Info (os)
-import System.IO (Handle, hGetLine)
+import System.IO (Handle, hGetLine, hSetBuffering, BufferMode (LineBuffering), stdout)
import System.Process (
CreateProcess (std_err, std_out),
ProcessHandle,
@@ -484,6 +484,7 @@ stopProcess process = do
-- | 主入口
main :: IO ()
main = do
+ hSetBuffering stdout LineBuffering
args <- getArgs
unless (length args <= 2) (ioError (userError "usage: benchmark4csql [users=10000] [orders=50000]"))
let values = take 2 (args ++ drop (length args) ["10000", "50000"])
@@ -588,9 +589,10 @@ pressureScenarios bench u o = do
_ <- runSql bench "CREATE TABLE writes (id int, name str, age int)"
_ <- batchInsert bench "200 individual durable INSERTs" 200 (\i -> printf "INSERT INTO writes (id,name,age) VALUES (%d,'w%d',%d)" i i i)
(milliseconds, ()) <- measure $ do
- _ <- runSql bench "BEGIN"
- forM_ [201 .. 400 :: Int] $ \i -> runSql bench (printf "INSERT INTO writes (id,name,age) VALUES (%d,'w%d',%d)" i i i) >> pure ()
- _ <- runSql bench "COMMIT"
+ (beginMs, _) <- measure (runSql bench "BEGIN")
+ (stagedMs, ()) <- measure $ forM_ [201 .. 400 :: Int] $ \i -> runSql bench (printf "INSERT INTO writes (id,name,age) VALUES (%d,'w%d',%d)" i i i) >> pure ()
+ (commitMs, _) <- measure (runSql bench "COMMIT")
+ printf "TXN_PHASE begin_ms=%.3f staged_ms=%.3f commit_ms=%.3f\n" beginMs stagedMs commitMs
pure ()
printf "WRITE transaction200 total_ms=%.3f rows_s=%.2f\n" milliseconds (200000 / milliseconds)
checkCount bench "writes" 400
diff --git a/chusql-bootstrap/test/Spec.hs b/chusql-bootstrap/test/Spec.hs
index 5722eb4..c45af06 100644
--- a/chusql-bootstrap/test/Spec.hs
+++ b/chusql-bootstrap/test/Spec.hs
@@ -81,7 +81,7 @@ ready = A.object ["status" A..= ("system" :: Text), "initialized" A..= True]
-- | 带最高权限属性的身份响应
identity :: Bool -> A.Value
-identity super = A.object ["status" A..= ("accounts" :: Text), "accounts" A..= [Account 1 "root" "existing-hash" 1 "" Nothing True super True False]]
+identity super = A.object ["status" A..= ("accounts" :: Text), "accounts" A..= [Account 1 "root" "existing-hash" 1 "" Nothing True super True False False]]
-- | 已写入编号的类型响应
installed :: A.Value
diff --git a/chusql-cli/app/Main.hs b/chusql-cli/app/Main.hs
index b88ef36..8f41076 100644
--- a/chusql-cli/app/Main.hs
+++ b/chusql-cli/app/Main.hs
@@ -8,12 +8,14 @@ import ChuSQL.CLI.Script (stripTerminator)
import ChuSQL.Interface.Session (
Session,
authenticateSession,
+ authenticateSudoSession,
isPlainIdentifier,
newSession,
switchDatabase,
)
import ChuSQL.Interface.Config (ServerConfig (..), loadServerConfigAt, loadWebConfigAt, rootUserName)
import ChuSQL.Interface.Link (Client, closeClient, connectClient)
+import ChuSQL.Interface.Sudo (SudoCredential, readSudoCredential)
import ChuSQL.Interface.TOML (resolveConfigPath)
import Control.Exception (finally)
import Control.Monad (unless)
@@ -38,6 +40,7 @@ data Options = Options
, optExecute :: Maybe Text
, optHistory :: Maybe FilePath
, optConfig :: Maybe FilePath
+ , optSudo :: Bool
}
-- | 命令行参数解析器
@@ -66,6 +69,7 @@ optionsParser =
<*> optional (T.pack <$> strOption (long "execute" <> short 'e' <> metavar "SQL" <> help "Run one statement and exit"))
<*> optional (strOption (long "history" <> metavar "FILE" <> help "SQL history file (default ~/.chusql_history)"))
<*> optional (strOption (long "config" <> metavar "FILE" <> help "Config file to read (default: the fixed settings.toml under the system config directory)"))
+ <*> switch (long "sudo" <> help "Authenticate using the local root or elevated administrator identity")
-- | 把 --format 的取值解析成格式
formatReader :: String -> Either String OutputFormat
@@ -91,20 +95,29 @@ main = do
base <- loadWebConfigAt configPath
config <- loadServerConfigAt configPath
let rootName = rootUserName base
- userName = fromMaybe rootName (optUser options)
- password <- promptPassword userName
+ userName = fromMaybe (if optSudo options then scSudoUser config else rootName) (optUser options)
+ credential <- if optSudo options then do
+ if T.null userName then reportError "sudo_auth_user is not configured" >> exitFailure else pure ()
+ local <- readSudoCredential (scPort config)
+ case local of
+ Left message -> reportError message >> exitFailure
+ Right proof -> pure (Just proof)
+ else pure Nothing
+ password <- if optSudo options then pure "" else promptPassword userName
connected <- connectClient (T.pack (scHost config)) (scPort config)
case connected of
Left message -> reportError message >> exitFailure
Right client ->
- connect options client userName password
+ connect options client userName password credential
`finally` closeClient client
-- | 连 server、登录,之后转入会话
-connect :: Options -> Client -> Text -> Text -> IO ()
-connect options client userName password = do
+connect :: Options -> Client -> Text -> Text -> Maybe SudoCredential -> IO ()
+connect options client userName password credential = do
session <- newSession client
- signedIn <- authenticateSession session userName password
+ signedIn <- case credential of
+ Just proof -> authenticateSudoSession session proof userName
+ Nothing -> authenticateSession session userName password
case signedIn of
Left message -> reportError message >> exitFailure
Right () -> runSession options session
diff --git a/chusql-cli/src/ChuSQL/CLI/Format.hs b/chusql-cli/src/ChuSQL/CLI/Format.hs
index 4b1a275..1e7ca73 100644
--- a/chusql-cli/src/ChuSQL/CLI/Format.hs
+++ b/chusql-cli/src/ChuSQL/CLI/Format.hs
@@ -14,6 +14,7 @@ module ChuSQL.CLI.Format
import ChuSQL.Core.Model (Value (..))
import ChuSQL.Core.Protocol (QueryResult (..), queryResultJson)
+import ChuSQL.Core.Runtime (renderRuntimeValue)
import qualified Data.Aeson as A
import qualified Data.ByteString.Lazy as BL
import Data.Text (Text)
@@ -109,3 +110,4 @@ displayValue (VFloat d) = T.pack (show d)
displayValue (VStr s) = T.pack s
displayValue (VBool True) = "true"
displayValue (VBool False) = "false"
+displayValue (VRuntime _ value) = T.pack (renderRuntimeValue value)
diff --git a/chusql-cli/test/Spec.hs b/chusql-cli/test/Spec.hs
index 5fa30ea..f050f33 100644
--- a/chusql-cli/test/Spec.hs
+++ b/chusql-cli/test/Spec.hs
@@ -2,12 +2,13 @@
module Main (main) where
-import ChuSQL.CLI.Format (OutputFormat (..), parseFormat, renderCsv, renderJson, renderTable)
+import ChuSQL.CLI.Format (OutputFormat (..), parseFormat, renderCsv, renderJson, renderTable, displayValue)
import ChuSQL.CLI.History (historySettings, rememberStatement)
import ChuSQL.CLI.Password (readPasswordInput)
import ChuSQL.CLI.Script (Meta (..), errorHint, parseMeta, statementComplete, stripTerminator, takeStatement)
import ChuSQL.Core.Model (Value (..))
import ChuSQL.Core.Protocol (QueryResult (..), queryResultJson)
+import ChuSQL.Core.Runtime
import Data.Aeson (decode)
import qualified Data.Aeson as A
import qualified Data.ByteString.Lazy as BL
@@ -30,6 +31,10 @@ main = hspec spec
-- | 全部测试用例
spec :: Spec
spec = do
+ it "renders typed compound values without losing their wire identity" $ do
+ case resolveType builtinTypes (listType intType) of
+ Left err -> expectationFailure err
+ Right descriptor -> displayValue (VRuntime (descriptorId descriptor) (RList [RInt 1, RInt 2])) `shouldBe` "[1, 2]"
formatSpec
scriptSpec
passwordSpec
diff --git a/chusql-core/engine/chusql-core-engine.cabal b/chusql-core/engine/chusql-core-engine.cabal
index 66bf25f..ad5ad84 100644
--- a/chusql-core/engine/chusql-core-engine.cabal
+++ b/chusql-core/engine/chusql-core-engine.cabal
@@ -22,6 +22,12 @@ library
ChuSQL.Core.Engine.Error
ChuSQL.Core.Engine
ChuSQL.Core.Engine.Parallel
+ ChuSQL.Core.Engine.Normalization
+ ChuSQL.Core.Engine.Runtime.Types
+ ChuSQL.Core.Engine.Runtime.Functions
+ ChuSQL.Core.Engine.Runtime.Binding
+ ChuSQL.Core.Engine.Runtime.SQL
+ ChuSQL.Core.Engine.Runtime.Query
ChuSQL.Core.Engine.Semantic
ChuSQL.Core.Engine.Storage
ChuSQL.Core.Engine.Storage.FFI
@@ -39,6 +45,7 @@ library
, scientific
, text
, unordered-containers
+ , vector
extra-libraries: chusql_core_storage
-- 运行期找 libchusql_core_storage.so:$ORIGIN 让二进制与动态库同目录即可
if os(linux)
@@ -51,6 +58,8 @@ library
test-suite chusql-core-engine-test
type: exitcode-stdio-1.0
main-is: Spec.hs
+ other-modules: NormalizationSpec
+ RuntimeSpec
hs-source-dirs: test
build-depends: base
, chusql-core-engine
@@ -59,5 +68,7 @@ test-suite chusql-core-engine-test
, filepath
, hashable
, hspec
+ , bytestring
+ , aeson
default-language: Haskell2010
ghc-options: -Wall -threaded
diff --git a/chusql-core/engine/src/ChuSQL/Core/Engine.hs b/chusql-core/engine/src/ChuSQL/Core/Engine.hs
index 30e7abd..3890494 100644
--- a/chusql-core/engine/src/ChuSQL/Core/Engine.hs
+++ b/chusql-core/engine/src/ChuSQL/Core/Engine.hs
@@ -1,11 +1,10 @@
module ChuSQL.Core.Engine (runStatement, runStatementM, rowsOf) where
-import ChuSQL.Core.Engine.Algebra.Eval (evalCondForRowM, evalExprM, evalRelOpM)
+import ChuSQL.Core.Engine.Algebra.Eval (evalCondForRowM, evalExprM)
import ChuSQL.Core.Engine.Algebra.Expr (evalCondForRow, evalExpr)
-import ChuSQL.Core.Engine.Algebra.Optimize (optimize)
-import ChuSQL.Core.Engine.Algebra.Planner (translate)
+import ChuSQL.Core.Engine.Runtime.Query (bindQuery, executeQueryM)
import ChuSQL.Core.Model
-import ChuSQL.Core.Engine.Semantic (prepare)
+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
@@ -41,13 +40,13 @@ runStatementM q = do
-- | 按语句类型分发(已检查过)
runStatementUncheckedM :: (MonadStorage m) => Database -> Statement -> m (Either String [Row])
runStatementUncheckedM db q@Select{} =
- case translate q of
+ case bindQuery db q of
Left e -> pure (Left e)
- Right relOp -> evalRelOpM db (optimize db relOp)
+ Right bound -> executeQueryM bound
runStatementUncheckedM db q@SelectExpr{} =
- case translate q of
+ case bindQuery db q of
Left e -> pure (Left e)
- Right relOp -> evalRelOpM db (optimize db relOp)
+ Right bound -> executeQueryM bound
runStatementUncheckedM db (Insert tbl cols rows) =
case lookupTable db tbl of
Left e -> pure (Left e)
@@ -241,7 +240,9 @@ checkPlan table = mapM parse (mapMaybe checked (tableCols table))
-- | 解析一列的 CHECK 文本
parse (name, text) = case parseExpression text of
Left err -> Left ("CHECK on " ++ name ++ ": " ++ firstLine err)
- Right e -> Right (name, e)
+ Right e -> do
+ bound <- prepareRowExpression (tableCols table) e
+ Right (name, bound)
-- | 执行解析好的 CHECK 条件
runChecks :: CheckPlan -> Row -> Either String ()
diff --git a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Eval.hs b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Eval.hs
index 05ce3bf..284ada8 100644
--- a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Eval.hs
+++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Eval.hs
@@ -36,6 +36,7 @@ valueLiteral (VInt n) = LitInt n
valueLiteral (VFloat d) = LitFloat d
valueLiteral (VStr s) = LitStr s
valueLiteral (VBool b) = LitBool b
+valueLiteral (VRuntime tid value) = RuntimeLiteral tid value
valueLiteral VNull = LitNull
-- | 点查退化成扫描时用的条件
@@ -362,6 +363,18 @@ scalarResult rows = do
-- | 把子查询求值成字面量;子查询的错直接往上传
substSubqueries :: (MonadStorage m) => Database -> Expr -> Row -> m (Either String Expr)
substSubqueries db e env = case e of
+ Construct tid arguments -> do
+ bound <- mapM (\argument -> substSubqueries db argument env) arguments
+ pure (Construct tid <$> sequence bound)
+ BoundConstruct tid arguments -> do
+ bound <- mapM (\argument -> substSubqueries db argument env) arguments
+ pure (BoundConstruct tid <$> sequence bound)
+ FunctionCall name arguments -> do
+ bound <- mapM (\argument -> substSubqueries db argument env) arguments
+ pure (FunctionCall name <$> sequence bound)
+ BoundFunction fid tid arguments -> do
+ bound <- mapM (\argument -> substSubqueries db argument env) arguments
+ pure (BoundFunction fid tid <$> sequence bound)
ScalarSub sq -> scalarResult <$> runSubqueryRows db sq env
InSub a sq negated -> do
lhs <- substSubqueries db a env
diff --git a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Expr.hs b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Expr.hs
index 8b4abf0..a5f261f 100644
--- a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Expr.hs
+++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Expr.hs
@@ -3,6 +3,7 @@ module ChuSQL.Core.Engine.Algebra.Expr (evalExpr, evalCondForRow, colsInExpr, su
import ChuSQL.Core.Model
import ChuSQL.Core.Engine.Syntax.AST
import ChuSQL.Core.Engine.Builtin (Operator (..), compareNode, executeOperator, inValues, threeValuedNot)
+import ChuSQL.Core.Engine.Runtime.SQL (invokeSQLFunction, constructSQLValue)
-- 表达式求值:三值逻辑,NULL 参与运算结果仍是 NULL。
@@ -10,6 +11,12 @@ import ChuSQL.Core.Engine.Builtin (Operator (..), compareNode, executeOperator,
evalExpr :: Expr -> Row -> Either String Value
evalExpr (Col name) row =
maybe (Left ("unknown column: " ++ name)) Right (lookup name 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
+evalExpr (RuntimeLiteral tid value) _ = coerceValue (CRuntime tid) (VRuntime tid value)
+evalExpr (BoundFunction fid tid arguments) row =
+ mapM (\argument -> evalExpr argument row) arguments >>= invokeSQLFunction fid tid
evalExpr LitNull _ = Right VNull
evalExpr (LitInt n) _ = Right (VInt n)
evalExpr (LitFloat d) _ = Right (VFloat d)
@@ -63,6 +70,10 @@ quantifiedCompare op lhs vs q = case vs of
-- | 表达式用到了哪些列
colsInExpr :: Expr -> [String]
colsInExpr (Col c) = [c]
+colsInExpr (Construct _ arguments) = concatMap colsInExpr arguments
+colsInExpr (BoundConstruct _ arguments) = concatMap colsInExpr arguments
+colsInExpr (FunctionCall _ arguments) = concatMap colsInExpr arguments
+colsInExpr (BoundFunction _ _ arguments) = concatMap colsInExpr arguments
colsInExpr (Add a b) = colsInExpr a ++ colsInExpr b
colsInExpr (Sub a b) = colsInExpr a ++ colsInExpr b
colsInExpr (Mul a b) = colsInExpr a ++ colsInExpr b
@@ -93,6 +104,10 @@ colsInExpr _ = []
-- | 表达式里子查询引用的外层列
subqueryRefsIn :: Expr -> [String]
subqueryRefsIn e = case e of
+ Construct _ arguments -> concatMap subqueryRefsIn arguments
+ BoundConstruct _ arguments -> concatMap subqueryRefsIn arguments
+ FunctionCall _ arguments -> concatMap subqueryRefsIn arguments
+ BoundFunction _ _ arguments -> concatMap subqueryRefsIn arguments
ScalarSub sq -> subqueryRefs sq
InSub a sq _ -> subqueryRefsIn a ++ subqueryRefs sq
InList a es _ -> concatMap subqueryRefsIn (a : es)
@@ -126,6 +141,10 @@ subqueryRefsIn e = case e of
-- | 表达式里有没有除法(除法可能报错,不能提前或延后求值)
hasDivision :: Expr -> Bool
hasDivision e = case e of
+ Construct _ arguments -> any hasDivision arguments
+ BoundConstruct _ arguments -> any hasDivision arguments
+ FunctionCall _ arguments -> any hasDivision arguments
+ BoundFunction _ _ arguments -> any hasDivision arguments
Div _ _ -> True
Neg a -> hasDivision a
Add a b -> both a b
@@ -148,6 +167,10 @@ hasDivision e = case e of
-- | 表达式里有没有子查询
hasSubquery :: Expr -> Bool
hasSubquery e = case e of
+ Construct _ arguments -> any hasSubquery arguments
+ BoundConstruct _ arguments -> any hasSubquery arguments
+ FunctionCall _ arguments -> any hasSubquery arguments
+ BoundFunction _ _ arguments -> any hasSubquery arguments
ScalarSub _ -> True
InSub _ _ _ -> True
InList a es _ -> any hasSubquery (a : es)
@@ -181,6 +204,10 @@ hasSubquery e = case e of
-- | 表达式里出现的聚合调用(按出现顺序)
aggregatesIn :: Expr -> [Expr]
aggregatesIn e = case e of
+ Construct _ arguments -> concatMap aggregatesIn arguments
+ BoundConstruct _ arguments -> concatMap aggregatesIn arguments
+ FunctionCall _ arguments -> concatMap aggregatesIn arguments
+ BoundFunction _ _ arguments -> concatMap aggregatesIn arguments
a@CountAll -> [a]
a@(CountOf _) -> [a]
a@(SumOf _) -> [a]
@@ -215,6 +242,10 @@ hasAggregate = not . null . aggregatesIn
-- | 聚合之外的列引用(聚合参数里的列不算)
bareColumns :: Expr -> [String]
bareColumns e = case e of
+ Construct _ arguments -> concatMap bareColumns arguments
+ BoundConstruct _ arguments -> concatMap bareColumns arguments
+ FunctionCall _ arguments -> concatMap bareColumns arguments
+ BoundFunction _ _ arguments -> concatMap bareColumns arguments
Col c -> [c]
Add a b -> both a b
Sub a b -> both a b
diff --git a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Optimize.hs b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Optimize.hs
index edaf178..45ef888 100644
--- a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Optimize.hs
+++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Optimize.hs
@@ -139,6 +139,7 @@ foldNode e
Right (VFloat d) -> LitFloat d
Right (VStr s) -> LitStr s
Right (VBool b) -> LitBool b
+ Right (VRuntime tid value) -> RuntimeLiteral tid value
Left _ -> e
| otherwise = e
@@ -149,6 +150,8 @@ foldConstants (Sub a b) = foldNode (Sub (foldConstants a) (foldConstants b))
foldConstants (Mul a b) = foldNode (Mul (foldConstants a) (foldConstants b))
foldConstants (Div a b) = foldNode (Div (foldConstants a) (foldConstants b))
foldConstants (Neg a) = foldNode (Neg (foldConstants a))
+foldConstants (BoundConstruct tid arguments) = BoundConstruct tid (map foldConstants arguments)
+foldConstants (BoundFunction fid tid arguments) = foldNode (BoundFunction fid tid (map foldConstants arguments))
foldConstants (Gt a b) = foldNode (Gt (foldConstants a) (foldConstants b))
foldConstants (Lt a b) = foldNode (Lt (foldConstants a) (foldConstants b))
foldConstants (Eq a b) = foldNode (Eq (foldConstants a) (foldConstants b))
diff --git a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Planner.hs b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Planner.hs
index 6fa8ebd..753bb50 100644
--- a/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Planner.hs
+++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Algebra/Planner.hs
@@ -37,6 +37,10 @@ rewriteAggs :: [(String, Expr)] -> Expr -> Expr
rewriteAggs aggs e = case [label | (label, agg) <- aggs, agg == e] of
(label : _) -> Col label
[] -> case e of
+ Construct tid arguments -> Construct tid (map go arguments)
+ BoundConstruct tid arguments -> BoundConstruct tid (map go arguments)
+ FunctionCall name arguments -> FunctionCall name (map go arguments)
+ BoundFunction fid tid arguments -> BoundFunction fid tid (map go arguments)
Add a b -> Add (go a) (go b)
Sub a b -> Sub (go a) (go b)
Mul a b -> Mul (go a) (go b)
diff --git a/chusql-core/engine/src/ChuSQL/Core/Engine/Normalization.hs b/chusql-core/engine/src/ChuSQL/Core/Engine/Normalization.hs
new file mode 100644
index 0000000..9ba57b4
--- /dev/null
+++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Normalization.hs
@@ -0,0 +1,125 @@
+module ChuSQL.Core.Engine.Normalization where
+
+import ChuSQL.Core.Model (Column)
+import Data.List (nub, sort, subsequences, (\\))
+
+-- 只读范式分析:声明验证、阿姆斯特朗闭包、候选键与分解建议。
+
+type Attributes = [String]
+data Dependency = Dependency Attributes Attributes deriving (Eq, Show)
+data Verdict = Satisfied | Violated | Insufficient deriving (Eq, Show)
+data NormalForm = NF1 | NF2 | NF3 | BCNF deriving (Eq, Show)
+data Declaration = Declaration
+ { declaredKeys :: [Attributes]
+ , dependencies :: [Dependency]
+ , dependenciesComplete :: Bool
+ , atomicAttributes :: Maybe Bool
+ } deriving (Eq, Show)
+data Finding = Finding
+ { normalForm :: NormalForm
+ , verdict :: Verdict
+ , witnesses :: [Dependency]
+ , condition :: String
+ } deriving (Eq, Show)
+data Decomposition = Decomposition
+ { relations :: [Attributes]
+ , losslessJoin :: Verdict
+ , dependencyPreserving :: Verdict
+ } deriving (Eq, Show)
+data Analysis = Analysis
+ { candidateKeys :: [Attributes]
+ , findings :: [Finding]
+ , suggestions :: [Decomposition]
+ } deriving (Eq, Show)
+
+-- 规范化属性集合。
+canonical :: Attributes -> Attributes
+canonical = sort . nub
+
+-- 判断属性集合包含关系。
+subset :: Attributes -> Attributes -> Bool
+subset xs ys = all (`elem` ys) xs
+
+-- 用自反、增广和传递规则求闭包。
+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
+
+-- 判断声明是否蕴含依赖。
+implies :: [Dependency] -> Dependency -> Bool
+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]]
+ where
+ -- 判断闭包是否覆盖关系。
+ super x = subset attrs (attributeClosure f x)
+
+-- 投影关系上的全部非平凡依赖。
+projectDependencies :: [Dependency] -> Attributes -> [Dependency]
+projectDependencies f attrs =
+ [Dependency x [a] | x <- subsequences attrs,
+ a <- attributeClosure f x, a `elem` attrs, a `notElem` x]
+
+-- 验证声明并生成只读范式报告。
+analyzeNormalization :: [(String, Column)] -> Declaration -> Either String Analysis
+analyzeNormalization schema declaration
+ | null attrs = Left "normalization: empty schema"
+ | length attrs /= length schema = Left "normalization: duplicate columns"
+ | length attrs > 12 = Left "normalization: exhaustive analysis supports at most 12 columns"
+ | any invalidDependency supplied = Left "normalization: unknown or duplicate dependency attributes"
+ | any invalidKey rawKeys = Left "normalization: unknown or duplicate key attributes"
+ | any (`notElem` keys) normalizedKeys = Left "normalization: declared candidate key is not minimal"
+ | otherwise = Right (Analysis keys results decompositions)
+ where
+ attrs = canonical (map fst schema)
+ supplied = dependencies declaration
+ rawKeys = declaredKeys declaration
+ normalizedKeys = map canonical rawKeys
+ f = supplied ++ [Dependency k attrs | k <- normalizedKeys]
+ keys = keysOf attrs f
+ prime = nub (concat keys)
+ complete = dependenciesComplete declaration
+ atomic = case atomicAttributes declaration of
+ Just True -> Satisfied
+ 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]
+ decompositions = [split d | d <- take 1 boyce, complete, atomic == Satisfied]
+ -- 校验字段集合。
+ valid xs = length xs == length (canonical xs) && subset xs attrs
+ -- 校验依赖声明。
+ invalidDependency (Dependency a b) = not (valid a && valid b) || null b
+ -- 校验候选键声明。
+ invalidKey k = not (valid k)
+ -- 结合声明完整性判定范式。
+ 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
+ -- 说明各范式的必要条件。
+ rule NF1 = "attributes must have atomic domains"
+ rule NF2 = "no nonprime attribute depends on a proper subset of any candidate key"
+ rule NF3 = "each nontrivial dependency has a superkey determinant or a prime dependent"
+ rule BCNF = "each nontrivial dependency has a superkey determinant"
+ -- 按违反依赖生成一次无损分解。
+ split (Dependency x y) = Decomposition parts Satisfied preservation
+ where
+ parts = [canonical (x ++ y), attrs \\ (y \\ x)]
+ projected = concatMap (projectDependencies f) parts
+ preservation = if all (implies projected) f then Satisfied else Violated
diff --git a/chusql-core/engine/src/ChuSQL/Core/Engine/Runtime/Binding.hs b/chusql-core/engine/src/ChuSQL/Core/Engine/Runtime/Binding.hs
new file mode 100644
index 0000000..6d35ef8
--- /dev/null
+++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Runtime/Binding.hs
@@ -0,0 +1,177 @@
+module ChuSQL.Core.Engine.Runtime.Binding
+ ( Expression (..), TypedExpression, expressionType, bindExpression, evaluateExpression
+ , Plan (..), TypedPlan, planSchema, bindPlan, executePlan
+ ) where
+
+import ChuSQL.Core.Engine.Runtime.Types
+import ChuSQL.Core.Engine.Runtime.Functions
+import Control.Monad (filterM)
+import Data.List (nub, sort)
+
+-- 仅依据 schema 绑定表达式与查询,执行已解析的类型和函数身份。
+
+-- | 表达未绑定的类型化语言
+data Expression = Literal TypeExpr RuntimeValue | Reference String | Apply String [Expression]
+ | ListLiteral TypeExpr [Expression] | NothingLiteral TypeExpr | JustLiteral Expression
+ | TupleLiteral [Expression] deriving (Show, Eq)
+
+-- | 保存绑定后的表达式节点
+data ExpressionNode = Constant RuntimeValue | BoundReference String | BoundApply FunctionId [TypedExpression]
+ | BoundList [TypedExpression] | BoundNothing | BoundJust TypedExpression | BoundTuple [TypedExpression]
+ deriving (Show, Eq)
+
+-- | 保存表达式的确定类型
+data TypedExpression = TypedExpression TypeId ExpressionNode deriving (Show, Eq)
+
+-- | 提取表达式类型身份
+expressionType :: TypedExpression -> TypeId
+expressionType (TypedExpression tid _) = tid
+
+-- | 绑定类型、列与函数身份
+bindExpression :: TypeRegistry -> FunctionRegistry -> [(String, TypeId)] -> Expression -> Either String TypedExpression
+bindExpression types functions env expression = case expression of
+ Literal ty value -> do
+ 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))
+ [] -> Left ("unknown column: " ++ name)
+ _ -> Left ("ambiguous column: " ++ name)
+ Apply name arguments -> do
+ bound <- mapM bind arguments
+ sig <- resolveFunction functions name (map expressionType bound)
+ Right (TypedExpression (signatureResult sig) (BoundApply (signatureId sig) bound))
+ ListLiteral child arguments -> do
+ childId <- descriptorId <$> resolveType types child
+ bound <- mapM bind arguments
+ if all ((== childId) . expressionType) bound
+ then compound (listType child) (BoundList bound)
+ else Left "list element type mismatch"
+ NothingLiteral child -> compound (maybeType child) BoundNothing
+ JustLiteral argument -> do
+ bound <- bind argument
+ compound (maybeType (typeExpression (expressionType bound))) (BoundJust bound)
+ TupleLiteral arguments -> do
+ bound <- mapM bind arguments
+ compound (tupleType (map (typeExpression . expressionType) bound)) (BoundTuple bound)
+ where
+ -- | 绑定当前作用域的子表达式
+ bind = bindExpression types functions env
+ -- | 校验并构造复合表达式
+ compound ty node = do
+ tid <- descriptorId <$> resolveType types ty
+ Right (TypedExpression tid node)
+
+-- | 执行已绑定表达式并校验输出
+evaluateExpression :: TypeRegistry -> FunctionRegistry -> [(String, RuntimeValue)] -> TypedExpression -> Either 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
+ [] -> Left ("missing bound column: " ++ name)
+ _ -> Left ("duplicate bound column: " ++ name)
+ BoundApply fid arguments -> mapM evaluate arguments >>= invokeFunction types functions fid
+ BoundList arguments -> RList <$> mapM evaluate arguments
+ BoundNothing -> Right (RMaybe Nothing)
+ BoundJust argument -> RMaybe . Just <$> evaluate argument
+ BoundTuple arguments -> RTuple <$> mapM evaluate arguments
+ validateValue types tid value
+ Right value
+ where
+ -- | 执行当前行的子表达式
+ evaluate = evaluateExpression types functions row
+
+-- | 表达未绑定的关系计划
+data Plan = TableScan String | SingleRow | Projection [(String, Expression)] Plan
+ | Selection Expression Plan deriving (Show, Eq)
+
+-- | 保存绑定后的关系节点
+data PlanNode = BoundScan String | BoundSingleRow | BoundProjection [(String, TypedExpression)] TypedPlan
+ | BoundSelection TypedExpression TypedPlan deriving (Show, Eq)
+
+-- | 保存输出 schema 和关系节点
+data TypedPlan = TypedPlan [(String, TypeId)] PlanNode deriving (Show, Eq)
+
+-- | 提取计划的输出 schema
+planSchema :: TypedPlan -> [(String, TypeId)]
+planSchema (TypedPlan output _) = output
+
+-- | 使用目录 schema 绑定查询计划
+bindPlan :: TypeRegistry -> FunctionRegistry -> [(String, [(String, TypeExpr)])] -> Plan -> Either String TypedPlan
+bindPlan types functions catalog plan = case plan of
+ TableScan name -> case [columns | (key, columns) <- catalog, key == name] of
+ [columns] -> do
+ uniqueNames (map fst columns)
+ bound <- mapM bindColumn columns
+ Right (TypedPlan bound (BoundScan name))
+ [] -> Left ("unknown table: " ++ name)
+ _ -> Left ("ambiguous table: " ++ name)
+ SingleRow -> Right (TypedPlan [] BoundSingleRow)
+ Projection items source -> do
+ child <- bind source
+ uniqueNames (map fst items)
+ bound <- mapM (bindItem (planSchema child)) items
+ Right (TypedPlan [(name, expressionType value) | (name, value) <- bound] (BoundProjection bound child))
+ Selection condition source -> do
+ child <- bind source
+ predicate <- bindExpression types functions (planSchema child) condition
+ bool <- descriptorId <$> resolveType types boolType
+ if expressionType predicate == bool
+ then Right (TypedPlan (planSchema child) (BoundSelection predicate child))
+ else Left "selection predicate must be Bool"
+ where
+ -- | 绑定子计划
+ bind = bindPlan types functions catalog
+ -- | 解析 schema 列的类型身份
+ bindColumn (name, ty) = do
+ tid <- descriptorId <$> resolveType types ty
+ Right (name, tid)
+ -- | 绑定投影项
+ bindItem env (name, value) = do
+ bound <- bindExpression types functions env value
+ Right (name, bound)
+
+-- | 拒绝重复输出列
+uniqueNames :: [String] -> Either String ()
+uniqueNames names
+ | nub names == names = Right ()
+ | otherwise = Left "duplicate schema column"
+
+-- | 执行绑定计划并校验存储返回值
+executePlan :: TypeRegistry -> FunctionRegistry -> (String -> Either String [[(String, RuntimeValue)]])
+ -> TypedPlan -> Either String [[(String, RuntimeValue)]]
+executePlan types functions scan (TypedPlan output node) = do
+ rows <- case node of
+ BoundScan name -> scan name
+ BoundSingleRow -> Right [[]]
+ BoundProjection items child -> do
+ input <- executePlan types functions scan child
+ mapM (project items) input
+ BoundSelection condition child -> do
+ input <- executePlan types functions scan child
+ filterM (keep condition) input
+ mapM_ (validateRow output) rows
+ Right [[(name, value) | (name, _) <- output, Just value <- [lookup name row]] | row <- rows]
+ where
+ -- | 计算一行的投影项
+ project items row = mapM (projectItem row) items
+ -- | 计算命名输出项
+ projectItem row (name, expression) = do
+ value <- evaluateExpression types functions row expression
+ Right (name, value)
+ -- | 计算选择条件
+ keep condition row = do
+ value <- evaluateExpression types functions row condition
+ case value of
+ RBool result -> Right result
+ _ -> Left "bound predicate returned a non-Bool value"
+ -- | 校验存储行的完整 schema
+ validateRow columns row
+ | sort (map fst row) /= sort (map fst columns) = Left "storage row schema mismatch"
+ | otherwise = mapM_ (validateColumn row) columns
+ -- | 按列名校验返回值的类型
+ validateColumn row (name, tid) = case lookup name row of
+ Just value -> validateValue types tid value
+ Nothing -> Left ("missing bound column: " ++ name)
diff --git a/chusql-core/engine/src/ChuSQL/Core/Engine/Runtime/Functions.hs b/chusql-core/engine/src/ChuSQL/Core/Engine/Runtime/Functions.hs
new file mode 100644
index 0000000..21eed20
--- /dev/null
+++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Runtime/Functions.hs
@@ -0,0 +1,83 @@
+module ChuSQL.Core.Engine.Runtime.Functions
+ ( FunctionId (..), FunctionSignature (..), FunctionRegistry
+ , builtinFunctions, resolveFunction, resolveNullableFunction, functionSignature, invokeFunction
+ ) where
+
+import ChuSQL.Core.Engine.Runtime.Types
+import Data.Char (toLower)
+
+-- 内建函数的固定身份、签名解析和类型化调用。
+
+-- | 标识稳定的内建函数
+newtype FunctionId = FunctionId Int deriving (Show, Eq, Ord)
+
+-- | 描述已解析的函数签名
+data FunctionSignature = FunctionSignature
+ { signatureId :: FunctionId, signatureName :: String
+ , signatureArguments :: [TypeId], signatureResult :: TypeId
+ } deriving (Show, Eq)
+
+-- | 保存函数签名和实现
+newtype FunctionRegistry = FunctionRegistry [(FunctionSignature, [RuntimeValue] -> Either String RuntimeValue)]
+
+-- | 从内建元数据恢复函数注册表
+builtinFunctions :: TypeRegistry -> Either String FunctionRegistry
+builtinFunctions types = do
+ int <- descriptorId <$> resolveType types intType
+ bool <- descriptorId <$> resolveType types boolType
+ string <- descriptorId <$> resolveType types stringType
+ Right (FunctionRegistry
+ [(FunctionSignature (FunctionId 1) "length" [string] int, stringLength)
+ ,(FunctionSignature (FunctionId 2) "not" [bool] bool, booleanNot)])
+
+-- | 按名称和完整参数类型解析函数
+resolveFunction :: FunctionRegistry -> String -> [TypeId] -> Either String FunctionSignature
+resolveFunction (FunctionRegistry functions) name arguments =
+ case [sig | (sig, _) <- functions, map toLower (signatureName sig) == map toLower name
+ , signatureArguments sig == arguments] of
+ [sig] -> Right sig
+ [] -> Left ("no matching function signature: " ++ name)
+ _ -> Left ("ambiguous function signature: " ++ name)
+
+-- | 按可空 SQL 参数解析确定签名
+resolveNullableFunction :: FunctionRegistry -> String -> [Maybe TypeId] -> Either String FunctionSignature
+resolveNullableFunction (FunctionRegistry functions) name arguments =
+ case [sig | (sig, _) <- functions, map toLower (signatureName sig) == map toLower name
+ , length arguments == length (signatureArguments sig)
+ , and (zipWith matches arguments (signatureArguments sig))] of
+ [sig] -> Right sig
+ [] -> Left ("no matching function signature: " ++ name)
+ _ -> Left ("ambiguous function signature: " ++ name)
+ where
+ -- | 匹配具体参数或未定型 NULL
+ matches Nothing _ = True
+ matches (Just supplied) expected = supplied == expected
+
+-- | 按固定身份查找函数签名
+functionSignature :: FunctionRegistry -> FunctionId -> Either String FunctionSignature
+functionSignature (FunctionRegistry functions) fid = case [sig | (sig, _) <- functions, signatureId sig == fid] of
+ [sig] -> Right sig
+ _ -> Left ("unknown function identity: " ++ show fid)
+
+-- | 校验参数并执行已解析的函数
+invokeFunction :: TypeRegistry -> FunctionRegistry -> FunctionId -> [RuntimeValue] -> Either String RuntimeValue
+invokeFunction types (FunctionRegistry functions) fid values =
+ case [(sig, implementation) | (sig, implementation) <- functions, signatureId sig == fid] of
+ [(sig, implementation)]
+ | length values == length (signatureArguments sig) -> do
+ sequence_ (zipWith (validateValue types) (signatureArguments sig) values)
+ result <- implementation values
+ validateValue types (signatureResult sig) result
+ Right result
+ | otherwise -> Left "function argument count mismatch"
+ _ -> Left ("unknown function identity: " ++ show fid)
+
+-- | 计算字符串字符个数
+stringLength :: [RuntimeValue] -> Either String RuntimeValue
+stringLength [RString text] = Right (RInt (length text))
+stringLength _ = Left "length expects String"
+
+-- | 计算布尔取反
+booleanNot :: [RuntimeValue] -> Either String RuntimeValue
+booleanNot [RBool value] = Right (RBool (not value))
+booleanNot _ = Left "not expects Bool"
diff --git a/chusql-core/engine/src/ChuSQL/Core/Engine/Runtime/Query.hs b/chusql-core/engine/src/ChuSQL/Core/Engine/Runtime/Query.hs
new file mode 100644
index 0000000..c7bfa6b
--- /dev/null
+++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Runtime/Query.hs
@@ -0,0 +1,65 @@
+module ChuSQL.Core.Engine.Runtime.Query
+ ( BoundQuery, QueryType (..), queryTypes, bindQuery, executeQueryM
+ ) where
+
+import ChuSQL.Core.Engine.Algebra.Eval (evalRelOpM)
+import ChuSQL.Core.Engine.Algebra.Op (RelOp)
+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.Runtime.SQL (sqlType)
+import ChuSQL.Core.Engine.Runtime.Types (TypeId)
+import ChuSQL.Core.Model
+
+-- SQL 查询绑定边界:保存解析后的类型、函数身份及输出 schema。
+
+-- | 保存运行时或兼容列类型
+data QueryType = RuntimeType TypeId | CompatibilityType ColumnType | NullType deriving (Show, Eq)
+
+-- | 保存仅由 schema 绑定的执行计划
+data BoundQuery = BoundQuery Database [(String, Column)] [(String, QueryType)] RelOp
+
+-- | 查询绑定计划的输出类型身份
+queryTypes :: BoundQuery -> [(String, QueryType)]
+queryTypes (BoundQuery _ _ types _) = types
+
+-- | 绑定 SQL 查询并优化确定的计划
+bindQuery :: Database -> Statement -> Either String BoundQuery
+bindQuery catalog statement = do
+ let schemaOnly = [(name, table {tableRows = []}) | (name, table) <- catalog]
+ resolved <- prepare schemaOnly statement
+ columns <- querySchema schemaOnly resolved
+ plan <- translate resolved
+ types <- mapM typeOf columns
+ Right (BoundQuery schemaOnly columns types (optimize schemaOnly plan))
+ where
+ -- | 解析输出列的运行时身份
+ typeOf (name, column) = do
+ ty <- bindingType (columnType column)
+ Right (name, ty)
+ -- | 保留阶段外类型的兼容表示
+ bindingType CNull = Right NullType
+ 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)
+ where
+ -- | 校验完整输出行而不重新解析函数
+ validateRow row
+ | map fst row /= map fst columns = Left "bound query output schema mismatch"
+ | otherwise = mapM validateColumn (zip columns row)
+ -- | 校验输出列的物理值
+ validateColumn ((name, column), (_, value))
+ | columnType column == CNull, value == VNull = Right (name, value)
+ | valueFits (columnType column) value = Right (name, value)
+ | otherwise = Left ("bound query output type mismatch: " ++ name)
diff --git a/chusql-core/engine/src/ChuSQL/Core/Engine/Runtime/SQL.hs b/chusql-core/engine/src/ChuSQL/Core/Engine/Runtime/SQL.hs
new file mode 100644
index 0000000..a2b816c
--- /dev/null
+++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Runtime/SQL.hs
@@ -0,0 +1,92 @@
+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.Model (ColumnType (..), Value (..))
+
+-- SQL 现有标量表示与已解析运行时函数之间的类型边界。
+
+-- | 解析 SQL 标量调用的固定签名
+resolveSQLFunction :: String -> [Maybe ColumnType] -> Either String FunctionSignature
+resolveSQLFunction name arguments = do
+ functions <- builtinFunctions builtinTypes
+ types <- mapM (traverse sqlType) arguments
+ resolveNullableFunction functions name types
+
+-- | 解析现有标量列的逻辑类型
+sqlType :: ColumnType -> Either String TypeId
+sqlType (CDomain _ base) = sqlType base
+sqlType (CRuntime tid) = describeType builtinTypes tid >> Right tid
+sqlType ty = do
+ expression <- case ty of
+ CInt -> Right intType
+ CBigInt -> Right intType
+ CSmallInt -> Right intType
+ CFloat -> Right doubleType
+ CDouble -> Right doubleType
+ CDecimal{} -> Right doubleType
+ CBool -> Right boolType
+ CStr -> Right stringType
+ CVarchar{} -> Right stringType
+ CChar{} -> Right stringType
+ _ -> Left ("unsupported runtime function argument type: " ++ show ty)
+ descriptorId <$> resolveType builtinTypes expression
+
+-- | 将函数输出类型映射为 SQL schema
+sqlResultType :: TypeId -> Either String ColumnType
+sqlResultType tid
+ | typeExpression tid == intType = Right CInt
+ | typeExpression tid == doubleType = Right CDouble
+ | typeExpression tid == boolType = Right CBool
+ | typeExpression tid == stringType = Right CStr
+ | otherwise = Left "runtime function result needs a composite SQL representation"
+
+-- | 执行固定身份的 SQL 标量函数
+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
+ where
+ -- | 校验具体参数并保留 SQL NULL
+ validateArgument _ VNull = Right ()
+ validateArgument tid value = runtimeValue value >>= validateValue builtinTypes tid
+
+-- | 转换现有 SQL 标量值
+runtimeValue :: Value -> Either String RuntimeValue
+runtimeValue (VInt n) = Right (RInt n)
+runtimeValue (VFloat n) = Right (RDouble n)
+runtimeValue (VBool b) = Right (RBool b)
+runtimeValue (VStr text) = Right (RString text)
+runtimeValue (VRuntime tid value) = validateValue builtinTypes tid value >> Right value
+runtimeValue VNull = Left "untyped SQL NULL has no runtime scalar value"
+
+-- | 转换运行时标量结果
+sqlValue :: RuntimeValue -> Either String Value
+sqlValue (RInt n) = Right (VInt n)
+sqlValue (RDouble n) = Right (VFloat n)
+sqlValue (RBool b) = Right (VBool b)
+sqlValue (RString text) = Right (VStr text)
+sqlValue _ = Left "composite runtime result has no SQL scalar representation"
+
+-- | 按已解析构造器生成类型化值
+constructSQLValue :: TypeId -> [Value] -> Either String Value
+constructSQLValue tid values = do
+ arguments <- mapM runtimeValue values
+ value <- case typeExpression tid of
+ TypeApply (TypeConstructorId 5) [_] -> Right (RList arguments)
+ TypeApply (TypeConstructorId 6) [_] -> case arguments of
+ [] -> Right (RMaybe Nothing)
+ [argument] -> Right (RMaybe (Just argument))
+ _ -> Left "Maybe constructor expects zero or one argument"
+ TypeApply (TypeConstructorId 7) _ -> Right (RTuple arguments)
+ _ -> Left "type is not a composite constructor"
+ validateValue builtinTypes tid value
+ Right (VRuntime tid value)
diff --git a/chusql-core/engine/src/ChuSQL/Core/Engine/Runtime/Types.hs b/chusql-core/engine/src/ChuSQL/Core/Engine/Runtime/Types.hs
new file mode 100644
index 0000000..19e35f9
--- /dev/null
+++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Runtime/Types.hs
@@ -0,0 +1,5 @@
+module ChuSQL.Core.Engine.Runtime.Types (module ChuSQL.Core.Runtime) where
+
+import ChuSQL.Core.Runtime
+
+-- 导出共享的运行时类型身份和物理编码契约。
diff --git a/chusql-core/engine/src/ChuSQL/Core/Engine/Semantic.hs b/chusql-core/engine/src/ChuSQL/Core/Engine/Semantic.hs
index e0675a9..9d8d45a 100644
--- a/chusql-core/engine/src/ChuSQL/Core/Engine/Semantic.hs
+++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Semantic.hs
@@ -1,9 +1,12 @@
-module ChuSQL.Core.Engine.Semantic (check, prepare) where
+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 Data.Char (isAlphaNum, isAscii, toLower)
import Data.List (intercalate, isInfixOf, isPrefixOf, isSuffixOf, nub)
@@ -21,6 +24,12 @@ prefixColumns mAlias cols = [(prefix ++ c, ty) | (c, ty) <- cols]
where
prefix = maybe "" (++ ".") mAlias
+-- | 从 schema 推导查询输出列
+querySchema :: Database -> Statement -> Either String [(String, Column)]
+querySchema db statement = do
+ columns <- inferSubqueryOutput db (Scope [] []) (Subquery statement [])
+ Right [(name, plainColumn (derivedType ty)) | (name, ty) <- columns]
+
-- | 检查期的作用域:本层优先,外层兜底
data Scope = Scope
{ scopeInner :: [(String, Column)]
@@ -103,6 +112,7 @@ checkGrouping env keys items
| null keys && not (any (hasAggregate . snd) items) = Right ()
| otherwise = do
checkColumns "GROUP BY" env keys
+ mapM_ (checkRuntimeColumn "GROUP BY" env) keys
mapM_ okCol (nub (concatMap (bareColumns . snd) items))
where
-- | 这一列能不能出现在分组查询里
@@ -127,6 +137,23 @@ inferExpr _ _ _ (LitBool _) = Right (InferType CBool)
inferExpr _ _ _ (LitDate _) = Right (InferType CDate)
inferExpr _ _ _ (LitTimestamp _) = Right (InferType CTimestamp)
inferExpr _ _ _ (LitBlob _) = Right (InferType CBlob)
+inferExpr db place env (Construct ty arguments) = InferType . CRuntime <$> inferConstruction db place env ty arguments
+inferExpr db place env (BoundConstruct tid arguments) = do
+ inferred <- inferConstruction db place env (typeExpression tid) arguments
+ if inferred == tid then Right (InferType (CRuntime tid)) else Left "constructor result identity mismatch"
+inferExpr _ _ _ (RuntimeLiteral tid value) = validateValue builtinTypes tid value >> Right (InferType (CRuntime tid))
+inferExpr db place env (FunctionCall name arguments) = do
+ inferred <- mapM (inferExpr db place env) arguments
+ sig <- resolveSQLFunction name (map inferredArgument inferred)
+ InferType <$> sqlResultType (signatureResult sig)
+inferExpr db place env (BoundFunction fid tid arguments) = do
+ functions <- builtinFunctions builtinTypes
+ expected <- functionSignature functions fid
+ inferred <- mapM (inferExpr db place env) arguments
+ resolved <- resolveSQLFunction (signatureName expected) (map inferredArgument inferred)
+ if signatureId resolved == fid && signatureResult resolved == tid
+ then InferType <$> sqlResultType tid
+ else Left "bound function signature identity mismatch"
inferExpr db place env (Add a b) = arithmetic db place env a b
inferExpr db place env (Sub a b) = arithmetic db place env a b
inferExpr db place env (Mul a b) = arithmetic db place env a b
@@ -160,8 +187,8 @@ inferExpr db place env (AvgOf a) = do
InferType x
| numericType x -> Right (InferType CFloat)
| otherwise -> Left (place ++ ": AVG needs a number, got " ++ typeLabel x)
-inferExpr db place env (MinOf a) = aggregateArg db place env a
-inferExpr db place env (MaxOf a) = aggregateArg db place env a
+inferExpr db place env (MinOf a) = aggregateArg db place env a >>= requireOrdering "MIN"
+inferExpr db place env (MaxOf a) = aggregateArg db place env a >>= requireOrdering "MAX"
inferExpr db place env (ScalarSub sq) = do
cols <- inferSubqueryOutput db env sq
case cols of
@@ -386,18 +413,18 @@ ddlTargets statement = case statement of
-- | 从 schema 解析列上的命名类型
resolveDomainTypes :: Database -> Statement -> Either String Statement
resolveDomainTypes db statement = case statement of
- CreateDomain name base -> CreateDomain name <$> resolveType base
+ CreateDomain name base -> CreateDomain name <$> resolveDomainType base
CreateTable name cols -> CreateTable name <$> mapM resolveColumnType cols
AddColumn name col -> AddColumn name <$> resolveColumnType col
- AlterColumnType name col ty -> AlterColumnType name col <$> resolveType ty
+ AlterColumnType name col ty -> AlterColumnType name col <$> resolveDomainType ty
_ -> Right statement
where
-- | 解析一列的类型
resolveColumnType (name, col) = do
- ty <- resolveType (columnType col)
+ ty <- resolveDomainType (columnType col)
Right (name, col {columnType = ty})
-- | 从类型目录取基础类型
- resolveType = resolveWith []
+ resolveDomainType = resolveWith []
-- | 递归解析类型并拒绝循环定义
resolveWith seen (CDomain name _)
| map toLower name `elem` seen = Left ("cyclic domain definition: " ++ name)
@@ -427,6 +454,7 @@ checkResolvedWith db outer q = case q of
checkColumns "SELECT" full cols
checkGrouping full groupBy (map (\c -> (c, Col c)) cols)
checkColumns "ORDER BY" full (map fst orderBy)
+ mapM_ (checkRuntimeColumn "ORDER BY" full . fst) orderBy
mapM_ (checkNoAggregate "WHERE") mWhere
mapM_ (checkBool db "WHERE" full) mWhere
SelectExpr items fromC mWhere groupBy orderBy _ -> do
@@ -434,7 +462,7 @@ checkResolvedWith db outer q = case q of
let full = Scope env outer
mapM_ (\item -> () <$ inferExpr db "SELECT" full (snd item)) items
checkGrouping full groupBy (items ++ [(c, Col c) | (c, _) <- orderBy, c `notElem` map fst items])
- mapM_ (checkOrder full items . fst) orderBy
+ mapM_ (checkOrder db full items . fst) orderBy
mapM_ (checkNoAggregate "WHERE") mWhere
mapM_ (checkBool db "WHERE" full) mWhere
Insert tbl cols rows -> do
@@ -468,7 +496,7 @@ checkResolvedWith db outer q = case q of
CreateIndex tbl col -> do
t <- lookupTable db tbl
if col `elem` tableCols' t
- then Right ()
+ then checkRuntimeColumn "CREATE INDEX" (Scope (tableCols t) []) col
else Left ("CREATE INDEX: unknown column: " ++ col)
DropIndex tbl col -> do
t <- lookupTable db tbl
@@ -581,14 +609,22 @@ checkColumnDefs names cols = do
mapM_ (checkTypeParameters . columnType . snd) cols
mapM_ (\(_, c) -> mapM_ (checkDefault c) (columnDefault c)) cols
mapM_ (\(_, c) -> checkAutoIncrement c) cols
+ mapM_ checkRuntimeKey cols
if length [() | (_, c) <- cols, columnAutoIncrement c] > 1
then Left "CREATE TABLE: at most one AUTO_INCREMENT column is allowed"
else Right ()
checkChecks names cols
+ where
+ -- | 拒绝缺少能力实现的运行时键
+ checkRuntimeKey (_, column)
+ | typeClassOf (columnType column) == RuntimeClass
+ , columnUnique column || columnPrimaryKey column = Left "runtime type key capability is not registered"
+ | otherwise = Right ()
-- | 校验列类型的长度和精度参数
checkTypeParameters :: ColumnType -> Either String ()
checkTypeParameters (CDomain _ base) = checkTypeParameters base
+checkTypeParameters (CRuntime tid) = () <$ describeType builtinTypes tid
checkTypeParameters CNull = Left "NULL is only valid as an inferred query output"
checkTypeParameters (CDecimal p s)
| p <= 0 || p > 308 || s < 0 || s > p = Left "invalid decimal precision or scale"
@@ -707,6 +743,17 @@ resolveExpr ::
Expr ->
Either String Expr
resolveExpr db env outer f e = case e of
+ Construct ty arguments -> do
+ tid <- inferConstruction db "constructor" (Scope env outer) ty arguments
+ bound <- mapM (resolveExpr db env outer f) arguments
+ Right (BoundConstruct tid bound)
+ BoundConstruct tid arguments -> BoundConstruct tid <$> mapM (resolveExpr db env outer f) arguments
+ FunctionCall name arguments -> do
+ inferred <- mapM (inferExpr db "function" (Scope env outer)) arguments
+ sig <- resolveSQLFunction name (map inferredArgument inferred)
+ bound <- mapM (resolveExpr db env outer f) arguments
+ Right (BoundFunction (signatureId sig) (signatureResult sig) bound)
+ BoundFunction fid tid arguments -> BoundFunction fid tid <$> mapM (resolveExpr db env outer f) arguments
Col c -> Col <$> f c
Add a b -> binary Add a b
Sub a b -> binary Sub a b
@@ -739,6 +786,36 @@ resolveExpr db env outer f e = case e of
-- | 递归处理两个子表达式
binary ctor a b = ctor <$> resolveExpr db env outer f a <*> resolveExpr db env outer f b
+-- | 校验具体构造器和参数逻辑类型
+inferConstruction :: Database -> String -> Scope -> TypeExpr -> [Expr] -> Either String TypeId
+inferConstruction db place env ty@(TypeApply cid parameters) arguments = do
+ descriptor <- resolveType builtinTypes ty
+ expected <- case (cid, parameters) of
+ (TypeConstructorId 5, [child]) -> Right (replicate (length arguments) child)
+ (TypeConstructorId 6, [child]) | length arguments <= 1 -> Right (replicate (length arguments) child)
+ (TypeConstructorId 7, fields) | length fields == length arguments -> Right fields
+ _ -> Left "composite constructor argument count mismatch"
+ inferred <- mapM (inferExpr db place env) arguments
+ supplied <- mapM argumentIdentity inferred
+ 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
+
+-- | 使用单行 schema 绑定表达式
+prepareRowExpression :: [(String, Column)] -> Expr -> Either String Expr
+prepareRowExpression columns expression = do
+ bound <- resolveExpr [] columns [] (nameIn columns "expression") expression
+ _ <- inferExpr [] "expression" (Scope columns []) bound
+ Right bound
+
+-- | 将推导类型转换为可空参数类型
+inferredArgument :: InferredType -> Maybe ColumnType
+inferredArgument InferNull = Nothing
+inferredArgument (InferType ty) = Just ty
+
-- | 解析子查询,并记下它引用的外层列
resolveSubquery :: Database -> [(String, Column)] -> Subquery -> Either String Subquery
resolveSubquery db outer sq = do
@@ -822,12 +899,25 @@ resolveSelect db q@SelectExpr{} = resolveQuery db [] q (selectItems q)
resolveSelect _ q = Right q
-- | 检查排序输出别名或来源列
-checkOrder :: Scope -> [(String, Expr)] -> String -> Either String ()
-checkOrder env items column = case [e | (label, e) <- items, label == column] of
- [] -> checkColumns "ORDER BY" env [column]
- [_] -> Right ()
+checkOrder :: Database -> Scope -> [(String, Expr)] -> String -> Either String ()
+checkOrder db env items column = case [e | (label, e) <- items, label == column] of
+ [] -> checkRuntimeColumn "ORDER BY" env column
+ [expression] -> () <$ (inferExpr db "ORDER BY" env expression >>= requireOrdering "ORDER BY")
_ -> Left ("ORDER BY: ambiguous projection alias: " ++ column)
+-- | 校验列已有排序能力
+checkRuntimeColumn :: String -> Scope -> String -> Either String ()
+checkRuntimeColumn place env column = do
+ (_, definition) <- scopeAt place env column
+ () <$ requireOrdering place (InferType (columnType definition))
+
+-- | 拒绝尚未注册能力的复合类型
+requireOrdering :: String -> InferredType -> Either String InferredType
+requireOrdering place inferred@(InferType ty)
+ | typeClassOf ty == RuntimeClass = Left (place ++ ": runtime type capability is not registered")
+ | otherwise = Right inferred
+requireOrdering _ inferred = Right inferred
+
-- | 解析投影、条件、排序与连接引用
resolveQuery :: Database -> [(String, Column)] -> Statement -> [(String, Expr)] -> Either String Statement
resolveQuery db outer q items = do
diff --git a/chusql-core/engine/src/ChuSQL/Core/Engine/Syntax/AST.hs b/chusql-core/engine/src/ChuSQL/Core/Engine/Syntax/AST.hs
index 2db58f3..fd45f9d 100644
--- a/chusql-core/engine/src/ChuSQL/Core/Engine/Syntax/AST.hs
+++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Syntax/AST.hs
@@ -1,6 +1,8 @@
module ChuSQL.Core.Engine.Syntax.AST (Statement (..), FromClause (..), JoinKind (..), Expr (..), Subquery (..), Quantifier (..), CompareOp (..), SortDir (..), makeSelect) where
import ChuSQL.Core.Model (Column (..), ColumnType, Value (..))
+import ChuSQL.Core.Engine.Runtime.Types (TypeId, TypeExpr, RuntimeValue)
+import ChuSQL.Core.Engine.Runtime.Functions (FunctionId)
-- 语法树:语句、数据来源、条件表达式、排序方向。
@@ -112,6 +114,11 @@ data CompareOp
-- | 条件表达式:列、字面量、算术、比较、逻辑、聚合与子查询
data Expr
= Col String
+ | FunctionCall String [Expr]
+ | BoundFunction FunctionId TypeId [Expr]
+ | Construct TypeExpr [Expr]
+ | BoundConstruct TypeId [Expr]
+ | RuntimeLiteral TypeId RuntimeValue
| LitNull
| LitInt Int
| LitFloat Double
diff --git a/chusql-core/engine/src/ChuSQL/Core/Engine/Syntax/Parser.hs b/chusql-core/engine/src/ChuSQL/Core/Engine/Syntax/Parser.hs
index 1ab2e83..4248061 100644
--- a/chusql-core/engine/src/ChuSQL/Core/Engine/Syntax/Parser.hs
+++ b/chusql-core/engine/src/ChuSQL/Core/Engine/Syntax/Parser.hs
@@ -5,6 +5,7 @@ module ChuSQL.Core.Engine.Syntax.Parser (parseStatement, parseExpression) where
import ChuSQL.Core.Model (Column (..), ColumnType (..), Value (..), plainColumn)
import ChuSQL.Core.Engine.Builtin (Builtin (..), builtinName, builtinNames, builtinNode, compareNode)
import ChuSQL.Core.Engine.Syntax.AST
+import ChuSQL.Core.Engine.Runtime.Types
import Control.Monad (void)
import Control.Monad.Combinators.Expr (Operator (..), makeExprParser)
import Data.Char (isAlpha, isAlphaNum, isHexDigit, isSpace, toLower, toUpper)
@@ -167,6 +168,8 @@ atom =
, LitInt <$> integer
, LitStr <$> stringLit
, try aggregateCall
+ , try constructorCall
+ , try scalarFunctionCall
, try existsPredicate
, Col <$> qualifiedName
, try scalarSubquery
@@ -202,6 +205,33 @@ inPredicate = do
subquerySelect :: Parser Subquery
subquerySelect = Subquery <$> queryStatement <*> pure []
+-- | 读取带具体类型参数的构造器
+constructorCall :: Parser Expr
+constructorCall = do
+ cid <- (TypeConstructorId 5 <$ keyword "list") <|> (TypeConstructorId 6 <$ keyword "maybe")
+ <|> (TypeConstructorId 7 <$ keyword "tuple")
+ arguments <- between (symbol "<") (symbol ">") (sepBy runtimeTypeP (symbol ","))
+ values <- between (symbol "(") (symbol ")") (sepBy expr (symbol ","))
+ pure (Construct (TypeApply cid arguments) values)
+
+-- | 解析运行时逻辑类型语法
+runtimeTypeP :: Parser TypeExpr
+runtimeTypeP = choice
+ [ intType <$ keyword "int", doubleType <$ keyword "double", boolType <$ keyword "bool"
+ , stringType <$ keyword "string", stringType <$ keyword "str", stringType <$ keyword "text"
+ , listType <$> between (symbol "[") (symbol "]") runtimeTypeP
+ , listType <$> (keyword "list" *> runtimeTypeP)
+ , maybeType <$> (keyword "maybe" *> runtimeTypeP)
+ , tupleType <$> between (symbol "(") (symbol ")") (sepBy runtimeTypeP (symbol ","))
+ ]
+
+-- | 读取待绑定的标量函数调用
+scalarFunctionCall :: Parser Expr
+scalarFunctionCall = do
+ name <- identifier
+ arguments <- between (symbol "(") (symbol ")") (sepBy expr (symbol ","))
+ pure (FunctionCall name arguments)
+
-- | 聚合调用:名称与节点都由内置函数表给出
aggregateCall :: Parser Expr
aggregateCall = choice (countAll : map named builtinNames)
@@ -480,7 +510,9 @@ alterUserStatement = do
-- | 读身份属性开关
identityAttribute :: Parser (String, Bool)
identityAttribute = choice
- [ ("system_catalog_manager", True) <$ keyword "system_catalog_manager"
+ [ ("allow_sudo_auth", True) <$ keyword "allow_sudo_auth"
+ , ("allow_sudo_auth", False) <$ keyword "noallow_sudo_auth"
+ , ("system_catalog_manager", True) <$ keyword "system_catalog_manager"
, ("system_catalog_manager", False) <$ keyword "nosystem_catalog_manager"
, ("login", True) <$ keyword "login", ("login", False) <$ keyword "nologin"
, ("superuser", True) <$ keyword "superuser", ("superuser", False) <$ keyword "nosuperuser"
@@ -525,12 +557,16 @@ privilegeName =
, "INSERT" <$ keyword "insert"
, "UPDATE" <$ keyword "update"
, "DELETE" <$ keyword "delete"
+ , "CONNECT" <$ keyword "connect"
+ , "CREATE" <$ keyword "create"
, "ALL" <$ keyword "all"
]
-- | ON 后面的授权对象:* 或一张表
grantObject :: Parser String
-grantObject = ("*" <$ symbol "*") <|> tableName
+grantObject = try (("database:" ++) <$> (keyword "database" *> identifier))
+ <|> try ((++ ".*") <$> (identifier <* symbol "." <* symbol "*"))
+ <|> ("*" <$ symbol "*") <|> (optional (keyword "table") *> tableName)
-- | 可选的 WITH GRANT OPTION
grantOption :: Parser Bool
@@ -597,7 +633,8 @@ sizeParen = try (between (symbol "(") (symbol ")") integer)
columnTypeP :: Parser ColumnType
columnTypeP =
choice
- [ keyword "integer" >> pure CInt
+ [ try runtimeColumnType
+ , keyword "integer" >> pure CInt
, keyword "int" >> pure CInt
, keyword "bigint" >> pure CBigInt
, keyword "smallint" >> pure CSmallInt
@@ -619,6 +656,20 @@ columnTypeP =
, (\name -> CDomain (map toLower name) CStr) <$> identifier
]
+-- | 解析复合列的稳定类型身份
+runtimeColumnType :: Parser ColumnType
+runtimeColumnType = do
+ expression <- choice
+ [ listType <$> between (symbol "[") (symbol "]") runtimeTypeP
+ , listType <$> (keyword "list" *> runtimeTypeP)
+ , maybeType <$> (keyword "maybe" *> runtimeTypeP)
+ , tupleType <$> between (symbol "(") (symbol ")") (sepBy runtimeTypeP (symbol ","))
+ , stringType <$ keyword "string"
+ ]
+ case resolveType builtinTypes expression of
+ Left err -> fail err
+ Right descriptor -> pure (CRuntime (descriptorId descriptor))
+
-- | DECIMAL 的精度与小数位
decimalType :: Parser ColumnType
decimalType = do
@@ -744,6 +795,10 @@ expandClause env fromC = case fromC of
-- | 表达式里的子查询也要展开
expandExpr :: [(String, Statement)] -> Expr -> Either String Expr
expandExpr env e = case e of
+ Construct tid arguments -> Construct tid <$> mapM go arguments
+ BoundConstruct tid arguments -> BoundConstruct tid <$> mapM go arguments
+ FunctionCall name arguments -> FunctionCall name <$> mapM go arguments
+ BoundFunction fid tid arguments -> BoundFunction fid tid <$> mapM go arguments
Add a b -> Add <$> go a <*> go b
Sub a b -> Sub <$> go a <*> go b
Mul a b -> Mul <$> go a <*> go b
diff --git a/chusql-core/engine/test/NormalizationSpec.hs b/chusql-core/engine/test/NormalizationSpec.hs
new file mode 100644
index 0000000..1d004e4
--- /dev/null
+++ b/chusql-core/engine/test/NormalizationSpec.hs
@@ -0,0 +1,70 @@
+{-# LANGUAGE PatternSynonyms #-}
+
+module NormalizationSpec (spec) where
+
+import ChuSQL.Core.Engine.Normalization
+import ChuSQL.Core.Model (pattern TInt)
+import Test.Hspec
+
+-- 范式分析回归:公理闭包、候选键、信息不足与分解性质。
+
+-- 执行范式检查回归。
+spec :: Spec
+spec = do
+ describe "Armstrong normalization" $ do
+ it "computes reflexivity, augmentation and transitivity" $ do
+ attributeClosure [Dependency ["a"] ["b"], Dependency ["b"] ["c"]] ["a"]
+ `shouldBe` ["a", "b", "c"]
+ implies [Dependency ["a"] ["b"]] (Dependency ["a", "c"] ["b", "c"])
+ `shouldBe` True
+ attributeClosure [] ["a"] `shouldBe` ["a"]
+ it "accepts BCNF with a declared key" $ do
+ fmap (map verdict . findings) (analyzeNormalization schema
+ (Declaration [["a"]] [] True (Just True)))
+ `shouldBe` Right (replicate 4 Satisfied)
+ it "detects partial dependencies of composite keys" $ do
+ fmap (map verdict . findings) (analyzeNormalization schema
+ (Declaration [["a", "b"]] [Dependency ["a"] ["c"]] True (Just True)))
+ `shouldBe` Right [Satisfied, Violated, Violated, Violated]
+ it "detects transitive dependencies" $ do
+ fmap (map verdict . findings) (analyzeNormalization schema
+ (Declaration [["a"]] [Dependency ["b"] ["c"]] True (Just True)))
+ `shouldBe` Right [Satisfied, Satisfied, Violated, Violated]
+ it "distinguishes 3NF from BCNF using all candidate keys" $ do
+ fmap (map verdict . findings) (analyzeNormalization schema
+ (Declaration [["a", "b"]] [Dependency ["c"] ["b"]] True (Just True)))
+ `shouldBe` Right [Satisfied, Satisfied, Satisfied, Violated]
+ it "reports lossless decomposition that loses dependencies" $ do
+ fmap suggestions (analyzeNormalization schema
+ (Declaration [["a", "b"]] [Dependency ["c"] ["b"]] True (Just True)))
+ `shouldBe` Right [Decomposition [["b", "c"], ["a", "c"]] Satisfied Violated]
+ it "reports dependency preserving transitive split" $ do
+ fmap suggestions (analyzeNormalization schema
+ (Declaration [["a"]] [Dependency ["b"] ["c"]] True (Just True)))
+ `shouldBe` Right [Decomposition [["b", "c"], ["a", "b"]] Satisfied Satisfied]
+ it "does not certify incomplete dependencies" $ do
+ fmap (map verdict . findings) (analyzeNormalization schema
+ (Declaration [["a"]] [] False (Just True)))
+ `shouldBe` Right [Satisfied, Insufficient, Insufficient, Insufficient]
+ it "requires an atomicity declaration" $ do
+ fmap (map verdict . findings) (analyzeNormalization schema
+ (Declaration [] [] True Nothing)) `shouldBe` Right (replicate 4 Insufficient)
+ it "propagates a declared 1NF violation" $ do
+ fmap (map verdict . findings) (analyzeNormalization schema
+ (Declaration [] [] True (Just False))) `shouldBe` Right (replicate 4 Violated)
+ it "rejects unknown attributes" $ do
+ analyzeNormalization schema (Declaration [] [Dependency ["z"] ["a"]] True (Just True))
+ `shouldBe` Left "normalization: unknown or duplicate dependency attributes"
+ it "rejects nonminimal declared candidate keys" $ do
+ analyzeNormalization schema (Declaration [["a", "b"]]
+ [Dependency ["a"] ["b", "c"]] True (Just True))
+ `shouldBe` Left "normalization: declared candidate key is not minimal"
+ it "includes the empty candidate key for constant relations" $ do
+ fmap candidateKeys (analyzeNormalization schema
+ (Declaration [] [Dependency [] ["a", "b", "c"]] True (Just True))) `shouldBe` Right [[]]
+ it "rejects excessive exhaustive searches" $ do
+ analyzeNormalization [(show n, TInt) | n <- [1 :: Int .. 13]]
+ (Declaration [] [] True (Just True))
+ `shouldBe` Left "normalization: exhaustive analysis supports at most 12 columns"
+ where
+ schema = [("a", TInt), ("b", TInt), ("c", TInt)]
diff --git a/chusql-core/engine/test/RuntimeSpec.hs b/chusql-core/engine/test/RuntimeSpec.hs
new file mode 100644
index 0000000..64b2836
--- /dev/null
+++ b/chusql-core/engine/test/RuntimeSpec.hs
@@ -0,0 +1,222 @@
+module RuntimeSpec (spec) where
+
+import ChuSQL.Core.Engine.Runtime.Types
+import ChuSQL.Core.Engine.Runtime.Functions
+import ChuSQL.Core.Engine.Runtime.Binding
+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.Syntax.Parser (parseStatement)
+import ChuSQL.Core.Engine.Syntax.AST (Expr (..), Statement (..))
+import qualified ChuSQL.Core.Model as SQL
+import ChuSQL.Core.Protocol (valueToJSON, valueFromJSON)
+import Data.Aeson.Types (parseEither)
+import Data.Hashable (hash)
+import qualified Data.ByteString.Lazy.Char8 as BS
+import Test.Hspec
+
+-- 运行时类型、函数绑定、物理编码和类型化查询测试。
+
+-- | 检查运行时类型基础契约
+spec :: Spec
+spec = do
+ describe "runtime type registry" $ do
+ it "keeps a fixed identity encoding across releases" $ withType (listType intType) $ \tid ->
+ encodeType tid `shouldBe` BS.pack "[1,[5,[[1,[]]]]]"
+ it "parses nested Haskell-style type expressions" $
+ parseTypeExpr "(Int, [Maybe String])" `shouldBe` Right (tupleType [intType, listType (maybeType stringType)])
+ it "rejects constructor names without a token boundary" $
+ parseTypeExpr "MaybeInt" `shouldSatisfy` isFailure
+ it "restores stable nested identities from bootstrap metadata" $ do
+ let ty = tupleType [listType intType, maybeType stringType]
+ resolveType builtinTypes ty `shouldBe` resolveType builtinTypes ty
+ fmap descriptorPhysical (resolveType builtinTypes ty)
+ `shouldBe` Right (Product [Sequence SignedInteger, Optional Utf8])
+ it "distinguishes logical String from List Int" $
+ fmap descriptorId (resolveType builtinTypes stringType)
+ `shouldNotBe` fmap descriptorId (resolveType builtinTypes (listType intType))
+ it "rejects unknown constructors" $
+ resolveType builtinTypes (TypeApply (TypeConstructorId 99) []) `shouldSatisfy` isFailure
+ it "rejects unapplied List" $
+ resolveType builtinTypes (TypeApply (TypeConstructorId 5) []) `shouldSatisfy` isFailure
+ it "rejects arguments to Int" $
+ resolveType builtinTypes (TypeApply (TypeConstructorId 1) [intType]) `shouldSatisfy` isFailure
+ describe "runtime encoding" $ do
+ it "round trips nested lists, optional values and tuples" $ do
+ let ty = tupleType [listType (maybeType intType), stringType, doubleType, boolType]
+ value = RTuple [RList [RMaybe Nothing, RMaybe (Just (RInt 42))], RString "你好", RDouble 1.25, RBool True]
+ withType ty $ \tid -> do
+ (encodeValue builtinTypes tid value >>= decodeValue builtinTypes tid) `shouldBe` Right value
+ decodeType builtinTypes (encodeType tid) `shouldBe` Right tid
+ it "round trips the empty tuple" $ withType (tupleType []) $ \tid ->
+ (encodeValue builtinTypes tid (RTuple []) >>= decodeValue builtinTypes tid) `shouldBe` Right (RTuple [])
+ it "rejects mismatched list elements" $ withType (listType intType) $ \tid ->
+ encodeValue builtinTypes tid (RList [RString "bad"]) `shouldSatisfy` isFailure
+ it "rejects tuple arity mismatch" $ withType (tupleType [intType]) $ \tid ->
+ encodeValue builtinTypes tid (RTuple []) `shouldSatisfy` isFailure
+ it "rejects non-finite floating point values" $ withType doubleType $ \tid ->
+ encodeValue builtinTypes tid (RDouble (1 / 0)) `shouldSatisfy` isFailure
+ it "rejects unsupported type encoding versions" $
+ decodeType builtinTypes (BS.pack "[2,[1,[]]]") `shouldSatisfy` isFailure
+ it "rejects overflowed constructor identities" $
+ decodeType builtinTypes (BS.pack "[1,[18446744073709551617,[]]]") `shouldSatisfy` isFailure
+ it "rejects fractional integer payloads" $ withType intType $ \tid ->
+ decodeValue builtinTypes tid (BS.pack "[1,[1,[]],1.5]") `shouldSatisfy` isFailure
+ it "rejects encoded identity mismatch" $ withType intType $ \tid ->
+ decodeValue builtinTypes tid (BS.pack "[1,[4,[]],\"x\"]") `shouldSatisfy` isFailure
+ describe "runtime binder and executor" $ do
+ it "resolves case-insensitive length to a stable FunctionId" $ withFunctions $ \functions ->
+ withType stringType $ \string -> withType intType $ \int ->
+ resolveFunction functions "LENGTH" [string]
+ `shouldBe` Right (FunctionSignature (FunctionId 1) "length" [string] int)
+ it "binds and executes nested calls" $ withFunctions $ \functions -> do
+ let expression = Apply "not" [Apply "not" [Literal boolType (RBool True)]]
+ (bindExpression builtinTypes functions [] expression >>= evaluateExpression builtinTypes functions [])
+ `shouldBe` Right (RBool True)
+ 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 ->
+ invokeFunction builtinTypes functions (FunctionId 99) [] `shouldSatisfy` isFailure
+ it "rejects wrong runtime argument types" $ withFunctions $ \functions ->
+ invokeFunction builtinTypes functions (FunctionId 1) [RInt 1] `shouldSatisfy` isFailure
+ it "binds empty List and Nothing using explicit element types" $ withFunctions $ \functions -> do
+ let expression = TupleLiteral [ListLiteral intType [], NothingLiteral stringType]
+ (bindExpression builtinTypes functions [] expression >>= evaluateExpression builtinTypes functions [])
+ `shouldBe` Right (RTuple [RList [], RMaybe Nothing])
+ it "rejects mixed logical list types" $ withFunctions $ \functions ->
+ bindExpression builtinTypes functions [] (ListLiteral intType [Literal doubleType (RDouble 1)]) `shouldSatisfy` isFailure
+ it "binds queries using schema without reading storage" $ withFunctions $ \functions -> do
+ let catalog = [("items", [("name", stringType), ("enabled", boolType)])]
+ query = Projection [("size", Apply "length" [Reference "name"])]
+ (Selection (Reference "enabled") (TableScan "items"))
+ scan name = if name == "items" then Right
+ [[("name", RString "甲乙"), ("enabled", RBool True)]
+ ,[("name", RString "hidden"), ("enabled", RBool False)]] else Left "unknown storage table"
+ (bindPlan builtinTypes functions catalog query >>= executePlan builtinTypes functions scan)
+ `shouldBe` Right [[("size", RInt 2)]]
+ it "uses catalog existence even when storage could return rows" $ withFunctions $ \functions ->
+ bindPlan builtinTypes functions [] (TableScan "missing") `shouldSatisfy` isFailure
+ it "rejects non-boolean filters" $ withFunctions $ \functions ->
+ bindPlan builtinTypes functions [] (Selection (Literal intType (RInt 1)) SingleRow) `shouldSatisfy` isFailure
+ it "rejects schema drift returned by storage" $ withFunctions $ \functions ->
+ (bindPlan builtinTypes functions [("t", [("x", intType)])] (TableScan "t")
+ >>= executePlan builtinTypes functions (const (Right [[("x", RString "wrong")]]))) `shouldSatisfy` isFailure
+ it "propagates storage failures" $ withFunctions $ \functions ->
+ (bindPlan builtinTypes functions [("t", [])] (TableScan "t")
+ >>= executePlan builtinTypes functions (const (Left "storage unavailable"))) `shouldBe` Left "storage unavailable"
+ describe "SQL runtime functions" $ do
+ 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 "rejects composite ordering without an Ord implementation" $
+ sql "SELECT LIST(1) AS xs ORDER BY xs" `shouldSatisfy` isFailure
+ it "rejects composite extrema without an Ord implementation" $
+ sql "SELECT MIN(LIST(1))" `shouldSatisfy` isFailure
+ it "rejects unique runtime columns without a key implementation" $
+ (parseStatement "CREATE TABLE keyed (xs [Int] UNIQUE)" >>= runStatement []) `shouldSatisfy` isFailure
+ it "rejects primary runtime keys without a key implementation" $
+ (parseStatement "CREATE TABLE keyed (xs [Int] PRIMARY KEY)" >>= runStatement []) `shouldSatisfy` isFailure
+ it "constructs nested compound values in SQL" $ withType (listType (maybeType intType)) $ \tid ->
+ sql "SELECT LIST(MAYBE(), MAYBE(42))"
+ `shouldBe` Right [[SQL.VRuntime tid (RList [RMaybe Nothing, RMaybe (Just (RInt 42))])]]
+ it "constructs heterogeneous tuples with stable type identity" $ withType (tupleType [intType, stringType]) $ \tid ->
+ sql "SELECT TUPLE(1, 'a')" `shouldBe` Right [[SQL.VRuntime tid (RTuple [RInt 1, RString "a"])]]
+ it "rejects constructor argument type mismatch" $
+ sql "SELECT LIST('bad')" `shouldSatisfy` isFailure
+ it "rejects constructor arity mismatch" $
+ sql "SELECT MAYBE(1, 2)" `shouldSatisfy` isFailure
+ it "rejects SQL NULL inside a typed compound value" $
+ sql "SELECT LIST(NULL)" `shouldSatisfy` isFailure
+ it "round trips typed runtime values through the shared wire protocol" $
+ withType (maybeType (listType intType)) $ \tid -> do
+ let value = SQL.VRuntime tid (RMaybe (Just (RList [RInt 1, RInt 2])))
+ parseEither valueFromJSON (valueToJSON value) `shouldBe` Right value
+ it "round trips typed schema identities through catalog type names" $
+ withType (tupleType [intType, listType stringType]) $ \tid ->
+ SQL.parseColumnType (SQL.typeName (SQL.CRuntime tid)) `shouldBe` Just (SQL.CRuntime tid)
+ it "keeps hashing consistent for signed floating zero" $ withType (listType doubleType) $ \tid -> do
+ let positive = SQL.VRuntime tid (RList [RDouble 0])
+ negative = SQL.VRuntime tid (RList [RDouble (-0)])
+ positive `shouldBe` negative
+ hash positive `shouldBe` hash negative
+ it "creates and writes compound columns through SQL" $ do
+ let result = do
+ create <- parseStatement "CREATE TABLE composite (xs [Int], opt Maybe String, pair (Int, String))"
+ (db, _) <- runStatement [] create
+ insert <- parseStatement "INSERT INTO composite (xs, opt, pair) VALUES (LIST(1, 2), MAYBE(), TUPLE(3, 'x'))"
+ (updated, _) <- runStatement db insert
+ select <- parseStatement "SELECT xs, opt, pair FROM composite"
+ rowsOf (runStatement updated select)
+ result `shouldSatisfy` either (const False) ((== 1) . length)
+ it "stores logical String with the generic runtime encoding" $ do
+ let result = do
+ create <- parseStatement "CREATE TABLE strings (text String)"
+ (db, _) <- runStatement [] create
+ insert <- parseStatement "INSERT INTO strings (text) VALUES ('abc')"
+ (updated, _) <- runStatement db insert
+ select <- parseStatement "SELECT length(text) FROM strings"
+ map (map snd) <$> rowsOf (runStatement updated select)
+ result `shouldBe` Right [[SQL.VInt 3]]
+ it "runs length and not through resolved identities" $
+ sql "SELECT length('你好'), not(TRUE)" `shouldBe` Right [[SQL.VInt 2, SQL.VBool False]]
+ it "preserves nullable SQL function semantics" $
+ sql "SELECT length(NULL), not(NULL)" `shouldBe` Right [[SQL.VNull, SQL.VNull]]
+ it "binds calls before execution" $ do
+ let bound = parseStatement "SELECT length('abc')" >>= prepare []
+ case bound of
+ Right SelectExpr {selectItems = [(_, BoundFunction (FunctionId 1) _ [_])]} -> pure ()
+ _ -> expectationFailure (show bound)
+ it "rejects unknown SQL functions before scanning" $
+ sql "SELECT missing('abc')" `shouldSatisfy` isFailure
+ it "rejects wrong argument count" $
+ sql "SELECT length('a', 'b')" `shouldSatisfy` isFailure
+ it "rejects wrong SQL argument type" $
+ sql "SELECT length(1)" `shouldSatisfy` isFailure
+ it "finds references inside calls for projection pushdown" $
+ sql "SELECT length(name) FROM t WHERE not(enabled)" `shouldBe` Right [[SQL.VInt 3]]
+ it "rewrites aggregates nested in calls" $
+ sql "SELECT length(MIN(name)) FROM t" `shouldBe` Right [[SQL.VInt 2]]
+ it "executes subqueries nested in function arguments" $
+ sql "SELECT length((SELECT MIN(name) FROM t))" `shouldBe` Right [[SQL.VInt 2]]
+ it "expands CTEs inside function arguments" $
+ sql "WITH names AS (SELECT name FROM t) SELECT length((SELECT MIN(name) FROM names))"
+ `shouldBe` Right [[SQL.VInt 2]]
+ it "binds CHECK functions using the row schema" $ do
+ let result = do
+ statement <- parseStatement "CREATE TABLE checked (name TEXT CHECK (length(name) > 1))"
+ (db, _) <- runStatement [] statement
+ insert <- parseStatement "INSERT INTO checked (name) VALUES ('abc')"
+ runStatement db insert
+ result `shouldSatisfy` either (const False) (const True)
+
+-- | 通过正式入口执行测试 SQL
+sql :: String -> Either String [[SQL.Value]]
+sql source = do
+ statement <- parseStatement source
+ rows <- rowsOf (runStatement database statement)
+ Right (map (map snd) rows)
+ where
+ -- | 提供函数调用测试表
+ 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)]
+
+-- | 判断结果是否明确失败
+isFailure :: Either a b -> Bool
+isFailure (Left _) = True
+isFailure _ = False
+
+-- | 准备已解析类型身份
+withType :: TypeExpr -> (TypeId -> Expectation) -> Expectation
+withType ty action = case resolveType builtinTypes ty of
+ Left err -> expectationFailure err
+ Right descriptor -> action (descriptorId descriptor)
+
+-- | 准备内建函数注册表
+withFunctions :: (FunctionRegistry -> Expectation) -> Expectation
+withFunctions action = case builtinFunctions builtinTypes of
+ Left err -> expectationFailure err
+ Right functions -> action functions
diff --git a/chusql-core/engine/test/Spec.hs b/chusql-core/engine/test/Spec.hs
index 956d643..590a664 100644
--- a/chusql-core/engine/test/Spec.hs
+++ b/chusql-core/engine/test/Spec.hs
@@ -31,6 +31,8 @@ import System.Directory (createDirectoryIfMissing, getTemporaryDirectory, remove
import System.FilePath ((>))
import System.Timeout (timeout)
import Test.Hspec
+import qualified NormalizationSpec
+import qualified RuntimeSpec
-- 引擎层的 hspec 测试:语法、执行、优化器与存储链路行为。
@@ -116,7 +118,12 @@ domainSql db (sql : rest) = do
-- | 跑引擎层全部 hspec 用例
main :: IO ()
main = hspec $ do
+ NormalizationSpec.spec
+ RuntimeSpec.spec
describe "identity syntax" $ do
+ it "parses independent sudo authentication switches" $ do
+ parseStatement "ALTER USER worker ALLOW_SUDO_AUTH" `shouldBe` Right (AlterIdentity "worker" [("allow_sudo_auth", True)])
+ parseStatement "ALTER ROLE worker NOALLOW_SUDO_AUTH" `shouldBe` Right (AlterIdentity "worker" [("allow_sudo_auth", False)])
it "parses role and user attributes with their aliases" $ do
parseStatement "ALTER ROLE worker LOGIN SUPERUSER ENABLED" `shouldBe` Right (AlterIdentity "worker" [("login", True), ("superuser", True), ("enabled", True)])
parseStatement "ALTER USER worker NOLOGIN NOSUPERUSER DISABLED" `shouldBe` Right (AlterIdentity "worker" [("login", False), ("superuser", False), ("enabled", False)])
@@ -1056,6 +1063,16 @@ main = hspec $ do
parseStatement "GRANT SELECT ON users TO analyst WITH"
`shouldSatisfy` isLeft
+ it "parses database scopes, explicit tables and qualified wildcards" $ do
+ parseStatement "GRANT CONNECT, CREATE ON DATABASE sales TO analyst"
+ `shouldBe` Right (GrantPrivileges ["CONNECT", "CREATE"] "database:sales" "analyst" False)
+ parseStatement "REVOKE CONNECT ON DATABASE sales FROM PUBLIC"
+ `shouldBe` Right (RevokePrivileges ["CONNECT"] "database:sales" "PUBLIC")
+ parseStatement "GRANT SELECT ON TABLE sales.orders TO analyst"
+ `shouldBe` Right (GrantPrivileges ["SELECT"] "sales.orders" "analyst" False)
+ parseStatement "REVOKE ALL ON sales.* FROM analyst"
+ `shouldBe` Right (RevokePrivileges ["ALL"] "sales.*" "analyst")
+
it "parses REVOKE symmetrically" $ do
parseStatement "REVOKE SELECT ON users FROM analyst"
`shouldBe` Right (RevokePrivileges ["SELECT"] "users" "analyst")
@@ -2077,6 +2094,27 @@ main = hspec $ do
]
describe "ChuSQL.Core.Engine.Storage.IPC" $ do
+ it "persists runtime type identities and nested values across restart" $ do
+ withTestSession $ \srv -> do
+ -- | 解析并通过真实存储执行 SQL
+ let query source = case parseStatement source of
+ Left err -> expectationFailure err >> pure (Left err)
+ Right statement -> runIPCStorage (runStatementM statement)
+ originalRows <- withServerEnv srv $ do
+ query "CREATE TABLE compound (id Int, xs [Int], opt Maybe String, pair (Int, String), text String)" `shouldReturn` Right []
+ query "INSERT INTO compound (id, xs, opt, pair, text) VALUES (1, LIST(1, 2), MAYBE('x'), TUPLE(3, 'y'), 'abc')" `shouldReturn` Right []
+ query "SELECT xs, opt, pair, length(text) FROM compound"
+ originalRows `shouldSatisfy` isRight
+ closeConnection
+ openSessionIn (dataDir srv)
+ reloadedRows <- withServerEnv srv $ query "SELECT xs, opt, pair, length(text) FROM compound"
+ reloadedRows `shouldBe` originalRows
+ withServerEnv srv $ do
+ query "INSERT INTO compound (xs) VALUES (LIST('wrong'))" >>= (`shouldSatisfy` isLeft)
+ query "UPDATE compound SET xs = LIST(9) WHERE id = 1" `shouldReturn` Right []
+ updated <- query "SELECT xs FROM compound WHERE id = 1"
+ updated `shouldSatisfy` isRight
+
it "projects scan columns and preserves constant rows" $ do
withTestSession $ \srv -> withServerEnv srv $ do
runIPCStorage (insertMany "projected" [[("id", VInt 1), ("name", VStr "one")], [("id", VInt 2), ("name", VNull)]]) `shouldReturn` Right ()
diff --git a/chusql-core/model/chusql-core-model.cabal b/chusql-core/model/chusql-core-model.cabal
index 2b4c6d6..677f4e3 100644
--- a/chusql-core/model/chusql-core-model.cabal
+++ b/chusql-core/model/chusql-core-model.cabal
@@ -13,10 +13,13 @@ library
hs-source-dirs: src
exposed-modules: ChuSQL.Core.Model
ChuSQL.Core.Protocol
+ ChuSQL.Core.Runtime
build-depends: base >= 4.14 && < 5
, aeson
, hashable
, scientific
, text
+ , bytestring
+ , vector
default-language: Haskell2010
ghc-options: -Wall
diff --git a/chusql-core/model/src/ChuSQL/Core/Model.hs b/chusql-core/model/src/ChuSQL/Core/Model.hs
index 23d9eaa..c96bcae 100644
--- a/chusql-core/model/src/ChuSQL/Core/Model.hs
+++ b/chusql-core/model/src/ChuSQL/Core/Model.hs
@@ -48,6 +48,8 @@ import Data.Char (isDigit, isSpace, toLower)
import Data.Hashable (Hashable (..))
import Data.List (isPrefixOf, isSuffixOf, stripPrefix)
import Data.Maybe (fromMaybe, mapMaybe)
+import ChuSQL.Core.Runtime
+import qualified Data.ByteString.Lazy.Char8 as BS
-- 数据模型:列类型、列约束、值、行、表、数据库,外加查表小工具。
@@ -68,6 +70,7 @@ data ColumnType
| CBlob
| CNull
| CDomain String ColumnType
+ | CRuntime TypeId
deriving (Show, Eq)
-- | 列定义:类型 + 约束;约束全部取默认值就是普通可空列。
@@ -121,6 +124,7 @@ typeLabel CTimestamp = "TTimestamp"
typeLabel CBlob = "TBlob"
typeLabel CNull = "NULL"
typeLabel (CDomain name _) = name
+typeLabel (CRuntime tid) = renderTypeExpr (typeExpression tid)
-- | 线上/磁盘上的类型名
typeName :: ColumnType -> String
@@ -139,6 +143,7 @@ typeName CTimestamp = "timestamp"
typeName CBlob = "blob"
typeName CNull = "null"
typeName (CDomain name base) = "domain(" ++ name ++ "," ++ typeName base ++ ")"
+typeName (CRuntime tid) = "runtime(" ++ BS.unpack (encodeType tid) ++ ")"
-- | 生成类型定义的目录键
domainTableName :: String -> String
@@ -151,6 +156,9 @@ isDomainTable name = "__system_domain_" `isPrefixOf` map toLower name
-- | 解析类型名,可带长度/精度参数;不认识的给 Nothing
parseColumnType :: String -> Maybe ColumnType
parseColumnType raw
+ | Just inner <- stripPrefix "runtime(" raw
+ , Just body <- stripSuffixMaybe ")" inner =
+ either (const Nothing) (Just . CRuntime) (decodeType builtinTypes (BS.pack body))
| Just inner <- stripPrefix "domain(" raw
, Just body <- stripSuffixMaybe ")" inner
, (name, ',' : definition) <- break (== ',') body =
@@ -180,7 +188,7 @@ parseColumnType raw
("date", []) -> Just CDate
("timestamp", []) -> Just CTimestamp
("blob", []) -> Just CBlob
- _ -> Nothing
+ _ -> either (const Nothing) (Just . CRuntime . descriptorId) (parseTypeExpr raw >>= resolveType builtinTypes)
where
-- | 去掉空白的类型名
cleaned = map toLower (filter (not . isSpace) raw)
@@ -231,6 +239,7 @@ data TypeClass
| TemporalClass
| BooleanClass
| NullClass
+ | RuntimeClass
deriving (Show, Eq)
-- | 整型大类判断
@@ -250,12 +259,15 @@ typeClassOf CTimestamp = TemporalClass
typeClassOf CBool = BooleanClass
typeClassOf CNull = NullClass
typeClassOf (CDomain _ base) = typeClassOf base
+typeClassOf CRuntime{} = RuntimeClass
-- | 同大类可赋值;日期/时间列接受字符串字面量
assignable :: ColumnType -> ColumnType -> Bool
assignable target source
| source == CNull = True
| target == source = True
+ | CRuntime tid <- target = Just (typeExpression tid) == scalarTypeExpression source
+ | typeClassOf target == RuntimeClass || typeClassOf source == RuntimeClass = False
| typeClassOf target == typeClassOf source = True
| typeClassOf target == TemporalClass && typeClassOf source == TextClass = True
| otherwise = False
@@ -270,7 +282,9 @@ integerType _ = False
-- | 比较用的类型族:同大类可比,日期时间与文本互比
comparableTypes :: ColumnType -> ColumnType -> Bool
-comparableTypes a b = a == CNull || b == CNull || sameClass || crossTemporal
+comparableTypes a b
+ | typeClassOf a == RuntimeClass || typeClassOf b == RuntimeClass = False
+ | otherwise = a == CNull || b == CNull || sameClass || crossTemporal
where
-- | 两个类型同属一个大类
sameClass = typeClassOf a == typeClassOf b
@@ -288,6 +302,10 @@ numericType t = typeClassOf t == NumericClass
-- | 值能不能放进这一列(先不看可空)
valueFits :: ColumnType -> Value -> Bool
valueFits (CDomain _ base) value = valueFits base value
+valueFits (CRuntime expected) (VRuntime supplied value) = expected == supplied && validateValue builtinTypes expected value == Right ()
+valueFits (CRuntime expected) value = case scalarRuntimeValue value of
+ Just runtime -> validateValue builtinTypes expected runtime == Right ()
+ Nothing -> value == VNull
valueFits _ VNull = True
valueFits CInt (VInt _) = True
valueFits CBigInt (VInt _) = True
@@ -326,6 +344,15 @@ coerceValue t v = case t of
CBlob -> toText t v
CNull -> Left "a NULL output cannot contain a non-NULL value"
CDomain _ base -> coerceValue base v
+ CRuntime expected -> case v of
+ VRuntime supplied value | supplied == expected -> do
+ validateValue builtinTypes expected value
+ Right v
+ _ -> case scalarRuntimeValue v of
+ Just value -> do
+ validateValue builtinTypes expected value
+ Right (VRuntime expected value)
+ Nothing -> Left "runtime value type identity mismatch"
-- | 整数收进整型列
toInt :: ColumnType -> Value -> Either String Value
@@ -422,6 +449,7 @@ data Value
| VFloat Double
| VStr String
| VBool Bool
+ | VRuntime TypeId RuntimeValue
deriving (Show, Eq)
-- | 值的比较关键字:整值浮点折成整数
@@ -450,6 +478,7 @@ compareValue a b = case (canonicalValue a, canonicalValue b) of
(VFloat x, VFloat y) -> compare x y
(VStr x, VStr y) -> compare x y
(VBool x, VBool y) -> compare x y
+ (VRuntime tx x, VRuntime ty y) -> compare (encodeType tx, show (canonicalRuntime x)) (encodeType ty, show (canonicalRuntime y))
(x, y) -> compare (valueRank x) (valueRank y)
-- | 族的先后,只用来给异族值定序
@@ -459,6 +488,7 @@ valueRank (VInt _) = 1
valueRank (VFloat _) = 1
valueRank (VStr _) = 2
valueRank (VBool _) = 3
+valueRank VRuntime{} = 4
-- | 相等语义:比较关键字相同即相等,与 hashValue 配套
valuesEqual :: Value -> Value -> Bool
@@ -472,6 +502,35 @@ hashValue v = case canonicalValue v of
VFloat d -> hashWithSalt 1 d
VStr t -> hashWithSalt 2 t
VBool b -> hashWithSalt 3 b
+ VRuntime tid value -> hashWithSalt 4 (BS.unpack (encodeType tid), show (canonicalRuntime value))
+
+-- | 解析现有标量列的逻辑类型
+scalarTypeExpression :: ColumnType -> Maybe TypeExpr
+scalarTypeExpression (CDomain _ base) = scalarTypeExpression base
+scalarTypeExpression CInt = Just intType
+scalarTypeExpression CBigInt = Just intType
+scalarTypeExpression CSmallInt = Just intType
+scalarTypeExpression CDouble = Just doubleType
+scalarTypeExpression CFloat = Just doubleType
+scalarTypeExpression CBool = Just boolType
+scalarTypeExpression CStr = Just stringType
+scalarTypeExpression _ = Nothing
+
+-- | 转换现有标量值
+scalarRuntimeValue :: Value -> Maybe RuntimeValue
+scalarRuntimeValue (VInt n) = Just (RInt n)
+scalarRuntimeValue (VFloat n) = Just (RDouble n)
+scalarRuntimeValue (VBool b) = Just (RBool b)
+scalarRuntimeValue (VStr text) = Just (RString text)
+scalarRuntimeValue _ = Nothing
+
+-- | 统一复合值中的浮点零关键字
+canonicalRuntime :: RuntimeValue -> RuntimeValue
+canonicalRuntime (RDouble n) | n == 0 = RDouble 0
+canonicalRuntime (RList values) = RList (map canonicalRuntime values)
+canonicalRuntime (RMaybe value) = RMaybe (fmap canonicalRuntime value)
+canonicalRuntime (RTuple values) = RTuple (map canonicalRuntime values)
+canonicalRuntime value = value
-- | 值按类型打散搅拌
instance Hashable Value where
diff --git a/chusql-core/model/src/ChuSQL/Core/Protocol.hs b/chusql-core/model/src/ChuSQL/Core/Protocol.hs
index 439d163..0c85f94 100644
--- a/chusql-core/model/src/ChuSQL/Core/Protocol.hs
+++ b/chusql-core/model/src/ChuSQL/Core/Protocol.hs
@@ -16,6 +16,7 @@ module ChuSQL.Core.Protocol (
) where
import ChuSQL.Core.Model (Histogram (..), Row, Value (..))
+import ChuSQL.Core.Runtime (builtinTypes, runtimeToJSON, runtimeFromJSON)
import Data.Aeson (FromJSON (..), ToJSON (..), object, withObject, (.:), (.:?), (.!=), (.=))
import qualified Data.Aeson as A
import qualified Data.Aeson.Key as K
@@ -78,7 +79,7 @@ data Request
| ReqIdentityInitialize T.Text
| ReqRoleCreate T.Text
| ReqCatalogManage Request
- | ReqIdentityAlter T.Text (Maybe Bool) (Maybe Bool) (Maybe Bool) (Maybe Bool)
+ | ReqIdentityAlter T.Text (Maybe Bool) (Maybe Bool) (Maybe Bool) (Maybe Bool) (Maybe Bool)
| ReqAccountCreate T.Text T.Text
| ReqAccountReset T.Text T.Text
| ReqAccountLogin T.Text (Maybe T.Text)
@@ -142,8 +143,8 @@ instance ToJSON Request where
toJSON ReqAccountsList = object ["method" .= ("accounts_list" :: T.Text)]
toJSON (ReqIdentityInitialize u) = object ["method" .= ("identity_initialize" :: T.Text), "administrator" .= u]
toJSON (ReqRoleCreate u) = object ["method" .= ("role_create" :: T.Text), "user" .= u]
- toJSON (ReqIdentityAlter u login super enabled manager) = object ["method" .= ("identity_alter" :: T.Text), "user" .= u,
- "can_login" .= login, "is_superuser" .= super, "enabled" .= enabled, "system_catalog_manager" .= manager]
+ toJSON (ReqIdentityAlter u login super enabled manager sudo) = object ["method" .= ("identity_alter" :: T.Text), "user" .= u,
+ "can_login" .= login, "is_superuser" .= super, "enabled" .= enabled, "system_catalog_manager" .= manager, "allow_sudo_auth" .= sudo]
toJSON (ReqAccountCreate u h) = object ["method" .= ("account_create" :: T.Text), "user" .= u, "password_hash" .= h]
toJSON (ReqAccountReset u h) = object ["method" .= ("account_reset" :: T.Text), "user" .= u, "password_hash" .= h]
toJSON (ReqAccountLogin u at) = object ["method" .= ("account_login" :: T.Text), "user" .= u, "at" .= at]
@@ -316,6 +317,7 @@ data Account = Account
, accountIsSuperuser :: Bool
, accountEnabled :: Bool
, accountSystemCatalogManager :: Bool
+ , accountAllowSudoAuth :: Bool
} deriving (Eq)
-- | 账号的展示:只露账号名与版本号
@@ -327,14 +329,14 @@ instance FromJSON Account where
parseJSON = withObject "Account" $ \o -> Account <$> o .: "id" <*> o .: "user"
<*> o .: "password_hash" <*> o .: "revision"
<*> o .:? "registered_at" .!= "" <*> o .:? "last_login_at"
- <*> o .:? "can_login" .!= True <*> o .:? "is_superuser" .!= False <*> o .:? "enabled" .!= True <*> o .:? "system_catalog_manager" .!= False
+ <*> o .:? "can_login" .!= True <*> o .:? "is_superuser" .!= False <*> o .:? "enabled" .!= True <*> o .:? "system_catalog_manager" .!= False <*> o .:? "allow_sudo_auth" .!= False
-- | 账号编码成 JSON
instance ToJSON Account where
toJSON a = object ["id" .= accountId a, "user" .= accountUser a,
"password_hash" .= accountHash a, "revision" .= accountRevision a,
"registered_at" .= accountRegisteredAt a, "last_login_at" .= accountLastLoginAt a,
- "can_login" .= accountCanLogin a, "is_superuser" .= accountIsSuperuser a, "enabled" .= accountEnabled a, "system_catalog_manager" .= accountSystemCatalogManager a]
+ "can_login" .= accountCanLogin a, "is_superuser" .= accountIsSuperuser a, "enabled" .= accountEnabled a, "system_catalog_manager" .= accountSystemCatalogManager a, "allow_sudo_auth" .= accountAllowSudoAuth a]
-- | 存储层响应
data Response
@@ -373,6 +375,7 @@ valueToJSON (VInt n) = A.Number (fromIntegral n)
valueToJSON (VFloat d) = A.Number (fromFloatDigits d)
valueToJSON (VStr s) = A.String (T.pack s)
valueToJSON (VBool b) = A.Bool b
+valueToJSON (VRuntime tid value) = object ["__chusql_runtime_v1" .= runtimeToJSON tid value]
-- | JSON 解回值
valueFromJSON :: A.Value -> Parser Value
@@ -383,6 +386,9 @@ valueFromJSON (A.Number n) =
Left d -> pure (VFloat d)
valueFromJSON (A.String s) = pure (VStr (T.unpack s))
valueFromJSON (A.Bool b) = pure (VBool b)
+valueFromJSON (A.Object fields)
+ | KM.size fields == 1, Just payload <- KM.lookup "__chusql_runtime_v1" fields =
+ either fail (pure . uncurry VRuntime) (runtimeFromJSON builtinTypes payload)
valueFromJSON _ = fail "unsupported value type"
-- | 一行编码成 JSON
diff --git a/chusql-core/model/src/ChuSQL/Core/Runtime.hs b/chusql-core/model/src/ChuSQL/Core/Runtime.hs
new file mode 100644
index 0000000..2470937
--- /dev/null
+++ b/chusql-core/model/src/ChuSQL/Core/Runtime.hs
@@ -0,0 +1,293 @@
+module ChuSQL.Core.Runtime
+ ( TypeConstructorId (..), TypeId, TypeExpr (..), Kind (..)
+ , TypeRegistry, TypeDescriptor (..), PhysicalType (..), RuntimeValue (..)
+ , builtinTypes, resolveType, describeType, typeExpression
+ , intType, doubleType, boolType, stringType, listType, maybeType, tupleType, renderTypeExpr, parseTypeExpr, renderRuntimeValue
+ , validateValue, encodeValue, decodeValue, encodeType, decodeType, runtimeToJSON, runtimeFromJSON
+ ) where
+
+import Data.Aeson (Value (..), eitherDecode, encode)
+import Data.Aeson.Types (Parser, parseEither)
+import qualified Data.ByteString.Lazy as BS
+import Data.Scientific (floatingOrInteger)
+import qualified Data.Text as T
+import qualified Data.Vector as V
+import Data.Char (isAlphaNum, toLower)
+import qualified Text.ParserCombinators.ReadP as P
+
+-- 运行时类型身份、构造器注册及版本化物理编码。
+
+-- | 标识稳定的类型构造器
+newtype TypeConstructorId = TypeConstructorId Int deriving (Show, Eq, Ord)
+
+-- | 表达逻辑类型应用
+data TypeExpr = TypeApply TypeConstructorId [TypeExpr] deriving (Show, Eq, Ord)
+
+-- | 标识完整的类型实例
+newtype TypeId = TypeId TypeExpr deriving (Show, Eq, Ord)
+
+-- | 描述构造器参数个数
+data Kind = TypeKind | TypeArrow Kind Kind | ProductKind deriving (Show, Eq)
+
+-- | 描述独立的物理表示
+data PhysicalType = SignedInteger | FloatingPoint | Boolean | Utf8 | Sequence PhysicalType
+ | Optional PhysicalType | Product [PhysicalType] deriving (Show, Eq)
+
+-- | 保存类型实例描述
+data TypeDescriptor = TypeDescriptor
+ { descriptorId :: TypeId, descriptorName :: String, descriptorPhysical :: PhysicalType
+ } deriving (Show, Eq)
+
+-- | 保存构造器元数据
+newtype TypeRegistry = TypeRegistry [(TypeConstructorId, String, Kind)] deriving (Show, Eq)
+
+-- | 表达运行时值
+data RuntimeValue = RInt Int | RDouble Double | RBool Bool | RString String
+ | RList [RuntimeValue] | RMaybe (Maybe RuntimeValue) | RTuple [RuntimeValue]
+ deriving (Show, Eq)
+
+-- | 恢复固定编号的内建构造器
+builtinTypes :: TypeRegistry
+builtinTypes = TypeRegistry
+ [(TypeConstructorId 1, "Int", TypeKind), (TypeConstructorId 2, "Double", TypeKind)
+ ,(TypeConstructorId 3, "Bool", TypeKind), (TypeConstructorId 4, "String", TypeKind)
+ ,(TypeConstructorId 5, "List", TypeArrow TypeKind TypeKind)
+ ,(TypeConstructorId 6, "Maybe", TypeArrow TypeKind TypeKind)
+ ,(TypeConstructorId 7, "Tuple", ProductKind)]
+
+-- | 构造整数类型表达式
+intType :: TypeExpr
+intType = TypeApply (TypeConstructorId 1) []
+
+-- | 构造浮点类型表达式
+doubleType :: TypeExpr
+doubleType = TypeApply (TypeConstructorId 2) []
+
+-- | 构造布尔类型表达式
+boolType :: TypeExpr
+boolType = TypeApply (TypeConstructorId 3) []
+
+-- | 构造字符串类型表达式
+stringType :: TypeExpr
+stringType = TypeApply (TypeConstructorId 4) []
+
+-- | 构造列表类型表达式
+listType :: TypeExpr -> TypeExpr
+listType t = TypeApply (TypeConstructorId 5) [t]
+
+-- | 构造可选类型表达式
+maybeType :: TypeExpr -> TypeExpr
+maybeType t = TypeApply (TypeConstructorId 6) [t]
+
+-- | 构造元组类型表达式
+tupleType :: [TypeExpr] -> TypeExpr
+tupleType = TypeApply (TypeConstructorId 7)
+
+-- | 渲染内建逻辑类型语法
+renderTypeExpr :: TypeExpr -> String
+renderTypeExpr ty@(TypeApply cid arguments) = case (cid, arguments) of
+ (TypeConstructorId 1, []) -> "Int"
+ (TypeConstructorId 2, []) -> "Double"
+ (TypeConstructorId 3, []) -> "Bool"
+ (TypeConstructorId 4, []) -> "String"
+ (TypeConstructorId 5, [child]) -> "[" ++ renderTypeExpr child ++ "]"
+ (TypeConstructorId 6, [child]) -> "Maybe " ++ renderTypeExpr child
+ (TypeConstructorId 7, fields) -> "(" ++ joinTypes fields ++ ")"
+ _ -> show ty
+ where
+ -- | 连接元组字段类型
+ joinTypes [] = ""
+ joinTypes [field] = renderTypeExpr field
+ joinTypes (field : fields) = renderTypeExpr field ++ ", " ++ joinTypes fields
+
+-- | 解析完整的内建逻辑类型文本
+parseTypeExpr :: String -> Either String TypeExpr
+parseTypeExpr source = case P.readP_to_S (P.skipSpaces *> expression <* P.skipSpaces <* P.eof) source of
+ [(ty, "")] -> Right ty
+ _ -> Left "invalid runtime type expression"
+ where
+ -- | 解析类型原子和构造器应用
+ expression = (intType <$ token "int") P.<++ (doubleType <$ token "double")
+ P.<++ (boolType <$ token "bool") P.<++ (stringType <$ token "string")
+ P.<++ (listType <$> between "[" "]" expression)
+ P.<++ (listType <$> (token "list" *> expression))
+ P.<++ (maybeType <$> (token "maybe" *> expression))
+ P.<++ (tupleType <$> between "(" ")" (P.sepBy expression (symbol ",")))
+ -- | 解析具有名称边界的类型关键字
+ token word = do
+ _ <- mapM character word
+ rest <- P.look
+ case rest of
+ c : _ | isAlphaNum c || c == '_' -> P.pfail
+ _ -> P.skipSpaces
+ -- | 不区分大小写匹配关键字字符
+ character c = P.satisfy ((== c) . toLower)
+ -- | 解析类型分隔符
+ symbol text = P.string text <* P.skipSpaces
+ -- | 解析括号包围的类型
+ between open close = P.between (symbol open) (symbol close)
+
+-- | 渲染运行时值的可读文本
+renderRuntimeValue :: RuntimeValue -> String
+renderRuntimeValue (RInt n) = show n
+renderRuntimeValue (RDouble n) = show n
+renderRuntimeValue (RBool b) = show b
+renderRuntimeValue (RString text) = show text
+renderRuntimeValue (RList values) = "[" ++ joinValues values ++ "]"
+renderRuntimeValue (RMaybe Nothing) = "Nothing"
+renderRuntimeValue (RMaybe (Just value)) = "Just (" ++ renderRuntimeValue value ++ ")"
+renderRuntimeValue (RTuple values) = "(" ++ joinValues values ++ ")"
+
+-- | 连接复合值的字段文本
+joinValues :: [RuntimeValue] -> String
+joinValues [] = ""
+joinValues [value] = renderRuntimeValue value
+joinValues (value : values) = renderRuntimeValue value ++ ", " ++ joinValues values
+
+-- | 提取身份中的逻辑类型
+typeExpression :: TypeId -> TypeExpr
+typeExpression (TypeId t) = t
+
+-- | 解析类型实例并校验 kind
+resolveType :: TypeRegistry -> TypeExpr -> Either String TypeDescriptor
+resolveType registry@(TypeRegistry constructors) expression@(TypeApply cid args) = do
+ (name, kind) <- case [(n, k) | (c, n, k) <- constructors, c == cid] of
+ [entry] -> Right entry
+ _ -> Left ("unknown type constructor: " ++ show cid)
+ children <- mapM (resolveType registry) args
+ physical <- case (cid, kind, map descriptorPhysical children) of
+ (TypeConstructorId 1, TypeKind, []) -> Right SignedInteger
+ (TypeConstructorId 2, TypeKind, []) -> Right FloatingPoint
+ (TypeConstructorId 3, TypeKind, []) -> Right Boolean
+ (TypeConstructorId 4, TypeKind, []) -> Right Utf8
+ (TypeConstructorId 5, TypeArrow TypeKind TypeKind, [child]) -> Right (Sequence child)
+ (TypeConstructorId 6, TypeArrow TypeKind TypeKind, [child]) -> Right (Optional child)
+ (TypeConstructorId 7, ProductKind, fields) -> Right (Product fields)
+ _ -> Left ("invalid type constructor application: " ++ name)
+ Right (TypeDescriptor (TypeId expression) name physical)
+
+-- | 按身份查询类型描述
+describeType :: TypeRegistry -> TypeId -> Either String TypeDescriptor
+describeType registry = resolveType registry . typeExpression
+
+-- | 校验逻辑类型对应的值
+validateValue :: TypeRegistry -> TypeId -> RuntimeValue -> Either String ()
+validateValue registry tid value = do
+ descriptor <- describeType registry tid
+ validatePhysical (descriptorPhysical descriptor) value
+
+-- | 校验值的物理结构
+validatePhysical :: PhysicalType -> RuntimeValue -> Either String ()
+validatePhysical SignedInteger RInt{} = Right ()
+validatePhysical FloatingPoint (RDouble n)
+ | not (isNaN n || isInfinite n) = Right ()
+validatePhysical Boolean RBool{} = Right ()
+validatePhysical Utf8 RString{} = Right ()
+validatePhysical (Sequence child) (RList values) = mapM_ (validatePhysical child) values
+validatePhysical (Optional _) (RMaybe Nothing) = Right ()
+validatePhysical (Optional child) (RMaybe (Just value)) = validatePhysical child value
+validatePhysical (Product fields) (RTuple values)
+ | length fields == length values = sequence_ (zipWith validatePhysical fields values)
+validatePhysical physical _ = Left ("value does not fit physical type: " ++ show physical)
+
+-- | 编码带版本与类型身份的值
+encodeValue :: TypeRegistry -> TypeId -> RuntimeValue -> Either String BS.ByteString
+encodeValue registry tid value = do
+ validateValue registry tid value
+ Right (encode (runtimeToJSON tid value))
+
+-- | 表达协议中的类型化值封装
+runtimeToJSON :: TypeId -> RuntimeValue -> Value
+runtimeToJSON tid value = array [Number 1, typeJson (typeExpression tid), valueJson value]
+
+-- | 解析并校验协议类型化值
+runtimeFromJSON :: TypeRegistry -> Value -> Either String (TypeId, RuntimeValue)
+runtimeFromJSON registry json = do
+ (expression, payload) <- parseEither parseEnvelope json
+ descriptor <- resolveType registry expression
+ value <- parseEither (parseValue (descriptorPhysical descriptor)) payload
+ validateValue registry (descriptorId descriptor) value
+ Right (descriptorId descriptor, value)
+
+-- | 解码并校验版本和类型身份
+decodeValue :: TypeRegistry -> TypeId -> BS.ByteString -> Either String RuntimeValue
+decodeValue registry expected bytes = do
+ json <- eitherDecode bytes
+ (expression, payload) <- parseEither parseEnvelope json
+ descriptor <- resolveType registry expression
+ if descriptorId descriptor /= expected then Left "encoded value type identity mismatch" else do
+ value <- parseEither (parseValue (descriptorPhysical descriptor)) payload
+ validateValue registry expected value
+ Right value
+
+-- | 编码版本化类型身份
+encodeType :: TypeId -> BS.ByteString
+encodeType tid = encode (array [Number 1, typeJson (typeExpression tid)])
+
+-- | 解码并解析类型身份
+decodeType :: TypeRegistry -> BS.ByteString -> Either String TypeId
+decodeType registry bytes = do
+ json <- eitherDecode bytes
+ expression <- parseEither parseTypeEnvelope json
+ descriptorId <$> resolveType registry expression
+
+-- | 生成 JSON 数组
+array :: [Value] -> Value
+array = Array . V.fromList
+
+-- | 编码构造器应用
+typeJson :: TypeExpr -> Value
+typeJson (TypeApply (TypeConstructorId cid) args) = array [Number (fromIntegral cid), array (map typeJson args)]
+
+-- | 解析构造器应用
+parseTypeJson :: Value -> Parser TypeExpr
+parseTypeJson (Array values) = case V.toList values of
+ [Number n, Array args] -> case floatingOrInteger n :: Either Double Integer of
+ Right cid | cid > 0, cid <= fromIntegral (maxBound :: Int) -> TypeApply (TypeConstructorId (fromInteger cid)) <$> mapM parseTypeJson (V.toList args)
+ _ -> fail "invalid type constructor identity"
+ _ -> fail "invalid type expression"
+parseTypeJson _ = fail "invalid type expression"
+
+-- | 解析类型版本封装
+parseTypeEnvelope :: Value -> Parser TypeExpr
+parseTypeEnvelope (Array values) = case V.toList values of
+ [Number 1, expression] -> parseTypeJson expression
+ _ -> fail "unsupported type encoding version or invalid envelope"
+parseTypeEnvelope _ = fail "invalid type envelope"
+
+-- | 解析值版本封装
+parseEnvelope :: Value -> Parser (TypeExpr, Value)
+parseEnvelope (Array values) = case V.toList values of
+ [Number 1, expression, payload] -> do
+ ty <- parseTypeJson expression
+ pure (ty, payload)
+ _ -> fail "unsupported value encoding version or invalid envelope"
+parseEnvelope _ = fail "invalid value envelope"
+
+-- | 编码物理值
+valueJson :: RuntimeValue -> Value
+valueJson (RInt n) = Number (fromIntegral n)
+valueJson (RDouble n) = Number (realToFrac n)
+valueJson (RBool b) = Bool b
+valueJson (RString s) = String (T.pack s)
+valueJson (RList values) = array (map valueJson values)
+valueJson (RMaybe Nothing) = array []
+valueJson (RMaybe (Just value)) = array [valueJson value]
+valueJson (RTuple values) = array (map valueJson values)
+
+-- | 按物理描述解析值
+parseValue :: PhysicalType -> Value -> Parser RuntimeValue
+parseValue SignedInteger (Number n) = case floatingOrInteger n :: Either Double Integer of
+ Right i | i >= fromIntegral (minBound :: Int), i <= fromIntegral (maxBound :: Int) -> pure (RInt (fromInteger i))
+ _ -> fail "integer is fractional or out of range"
+parseValue FloatingPoint (Number n) = pure (RDouble (realToFrac n))
+parseValue Boolean (Bool b) = pure (RBool b)
+parseValue Utf8 (String s) = pure (RString (T.unpack s))
+parseValue (Sequence child) (Array values) = RList <$> mapM (parseValue child) (V.toList values)
+parseValue (Optional child) (Array values) = case V.toList values of
+ [] -> pure (RMaybe Nothing)
+ [value] -> RMaybe . Just <$> parseValue child value
+ _ -> fail "invalid optional value"
+parseValue (Product fields) (Array values)
+ | length fields == V.length values = RTuple <$> sequence (zipWith parseValue fields (V.toList values))
+parseValue _ _ = fail "invalid physical value"
diff --git a/chusql-core/storage/src/catalog.rs b/chusql-core/storage/src/catalog.rs
index e3ca26a..81b33f2 100644
--- a/chusql-core/storage/src/catalog.rs
+++ b/chusql-core/storage/src/catalog.rs
@@ -34,6 +34,8 @@ pub struct ColumnStat {
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TableSchema {
+ #[serde(default)]
+ pub object_id: u64,
#[serde(default)]
pub system: bool,
pub columns: Vec,
@@ -51,6 +53,10 @@ pub struct TableSchema {
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Catalog {
+ #[serde(default)]
+ pub database_id: u64,
+ #[serde(default)]
+ pub object_high_water: u64,
#[serde(default)]
pub identity_high_water: i64,
tables: BTreeMap,
@@ -68,7 +74,36 @@ impl Catalog {
if bytes.is_empty() {
return Ok(Catalog::default());
}
- serde_json::from_slice(&bytes).map_err(io::Error::other)
+ let mut catalog: Self = serde_json::from_slice(&bytes).map_err(io::Error::other)?;
+ catalog.upgrade_object_ids()?;
+ Ok(catalog)
+ }
+
+ /// 补齐旧目录对象编号并校验唯一性
+ fn upgrade_object_ids(&mut self) -> io::Result<()> {
+ self.object_high_water = self.object_high_water.max(self.database_id);
+ let mut ids = BTreeSet::new();
+ for schema in self.tables.values() {
+ if schema.object_id != 0 && !ids.insert(schema.object_id) {
+ return Err(io::Error::other("duplicate catalog object id"));
+ }
+ self.object_high_water = self.object_high_water.max(schema.object_id);
+ }
+ for schema in self.tables.values_mut() {
+ if schema.object_id == 0 {
+ self.object_high_water = self.object_high_water.checked_add(1)
+ .ok_or_else(|| io::Error::other("catalog object id exhausted"))?;
+ schema.object_id = self.object_high_water;
+ }
+ }
+ Ok(())
+ }
+
+ /// 分配不复用的对象编号
+ pub fn reserve_object_id(&mut self) -> io::Result {
+ self.object_high_water = self.object_high_water.checked_add(1)
+ .ok_or_else(|| io::Error::other("catalog object id exhausted"))?;
+ Ok(self.object_high_water)
}
/// 返回所有表的 (表名, schema)。
@@ -106,15 +141,28 @@ impl Catalog {
/// 建表;已存在报错
pub fn create_table(&mut self, table: &str, columns: Vec) -> io::Result<()> {
+ self.create_table_with_id(table, columns, None)
+ }
+
+ /// 按日志中的编号建立表
+ pub fn create_table_with_id(&mut self, table: &str, columns: Vec, object_id: Option) -> io::Result<()> {
if self.tables.contains_key(table) {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("table already exists: {}", table),
));
}
+ let object_id = match object_id {
+ Some(id) if id == 0 || self.tables.values().any(|schema| schema.object_id == id) => {
+ return Err(io::Error::other("invalid catalog object id"));
+ }
+ Some(id) => { self.object_high_water = self.object_high_water.max(id); id }
+ None => self.reserve_object_id()?,
+ };
self.tables.insert(
table.to_string(),
TableSchema {
+ object_id,
system: false,
columns,
dropped_columns: BTreeSet::new(),
@@ -188,6 +236,18 @@ impl Catalog {
let entry = self.tables.entry(table.to_string()).or_default();
entry.stats.clear();
entry.row_count = 0;
+ for row in rows {
+ for (name, value) in row {
+ if let Some(x) = value.as_f64() {
+ let stat = entry.stats.entry(name.clone()).or_default();
+ stat.lo = Some(stat.lo.map_or(x, |lo| lo.min(x)));
+ stat.hi = Some(stat.hi.map_or(x, |hi| hi.max(x)));
+ if stat.hist.is_empty() {
+ stat.hist = vec![0; HIST_BUCKETS];
+ }
+ }
+ }
+ }
}
let mut changed = false;
for row in rows {
diff --git a/chusql-core/storage/src/protocol.rs b/chusql-core/storage/src/protocol.rs
index 391d7fc..edb0e48 100644
--- a/chusql-core/storage/src/protocol.rs
+++ b/chusql-core/storage/src/protocol.rs
@@ -25,6 +25,8 @@ pub struct Account {
pub is_superuser: bool,
#[serde(default)]
pub system_catalog_manager: bool,
+ #[serde(default)]
+ pub allow_sudo_auth: bool,
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default)]
@@ -96,7 +98,7 @@ pub enum Request {
AccountsList,
IdentityInitialize { administrator: String },
RoleCreate { user: String },
- IdentityAlter { user: String, can_login: Option, is_superuser: Option, enabled: Option, system_catalog_manager: Option },
+ IdentityAlter { user: String, can_login: Option, is_superuser: Option, enabled: Option, system_catalog_manager: Option, allow_sudo_auth: Option },
AccountCreate { user: String, password_hash: String },
AccountReset { user: String, password_hash: String },
AccountLogin {
@@ -305,11 +307,16 @@ pub enum Response {
},
Error { message: String },
Catalog { schemas: Vec },
+ Object { database_id: u64, object_id: Option },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TableSchemaWire {
pub table: String,
+ #[serde(default)]
+ pub database_id: u64,
+ #[serde(default)]
+ pub object_id: u64,
pub columns: Vec,
#[serde(default)]
pub row_count: u64,
diff --git a/chusql-core/storage/src/runtime.rs b/chusql-core/storage/src/runtime.rs
index 5f89626..a0d4b0e 100644
--- a/chusql-core/storage/src/runtime.rs
+++ b/chusql-core/storage/src/runtime.rs
@@ -30,10 +30,48 @@ struct Server {
type TableDescription = (Vec, u64, Vec, Vec);
impl Server {
+ /// 读取受保护的对象权限快照
+ fn object_acl_read(&self) -> std::io::Result> {
+ if !self.table_exists("__system_object_acl")? { return Ok(Vec::new()); }
+ let rows = self.with_existing_table("__system_object_acl", |table| table.scan())?;
+ let mut payload = String::new();
+ for (index, row) in rows.iter().enumerate() {
+ if row.get("id").and_then(|value| value.as_u64()) != Some(index as u64 + 1) {
+ return Err(std::io::Error::other("invalid object ACL chunk sequence"));
+ }
+ payload.push_str(row.get("payload").and_then(|value| value.as_str())
+ .ok_or_else(|| std::io::Error::other("invalid object ACL chunk"))?);
+ }
+ if rows.is_empty() { return Err(std::io::Error::other("empty object ACL snapshot")); }
+ Ok(vec![serde_json::from_value(serde_json::json!({"id":1,"payload":payload})).map_err(std::io::Error::other)?])
+ }
+
+ /// 比较并原子替换对象权限快照
+ fn object_acl_replace(&self, expected: Option<&str>, payload: &str) -> std::io::Result<()> {
+ let rows = self.object_acl_read()?;
+ let current = rows.first().and_then(|row| row.get("payload")).and_then(|value| value.as_str());
+ if current != expected { return Err(std::io::Error::other("object ACL changed concurrently")); }
+ let _: serde_json::Value = serde_json::from_str(payload).map_err(std::io::Error::other)?;
+ let mut ops = Vec::new();
+ if !self.table_exists("__system_object_acl")? {
+ ops.push(WalOp::CreateTable { table: "__system_object_acl".into(),
+ columns: vec![SchemaColumn { name: "id".into(), ty: "int".into(), ..Default::default() },
+ SchemaColumn { name: "payload".into(), ty: "str".into(), ..Default::default() }], object_id: None });
+ }
+ let chars: Vec = payload.chars().collect();
+ let rows = chars.chunks(256).enumerate().map(|(index, chunk)| {
+ serde_json::from_value(serde_json::json!({"id":index + 1,"payload":chunk.iter().collect::()}))
+ .map_err(std::io::Error::other)
+ }).collect::>>()?;
+ ops.push(WalOp::ReplaceAll { table: "__system_object_acl".into(), rows });
+ self.group_via_wal(&ops)
+ }
+
/// 新建服务器状态
fn new(cfg: Config) -> std::io::Result {
std::fs::create_dir_all(&cfg.data_dir)?;
let catalog = Catalog::load(cfg.data_dir.join("catalog.json"))?;
+ catalog.save(cfg.data_dir.join("catalog.json"))?;
let wal = Wal::new(cfg.data_dir.join("wal.log"));
Ok(Server {
cfg,
@@ -306,11 +344,11 @@ impl Server {
let _ = std::fs::remove_file(self.index_path(table, column));
Ok(())
}
- WalOp::CreateTable { table, columns } => {
+ WalOp::CreateTable { table, columns, object_id } => {
self.with_table(table, |_t| Ok(()))?;
let mut c = self.catalog.lock().map_err(|_| std::io::Error::other("catalog lock poisoned"))?;
- c.create_table(table, columns.clone())?;
- if table == TYPES_TABLE { c.mark_system(table)?; }
+ c.create_table_with_id(table, columns.clone(), *object_id)?;
+ if table == TYPES_TABLE || table == "__system_object_acl" { c.mark_system(table)?; }
c.save(self.catalog_path())
}
WalOp::DropTable { table } => {
@@ -506,12 +544,22 @@ impl Server {
fn group_via_wal(&self, ops: &[WalOp]) -> std::io::Result<()> {
std::fs::create_dir_all(&self.cfg.data_dir)?;
let wal = &self.wal;
- let commit_lsn = wal.append_group(ops)?;
+ let mut ops = std::borrow::Cow::Borrowed(ops);
+ if ops.iter().any(|op| matches!(op, WalOp::CreateTable { object_id: None, .. })) {
+ let mut catalog = self.catalog.lock().map_err(|_| std::io::Error::other("catalog lock poisoned"))?;
+ for op in ops.to_mut() {
+ if let WalOp::CreateTable { object_id, .. } = op
+ && object_id.is_none() {
+ *object_id = Some(catalog.reserve_object_id()?);
+ }
+ }
+ }
+ let commit_lsn = wal.append_group(&ops)?;
self.recovery_required.store(true, Ordering::Release);
- for op in ops {
+ for op in ops.iter() {
self.apply_op(op)?;
}
- for op in ops {
+ for op in ops.iter() {
self.flush_op_tables(op)?;
}
self.flush_all_tables()?;
@@ -639,7 +687,16 @@ impl Server {
let c = self.catalog.lock().unwrap();
Ok(c.describe(table).is_some_and(|s| !s.columns.iter().any(|col| &col.name == column)))
}
- WalOp::CreateTable { table, .. } => self.table_exists(table),
+ WalOp::CreateTable { table, object_id, .. } => {
+ let catalog = self.catalog.lock().map_err(|_| std::io::Error::other("catalog lock poisoned"))?;
+ match catalog.describe(table) {
+ Some(schema) if object_id.is_some_and(|id| id != schema.object_id) => {
+ Err(std::io::Error::other("WAL catalog object id mismatch"))
+ }
+ Some(_) => Ok(true),
+ None => Ok(false),
+ }
+ }
WalOp::DropTable { table } => Ok(!self.table_exists(table)?
&& !self.table_path(table).exists()
&& !self.index_path(table, "id").exists()),
@@ -1063,11 +1120,11 @@ impl Server {
Request::CatalogManage { command } => {
let target = match command.as_ref() {
Request::AccountCreate { user, .. } | Request::AccountReset { user, .. } | Request::AccountDrop { user }
- | Request::IdentityAlter { user, is_superuser: None, system_catalog_manager: None, .. } => Some(account_name(user)?),
+ | Request::IdentityAlter { user, is_superuser: None, system_catalog_manager: None, allow_sudo_auth: None, .. } => Some(account_name(user)?),
Request::AccountsList => None,
_ => return Err(Error::other("superuser required for this catalog operation")),
};
- if accounts.iter().any(|a| target.as_ref() == Some(&a.user) && (a.is_superuser || a.system_catalog_manager)) {
+ if accounts.iter().any(|a| target.as_ref() == Some(&a.user) && (a.is_superuser || a.system_catalog_manager || a.allow_sudo_auth)) {
return Err(Error::other("superuser required to manage privileged identities"));
}
*command
@@ -1116,10 +1173,13 @@ impl Server {
}
let unified = self.catalog.lock().map_err(|_| Error::other("catalog lock poisoned"))?.describe(IDENTITIES_TABLE).is_some();
if !accounts.is_empty() { require_superuser(&accounts)?; }
- if unified && previous == accounts { return Ok(accounts); }
+ let sudo_column = self.catalog.lock().map_err(|_| Error::other("catalog lock poisoned"))?.describe(IDENTITIES_TABLE)
+ .is_some_and(|schema| schema.columns.iter().any(|column| column.name == "allow_sudo_auth"));
+ if unified && sudo_column && previous == accounts { return Ok(accounts); }
}
Request::AccountCreate { user, password_hash } => {
let name = account_name(&user)?;
+ if name == "public" { return Err(Error::other("public is reserved for default privileges")); }
if accounts.iter().any(|a| a.user == name) {
return Err(Error::new(ErrorKind::AlreadyExists, "account already exists"));
}
@@ -1129,6 +1189,7 @@ impl Server {
}
Request::RoleCreate { user } => {
let name = account_name(&user)?;
+ if name == "public" { return Err(Error::other("public is reserved for default privileges")); }
if accounts.iter().any(|a| a.user == name) { return Err(Error::new(ErrorKind::AlreadyExists, "identity already exists")); }
let id = accounts.iter().map(|a| a.id).max().unwrap_or(0).max(self.catalog.lock().map_err(|_| Error::other("catalog lock poisoned"))?.identity_high_water)
.checked_add(1).ok_or_else(|| Error::other("account id exhausted"))?;
@@ -1136,13 +1197,14 @@ impl Server {
role.can_login = false;
accounts.push(role);
}
- Request::IdentityAlter { user, can_login, is_superuser, enabled, system_catalog_manager } => {
+ Request::IdentityAlter { user, can_login, is_superuser, enabled, system_catalog_manager, allow_sudo_auth } => {
let name = account_name(&user)?;
let account = accounts.iter_mut().find(|a| a.user == name).ok_or_else(|| Error::new(ErrorKind::NotFound, "unknown account"))?;
account.can_login = can_login.unwrap_or(account.can_login);
account.is_superuser = is_superuser.unwrap_or(account.is_superuser);
account.enabled = enabled.unwrap_or(account.enabled);
account.system_catalog_manager = system_catalog_manager.unwrap_or(account.system_catalog_manager);
+ account.allow_sudo_auth = allow_sudo_auth.unwrap_or(account.allow_sudo_auth);
account.revision = account.revision.checked_add(1).ok_or_else(|| Error::other("account revision exhausted"))?;
if had_superuser { require_superuser(&accounts)?; }
}
@@ -1226,7 +1288,7 @@ impl Server {
}
if existing.is_some() && original == rows { return Ok(rows); }
let mut ops = Vec::new();
- if existing.is_none() { ops.push(WalOp::CreateTable { table: TYPES_TABLE.into(), columns }); }
+ if existing.is_none() { ops.push(WalOp::CreateTable { table: TYPES_TABLE.into(), columns, object_id: None }); }
ops.push(WalOp::ReplaceAll { table: TYPES_TABLE.into(), rows: rows.clone() });
self.recovery_required.store(true, Ordering::Release);
self.group_via_wal(&ops)?;
@@ -1499,6 +1561,8 @@ impl Server {
.filter(|(name, _)| !blocked_table(name))
.map(|(name, ts)| TableSchemaWire {
table: name.to_string(),
+ database_id: c.database_id,
+ object_id: ts.object_id,
columns: ts.columns.clone(),
row_count: ts.row_count,
indexes: index_wires(ts),
@@ -1543,7 +1607,7 @@ impl Server {
};
}
let tname = table.clone();
- let op = WalOp::CreateTable { table, columns };
+ let op = WalOp::CreateTable { table, columns, object_id: None };
match self.write_via_wal(&op) {
Ok(()) => Response::Ok,
Err(e) => {
@@ -1733,6 +1797,7 @@ fn identity_rows(accounts: &[Account]) -> std::io::Result> {
("can_login".into(), serde_json::json!(a.can_login)),
("is_superuser".into(), serde_json::json!(a.is_superuser)),
("system_catalog_manager".into(), serde_json::json!(a.system_catalog_manager)),
+ ("allow_sudo_auth".into(), serde_json::json!(a.allow_sudo_auth)),
("enabled".into(), serde_json::json!(a.enabled)),
("identity_version".into(), serde_json::json!(a.identity_version)),
("registered_at".into(), serde_json::Value::String(a.registered_at.clone())),
@@ -1764,6 +1829,7 @@ fn identity_columns() -> Vec {
SchemaColumn { name: "can_login".into(), ty: "bool".into(), nullable: false, ..Default::default() },
SchemaColumn { name: "is_superuser".into(), ty: "bool".into(), nullable: false, ..Default::default() },
SchemaColumn { name: "system_catalog_manager".into(), ty: "bool".into(), nullable: false, ..Default::default() },
+ SchemaColumn { name: "allow_sudo_auth".into(), ty: "bool".into(), nullable: false, ..Default::default() },
SchemaColumn { name: "enabled".into(), ty: "bool".into(), nullable: false, ..Default::default() },
SchemaColumn { name: "identity_version".into(), ty: "int".into(), nullable: false, ..Default::default() },
SchemaColumn { name: "registered_at".into(), ty: "timestamp".into(),
@@ -1805,6 +1871,10 @@ fn decode_account_row(row: &Row) -> std::io::Result {
can_login: row.get("can_login").and_then(serde_json::Value::as_bool).unwrap_or(true),
is_superuser: row.get("is_superuser").and_then(serde_json::Value::as_bool).unwrap_or(false),
system_catalog_manager: row.get("system_catalog_manager").and_then(serde_json::Value::as_bool).unwrap_or(false),
+ allow_sudo_auth: match row.get("allow_sudo_auth") {
+ None => false,
+ Some(value) => value.as_bool().ok_or_else(|| std::io::Error::other("invalid sudo authentication attribute"))?,
+ },
enabled: row.get("enabled").and_then(serde_json::Value::as_bool).unwrap_or(true),
identity_version: row.get("identity_version").and_then(serde_json::Value::as_u64).unwrap_or(0) as u8,
})
@@ -1880,6 +1950,7 @@ fn new_account(id: i64, user: &str, password_hash: String) -> std::io::Result std::io::Result<()> {
+ let mut catalog = server.catalog.lock().map_err(|_| std::io::Error::other("catalog lock poisoned"))?;
+ if catalog.database_id == 0 {
+ let mut system = self.system.catalog.lock().map_err(|_| std::io::Error::other("catalog lock poisoned"))?;
+ let id = system.reserve_object_id()?;
+ system.save(self.system.catalog_path())?;
+ catalog.database_id = id;
+ catalog.save(server.catalog_path())?;
+ }
+ Ok(())
}
/// 按名字打开或新建一个库
@@ -2049,6 +2150,42 @@ impl Databases {
if !name.is_empty() && !valid_database(&name) { return Err(Error::other("invalid database name")); }
let mut named = self.named.lock().map_err(|_| Error::other("database lock poisoned"))?;
match method.as_str() {
+ "object_acl_read" => {
+ drop(named);
+ let _gate = self.system.gate.read().map_err(|_| Error::other("database gate poisoned"))?;
+ if self.system.recovery_required.load(Ordering::Acquire) { return Err(Error::other("recovery required")); }
+ return Ok(Response::Rows { rows: self.system.object_acl_read()? });
+ }
+ "object_acl_replace" => {
+ drop(named);
+ let _gate = self.system.gate.write().map_err(|_| Error::other("database gate poisoned"))?;
+ if self.system.recovery_required.load(Ordering::Acquire) { return Err(Error::other("recovery required")); }
+ let expected = match value.get("expected") {
+ None | Some(serde_json::Value::Null) => None,
+ Some(serde_json::Value::String(value)) => Some(value.as_str()),
+ _ => return Err(Error::other("invalid expected ACL payload")),
+ };
+ let payload = value.get("payload").and_then(|value| value.as_str()).ok_or_else(|| Error::other("missing ACL payload"))?;
+ self.system.object_acl_replace(expected, payload)?;
+ return Ok(Response::Ok);
+ }
+ "resolve_object" => {
+ let server = if name == SYSTEM_DATABASE { &self.system }
+ else { named.get(&name).ok_or_else(|| Error::other("unknown database"))? };
+ let _gate = server.gate.read().map_err(|_| Error::other("database gate poisoned"))?;
+ if server.recovery_required.load(Ordering::Acquire) { return Err(Error::other("recovery required")); }
+ let catalog = server.catalog.lock().map_err(|_| Error::other("catalog lock poisoned"))?;
+ let object_id = match value.get("table") {
+ None => None,
+ Some(serde_json::Value::String(table)) if !blocked_table(table) => {
+ let schema = catalog.describe(table).ok_or_else(|| Error::other("unknown table"))?;
+ if schema.object_id == 0 { return Err(Error::other("missing catalog object id")); }
+ Some(schema.object_id)
+ }
+ _ => return Err(Error::other("invalid object table")),
+ };
+ return Ok(Response::Object { database_id: catalog.database_id, object_id });
+ }
"all_catalogs" => {
let servers: Vec<(String, Arc)> =
std::iter::once((SYSTEM_DATABASE.to_string(), Arc::clone(&self.system)))
@@ -2082,6 +2219,7 @@ impl Databases {
cfg.data_dir = cfg.data_dir.join("databases").join(&name);
std::fs::create_dir(&cfg.data_dir)?;
let server = Server::new(cfg)?;
+ self.assign_database_id(&server)?;
server.catalog.lock().map_err(|_| Error::other("catalog lock poisoned"))?.save(server.catalog_path())?;
named.insert(name, Arc::new(server));
return Ok(Response::Ok);
@@ -2215,7 +2353,11 @@ fn maintenance_candidate(cfg: Config, mode: &str, user: &str, hash: &str, types:
}
let repair = mode == "repair";
let catalog_path = cfg.data_dir.join("catalog.json");
- let schemas = maintenance_schemas();
+ let mut schemas = maintenance_schemas();
+ if cfg.data_dir.join("__system_object_acl.db").is_file() {
+ schemas.push(("__system_object_acl", vec![SchemaColumn { name: "id".into(), ty: "int".into(), ..Default::default() },
+ SchemaColumn { name: "payload".into(), ty: "str".into(), ..Default::default() }]));
+ }
let mut missing_data = Vec::new();
let mut catalog = match Catalog::load(&catalog_path) {
Ok(catalog) => catalog,
diff --git a/chusql-core/storage/src/wal.rs b/chusql-core/storage/src/wal.rs
index 412449b..09f2e15 100644
--- a/chusql-core/storage/src/wal.rs
+++ b/chusql-core/storage/src/wal.rs
@@ -61,6 +61,7 @@ pub enum WalOp {
CreateTable {
table: String,
columns: Vec,
+ object_id: Option,
},
DropTable {
table: String,
@@ -92,6 +93,8 @@ struct ReplaceSchemaPayload {
#[derive(Debug, Serialize, Deserialize)]
struct CreateTablePayload {
columns: Vec,
+ #[serde(default)]
+ object_id: Option,
}
pub struct Wal {
@@ -416,10 +419,10 @@ fn encode(op: &WalOp, lsn: u64) -> io::Result> {
.map_err(io::Error::other)?;
(OP_REPLACE_SCHEMA, table.clone(), p)
}
- WalOp::CreateTable { table, columns } => (
+ WalOp::CreateTable { table, columns, object_id } => (
OP_CREATE_TABLE,
table.clone(),
- serde_json::to_vec(&CreateTablePayload { columns: columns.clone() })
+ serde_json::to_vec(&CreateTablePayload { columns: columns.clone(), object_id: *object_id })
.map_err(io::Error::other)?,
),
WalOp::DropTable { table } => (OP_DROP_TABLE, table.clone(), Vec::new()),
@@ -536,6 +539,7 @@ fn decode_body(body: &[u8]) -> Result<(u64, WalOp), String> {
WalOp::CreateTable {
table,
columns: p.columns,
+ object_id: p.object_id,
}
}
OP_DROP_TABLE => WalOp::DropTable { table },
diff --git a/chusql-core/storage/tests/catalog_test.rs b/chusql-core/storage/tests/catalog_test.rs
index bfc7dbd..eb71144 100644
--- a/chusql-core/storage/tests/catalog_test.rs
+++ b/chusql-core/storage/tests/catalog_test.rs
@@ -4,6 +4,55 @@ use serde_json::json;
// 数据字典测试:补列、去重、未知表。
+/// 表删除重建和重载均保留编号规则
+#[test]
+fn object_ids_survive_reload_and_are_not_reused() {
+ let dir = tempfile::tempdir().unwrap();
+ let path = dir.path().join("catalog.json");
+ let mut catalog = Catalog::default();
+ catalog.create_table("t", vec![]).unwrap();
+ let old = catalog.describe("t").unwrap().object_id;
+ assert_ne!(old, 0);
+ catalog.save(&path).unwrap();
+ let mut catalog = Catalog::load(&path).unwrap();
+ assert_eq!(catalog.describe("t").unwrap().object_id, old);
+ catalog.drop_table("t").unwrap();
+ catalog.save(&path).unwrap();
+ let mut catalog = Catalog::load(&path).unwrap();
+ catalog.create_table("t", vec![]).unwrap();
+ assert!(catalog.describe("t").unwrap().object_id > old);
+}
+
+/// 旧目录迁移保留已有编号并拒绝重复
+#[test]
+fn legacy_object_ids_upgrade_without_collisions() {
+ let dir = tempfile::tempdir().unwrap();
+ let path = dir.path().join("catalog.json");
+ std::fs::write(&path, json!({"tables": {
+ "a": {"columns": [], "object_id": 42},
+ "b": {"columns": []}
+ }}).to_string()).unwrap();
+ let catalog = Catalog::load(&path).unwrap();
+ assert_eq!(catalog.describe("a").unwrap().object_id, 42);
+ assert_eq!(catalog.describe("b").unwrap().object_id, 43);
+ catalog.save(&path).unwrap();
+ assert_eq!(Catalog::load(&path).unwrap().describe("b").unwrap().object_id, 43);
+ std::fs::write(&path, json!({"tables": {
+ "a": {"columns": [], "object_id": 42},
+ "b": {"columns": [], "object_id": 42}
+ }}).to_string()).unwrap();
+ assert!(Catalog::load(&path).unwrap_err().to_string().contains("duplicate"));
+}
+
+/// 编号耗尽明确失败且不建立对象
+#[test]
+fn exhausted_object_ids_fail_without_creating_table() {
+ let mut catalog = Catalog::default();
+ catalog.object_high_water = u64::MAX;
+ assert!(catalog.create_table("t", vec![]).is_err());
+ assert!(catalog.describe("t").is_none());
+}
+
/// 用键值对构造一行
@@ -154,6 +203,28 @@ fn capped_distinct_keeps_histogram_and_other_columns_accurate() {
assert_eq!(catalog.describe("t").unwrap().stats["id"].distinct, 4000);
}
+/// 重建直方图不受行顺序影响
+#[test]
+fn rebuilt_histogram_has_exact_buckets_in_any_order() {
+ let rows: Vec = (0..160).map(|n| row(&[("v", json!(n))])).collect();
+ let mut forward = Catalog::default();
+ forward.rebuild_stats("t", &rows);
+ let mut reverse = Catalog::default();
+ let reversed: Vec = rows.iter().rev().cloned().collect();
+ reverse.rebuild_stats("t", &reversed);
+ let stat = &forward.describe("t").unwrap().stats["v"];
+ assert_eq!(stat.hist, vec![10; 16]);
+ assert_eq!(stat.hist, reverse.describe("t").unwrap().stats["v"].hist);
+ forward.record_insert("t", &row(&[("v", json!(10000))]));
+ forward.record_delete("t", &[row(&[("v", json!(10000))])]);
+ forward.rebuild_stats("t", &rows[80..]);
+ let repaired = &forward.describe("t").unwrap().stats["v"];
+ assert_eq!(repaired.lo, Some(80.0));
+ assert_eq!(repaired.hi, Some(159.0));
+ assert_eq!(repaired.hist, vec![5; 16]);
+ assert_eq!(forward.describe("t").unwrap().row_count, 80);
+}
+
/// 老类型名并到新写法,参数保留,未知类型原样
#[test]
fn normalize_type_maps_legacy_names() {
diff --git a/chusql-core/storage/tests/runtime_test.rs b/chusql-core/storage/tests/runtime_test.rs
index a77773a..4b1bfbf 100644
--- a/chusql-core/storage/tests/runtime_test.rs
+++ b/chusql-core/storage/tests/runtime_test.rs
@@ -5,11 +5,165 @@ use chusql_core_storage::runtime::Storage;
// 进程内存储集成测试:协议请求、索引、账号与配置。
+/// 权限快照支持跨页文本并拒绝旧版本覆盖
+#[test]
+fn object_acl_snapshots_are_private_and_compare_versions() -> Result<(), Box> {
+ let (server, data) = start_server();
+ let mut c = connect()?;
+ let payload = serde_json::json!({"grants": "权限\"\\".repeat(4000)}).to_string();
+ request_ok(&mut c, serde_json::json!({"method":"object_acl_replace","expected":null,"payload":payload}))?;
+ let current: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"object_acl_read"}"#))?;
+ assert_eq!(current["rows"][0]["payload"], payload);
+ let conflict = send(&mut c, r#"{"method":"object_acl_replace","expected":null,"payload":"{}"}"#);
+ assert!(conflict.contains("changed concurrently"), "{conflict}");
+ assert!(send(&mut c, r#"{"method":"scan","database":"system","table":"__system_object_acl"}"#).contains("reserved system table"));
+ assert!(!send(&mut c, r#"{"method":"all_catalogs"}"#).contains("__system_object_acl"));
+ drop(c);
+ drop(server);
+ let _restarted = restart_at(data.path())?;
+ let mut c = connect()?;
+ let reloaded: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"object_acl_read"}"#))?;
+ assert_eq!(reloaded, current);
+ Ok(())
+}
+
+/// 权限并发替换只能有一个版本提交
+#[test]
+fn concurrent_object_acl_replacement_preserves_one_winner() -> Result<(), Box> {
+ let (_server, _data) = start_server();
+ let mut c = connect()?;
+ let workers: Vec<_> = (0..8).map(|index| {
+ let storage = Arc::clone(&c.0);
+ std::thread::spawn(move || storage.request_line(&serde_json::json!({
+ "method":"object_acl_replace","expected":null,"payload":serde_json::json!({"winner":index}).to_string()
+ }).to_string()))
+ }).collect();
+ let replies: Vec = workers.into_iter().map(|worker| serde_json::from_str(&worker.join().unwrap()).unwrap()).collect();
+ assert_eq!(replies.iter().filter(|reply| reply["status"] == "ok").count(), 1);
+ assert_eq!(replies.iter().filter(|reply| reply["message"].as_str().is_some_and(|message| message.contains("changed concurrently"))).count(), 7);
+ let loaded: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"object_acl_read"}"#))?;
+ assert!(serde_json::from_str::(loaded["rows"][0]["payload"].as_str().unwrap())?["winner"].is_number());
+ Ok(())
+}
+
+/// 权限快照落盘失败后重放完整替换
+#[test]
+fn object_acl_commit_failure_replays_the_complete_snapshot() -> Result<(), Box> {
+ let (server, data) = start_server();
+ let mut c = connect()?;
+ let pending = data.path().join("system/catalog.json.pending");
+ std::fs::create_dir(&pending)?;
+ let failed = send(&mut c, r#"{"method":"object_acl_replace","expected":null,"payload":"{\"version\":1}"}"#);
+ assert!(failed.contains("error"), "{failed}");
+ assert!(send(&mut c, r#"{"method":"object_acl_read"}"#).contains("recovery required"));
+ drop(c);
+ drop(server);
+ std::fs::remove_dir(pending)?;
+ let _restarted = restart_at(data.path())?;
+ let mut c = connect()?;
+ let loaded: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"object_acl_read"}"#))?;
+ assert_eq!(loaded["rows"][0]["payload"], "{\"version\":1}");
+ Ok(())
+}
+
+/// 对象编号区分跨库同名及删除重建
+#[test]
+fn object_identity_tracks_database_and_table_incarnations() -> Result<(), Box> {
+ let (server, data) = start_server();
+ let mut c = connect()?;
+ request_ok(&mut c, serde_json::json!({"method":"create_database","database":"other"}))?;
+ for database in ["main", "other"] {
+ request_ok(&mut c, serde_json::json!({"method":"create_table","database":database,"table":"items","columns":[{"name":"id","ty":"int"}]}))?;
+ }
+ let old: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"resolve_object","database":"main","table":"items"}"#))?;
+ let other: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"resolve_object","database":"other","table":"items"}"#))?;
+ assert_eq!(old["status"], "object");
+ assert_ne!(old["database_id"], other["database_id"]);
+ assert!(old["object_id"].as_u64().unwrap() > 0);
+ let catalog: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"list_catalog","database":"main"}"#))?;
+ let info = catalog["schemas"].as_array().unwrap().iter().find(|entry| entry["table"] == "items").unwrap();
+ assert_eq!(info["database_id"], old["database_id"]);
+ assert_eq!(info["object_id"], old["object_id"]);
+ drop(c);
+ drop(server);
+ let restarted = restart_at(data.path())?;
+ let mut c = connect()?;
+ let stable: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"resolve_object","database":"main","table":"items"}"#))?;
+ assert_eq!(stable, old);
+ request_ok(&mut c, serde_json::json!({"method":"drop_table","database":"main","table":"items"}))?;
+ assert!(send(&mut c, r#"{"method":"resolve_object","database":"main","table":"items"}"#).contains("unknown table"));
+ request_ok(&mut c, serde_json::json!({"method":"create_table","database":"main","table":"items","columns":[{"name":"id","ty":"int"}]}))?;
+ let new: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"resolve_object","database":"main","table":"items"}"#))?;
+ assert_eq!(new["database_id"], old["database_id"]);
+ assert_ne!(new["object_id"], old["object_id"]);
+ request_ok(&mut c, serde_json::json!({"method":"drop_database","database":"other"}))?;
+ request_ok(&mut c, serde_json::json!({"method":"create_database","database":"other"}))?;
+ let new_database: serde_json::Value = serde_json::from_str(&send(&mut c, r#"{"method":"resolve_object","database":"other"}"#))?;
+ assert_ne!(new_database["database_id"], other["database_id"]);
+ assert!(new_database["object_id"].is_null());
+ drop(c);
+ drop(restarted);
+ Ok(())
+}
+
thread_local! {
/// 当前线程的存储实例:一个用例一个线程,用例内部按顺序起/停
static CURRENT: RefCell