兼容标准sql语法的mongodb数据库操作类

1、背景:

近期因为业务需求,需要将原来一个基于sqlserver的业务,切换到mongodb上。由于前端系统上线时间较长,做大范围的修改风险较高,就考虑能不能封装一个mongodb的sql操作类,解决日常的sql操作,以避免前台的大范围修改和测试。

(当然,为啥要sqlserver切mongodb?我们也很清楚他们是两种完全不同类型的数据库,但业务端就是这个样子了,so,世界是动态发展,业务也是千变万化的,技术只能吃药啊啊啊…)

2、依赖平台介绍

开发语言 :C# 5.0 
框架版本: .net framework 4.5.2(本来是3.5,这次升到了4.5.2) 
mongodb: 3.6-4.2 (服务器是win2008 ,所以不能太高版本)

mongodb driver:2.11.5 (嗯,依赖于mongdb 版本、.net framework版本,各种百度加尝试后,最终确认这个版本)

3、程序结构

示例程序:MongoSqlExample.cs
封装接口类:MongoSqlHelper.cs // 裸露的前端接口
基础类:MongoSqlClient.cs  //封装所有sql 转mongodb 操作json动作
异常处理类:LogAndException.cs // throw exception 同时记error日志
依赖:MongoDB.Bson ,MongoDB.Driver,MongoDB.Driver.Core,Newtonsoft.Json,log4.net

4、效果&用法

//查询返回 DataTable
//DataTable dt = MongoSqlHelper.Select("SELECT pkid,col1,col2 FROM table1 WHERE pkid=1 ORDER BY pkid ASC");;

//插入
//int rows = MongoSqlHelper.Insert("INSERT INTO table1(pkid,col1,col2) VALUES (1,'test','test2')");

//更新
//int rows = MongoSqlHelper.Update("UPDATE table1 SET col1='new name' WHERE pkid=1");

//删除
//int rows = MongoSqlHelper.Delete("DELETE FROM table WHERE pkid=1");

//计数
//int cnt = MongoSqlHelper.Count("SELECT COUNT(*) FROM table1 WHERE pkid=1");

//单行单列
//object name = MongoSqlHelper.ExecuteGetSingle("SELECT col1 FROM table1 WHERE pkid=1");

//多行单列
//List<object> list = MongoSqlHelper.ExecuteGetList("SELECT distinct pkid FROM table1 WHERE col2='test2' ");

//第一行第一列标准 Scalar 兼容(C# 5.0)
//object cnt = MongoSqlHelper.ExecuteScalar("SELECT COUNT(*) FROM table1 WHERE col1='test1' ");

//泛型 Scalar
//int cnt = MongoSqlHelper.ExecuteScalar<int>("SELECT COUNT(*) FROM table1 WHERE col1='test1' ");

5、下载链接

不给了,已经上传到github

6、代码

1)MongoSqlExample.cs

// ===================================================================================
//  MongoSqlClient  C# 5.0 示例程序
// ===================================================================================

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.IO;
using System.Text;
using log4net;
using log4net.Core;
using log4net.Appender;
using log4net.Layout;
using log4net.Config;
using log4net.Repository.Hierarchy;


namespace MongoSql.Demo
{
    internal class Program
    {
        private static string table="mongosql_table";

        private static void Main(string[] args)
        {
            /* 1. 设置日志输出到控制台 */
            //BasicConfigurator.Configure();
            
            /* 2. 设置日志输出到Log.txt */
            // 获取当前日志库的根日志器
            var hierarchy = (Hierarchy)LogManager.GetRepository();    
            // 创建一个文件Appender
            var fileAppender = new FileAppender
            {
                File = "Logs/Log.txt", // 指定日志文件路径
                AppendToFile = true,  // 是否追加到文件
                Layout = new PatternLayout("%date [%thread] %-5level %logger - %message%newline"), // 日志格式
                Threshold = Level.Info, // 设置日志级别为 INFO
                Encoding = Encoding.UTF8 // 设置文件编码为 UTF-8
            };
            fileAppender.ActivateOptions(); // 激活Appender    
            // 将文件Appender添加到根日志器
            hierarchy.Root.AddAppender(fileAppender);    
            // 设置根日志器的级别为 INFO
            hierarchy.Root.Level = Level.Info;

            /* 3. 自动初始化一些数据(不硬编码) */
            InitDemoData();
            

            /* 4. 键盘交互菜单 */
            string cmd;
            Console.WriteLine("=== MongoSql 交互演示 ===");
            while (true)
            {
                Console.WriteLine("\n【1】查询  【2】插入  【3】更新  【4】删除  【5】统计 【6】查询单行单列 【7】查询单列 【0】退出");
                Console.Write("请输入操作编号:");
                cmd = Console.ReadLine();
                try
                {
                    switch (cmd)
                    {
                        case "1": Query(); break;
                        case "2": Insert(); break;
                        case "3": Update(); break;
                        case "4": Delete(); break;
                        case "5": Count(); break;
                        case "6": GetColValueSingle(); break;
                        case "7": GetColValueMulti(); break;                        
                        case "0": return;
                        default: Console.WriteLine("无效编号"); break;
                    }
                }
                catch (MongoSqlException mex)
                {
                    Console.WriteLine("⚠️ SQL 错误:" + mex.Message);
                }
                catch (Exception ex)
                {
                    Console.WriteLine("⚠️ 异常:" + ex.Message);
                }
            }
        }

        #region 初始化演示数据(零硬编码)
        private static void InitDemoData()
        {
            Console.Write("首次运行,是否插入演示数据?(y/n 默认y):");
            if (Console.ReadLine().ToLower() == "n") return;

            /* 完全动态字段,实体仅做文档 */
            var sql = string.Format(
                "INSERT INTO {0}(Name,Price,Stock,CreateTime) VALUES " +
                "('键盘',99,200,'{1}')," +
                "('鼠标',59,150,'{1}')," +
                "('显示器',999,30,'{1}')",
                table, DateTime.Now.ToString("yyyy-MM-dd"));

            int n = MongoSqlHelper.Insert(sql);
            Console.WriteLine("已插入 {0} 条演示数据", n);
        }
        #endregion

        #region 各操作(均读键盘,零硬编码)
        private static void Query()
        {
            Console.Write("请输入 WHERE 子句(直接回车=全表):");
            string where = Console.ReadLine();
            Console.Write("请输入 ORDER BY 子句(直接回车=不排序):");
            string order = Console.ReadLine();
            Console.Write("请输入 TOP 数量(直接回车=不限):");
            string topStr = Console.ReadLine();
            int? top = null;
            if (!string.IsNullOrWhiteSpace(topStr)) top = int.Parse(topStr);

            /* 动态拼接 SELECT */
            string sql = string.Format("SELECT {0} * FROM {1} {2} {3}",
                                       top.HasValue ? "TOP " + top.Value : "",
                                       table,
                                       string.IsNullOrWhiteSpace(where) ? "" : "WHERE " + where,
                                       string.IsNullOrWhiteSpace(order) ? "" : "ORDER BY " + order);

            DataTable dt = MongoSqlHelper.Select(sql);
            if (dt.Rows.Count == 0)
            {
                Console.WriteLine("(无数据)");
                return;
            }

            /* 动态打印表头、行 */
            foreach (DataColumn c in dt.Columns) Console.Write(c.ColumnName + "\t");
            Console.WriteLine();
            foreach (DataRow r in dt.Rows)
            {
                foreach (DataColumn c in dt.Columns) Console.Write(r[c] + "\t");
                Console.WriteLine();
            }
        }

        private static void Insert()
        {
            /* 支持动态列数 */
            Console.Write("请输入列名,用逗号分隔(例:Name,Price,Stock):");
            string cols = Console.ReadLine();
            Console.Write("请输入 VALUES 子句(例:('键盘',99,200),('鼠标',59,150)):");
            string vals = Console.ReadLine();

            string sql = string.Format("INSERT INTO {0}({1}) VALUES {2}",
                                       table, cols, vals);
            int n = MongoSqlHelper.Insert(sql);
            Console.WriteLine("成功插入 {0} 条", n);
        }

        private static void Update()
        {
            Console.Write("请输入 SET 子句(例:Price=88,Stock=300):");
            string set = Console.ReadLine();
            Console.Write("请输入 WHERE 子句(例:Name='键盘'):");
            string where = Console.ReadLine();

            string sql = string.Format("UPDATE {0} SET {1} WHERE {2}",
                                       table, set, where);
            int n = MongoSqlHelper.Update(sql);
            Console.WriteLine("成功更新 {0} 条", n);
        }

        private static void Delete()
        {
            Console.Write("请输入 WHERE 子句(例:Price<60):");
            string where = Console.ReadLine();

            string sql = string.Format("DELETE FROM {0} WHERE {1}", table, where);
            int n = MongoSqlHelper.Delete(sql);
            Console.WriteLine("成功删除 {0} 条", n);
        }

        private static void Count()
        {
            Console.Write("请输入 WHERE 子句(直接回车=全表):");
            string where = Console.ReadLine();
            string sql = string.Format("SELECT COUNT(*) FROM {0} {1}",
                                       table,
                                       string.IsNullOrWhiteSpace(where) ? "" : "WHERE " + where);
            long c = MongoSqlHelper.Count(sql);
            Console.WriteLine("满足条件记录数:" + c);
        }
        
        private static void GetColValueSingle()
        {
            
            /**/
            Console.Write("请输入列名,用逗号分隔(例:Name,Price,Stock):");
            string col = Console.ReadLine();
            Console.Write("请输入 WHERE 子句(直接回车=取第一行):");
            string where = Console.ReadLine();
            string sql = string.Format("SELECT {0} FROM  {1} {2}",
                                       col, table,
                                       string.IsNullOrWhiteSpace(where) ? "" : "WHERE " + where);
            var obj = MongoSqlHelper.ExecuteGetSingle(sql);
            Console.WriteLine("获取到字段{0}值 {1} ", col,obj);
        }
        
        private static void GetColValueMulti()
        {
                        /**/
            Console.Write("请输入列名,用逗号分隔(例:Name,Price,Stock):");
            string col = Console.ReadLine();
            Console.Write("请输入 WHERE 子句(直接回车=全表):");
            string where = Console.ReadLine();            
            string sql = string.Format("SELECT {0} FROM  {1} {2}",
                                       col, table,
                                       string.IsNullOrWhiteSpace(where) ? "" : "WHERE " + where);
            List<object> dlists = MongoSqlHelper.ExecuteGetList(sql);
            foreach (var val in dlists)
            {
                Console.Write(val + "\t");
                Console.WriteLine();
            }
        }
                
        #endregion
    }
}


2)MongoSqlHelper.cs

// ===================================================================================
//  MongoSqlClient  C# 5.0 兼容完整版 
//  封装类:MongoSqlHelper
//  说明:数据库连接参数读取config文件
//  依赖:log4.net
// ===================================================================================

using System;
using System.Collections.Generic;
using System.Data;
using System.Text.RegularExpressions;
using System.Configuration;
using log4net;


/* 相关版本:MongoDB 驱动 2.11.5 + MongoDB 3.6、4.2 + C# 5.0 + .NET 4.5.2  
 
 一、已支持
  1. 等值/范围/IN 过滤
     SELECT * FROM tbl WHERE status=1 AND price>100 AND city IN ('bj','sh')
  2. TOP 
     SELECT TOP 50 * FROM tbl WHERE uid=1 ORDER BY createTime DESC
  3. COUNT / SUM / MAX / MIN / AVG
     SELECT COUNT(*) FROM tbl WHERE uid=1
     SELECT SUM(amount) AS total FROM tbl WHERE uid=1
     *****只允许单个查询,不允许同时查count 和max,要同时查多个聚合,请分别提交,譬如:****
     "SELECT COUNT(*),MAX(price) FROM tbl"   --- 不支持
  4. DISTINCT
     SELECT DISTINCT uid FROM tbl
  5. 一次插入多行
     INSERT INTO tbl(a,b) VALUES (1,'a'),(2,'b')
  7. 参数化写法(仅支持“拼好字符串”模式,不再传字典):
     string sql = "SELECT * FROM tbl WHERE uid=1 AND status=2";   // 直接拼
     DataTable dt = MongoSqlHelper.Select(sql);

 二、不支持
  · JOIN / 子查询 / UNION / 事务 / HAVING / 行锁
  · 表达式索引(如 $toInt:field)—— 3.6 不支持,必须预存字段再建索引
  · OFFSET / SKIP + LIMIT 组合(驱动支持,但本类未暴露 SKIP)
  · 视图、存储过程、触发器
  . CAST(驱动 2.11.5 自动用 $toInt/$toDate,但 3.6 无表达式索引,仅聚合场景,)
     SELECT CAST(price AS INT) AS priceInt FROM tbl ORDER BY CAST(price AS INT) ASC
     —— 会走聚合管道,**无索引**,大表慎用,所以也取消支持。

 三、能跑但会触发全表扫描,效率较低,数据量大时谨慎使用
  · LIKE '%xx%'   →  MongoDB $regex,3.6 不支持索引
  · 对 CAST/计算字段排序/过滤   →  走聚合,无索引

 四、版本要求
  · 驱动 2.11.5 最低要求 MongoDB 2.6,已测试 3.6+
  · .NET Framework 不得低于 4.5.2(驱动硬性要求)
  · C# 5.0 无 await/async,本类全部同步接口,无异步版本

 五、性能优化建议
  1. 凡是用于 WHERE / ORDER BY 的字段,务必转为纯类型,并建索引。
譬如price原来是字符型,用下述脚本可以不重建改成int型: db.col.updateMany({},[{$set:{priceInt:{$toInt:"$price"}}}]) db.col.createIndex({priceInt:1}) 2. 内存缓存限额在 app.config 配置,可考虑 MongoSql 100 MB
*/ namespace MongoSql.Demo { public static class MongoSqlHelper { private static readonly MongoSqlClient Client; private static readonly ILog logger = LogManager.GetLogger(typeof(MongoSqlHelper)); static MongoSqlHelper() { try { string conn = ConfigurationManager.AppSettings["MongoConn"]; string db = ConfigurationManager.AppSettings["MongoDb"]; if (string.IsNullOrEmpty(conn) || string.IsNullOrEmpty(db)) throw new Exception("AppSettings 缺少 MongoConn 或 MongoDb"); Client = new MongoSqlClient(conn, db, new List<string> { "table1", "table2", "mongosql_table" }); } catch (Exception ex) { LogAndException.Throw(ex,"MongoSqlClient 初始化失败..."); } } #region 通用日志模板 private static void LogEnter([System.Runtime.CompilerServices.CallerMemberName] string method = "", string sql = "") { logger.Info("[执行sql开始]" + method + " || SQL: " + sql); } private static void LogExit(string method, object ret) { logger.Info("[执行sql结束]" + method + " || Return: " + (ret ?? "null")); } #endregion #region ------- SQL 语法安检 ------- // 语法预检:非法字符、永不支持语法、CAST+ORDER BY、LIKE 全表扫 全部处理。 // 正常引号允许出现,譬如:Name="abc' // 配置项可关闭 CAST 排序拦截。 private static void CheckSql(string sql) { if (string.IsNullOrWhiteSpace(sql)) throw new Exception("SQL 为空"); string upper = sql.ToUpper().Trim(); /* 1. 危险字符(控制符、分号、反斜杠)*/ if (Regex.IsMatch(sql, @"[\x00-\x08\x0B-\x0C\x0E-\x1F;\\]")) throw new Exception("SQL 含非法字符(控制符、分号、反斜杠)"); /* 2. 不支持的语法——整词匹配,防止字段名误杀 */ string[] never = { "JOIN", "INNER JOIN", "LEFT JOIN", "RIGHT JOIN", "FULL JOIN", "HAVING", "UNION", "TRANSACTION", "BEGIN", "ROLLBACK", "COMMIT", "CREATE", "DROP", "ALTER", "GRANT", "REVOKE","LIMIT" }; foreach (string kw in never) if (Regex.IsMatch(upper, @"\b" + Regex.Escape(kw) + @"\b", RegexOptions.IgnoreCase)) throw new Exception("SQL 含不支持的语法:" + kw); /* 3. CAST + ORDER BY——提示不支持 */ if ( Regex.IsMatch(upper, @"\bCAST\s*\(") && Regex.IsMatch(upper, @"\bORDER\s+BY\b")) throw new Exception("CAST+ORDER BY 无索引,暂不支持!"); /* 4. 性能陷阱——提示不支持*/ if (Regex.IsMatch(sql, @"\bLIKE\s+'%[^']*%'", RegexOptions.IgnoreCase)) throw new Exception("SQL 含 LIKE '%xx%',会全表扫描,暂不支持!"); /* 5. 聚合函数数量——提示只允许 1 个,不支持同时count,sum 之类 */ int aggCount = 0; string[] aggs = { "COUNT", "SUM", "MAX", "MIN", "AVG" }; foreach (string a in aggs) aggCount += Regex.Matches(upper, @"\b" + a + @"\b", RegexOptions.IgnoreCase).Count; if (aggCount > 1) throw new Exception("一条 SQL 只允许 1 个聚合函数(COUNT/SUM/MAX/MIN/AVG),请分别提交"); } #endregion //<summary> //查询返回 DataTable //完整示例: //DataTable dt = MongoSqlHelper.Select("SELECT pkid,col1,col2 FROM table1 WHERE pkid=1 ORDER BY pkid ASC"); //</summary> public static DataTable Select(string sql) { CheckSql(sql); LogEnter("Select", sql); DataTable dt = Client.ExecuteDataTable(sql, null); if (dt == null || dt.Rows.Count == 0) logger.Warn("Select 返回空结果集"); LogExit("Select", dt.Rows.Count + ""); return dt; } //<summary> //插入 //完整示例: //int rows = MongoSqlHelper.Insert("INSERT INTO table1(pkid,col1,col2) VALUES (1,'test','test2')"); //</summary> public static int Insert(string sql) { CheckSql(sql); LogEnter("Insert", sql); int rows = Client.ExecuteInsert(sql); LogExit("Insert", rows + " 行插入"); return rows; } //<summary> //更新 //完整示例: //int rows = MongoSqlHelper.Update("UPDATE table1 SET col1='new name' WHERE pkid=1"); //</summary> public static int Update(string sql) { CheckSql(sql); LogEnter("Update", sql); int rows = Client.ExecuteUpdate(sql, null); LogExit("Update", rows + " 行更新"); return rows; } //<summary> //删除 //完整示例: //int rows = MongoSqlHelper.Delete("DELETE FROM table WHERE pkid=1"); //</summary> public static int Delete(string sql) { CheckSql(sql); LogEnter("Delete", sql); int rows = Client.ExecuteDelete(sql, null); LogExit("Delete", rows + " 行删除"); return rows; } //<summary> //计数 //完整示例: //int cnt = MongoSqlHelper.Count("SELECT COUNT(*) FROM table1 WHERE pkid=1"); //</summary> public static int Count(string sql) { CheckSql(sql); LogEnter("Count", sql); int cnt = Client.ExecuteCount(sql, null); LogExit("Count", cnt); return cnt; } //<summary> //单行单列 //完整示例: //object name = MongoSqlHelper.ExecuteGetSingle("SELECT col1 FROM table1 WHERE pkid=1"); //</summary> public static object ExecuteGetSingle(string sql) { CheckSql(sql); LogEnter("ExecuteGetSingle", sql); object val = Client.ExecuteGetSingle(sql, null); LogExit("ExecuteGetSingle", val); return val; } //<summary> //多行单列 //完整示例: //List<object> list = MongoSqlHelper.ExecuteGetList("SELECT distinct pkid FROM table1 WHERE col2='test2' "); //</summary> public static List<object> ExecuteGetList(string sql) { CheckSql(sql); LogEnter("ExecuteGetList", sql); List<object> list = Client.ExecuteGetList(sql, null); LogExit("ExecuteGetList", list == null ? " 0 条" : list.Count + ""); return list; } #region 标准 Scalar 兼容(C# 5.0) //<summary> //第一行第一列(DBNull → null) //完整示例: //object cnt = MongoSqlHelper.ExecuteScalar("SELECT COUNT(*) FROM table1 WHERE col1='test1' "); //</summary> public static object ExecuteScalar(string sql) { CheckSql(sql); LogEnter("ExecuteScalar", sql); DataTable dt = Select(sql); object val = (dt.Rows.Count == 0 || dt.Columns.Count == 0) ? null : dt.Rows[0][0]; val = val == DBNull.Value ? null : val; LogExit("ExecuteScalar", val); return val; } //<summary> //泛型 Scalar //完整示例: //int cnt = MongoSqlHelper.ExecuteScalar<int>("SELECT COUNT(*) FROM table1 WHERE col1='test1' "); //</summary> public static T ExecuteScalar<T>(string sql) { CheckSql(sql); object raw = ExecuteScalar(sql); if (raw == null || raw == DBNull.Value) return default(T); Type u = Nullable.GetUnderlyingType(typeof(T)); return u != null ? (T)Convert.ChangeType(raw, u) : (T)Convert.ChangeType(raw, typeof(T)); } #endregion } }

 

3)MongoSqlClient.cs

// ===================================================================================
//  MongoSqlClient  C# 5.0 兼容完整版  
//  基础类:MongoSqlClient  
//  说明:  含 Compass 可直接粘贴的执行脚本日志
//  依赖:MongoDB.Bson ,MongoDB.Driver,MongoDB.Driver.Core,Newtonsoft.Json,log4.net
// ===================================================================================
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Runtime.Caching;
using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Bson.Serialization;
using Newtonsoft.Json;
using log4net;

namespace MongoSql.Demo
{
    public sealed class MongoSqlClient : IDisposable
    {
        #region ── 私有字段 ──
        private readonly IMongoDatabase _db;
        private HashSet<string> _allowedTables;
        private static List<string> _collectionNames;
        private static readonly object _cacheLock = new object();
        private static DateTime _cacheExpires = DateTime.MinValue;
        private static readonly IBsonSerializer<BsonDocument> _docSerializer =
            BsonSerializer.SerializerRegistry.GetSerializer<BsonDocument>();

        private bool _disposed;
        private static readonly MemoryCache _parseCache = new MemoryCache("MongoSql");
        private static readonly ILog logger = LogManager.GetLogger(typeof(MongoSqlClient));
        #endregion

        #region ── 构造 / 释放 ──
        public MongoSqlClient(string connStr, string dbName, IEnumerable<string> allowedTables = null)
        {
            if (connStr == null) LogAndException.Throw(new ArgumentNullException("connStr"));
            if (dbName == null) LogAndException.Throw(new ArgumentNullException("dbName"));

            _db = new MongoClient(connStr).GetDatabase(dbName);

            _allowedTables = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
            {
                "table1",
                "table2"
            };

            if (allowedTables != null)
                _allowedTables = new HashSet<string>(allowedTables, StringComparer.OrdinalIgnoreCase);

            logger.Debug("表白名单:" + string.Join(", ", _allowedTables));
        }

        public void Dispose()
        {
            if (!_disposed) _disposed = true;
        }
        #endregion

        #region ── 表名校验 ──
        private void CheckTable(string table)
        {
            if (!_allowedTables.Contains(table))
                LogAndException.Throw(new Exception(string.Format("表 [{0}] 不在白名单内", table)));

            EnsureCollectionNamesUpToDate();
            if (!_collectionNames.Contains(table))
                //LogAndException.Throw(new Exception(string.Format("表 [{0}] 不存在于数据库中", table)));
                logger.Debug(string.Format("表 [{0}] 不存在于数据库中", table));
        }

        private void EnsureCollectionNamesUpToDate()
        {
            lock (_cacheLock)
            {
                if (DateTime.UtcNow >= _cacheExpires)
                {
                    _collectionNames = _db.ListCollectionNames().ToList();
                    _cacheExpires = DateTime.UtcNow.AddHours(1);
                }
            }
        }
        #endregion

        #region ── 1. 查询 => DataTable ──
        public DataTable ExecuteDataTable(string sql, IDictionary<string, object> parameters = null)
        {
            sql = CleanSql(sql);
            logger.Debug("ExecuteDataTable入口 sql=" + sql);
        
            try
            {
                var qi = CachedParse(sql, ParseSelect);
                CheckTable(qi.Table);
        
                if (qi.IsExists || qi.SubQuerySql != null)
                    return ExecuteExists(qi, parameters);
        
                if (qi.IsGroupBy)
                    return ExecuteGroupBy(qi, parameters);
        
                var coll = _db.GetCollection<BsonDocument>(qi.Table);
                var filter = BuildFilterAuto(qi.Where, parameters);
        
                /* ===== DISTINCT 去重 分支 ===== */
                if (qi.IsDistinct)
                {
                    var idDoc = new BsonDocument();
                    if (qi.Fields != null && qi.Fields.Length > 0)
                    {
                        foreach (var f in qi.Fields)
                            idDoc[f] = "$" + f;
                    }
                    else
                    {
                        idDoc["$$ROOT"] = "$$ROOT";
                    }
        
                    var pipeline = new List<BsonDocument>();
                    if (filter != FilterDefinition<BsonDocument>.Empty)
                        pipeline.Add(new BsonDocument("$match", filter.ToBsonDocument()));
                    pipeline.Add(new BsonDocument("$group", new BsonDocument("_id", idDoc)));
                    pipeline.Add(new BsonDocument("$replaceRoot", new BsonDocument("newRoot", "$_id")));
        
                    var docs = coll.Aggregate<BsonDocument>(pipeline.ToArray()).ToList();
        
                    // 打印 Compass 可执行脚本
                    string pipelineJson = "[" + string.Join(", ", pipeline.Select(p => p.ToJson())) + "]";
                    logger.Info("ExecuteDataTable DISTINCT 脚本(可直接粘贴 Compass):\r\n" +
                                 string.Format("db.{0}.aggregate({1})", qi.Table, pipelineJson));
        
                    logger.Debug("ExecuteDataTable DISTINCT 结果行数=" + docs.Count);
                    return ToDataTable(docs);
                }
                /* ============================== */
        
                var find = coll.Find(filter);
        
                bool isStar = qi.Fields != null && qi.Fields.Length == 1 && qi.Fields[0] == "*";
                if (isStar)
                    find = find.Project(Builders<BsonDocument>.Projection.Exclude("_noSuchField"));
                else if (qi.Fields != null && qi.Fields.Length > 0)
                {
                    var proj = Builders<BsonDocument>.Projection;
                    ProjectionDefinition<BsonDocument> p = null;
                    foreach (var f in qi.Fields)
                        p = p == null ? proj.Include(f) : p.Include(f);
                    find = find.Project(p);
                }
        
                // 排序
                BsonDocument sortDoc = null;
                if (!string.IsNullOrEmpty(qi.OrderBy))
                {
                    sortDoc = BuildSortAuto(qi.OrderBy);
                    find = find.Sort(sortDoc);
                }
                
                //支持top语法,不支持limit        
                if (qi.Top.HasValue && qi.Top.Value > 0)
                    find = find.Limit(qi.Top.Value);
                
                // 支持skip
                if (qi.Skip.HasValue && qi.Skip.Value > 0)
                    find = find.Skip(qi.Skip.Value);
        
                // 构造 Compass 日志
                string projectionClause;
                if (isStar || (qi.Fields == null || qi.Fields.Length == 0))
                    projectionClause = "{}";
                else
                    projectionClause = "{ " + string.Join(", ", qi.Fields.Select(f => "\"" + f + "\": 1")) + " }";
        
                string sortClause = sortDoc == null ? "" : ".sort(" + sortDoc.ToJson() + ")";
                string limitClause = qi.Top.HasValue ? ".limit(" + qi.Top.Value + ")" : "";
        
                logger.Info("ExecuteDataTable 脚本(可直接粘贴 Compass):\r\n" +
                             string.Format("db.{0}.find({1}, {2}){3}{4}",
                                           qi.Table,
                                           filter.Render(_docSerializer, BsonSerializer.SerializerRegistry).ToJson(),
                                           projectionClause,
                                           sortClause,
                                           limitClause));
        
                var list = find.ToList();
                logger.Debug("ExecuteDataTable 返回Table行数=" + list.Count);
                return ToDataTable(list);
            }
            catch (Exception ex)
            {
                LogAndException.Throw(ex);
                return null; // 返回空
            }
        }
        #endregion      
        
        #region ── 2. 插入 ──
        public int ExecuteInsert(string sql)
        {
            sql = CleanSql(sql);
            logger.Debug("ExecuteInsert sql=" + sql);

            try
            {
                var ii = CachedParse(sql, ParseInsertAuto);
                CheckTable(ii.Table);

                var docs = ii.Values.Select(row =>
                {
                    var doc = new BsonDocument();
                    for (int i = 0; i < ii.Columns.Count; i++)
                        doc[ii.Columns[i]] = BsonValue.Create(row[i]);
                    return doc;
                }).ToList();

                // 打印 Compass 可执行脚本
                string docsJson = "[" + string.Join(", ", docs.Select(d => d.ToJson())) + "]";
                logger.Info("ExecuteInsert 脚本(可直接粘贴 Compass):\r\n" +
                             string.Format("db.{0}.insertMany({1})", ii.Table, docsJson));

                _db.GetCollection<BsonDocument>(ii.Table).InsertMany(docs);
                logger.Debug("返回ExecuteInsert 插入行数=" + docs.Count);
                return docs.Count;
            }
            catch (Exception ex)
            {
                LogAndException.Throw(ex);
                return 0;
            }
        }
        #endregion

        #region ── 3. 更新 ──
        public int ExecuteUpdate(string sql, IDictionary<string, object> parameters = null)
        {
            sql = CleanSql(sql);
            logger.Debug("ExecuteUpdate sql=" + sql);
            try
            {
                var ui = CachedParse(sql, ParseUpdateAuto);
                CheckTable(ui.Table);

                var coll = _db.GetCollection<BsonDocument>(ui.Table);
                var filter = BuildFilterAuto(ui.Where, parameters);
                var upd = Builders<BsonDocument>.Update;
                var sets = ui.SetList.Select(kv => upd.Set(kv.Key, BsonValue.Create(kv.Value))).ToList();

                //打印 Compass 可执行脚本
                string filterJson = filter.Render(_docSerializer, BsonSerializer.SerializerRegistry).ToString();
                string updateJson = upd.Combine(sets).Render(_docSerializer, BsonSerializer.SerializerRegistry).ToString();
                logger.Info("ExecuteUpdate 脚本(可直接粘贴 Compass):\r\n" +
                             string.Format("db.{0}.updateMany({1}, {2})",
                                           ui.Table, filterJson, updateJson));

                var result = coll.UpdateMany(filter, upd.Combine(sets));
                logger.Debug("返回ExecuteUpdate 修改行数=" + result.ModifiedCount);
                return (int)result.ModifiedCount;
            }
            catch (Exception ex)
            {
                LogAndException.Throw(ex);
                return 0;
            }
        }
        #endregion

        #region ── 4. 删除 ──
        public int ExecuteDelete(string sql, IDictionary<string, object> parameters = null)
        {
            sql = CleanSql(sql);
            logger.Debug("ExecuteDelete sql=" + sql);
            try
            {
                var di = CachedParse(sql, ParseDeleteAuto);
                CheckTable(di.Table);

                var coll = _db.GetCollection<BsonDocument>(di.Table);
                var filter = BuildFilterAuto(di.Where, parameters);

                //打印 Compass 可执行脚本
                string filterJson = filter.Render(_docSerializer, BsonSerializer.SerializerRegistry).ToString();
                logger.Info("ExecuteDelete 脚本(可直接粘贴 Compass):\r\n" +
                             string.Format("db.{0}.deleteMany({1})", di.Table, filterJson));

                var cnt = (int)coll.DeleteMany(filter).DeletedCount;
                logger.Debug("返回ExecuteDelete 删除行数=" + cnt);
                return cnt;
            }
            catch (Exception ex)
            {
                LogAndException.Throw(ex);
                return 0;
            }
        }
        #endregion

        #region ── 5. 统计 / 标量 ──
        public int ExecuteCount(string sql, IDictionary<string, object> parameters = null)
        {
            sql = CleanSql(sql);
            logger.Debug("ExecuteCount sql=" + sql);
            try
            {
                var qi = CachedParse(sql, ParseSelect);
                CheckTable(qi.Table);

                var coll = _db.GetCollection<BsonDocument>(qi.Table);
                var filter = BuildFilterAuto(qi.Where, parameters);

                //打印 Compass 可执行脚本
                string filterJson = filter.Render(_docSerializer, BsonSerializer.SerializerRegistry).ToString();
                logger.Info("ExecuteCount 脚本(可直接粘贴 Compass):\r\n" +
                             string.Format("db.{0}.countDocuments({1})", qi.Table, filterJson));

                int cnt = (int)coll.CountDocuments(filter);
                logger.Debug("返回ExecuteCount count行数=" + cnt);
                return cnt;
            }
            catch (Exception ex)
            {
                LogAndException.Throw(ex);
                return 0;
            }
        }
        #endregion

        #region ── 6. 单行单列查询 ──
        public string ExecuteGetSingle(string sql, IDictionary<string, object> parameters = null)
        {
            sql = CleanSql(sql);
            logger.Debug("ExecuteGetSingle sql=" + sql);

            try
            {
                var qi = CachedParse(sql, ParseSelect);
                CheckTable(qi.Table);

                if (qi.IsAggregate)
                {
                    object agg = ExecuteAggregate(qi, parameters);
                    string restr = agg == null ? null : agg.ToString();
                    logger.Debug("ExecuteGetSingle 带聚合查询,执行结果:" + restr);
                    return restr;
                }

                if (qi.Fields == null || qi.Fields.Length != 1)
                {
                    logger.Debug("字段数 ≠ 1");
                    throw new ArgumentException("只能查询单列");
                }

                var coll = _db.GetCollection<BsonDocument>(qi.Table);
                var filter = BuildFilterAuto(qi.Where, parameters);
                var proj = Builders<BsonDocument>.Projection.Include(qi.Fields[0]).Exclude("_id");

                //打印 Compass 可执行脚本
                string filterJson = filter.Render(_docSerializer, BsonSerializer.SerializerRegistry).ToString();
                string projJson = proj.Render(_docSerializer, BsonSerializer.SerializerRegistry).ToString();
                logger.Info("ExecuteGetSingle 脚本(可直接粘贴 Compass):\r\n" +
                             string.Format("db.{0}.find({1}, {2}).limit(1)",
                                           qi.Table, filterJson, projJson));

                var doc = coll.Find(filter).Project(proj).Limit(1).FirstOrDefault();
                object value = SafeGetValue(doc, qi.Fields[0]);
                logger.Debug("返回 ExecuteGetSingle 单行单列查询结果=" + (value ?? "null"));
                return value == null ? null : value.ToString();
            }
            catch (Exception ex)
            {
                LogAndException.Throw(ex);
                return null;
            }
        }
        #endregion
        
        #region ── 6.1. 多行单列查询 ──
        public List<object> ExecuteGetList(string sql, IDictionary<string, object> parameters = null)
        {
            sql = CleanSql(sql);
            logger.Debug("ExecuteGetList 入口 sql=" + sql);
        
            try
            {
                var qi = CachedParse(sql, ParseSelect);
                CheckTable(qi.Table);
        
                if (qi.Fields == null || qi.Fields.Length != 1)
                {
                    logger.Debug("字段数 ≠ 1");
                    throw new ArgumentException("SQL 必须仅查询一个列,如 SELECT Name FROM ...");
                }
        
                var coll   = _db.GetCollection<BsonDocument>(qi.Table);
                var filter = BuildFilterAuto(qi.Where, parameters);
                var proj   = Builders<BsonDocument>.Projection.Include(qi.Fields[0]).Exclude("_id");
                var find   = coll.Find(filter).Project(proj);
        
                if (qi.Top.HasValue && qi.Top.Value > 0)
                    find = find.Limit(qi.Top.Value);
               
        
                List<object> reli = find.ToList()
                                        .Select(doc => BsonTypeMapper.MapToDotNetValue(doc.GetValue(qi.Fields[0])))
                                        .ToList();
        
                //打印 Compass 可执行脚本
                string limitClause = qi.Top.HasValue && qi.Top.Value > 0
                                         ? ".limit(" + qi.Top.Value + ")"
                                         : "";
                string filterJson  = filter.Render(_docSerializer, BsonSerializer.SerializerRegistry).ToString();
                string projJson    = proj.Render(_docSerializer, BsonSerializer.SerializerRegistry).ToString();
        
                logger.Info("ExecuteGetList 脚本(可直接粘贴 Compass):\r\n" +
                             string.Format("db.{0}.find({1}, {2}){3}",
                                           qi.Table, filterJson, projJson, limitClause));
        
                logger.Debug("返回 ExecuteGetList 单列多行行数=" + reli.Count);
                return reli;
            }
            catch (Exception ex)
            {
                LogAndException.Throw(ex);
                return null;   // 抛出异常
            }
        }
        #endregion

        #region ── 7. 解析辅助类 ──
        private class Qi
        {
            public string Table;
            public string Where;
            public string OrderBy;
            public string[] Fields;
            public int? Top;
            public bool IsCount;
            public string CountAlias;
            public bool IsAggregate;
            public string AggFunc;
            public string AggField;
            public string AggAlias;
            public bool IsGroupBy;
            public string[] GroupFields;
            public string Having;
            public bool IsExists;
            public string SubQuerySql;
            public string SubQuerySelect;
            public string SubQueryForeign;
            public bool IsDistinct;
            public int? Skip;   // 新增 skip
        }

        private class InsertAuto
        {
            public string Table;
            public List<string> Columns;
            public List<List<object>> Values;
        }

        private class UpdateAuto
        {
            public string Table;
            public string Where;
            public Dictionary<string, object> SetList;
        }

        private class DeleteAuto
        {
            public string Table;
            public string Where;
        }
        #endregion

        #region ── 8. 正则模板 ──
        private static readonly Regex _cleanSpace =
            new Regex(@"\s+", RegexOptions.Compiled | RegexOptions.Multiline);

        private static readonly Regex _reSelect =
            new Regex(@"SELECT\s+(?:TOP\s+(?<top>\d+)\s+)?(?<fields>.+?)\s+FROM\s+(?<table>\w+)(?:\s+WHERE\s+(?<where>.*?))?(?:\s+ORDER\s+BY\s+(?<order>.*?))?$",
                RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);

        private static readonly Regex _reInsert =
            new Regex(@"INSERT\s+INTO\s+(\w+)\s*\(([^)]+)\)\s*VALUES\s*(.+)",
                RegexOptions.IgnoreCase | RegexOptions.Compiled);

        private static readonly Regex _reUpdate =
            new Regex(@"UPDATE\s+(\w+)\s+SET\s+(.+?)(?:\s+WHERE\s+(.+))?$",
                RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);

        private static readonly Regex _reDelete =
            new Regex(@"DELETE\s+FROM\s+(\w+)(?:\s+WHERE\s+(.+))?$",
                RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);

        private static readonly Regex KeyWordRegex =
            new Regex(@"\b(SELECT|FROM|WHERE|AND|OR|GROUP\s+BY|HAVING|EXISTS|NOT\s+EXISTS|ORDER\s+BY|TOP|AS|IN|LIKE|IS|NULL|ASC|DESC|BETWEEN|DISTINCT|SKIP|CAST)\b",
                RegexOptions.IgnoreCase | RegexOptions.Compiled);

        private static readonly Regex ObjectIdRegex =
            new Regex(@"(\w+)\s*=\s*['""]([0-9a-fA-F]{24})['""]",
                RegexOptions.Compiled | RegexOptions.IgnoreCase);
        #endregion

        #region ── 9. 解析入口 ──
        private static string UpperKeyWords(string sql)
        {
            return KeyWordRegex.Replace(sql, delegate(Match m) { return m.Value.ToUpper(); });
        }

        private static string CleanSql(string sql)
        {
            if (string.IsNullOrEmpty(sql)) return sql;
            return _cleanSpace.Replace(sql, " ").Trim();
        }

        private static Qi ParseSelect(string sql)
        {
            sql = CleanSql(sql);
            logger.Debug(string.Format("[ParseSelect] 原始 SQL:{0}", sql));
            if (sql.Length > 5000) sql = sql.Substring(0, 5000);
            sql = UpperKeyWords(sql);

            bool isDistinct = Regex.IsMatch(sql, @"\bDISTINCT\b", RegexOptions.IgnoreCase);
            sql = Regex.Replace(sql, @"\bDISTINCT\b", "", RegexOptions.IgnoreCase);

            int? top = null;
            var topM = Regex.Match(sql, @"TOP\s+(\d+)", RegexOptions.IgnoreCase);
            if (topM.Success)
            {
                top = int.Parse(topM.Groups[1].Value);
                sql = sql.Remove(topM.Index, topM.Length);
            }
            
            // 已有 top 捕获之后追加
            int? skip = null;
            var skipM = Regex.Match(sql, @"SKIP\s+(\d+)", RegexOptions.IgnoreCase);
            if (skipM.Success)
            {
                skip = int.Parse(skipM.Groups[1].Value);
                sql = sql.Remove(skipM.Index, skipM.Length);   // 去掉关键字
            }

            var fieldM = Regex.Match(sql, @"SELECT\s+(.+?)\s+FROM", RegexOptions.IgnoreCase);
            if (!fieldM.Success)
                LogAndException.Throw(new MongoSqlException("找不到Select 字段列表 from table", sql));

            string rawFields = fieldM.Groups[1].Value.Trim();
            sql = sql.Remove(fieldM.Index, fieldM.Length).Insert(fieldM.Index, "SELECT  FROM");

            var tblM = Regex.Match(sql, @"FROM\s+(\w+)", RegexOptions.IgnoreCase);
            if (!tblM.Success)
                LogAndException.Throw(new MongoSqlException("找不到select from 表名", sql));
            string table = tblM.Groups[1].Value;

            string wheres = null;
            var whereM = Regex.Match(sql, @"WHERE\s+(.+?)(?:\s+ORDER\s+BY|$)", RegexOptions.IgnoreCase | RegexOptions.Singleline);
            if (whereM.Success) wheres = whereM.Groups[1].Value.Trim();

            string order = null;
            var ordM = Regex.Match(sql, @"ORDER\s+BY\s+(.+)$", RegexOptions.IgnoreCase);
            if (ordM.Success) order = ordM.Groups[1].Value.Trim();

            var qi = new Qi
            {
                Table = table,
                Where = wheres,
                OrderBy = order,
                Top = top,
                IsDistinct = isDistinct,
                Skip = skip
            };

            var countMatch = Regex.Match(rawFields, @"COUNT\s*\(\s*\*\s*\)(?:\s+AS\s+(\w+))?", RegexOptions.IgnoreCase);
            if (countMatch.Success)
            {
                qi.IsCount = true;
                qi.CountAlias = countMatch.Groups[1].Success ? countMatch.Groups[1].Value : "Count";
                qi.Fields = new[] { qi.CountAlias };
            }
            else
            {
                var aggMatch = Regex.Match(rawFields, @"\b(SUM|MAX|MIN|AVG)\s*\(\s*([^)]+)\s*\)(?:\s+AS\s+(\w+))?", RegexOptions.IgnoreCase);
                if (aggMatch.Success)
                {
                    qi.IsAggregate = true;
                    qi.AggFunc = aggMatch.Groups[1].Value.ToUpper();
                    qi.AggField = aggMatch.Groups[2].Value.Trim();
                    qi.AggAlias = aggMatch.Groups[3].Success ? aggMatch.Groups[3].Value : qi.AggFunc;
                    qi.Fields = new[] { qi.AggAlias };
                }
                else
                {
                    qi.Fields = rawFields.Split(',').Select(s => s.Trim()).ToArray();
                }
            }

            var groupM = Regex.Match(sql, @"GROUP\s+BY\s+([^H]+?)(?:\s+HAVING|$)", RegexOptions.IgnoreCase | RegexOptions.Singleline);
            if (groupM.Success)
            {
                qi.IsGroupBy = true;
                qi.GroupFields = groupM.Groups[1].Value.Split(',').Select(s => s.Trim()).ToArray();
            }
            var havM = Regex.Match(sql, @"HAVING\s+(.+)$", RegexOptions.IgnoreCase | RegexOptions.Singleline);
            if (havM.Success) qi.Having = havM.Groups[1].Value.Trim();

            var existsM = Regex.Match(sql,
                @"\b(EXISTS|NOT\s+EXISTS)\s*\(\s*SELECT\s+\w+\s+FROM\s+(\w+)\s+WHERE\s+(\w+)\s*=\s*(\w+)\.(\w+)\s*\)",
                RegexOptions.IgnoreCase | RegexOptions.Singleline);
            if (existsM.Success)
            {
                qi.IsExists = existsM.Groups[1].Value.ToUpper() == "EXISTS";
                qi.SubQuerySql = existsM.Groups[0].Value;
                qi.SubQuerySelect = existsM.Groups[3].Value;
                qi.SubQueryForeign = existsM.Groups[5].Value;
            }

            logger.Debug(string.Format(
                "[ParseSelect] 拆分成功 Table={0} Cols={1} Where={2} Order by={3} IsCount={4} IsAggregate={5} IsDistinct={6}",
                table, rawFields, wheres, order, qi.IsCount, qi.IsAggregate, qi.IsDistinct));
            return qi;
        }
        
        
        //<summary>
        //解析 INSERT 语句
        //</summary>
        private static InsertAuto ParseInsertAuto(string sql)
        {
            sql = CleanSql(sql);
            logger.Debug(string.Format("[ParseInsertAuto] 原始 SQL:{0}", sql));
            sql = UpperKeyWords(sql);
        
            var m = _reInsert.Match(sql);
            if (!m.Success)
                LogAndException.Throw(new MongoSqlException("INSERT 语法错误", sql));
        
            var cols = m.Groups[2].Value.Split(',').Select(s => s.Trim()).ToList();
            var valRows = Regex.Matches(m.Groups[3].Value, @"\(([^)]+)\)", RegexOptions.Compiled)
                               .Cast<Match>()
                               .Select(m2 => m2.Groups[1].Value.Split(',').Select(v => ParseValue(v.Trim())).ToList())
                               .ToList();
            var table = m.Groups[1].Value;
        
            logger.Debug(string.Format("[ParseInsertAuto] 拆分成功 Table={0} Columns={1} 行数={2}",
                                       table, string.Join("|", cols), valRows.Count));
        
            return new InsertAuto
            {
                Table   = table,
                Columns = cols,
                Values  = valRows
            };
        }
        
        //<summary>
        //解析 UPDATE 语句
        //</summary>
        private static UpdateAuto ParseUpdateAuto(string sql)
        {
            sql = CleanSql(sql);
            logger.Debug(string.Format("[ParseUpdateAuto] 原始 SQL:{0}", sql));
            sql = UpperKeyWords(sql);
        
            var m = _reUpdate.Match(sql);
            if (!m.Success)
                LogAndException.Throw(new MongoSqlException("UPDATE 语法错误", sql));
        
            var setDict = m.Groups[2].Value.Split(',')
                           .Select(s => s.Split('='))
                           .ToDictionary(a => a[0].Trim(), a => ParseValue(a[1].Trim()));
        
            logger.Debug(string.Format("[ParseUpdateAuto] 拆分成功 Table={0} Set={1} Where={2}",
                                       m.Groups[1].Value,
                                       string.Join(";", setDict.Select(kv => kv.Key + "=" + kv.Value)),
                                       m.Groups[3].Value));
        
            return new UpdateAuto
            {
                Table   = m.Groups[1].Value,
                Where   = m.Groups[3].Value,
                SetList = setDict
            };
        }
        
        //<summary>
        //解析 DELETE 语句
        //</summary>
        private static DeleteAuto ParseDeleteAuto(string sql)
        {
            sql = CleanSql(sql);
            logger.Debug(string.Format("[ParseDeleteAuto] 原始 SQL:{0}", sql));
            sql = UpperKeyWords(sql);
        
            var m = _reDelete.Match(sql);
            if (!m.Success)
                LogAndException.Throw(new MongoSqlException("DELETE 语法错误", sql));
        
            logger.Debug(string.Format("[ParseDeleteAuto] 拆分成功 Table={0} Where={1}",
                                       m.Groups[1].Value, m.Groups[2].Value));
        
            return new DeleteAuto
            {
                Table = m.Groups[1].Value,
                Where = m.Groups[2].Value
            };
        }
        #endregion

        #region ── 14. 类型解析 ──
        private static object ParseValue(string raw)
        {
            if (string.IsNullOrWhiteSpace(raw)) return null;
            raw = raw.Trim();

            if ((raw.StartsWith("'") && raw.EndsWith("'")) ||
                (raw.StartsWith("\"") && raw.EndsWith("\"")))
                return raw.Substring(1, raw.Length - 2);

            if (raw.Equals("null", StringComparison.OrdinalIgnoreCase)) return null;

            bool b; if (bool.TryParse(raw, out b)) return b;

            if (raw.StartsWith("ObjectId(\"", StringComparison.OrdinalIgnoreCase) &&
                raw.EndsWith("\")") && raw.Length == 26)
                return new ObjectId(raw.Substring(10, 24));

            long l; if (Regex.IsMatch(raw, @"^-?\d+$") && long.TryParse(raw, out l)) return l;

            decimal d; if (Regex.IsMatch(raw, @"^-?\d+\.\d+$") && decimal.TryParse(raw, out d)) return d;

            DateTime dt; if (DateTime.TryParse(raw, out dt)) return dt;

            return raw;
        }
        #endregion

        #region ── 14.1 判断是否有效字段 ──
        //正则表达式来判断传入的字符串,是否符合编程规则
        private static readonly Regex FieldRegex = new Regex(@"^[a-zA-Z_]\w*$", RegexOptions.Compiled);

        private bool IsValidField(string field)
        {
            return FieldRegex.IsMatch(field);
        }
        #endregion

        #region ── 15. SQL→MongoDB 过滤器构造 ──        
        private FilterDefinition<BsonDocument> BuildFilterAuto(string where,
                                              IDictionary<string, object> paras,
                                              bool isHaving = false)
        {
            if (string.IsNullOrEmpty(where)) return FilterDefinition<BsonDocument>.Empty;
        
            where = ReplaceParameters(where, paras);
        
            // 1. 空值保护:任何空串/缺失 → null(不抛异常)
            where = Regex.Replace(where, @"(\w+)\s*=\s*$", "\"$1\":null");
            where = Regex.Replace(where, @"(\w+)\s*=\s*''", "\"$1\":null");
            where = Regex.Replace(where, @"(\w+)\s*=\s*""""", "\"$1\":null");
        
            // 2. 运算符映射
            where = Regex.Replace(where, @"(\w+)\s*>=\s*([^,\s}]+)", "\"$1\":{ \"$gte\" : $2 }");
            where = Regex.Replace(where, @"(\w+)\s*<=\s*([^,\s}]+)", "\"$1\":{ \"$lte\" : $2 }");
            where = Regex.Replace(where, @"(\w+)\s*!=\s*([^,\s}]+)", "\"$1\":{ \"$ne\"  : $2 }");
            where = Regex.Replace(where, @"(\w+)\s*<\s*([^,\s}]+)", "\"$1\":{ \"$lt\"  : $2 }");
            where = Regex.Replace(where, @"(\w+)\s*>\s*([^,\s}]+)", "\"$1\":{ \"$gt\"  : $2 }");
            where = Regex.Replace(where, @"(\w+)\s*=\s*([^,\s}]+)", "\"$1\":$2");
        
            // 3. 特殊操作
            where = Regex.Replace(where, @"(\w+)\s+LIKE\s+'([^']+)'", "\"$1\":{ \"$regex\":\"$2\" }");
            where = Regex.Replace(where, @"(\w+)\s+BETWEEN\s+([^'""\s]+)\s+AND\s+([^'""\s]+)",
                                "\"$1\":{ \"$gte\":$2 , \"$lte\":$3 }", RegexOptions.IgnoreCase);
        
            where = Regex.Replace(where, @"(\w+)\s+IN\s*\(([^)]+)\)", m =>
            {
                string field = m.Groups[1].Value;
                string inner = m.Groups[2].Value;
                var items = Regex.Matches(inner, @"['""]([^'""]*)['""]|([^,]+)")
                                 .Cast<Match>()
                                 .Select(x => x.Groups[1].Success
                                                   ? string.Format("\"{0}\"",
                                                       x.Groups[1].Value.Replace("\"", "\\\""))
                                                   : x.Groups[2].Value.Trim())
                                 .ToArray();
                return string.Format("\"{0}\":{{ \"$in\" : [{1}] }}", field, string.Join(",", items));
            }, RegexOptions.IgnoreCase);
        
            where = Regex.Replace(where, @"(\w+)\s+IS\s+NULL", "\"$1\":{ \"$exists\" : false }", RegexOptions.IgnoreCase);
        
            // 4. ObjectId
            where = ObjectIdRegex.Replace(where, "\"$1\":ObjectId(\"$2\")");
        
            // 5. 字段名加双引号
            where = Regex.Replace(where, @"\{([a-zA-Z]\w*):", "{\"$1\":");
            where = Regex.Replace(where, @"\s([a-zA-Z]\w*):", " \"$1\":");
        
            if (isHaving)
                where = Regex.Replace(where, @"\{""(\w+)"":", "{\"$$1\":");
        
            // 6. AND / OR
            if (Regex.IsMatch(where, @"\bAND\b", RegexOptions.IgnoreCase))
            {
                var parts = Regex.Split(where, @"\bAND\b", RegexOptions.IgnoreCase)
                                 .Select(p => "{" + p.Trim() + "}");
                where = "{ \"$and\" : [" + string.Join(",", parts) + "] }";
            }
            else if (Regex.IsMatch(where, @"\bOR\b", RegexOptions.IgnoreCase))
            {
                var parts = Regex.Split(where, @"\bOR\b", RegexOptions.IgnoreCase)
                                 .Select(p => "{" + p.Trim() + "}");
                where = "{ \"$or\" : [" + string.Join(",", parts) + "] }";
            }
            else
            {
                where = "{" + where + "}";
            }
        
            where = where.TrimEnd(' ', '\t', '\r', '\n', ';');        
            logger.Debug("[BuildFilterAuto] 生成过滤器 JSON:" + where);

            try
            {
                var filter = BsonDocument.Parse(where);
                logger.Debug("[BuildFilterAuto] 过滤器 JSON 解析结果=" + filter.ToBsonDocument().ToString());
                return filter;
            }
            catch (Exception ex)
            {
                LogAndException.Throw(new MongoSqlException("过滤器 JSON 解析失败", where, ex));
                return null;
            }
        }
        #endregion

        #region ── 16. 聚合 / GROUP BY / EXISTS 执行 ──
        private object ExecuteAggregate(Qi qi, IDictionary<string, object> parameters)
        {
            logger.Debug("ExecuteAggregate开始,函数: " + qi.AggFunc);
            var coll = _db.GetCollection<BsonDocument>(qi.Table);
            var filter = BuildFilterAuto(qi.Where, parameters);

            string op;
            switch (qi.AggFunc)
            {
                case "SUM": op = "$sum"; break;
                case "MAX": op = "$max"; break;
                case "MIN": op = "$min"; break;
                case "AVG": op = "$avg"; break;
                default:
                    logger.Error("[ExecuteAggregate] 不支持的聚合函数:" + qi.AggFunc);
                    throw new MongoSqlException("不支持的聚合函数", qi.AggFunc);
            }

            var pipeline = new List<BsonDocument>
            {
                new BsonDocument("$match", filter.Render(_docSerializer, BsonSerializer.SerializerRegistry)),
                new BsonDocument("$group", new BsonDocument
                {
                    { "_id", 1 },
                    { qi.AggAlias, new BsonDocument(op, "$" + qi.AggField) }
                })
            };

            //打印 Compass 可执行脚本
            string pipelineJson = "[" + string.Join(", ", pipeline.Select(p => p.ToJson())) + "]";
            logger.Info("ExecuteAggregate 脚本(可直接粘贴 Compass):\r\n" +
                         string.Format("db.{0}.aggregate({1})", qi.Table, pipelineJson));

            var result = coll.Aggregate<BsonDocument>(pipeline, new AggregateOptions()).FirstOrDefault();
            string value = result == null ? null : BsonTypeMapper.MapToDotNetValue(result[qi.AggAlias]).ToString();
            logger.Debug("返回 ExecuteAggregate 执行结果=" + (value ?? "null"));
            return value;
        }

        private DataTable ExecuteGroupBy(Qi qi, IDictionary<string, object> parameters)
        {
            logger.Debug("ExecuteGroupBy开始...");
            var coll = _db.GetCollection<BsonDocument>(qi.Table);
            var filter = BuildFilterAuto(qi.Where, parameters);

            var stages = new List<BsonDocument>
            {
                new BsonDocument("$match", filter.ToBsonDocument())
            };

            var groupId = new BsonDocument();
            foreach (var f in qi.GroupFields) groupId[f] = "$" + f;

            var accumulators = new BsonDocument { { "_id", groupId } };
            foreach (var field in qi.Fields)
            {
                if (qi.IsCount && field == qi.CountAlias)
                    accumulators[field] = new BsonDocument("$sum", 1);
                else if (qi.IsAggregate && field == qi.AggAlias)
                    accumulators[field] = new BsonDocument("$" + qi.AggFunc, "$" + qi.AggField);
                else if (Array.IndexOf(qi.GroupFields, field) >= 0)
                    accumulators[field] = new BsonDocument("$first", "$" + field);
            }
            stages.Add(new BsonDocument("$group", accumulators));

            if (!string.IsNullOrEmpty(qi.Having))
            {
                var havingBson = BuildFilterAuto(qi.Having, parameters, true);
                stages.Add(new BsonDocument("$match", havingBson.ToBsonDocument()));
            }

            if (!string.IsNullOrEmpty(qi.OrderBy))
            {
                var sortDoc = BsonDocument.Parse("{" + qi.OrderBy.Replace("DESC", "-1").Replace("ASC", "1") + "}");
                stages.Add(new BsonDocument("$sort", sortDoc));
            }

            if (qi.Top.HasValue && qi.Top.Value > 0)
                stages.Add(new BsonDocument("$limit", qi.Top.Value));

            //打印 Compass 可执行脚本
            string stagesJson = "[" + string.Join(", ", stages.Select(p => p.ToJson())) + "]";
            logger.Info("ExecuteGroupBy 脚本(可直接粘贴 Compass):\r\n" +
                         string.Format("db.{0}.aggregate({1})", qi.Table, stagesJson));

            var list = coll.Aggregate<BsonDocument>(stages).ToList();
            logger.Debug("ExecuteGroupBy 返回行数=" + list.Count);
            return ToDataTable(list);
        }

        private DataTable ExecuteExists(Qi qi, IDictionary<string, object> parameters)
        {
            logger.Debug("ExecuteExists 开始...");
            var subQi = CachedParse(qi.SubQuerySql, ParseSelect);
            var subColl = _db.GetCollection<BsonDocument>(subQi.Table);
            var subFilter = BuildFilterAuto(subQi.Where, parameters);

            var fieldName = subQi.SubQuerySelect.ToLowerInvariant();
            var subList = subColl.Find(subFilter)
                                 .Project(Builders<BsonDocument>.Projection.Include(fieldName))
                                 .ToList()
                                 .Select(d => d.GetValue(fieldName))
                                 .Distinct()
                                 .ToList();

            var mainColl = _db.GetCollection<BsonDocument>(qi.Table);
            var mainFilter = BuildFilterAuto(qi.Where, parameters);

            if (subList.Any())
            {
                var inFilter = qi.IsExists
                    ? Builders<BsonDocument>.Filter.In(qi.SubQueryForeign, subList)
                    : Builders<BsonDocument>.Filter.Nin(qi.SubQueryForeign, subList);
                mainFilter = Builders<BsonDocument>.Filter.And(mainFilter, inFilter);
            }
            else
            {
                if (qi.IsExists)
                    mainFilter = Builders<BsonDocument>.Filter.And(mainFilter,
                        Builders<BsonDocument>.Filter.Eq("_id", ObjectId.Empty));
            }

            var find = mainColl.Find(mainFilter);

            bool isStar = qi.Fields != null && qi.Fields.Length == 1 && qi.Fields[0] == "*";
            if (isStar)
                find = find.Project(Builders<BsonDocument>.Projection.Exclude("_noSuchField"));
            else if (qi.Fields != null && qi.Fields.Length > 0)
            {
                var proj = Builders<BsonDocument>.Projection;
                ProjectionDefinition<BsonDocument> p = null;
                foreach (var f in qi.Fields)
                    p = p == null ? proj.Include(f) : p.Include(f);
                find = find.Project(p);
            }

            if (!string.IsNullOrEmpty(qi.OrderBy))
                find = find.Sort(BuildSortAuto(qi.OrderBy));

            if (qi.Top.HasValue && qi.Top.Value > 0)
                find = find.Limit(qi.Top.Value);

            //打印 Compass 可执行脚本
            string projectionClause = isStar ? "{}" 
                : "{ " + string.Join(", ", qi.Fields.Select(f => "\"" + f + "\": 1")) + " }";
            string sortClause = !string.IsNullOrEmpty(qi.OrderBy) 
                ? ".sort(" + BuildSortAuto(qi.OrderBy).ToJson() + ")" 
                : "";
            string limitClause = qi.Top.HasValue ? ".limit(" + qi.Top.Value + ")" : "";

            logger.Info("ExecuteExists 脚本(可直接粘贴 Compass):\r\n" +
                         string.Format("db.{0}.find({1}, {2}){3}{4}",
                                       qi.Table,
                                       mainFilter.Render(_docSerializer, BsonSerializer.SerializerRegistry).ToJson(),
                                       projectionClause,
                                       sortClause,
                                       limitClause));

            var list = find.ToList();
            logger.Debug("ExecuteExists 返回行数=" + list.Count);
            return ToDataTable(list);
        }
        #endregion


        #region ── 17. 排序构造 ──
        private static readonly Regex CastRegex =
            new Regex(@"\bCAST\s*\(\s*([^)]+)\s+AS\s+(INT|LONG|DECIMAL|DOUBLE|STRING|DATE|BOOL)\s*\)",
                RegexOptions.IgnoreCase | RegexOptions.Compiled);
        
        private static readonly Dictionary<string, string> CastToMongo = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
        {
            {"INT", "$toInt"},
            {"LONG", "$toLong"},
            {"DECIMAL", "$toDecimal"},
            {"DOUBLE", "$toDouble"},
            {"STRING", "$toString"},
            {"DATE", "$toDate"},
            {"BOOL", "$toBool"}
        };

        //<summary>
        //解析 ORDER BY 中的 CAST(expr AS type),返回 MongoDB 表达式
        //</summary>
        private BsonValue ParseCastInOrderBy(string expr)
        {
            var m = CastRegex.Match(expr);
            if (!m.Success) return null;
        
            string rawExpr = m.Groups[1].Value.Trim();
            string targetType = m.Groups[2].Value.ToUpper();
        
            string mongoOp; 
            if (CastToMongo.TryGetValue(targetType, out mongoOp))
            {
                if (FieldRegex.IsMatch(rawExpr) && !rawExpr.StartsWith("$"))
                    rawExpr = "$" + rawExpr;
        
                return BsonDocument.Parse(
                    string.Format("{{ \"{0}\": \"{1}\" }}", mongoOp, rawExpr));
            }
        
            return null;
        }
        
        //<summary>
        //返回 BsonDocument,供日志打印;外部再 .Sort(...) 即可
        //</summary>
        private BsonDocument BuildSortAuto(string orderBy)
        {
            if (string.IsNullOrWhiteSpace(orderBy))
                return null;
        
            var doc = new BsonDocument();
            foreach (var piece in orderBy.Split(','))
            {
                var tmp = piece.Trim().Split(' ');
                string fld = tmp[0];
                int  dir = (tmp.Length > 1 && tmp[1].ToUpper() == "DESC") ? -1 : 1;
                
                // 尝试解析 CAST
                BsonValue sortKey = ParseCastInOrderBy(fld);
                if (sortKey == null)
                {
                    // 普通字段
                    sortKey = fld;
                }
                
                // MongoDB 排序键必须是字符串或表达式
                if (sortKey.IsBsonDocument)
                {
                    // 表达式排序,使用 $project + $sort(仅聚合场景)
                    // 但 MongoDB 4.4+ 支持直接表达式排序
                    doc.Add(fld, new BsonDocument
                    {
                        { "$meta", "expression" }, // 占位,实际用表达式
                        { "value", sortKey },
                        { "direction", dir }
                    });
                }
                else
                {
                    doc.Add(sortKey.AsString, dir);
                }
            }
            // 日志里可拷
            logger.Debug("[BuildSortAuto] 排序 JSON:" + doc.ToJson());
            return doc;
        }
        #endregion


        #region ── 18. 参数替换──
        private string ReplaceParameters(string text, IDictionary<string, object> paras)
        {
            if (text == null) return text;
            if (paras != null)
            {
                foreach (var kv in paras)
                {
                    object val = kv.Value;
                    string key = "@" + kv.Key;

                    if (val is IEnumerable && !(val is string))
                    {
                        text = text.Replace(key, JsonConvert.SerializeObject(val));
                        continue;
                    }

                    string jsonVal;
                    if (val == null) jsonVal = "null";
                    else if (val is string) jsonVal = "\"" + val.ToString().Replace("\"", "\\\"") + "\"";
                    else if (val is DateTime) jsonVal = "\"" + ((DateTime)val).ToString("o") + "\"";
                    else if (val is bool) jsonVal = val.ToString().ToLower();
                    else jsonVal = val.ToString();

                    text = text.Replace(key, jsonVal)
                               .Replace(":" + kv.Key, jsonVal);
                }
            }

            text = ObjectIdRegex.Replace(text, "\"$1\":ObjectId(\"$2\")");
            return text;
        }
        #endregion

        #region ── 19. BsonDocument→DataTable ──
        private static DataTable ToDataTable(List<BsonDocument> docs)
        {
            var dt = new DataTable();
            if (docs == null || docs.Count == 0)
            {
                logger.Debug("ToDataTable: 输入 docs 为 null 或 0 条,返回空表。");
                return dt;
            }

            var cols = new SortedSet<string>();
            foreach (var d in docs)
            {
                foreach (var e in d.Elements)
                {
                    if (e.Name == "_id" && e.Value.IsBsonDocument)
                    {
                        foreach (var sub in e.Value.AsBsonDocument)
                            cols.Add(sub.Name);
                    }
                    else
                    {
                        cols.Add(e.Name);
                    }
                }
            }

            foreach (var c in cols) dt.Columns.Add(c, typeof(object));
            logger.Debug("ToDataTable: 建列完成,列数=" + dt.Columns.Count);

            foreach (var d in docs)
            {
                DataRow r = dt.NewRow();
                foreach (var e in d.Elements)
                {
                    if (e.Name == "_id" && e.Value.IsBsonDocument)
                    {
                        foreach (var sub in e.Value.AsBsonDocument)
                            r[sub.Name] = BsonTypeMapper.MapToDotNetValue(sub.Value) ?? DBNull.Value;
                    }
                    else
                    {
                        r[e.Name] = BsonTypeMapper.MapToDotNetValue(e.Value) ?? DBNull.Value;
                    }
                }
                dt.Rows.Add(r);
            }

            logger.Debug("ToDataTable: 填行完成,行数=" + dt.Rows.Count);
            return dt;
        }
        #endregion

        #region ── 20. 缓存包装 ──
        private T CachedParse<T>(string sql, Func<string, T> parser) where T : class
        {
            var cached = _parseCache.Get(sql) as T;
            if (cached != null) return cached;

            var newValue = parser(sql);
            _parseCache.Set(sql, newValue, DateTimeOffset.UtcNow.AddHours(1));
            return newValue;
        }
        #endregion

        #region ── 21. 安全获取值 ──
        private static object SafeGetValue(BsonDocument doc, string fieldName)
        {
            if (doc == null) return null;
            BsonValue val;
            return doc.TryGetValue(fieldName, out val) ? BsonTypeMapper.MapToDotNetValue(val) : null;
        }
        #endregion
    }

    #region ── 统一异常 ──
    public sealed class MongoSqlException : Exception
    {
        public string Sql { get; private set; }
        public MongoSqlException(string message, string sql, Exception inner = null)
            : base(message + "  SQL=" + sql, inner) { Sql = sql; }
    }
    #endregion
}

4、LogAndException.cs

// ===================================================================================
//  MongoSqlClient  C# 5.0 兼容完整版 
//  封装类:LogAndException
//  说明:抛异常日志,自动记error日志,并且翻译系统错误信息成用户能理解的错误信息。
//  依赖:log4net类
// ===================================================================================
using System;
using System.ComponentModel;
using System.Data.SqlClient;
using System.Diagnostics;
using System.IO;
using log4net;

namespace MongoSql.Demo
{
    public static class LogAndException
    {
        private static readonly ILog logger = LogManager.GetLogger(typeof(LogAndException));

        //<summary>
        //      LogAndException.DebugRaw = false; // true代表调试期,直接裸抛,默认False不裸抛
        //      LogAndException.ThrowTech = false;   // 在调试期DebugRaw=false才生效,True返回[技术出错信息],False返回翻译过的[用户出错信息],默认False
        //</summary>
        public static bool DebugRaw { get; set; }
        public static bool ThrowTech { get; set; }
        
        //默认值
        static LogAndException()
        {
            DebugRaw = false;   // true代表调试期,直接裸抛,默认False不裸抛
            ThrowTech = false;   // 在调试期DebugRaw=false才生效,True返回[技术出错信息],False返回翻译过的[用户出错信息],默认False
        }


        public static void Throw(Exception original, string mes = null)
        {
            if (original == null) 
                throw new ArgumentNullException("original Exception is null ...");
        
            string user = GetUserMessage(original);
            string tech = GetTechMessage(original);
            string loc  = GetMyCodeLocation(original);
        
            //记录详细堆栈日志
            logger.Error(mes,original);
            //记录精简日志
            logger.Error(string.Format("LOC={0} \n MSG={1} \n USER={2} \n TECH={3}",
                                        loc, mes ?? "", user, tech));
        
            if (DebugRaw)
                // 裸抛原始错误信息
                throw original;
            else    
                // 抛用户友好错误提示
                throw new Exception(user, original);
        }
        
        public static string GetUserMessage(Exception ex)
        {
            if (IsConnectTimeout(ex))
                return "网络繁忙,请稍后重试;若仍无法使用,请联系客服。";

            SqlException sqlex = ex as SqlException;
            if (sqlex != null)
            {
                if (sqlex.Number == 18456) return "登录失败,请联系管理员确认账号密码。";
                if (sqlex.Number == 4060 || sqlex.Number == 233 || sqlex.Number == 2)
                    return "系统维护中,请稍后再试。";
                if (sqlex.Number == 1205) return "操作冲突,请稍后重试。";
            }


            return "系统繁忙,请稍后重试。";
        }

        public static string GetTechMessage(Exception ex)
        {
            SqlException sqlex = ex as SqlException;
            if (sqlex != null)
                return string.Format("(SQL错误号:{0} | 服务器:{1}) 消息:{2}",
                                      sqlex.Number, sqlex.Server, sqlex.Message);

            Win32Exception winex = ex as Win32Exception;
            if (winex != null)
            {
                uint code = (uint)winex.NativeErrorCode;
                return string.Format("(Win32错误码:0x{0:X8}) 消息:{1}", code, winex.Message);
            }

            return string.Format("其他异常类型:{0} | 消息:{1}",
                                  ex.GetType().Name, ex.Message);
        }


        
        private static string GetMyCodeLocation(Exception ex)
        {
            Exception root = ex;
            while (root.InnerException != null)
                root = root.InnerException;
            
            if (root == null) return "定位出错行失败,传入ex为null";
        
            var st = new StackTrace(root, true);
            if (st == null) return "定位出错行失败,StackTrace(ex, true)返回null)";   // 理论上不会出现,但防御
        
            var frames = st.GetFrames();
            if (frames == null) return "定位出错行失败,st.GetFrames()返回null)";        

            
            foreach (var frame in frames)
            {
                if (frame == null) continue;                 // 单帧可能为null
        
                string file = frame.GetFileName();
                if (!string.IsNullOrEmpty(file))
                {
                    return string.Format("定位出错信息成功:{0} at {1} in {2}:{3}",
                                         ex.GetType().Name,
                                         frame.GetMethod() == null ? "未知方法" : frame.GetMethod().Name,
                                         Path.GetFileName(file),
                                         frame.GetFileLineNumber());
                }
            }

            return "定位出错行失败,可能是(无PDB)";
        }        

        private static bool IsConnectTimeout(Exception ex)
        {
            SqlException sqlex = ex as SqlException;
            if (sqlex != null)
                return sqlex.Number == -2 || sqlex.Number == 258;

            Win32Exception winex = ex as Win32Exception;
            if (winex != null)
                return (uint)winex.NativeErrorCode == 258u;

            return false;
        }
    }
}

so,以上

// ===================================================================================
//  MongoSqlClient  C# 5.0 兼容完整版 
//  封装类:MongoSqlHelper
//  说明:数据库连接参数读取config文件
//  依赖:log4.net
// ===================================================================================

using System;
using System.Collections.Generic;
using System.Data;
using System.Text.RegularExpressions;
using System.Configuration;
using log4net;


/* 相关版本:MongoDB 驱动 2.11.5 + MongoDB 3.6、4.2 + C# 5.0 + .NET 4.5.2  
 
 一、已支持—— 已测试3.6,确定能跑
  1. 等值/范围/IN 过滤
     SELECT * FROM tbl WHERE status=1 AND price>100 AND city IN ('bj','sh')
  2. TOP 
     SELECT TOP 50 * FROM tbl WHERE uid=1 ORDER BY createTime DESC
  3. COUNT / SUM / MAX / MIN / AVG
     SELECT COUNT(*) FROM tbl WHERE uid=1
     SELECT SUM(amount) AS total FROM tbl WHERE uid=1
     *****只允许单个查询,不允许同时查count 和max,要同时查多个聚合,请分别提交,譬如:****
     "SELECT COUNT(*),MAX(price) FROM tbl"   --- 不支持
  4. DISTINCT
     SELECT DISTINCT uid FROM tbl
  5. 一次插入多行
     INSERT INTO tbl(a,b) VALUES (1,'a'),(2,'b')
  7. 参数化写法(仅支持“拼好字符串”模式,不再传字典):
     string sql = "SELECT * FROM tbl WHERE uid=1 AND status=2";   // 直接拼
     DataTable dt = MongoSqlHelper.Select(sql);

 二、不支持
  · JOIN / 子查询 / UNION / 事务 / HAVING / 行锁
  · 表达式索引(如 $toInt:field)—— 3.6 不支持,必须预存字段再建索引
  · OFFSET / SKIP + LIMIT 组合(驱动支持,但本类未暴露 SKIP)
  · 视图、存储过程、触发器
  . CAST(驱动 2.11.5 自动用 $toInt/$toDate,但 3.6 无表达式索引,仅聚合场景,)
     SELECT CAST(price AS INT) AS priceInt FROM tbl ORDER BY CAST(price AS INT) ASC
     —— 会走聚合管道,**无索引**,大表慎用,所以也取消支持。

 三、能跑但会触发全表扫描,数据量大时不建议使用
  · LIKE '%xx%'   →  MongoDB $regex,3.6 不支持索引
  · 对 CAST/计算字段排序/过滤   →  走聚合,无索引

 四、版本要求
  · 驱动 2.11.5 最低要求 MongoDB 2.6,已测试 3.6+
  · .NET Framework 不得低于 4.5.2(驱动硬性要求)
  · C# 5.0 无 await/async,本类全部同步接口,无异步版本

 五、性能建议
  1. 凡是用于 WHERE / ORDER BY 的字段,**务必预存为纯类型**并建索引:
        db.col.updateMany({},[{$set:{priceInt:{$toInt:"$price"}}}])
        db.col.createIndex({priceInt:1})
  2. 日志级别调至 DEBUG 可在 log4net 中看到 Compass 可直接粘贴的执行脚本
  3. 内存缓存限额在 app.config 配置(已加 MongoSql 100 MB)
 */
namespace MongoSql.Demo
{
    public static class MongoSqlHelper
    {
        private static readonly MongoSqlClient Client;
        private static readonly ILog logger = LogManager.GetLogger(typeof(MongoSqlHelper));
    
        static MongoSqlHelper()
        {
            try
            {
                string conn = ConfigurationManager.AppSettings["MongoConn"];
                string db   = ConfigurationManager.AppSettings["MongoDb"];
                if (string.IsNullOrEmpty(conn) || string.IsNullOrEmpty(db))
                    throw new Exception("AppSettings 缺少 MongoConn 或 MongoDb");
    

                Client = new MongoSqlClient(conn, db,
                          new List<string> {
                              "table1",
                              "table2",
                              "mongosql_table"
                          });
            }
            catch (Exception ex)
            {
                LogAndException.Throw(ex,"MongoSqlClient 初始化失败...");
            }
        }
    
        #region 通用日志模板
        private static void LogEnter([System.Runtime.CompilerServices.CallerMemberName] string method = "",
                                     string sql = "")
        {
            logger.Info("[执行sql开始]" + method + " || SQL: " + sql);
        }
    
        private static void LogExit(string method, object ret)
        {
            logger.Info("[执行sql结束]" + method + " || Return: " + (ret ?? "null"));
        }
        #endregion
        
        #region ------- SQL 语法安检 -------
        // 语法预检:非法字符、永不支持语法、CAST+ORDER BY、LIKE 全表扫 全部处理。
        // 正常引号允许出现,譬如:Name="abc'
        // 配置项可关闭 CAST 排序拦截。
        private static void CheckSql(string sql)
        {
            if (string.IsNullOrWhiteSpace(sql))
                throw new Exception("SQL 为空");
        
            string upper = sql.ToUpper().Trim();
        
            /* 1. 危险字符(控制符、分号、反斜杠)*/
            if (Regex.IsMatch(sql, @"[\x00-\x08\x0B-\x0C\x0E-\x1F;\\]"))
                throw new Exception("SQL 含非法字符(控制符、分号、反斜杠)");
        
            /* 2. 不支持的语法 */
            string[] never = { "JOIN", "INNER JOIN", "LEFT JOIN", "RIGHT JOIN", "FULL JOIN",
                               "HAVING", "UNION", "TRANSACTION", "BEGIN", "ROLLBACK", "COMMIT",
                               "CREATE", "DROP", "ALTER", "GRANT", "REVOKE","LIMIT" };
            foreach (string kw in never)
                if (Regex.IsMatch(upper, @"\b" + Regex.Escape(kw) + @"\b", RegexOptions.IgnoreCase))
                    throw new Exception("SQL 含不支持的语法:" + kw);
        
            /* 3. CAST + ORDER BY不支持的用法*/
            if ( Regex.IsMatch(upper, @"\bCAST\s*\(") &&
                Regex.IsMatch(upper, @"\bORDER\s+BY\b"))
                throw new Exception("CAST+ORDER BY 无索引,不支持!");
        
            /* 4. 性能陷阱——禁止 */
            if (Regex.IsMatch(sql, @"\bLIKE\s+'%[^']*%'", RegexOptions.IgnoreCase))
                throw new Exception("SQL 含 LIKE '%xx%',会全表扫描,不支持!");
            
            /* 5. 聚合函数数量——只允许 1 个 */
            int aggCount = 0;
            string[] aggs = { "COUNT", "SUM", "MAX", "MIN", "AVG" };
            foreach (string a in aggs)
                aggCount += Regex.Matches(upper, @"\b" + a + @"\b", RegexOptions.IgnoreCase).Count;
            
            if (aggCount > 1)
                throw new Exception("一条 SQL 只允许 1 个聚合函数(COUNT/SUM/MAX/MIN/AVG),请分别提交");
        }
        #endregion
    
    
        //<summary>
        //查询返回 DataTable
        //完整示例:
        //DataTable dt = MongoSqlHelper.Select("SELECT pkid,col1,col2 FROM table1 WHERE pkid=1 ORDER BY pkid ASC");
        //</summary>
        public static DataTable Select(string sql)
        {
            CheckSql(sql);
            LogEnter("Select", sql);
            DataTable dt = Client.ExecuteDataTable(sql, null);
            if (dt == null || dt.Rows.Count == 0)
                logger.Warn("Select 返回空结果集");
            LogExit("Select", dt.Rows.Count + " 行");
            return dt;
        }
    
        //<summary>
        //插入
        //完整示例:
        //int rows = MongoSqlHelper.Insert("INSERT INTO table1(pkid,col1,col2) VALUES (1,'test','test2')");
        //</summary>
        public static int Insert(string sql)
        {
            CheckSql(sql);
            LogEnter("Insert", sql);
            int rows = Client.ExecuteInsert(sql);
            LogExit("Insert", rows + " 行插入");
            return rows;
        }
    
        //<summary>
        //更新
        //完整示例:
        //int rows = MongoSqlHelper.Update("UPDATE table1 SET col1='new name' WHERE pkid=1");
        //</summary>        
        public static int Update(string sql)
        {
            CheckSql(sql);
            LogEnter("Update", sql);
            int rows = Client.ExecuteUpdate(sql, null);
            LogExit("Update", rows + " 行更新");
            return rows;
        }
    
        //<summary>
        //删除
        //完整示例:
        //int rows = MongoSqlHelper.Delete("DELETE FROM table WHERE pkid=1");
        //</summary>
        public static int Delete(string sql)
        {
            CheckSql(sql);
            LogEnter("Delete", sql);
            int rows = Client.ExecuteDelete(sql, null);
            LogExit("Delete", rows + " 行删除");
            return rows;
        }
    
        //<summary>
        //计数
        //完整示例:
        //int cnt = MongoSqlHelper.Count("SELECT COUNT(*) FROM table1 WHERE pkid=1");
        //</summary>
        public static int Count(string sql)
        {
            CheckSql(sql);
            LogEnter("Count", sql);
            int cnt = Client.ExecuteCount(sql, null);
            LogExit("Count", cnt);
            return cnt;
        }
    
        //<summary>
        //单行单列
        //完整示例:
        //object name = MongoSqlHelper.ExecuteGetSingle("SELECT col1 FROM table1 WHERE pkid=1");
        //</summary>
        public static object ExecuteGetSingle(string sql)
        {
            CheckSql(sql);
            LogEnter("ExecuteGetSingle", sql);
            object val = Client.ExecuteGetSingle(sql, null);
            LogExit("ExecuteGetSingle", val);
            return val;
        }
    
        //<summary>
        //多行单列
        //完整示例:
        //List<object> list = MongoSqlHelper.ExecuteGetList("SELECT distinct pkid FROM table1 WHERE col2='test2' ");
        //</summary>
        public static List<object> ExecuteGetList(string sql)
        {
            CheckSql(sql);
            LogEnter("ExecuteGetList", sql);
            List<object> list = Client.ExecuteGetList(sql, null);
            LogExit("ExecuteGetList", list == null ? " 0 条" : list.Count + " 条");
            return list;
        }
    
        #region 标准 Scalar 兼容(C# 5.0)
        //<summary>
        //第一行第一列(DBNull → null)
        //完整示例:
        //object cnt = MongoSqlHelper.ExecuteScalar("SELECT COUNT(*) FROM table1 WHERE col1='test1' ");
        //</summary>
        public static object ExecuteScalar(string sql)
        {
            CheckSql(sql);
            LogEnter("ExecuteScalar", sql);
            DataTable dt = Select(sql);
            object val = (dt.Rows.Count == 0 || dt.Columns.Count == 0) ? null : dt.Rows[0][0];
            val = val == DBNull.Value ? null : val;
            LogExit("ExecuteScalar", val);
            return val;
        }
    
        //<summary>
        //泛型 Scalar
        //完整示例:
        //int cnt = MongoSqlHelper.ExecuteScalar<int>("SELECT COUNT(*) FROM table1 WHERE col1='test1' ");
        //</summary>
        public static T ExecuteScalar<T>(string sql)
        {
            CheckSql(sql);
            object raw = ExecuteScalar(sql);
            if (raw == null || raw == DBNull.Value)
                return default(T);
    
            Type u = Nullable.GetUnderlyingType(typeof(T));
            return u != null
                ? (T)Convert.ChangeType(raw, u)
                : (T)Convert.ChangeType(raw, typeof(T));
        }
        #endregion
    }
}
posted @ 2025-09-23 09:27  黯然销魂掌2015  阅读(29)  评论(0)    收藏  举报

联系方式:qq 16906913