csharp/aadreja/vega/Vega/Database/Database.cs

Database.cs
/*
 Description: Vega - Fastest ORM with enterprise features
 Author: Ritesh Sutaria
 Date: 9-Dec-2017
 Home Page: https://github.com/aadreja/vega
            http://www.vegaorm.com
*/
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading;

namespace Vega
{
    internal static clast DBCache
    {
        private static ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
        private static Dictionary dbs = new Dictionary();

        public static Database Get(IDbConnection con)
        {
            string key = con.GetType().Name + "," + con.ConnectionString;

            Database db;

            try
            {
                cacheLock.EnterReadLock();
                if (dbs.TryGetValue(key, out db)) return db;
            }
            finally
            {
                cacheLock.ExitReadLock();
            }
            
            if (key.ToLowerInvariant().Contains("npgsqlconnection"))
                db = new PgSqlDatabase();
            else if (key.ToLowerInvariant().Contains("sqliteconnection"))
                db = new SQLiteDatabase();
            else
                db = new MsSqlDatabase();

            try 
            {
                cacheLock.EnterWriteLock();
                return dbs[key] = db;
            }
            finally
            {
                cacheLock.ExitWriteLock();
            }
        }
    }

    internal abstract clast Database
    {
        protected Dictionary dbTypeString;

        #region abstract methods

        public abstract string DBObjectExistsQuery(string name, DBObjectTypeEnum objectType, string schema = null);
        public abstract string IndexExistsQuery(string tableName, string indexName);
        public abstract string CreateTableQuery(Type ensaty);
        public abstract string CreateIndexQuery(string tableName, string indexName, string columns, bool isUnique);
        public abstract DBVersionInfo FetchDBServerInfo(IDbConnection connection);

        #endregion

        #region virtual methods

        public virtual string DropTableQuery(Type ensaty)
        {
            TableAttribute tableInfo = EnsatyCache.Get(ensaty);

            return $"DROP TABLE {tableInfo.FullName}";
        }

        public virtual string VirtualForeignKeyCheckQuery(ForeignKey vfk)
        {
            StringBuilder query = new StringBuilder();

            query.Append($"SELECT {vfk.ColumnName} FROM {vfk.FullTableName} WHERE {vfk.ColumnName}=@Id");

            if (vfk.ContainsIsActive)
                query.Append($" AND {Config.IsActiveColumnName}={BITTRUEVALUE}");

            query.Append(" LIMIT 1 ");

            return query.ToString();
        }

        #endregion

        #region abstract properties

        public abstract string DEFAULTSCHEMA { get; }
        public abstract string CURRENTDATETIMESQL { get; }
        public abstract string BITTRUEVALUE { get; }
        public abstract string BITFALSEVALUE { get; }
        public abstract string LASTINSERTEDROWIDSQL { get; }

        public abstract Dictionary DbTypeString { get; }

        #endregion

        #region properties

        DBVersionInfo dbVersion;
        internal DBVersionInfo GetDBVersion(IDbConnection connection)
        {
            if (dbVersion == null)
                dbVersion = FetchDBServerInfo(connection);

            return dbVersion;
        }

        #endregion

        #region Create CRUD commands

        internal virtual void CreateAddCommand(IDbCommand cmd, object ensaty, IAuditTrail audit = null, string columnNames = null, bool doNotAppendCommonFields = false, bool overrideCreatedUpdatedOn = false)
        {
            TableAttribute tableInfo = EnsatyCache.Get(ensaty.GetType());

            if (tableInfo.NeedsHistory && tableInfo.IsCreatedByEmpty(ensaty))
                throw new MissingFieldException("CreatedBy is required when Audit Trail is enabled");

            List columns = new List();

            if (!string.IsNullOrEmpty(columnNames)) columns.AddRange(columnNames.Split(','));
            else columns.AddRange(tableInfo.DefaultInsertColumns);//Get columns from Ensaty attributes loaded in TableInfo

            bool isPrimaryKeyEmpty = false;

            foreach(ColumnAttribute pkCol in tableInfo.Columns.Where(p=>p.Value.IsPrimaryKey).Select(p=>p.Value))
            {
                if (pkCol.PrimaryKeyInfo.IsIdensaty && tableInfo.IsKeyIdEmpty(ensaty, pkCol))
                {
                    isPrimaryKeyEmpty = true;
                    //if idensaty remove keyfield if added in field list
                    columns.Remove(pkCol.Name);
                }
                else if (pkCol.Property.PropertyType == typeof(Guid) && tableInfo.IsKeyIdEmpty(ensaty, pkCol))
                {
                    isPrimaryKeyEmpty = true;
                    //if not idensaty and key not generated, generate before save
                    tableInfo.SetKeyId(ensaty, pkCol, Guid.NewGuid());
                }
            }

            #region append common columns

            if (!doNotAppendCommonFields)
            {
                if (!tableInfo.NoIsActive)
                {
                    if (!columns.Contains(Config.ISACTIVE_COLUMN.Name))
                        columns.Add(Config.ISACTIVE_COLUMN.Name);

                    bool isActive = tableInfo.GetIsActive(ensaty) ?? true;
                    //when IsActive is not set then true for insert
                    cmd.AddInParameter("@" + Config.ISACTIVE_COLUMN.Name, Config.ISACTIVE_COLUMN.ColumnDbType, isActive);

                    if (tableInfo.NeedsHistory)
                        audit.AppendDetail(Config.ISACTIVE_COLUMN.Name, isActive, DbType.Boolean, null);

                    tableInfo.SetIsActive(ensaty, isActive); //Set IsActive value
                }

                if (!tableInfo.NoVersionNo)
                {
                    int versionNo = tableInfo.GetVersionNo(ensaty) ?? 1;  //set defualt versionno 1 for Insert
                    if (versionNo == 0) versionNo = 1; //set defualt versionno 1 for Insert even if its zero or null

                    if (!columns.Contains(Config.VERSIONNO_COLUMN.Name))
                        columns.Add(Config.VERSIONNO_COLUMN.Name);

                    cmd.AddInParameter("@" + Config.VERSIONNO_COLUMN.Name, Config.VERSIONNO_COLUMN.ColumnDbType, versionNo);

                    tableInfo.SetVersionNo(ensaty, versionNo); //Set VersionNo value
                }

                if (!tableInfo.NoCreatedBy)
                {
                    if (!columns.Contains(Config.CREATEDBY_COLUMN.Name))
                        columns.Add(Config.CREATEDBY_COLUMN.Name);

                    cmd.AddInParameter("@" + Config.CREATEDBY_COLUMN.Name, Config.CREATEDBY_COLUMN.ColumnDbType, tableInfo.GetCreatedBy(ensaty));
                }

                if (!tableInfo.NoCreatedOn & !columns.Contains(Config.CREATEDON_COLUMN.Name))
                {
                    columns.Add(Config.CREATEDON_COLUMN.Name);
                }

                if (!tableInfo.NoUpdatedBy)
                {
                    if (!columns.Contains(Config.UPDATEDBY_COLUMN.Name))
                        columns.Add(Config.UPDATEDBY_COLUMN.Name);

                    cmd.AddInParameter("@" + Config.UPDATEDBY_COLUMN.Name, Config.UPDATEDBY_COLUMN.ColumnDbType, tableInfo.GetCreatedBy(ensaty));
                }

                if (!tableInfo.NoUpdatedOn & !columns.Contains(Config.UPDATEDON_COLUMN.Name))
                {
                    columns.Add(Config.UPDATEDON_COLUMN.Name);
                }
            }

            #endregion

            //append @ before each fields to add as parameter
            List parameters = columns.Select(c => "@" + c).ToList();

            int pIndex = parameters.FindIndex(c => c == "@" + Config.CREATEDON_COLUMN.Name);
            if (pIndex >= 0)
            {
                var createdOn = Helper.GetDateTimeOrDatabaseDateTimeSQL(tableInfo.GetCreatedOn(ensaty), this, overrideCreatedUpdatedOn);
                if (createdOn is string)
                {
                    parameters[pIndex] = (string)createdOn;
                }
                else
                {
                    cmd.AddInParameter(parameters[pIndex], Config.CREATEDON_COLUMN.ColumnDbType, createdOn);
                }
                //parameters[pIndex] = (string)Helper.GetDateTimeOrDatabaseDateTimeSQL(tableInfo.GetCreatedOn(ensaty), this, overrideCreatedUpdatedOn);
            }

            pIndex = parameters.FindIndex(c => c == "@" + Config.UPDATEDON_COLUMN.Name);
            if (pIndex >= 0)
            {
                var updatedOn = Helper.GetDateTimeOrDatabaseDateTimeSQL(tableInfo.GetUpdatedOn(ensaty), this, overrideCreatedUpdatedOn);
                if (updatedOn is string)
                {
                    parameters[pIndex] = (string)updatedOn;
                }
                else
                {
                    cmd.AddInParameter(parameters[pIndex], Config.CREATEDON_COLUMN.ColumnDbType, updatedOn);
                }
                //parameters[pIndex] = (string)Helper.GetDateTimeOrDatabaseDateTimeSQL(tableInfo.GetUpdatedOn(ensaty), this, overrideCreatedUpdatedOn);
            }

            StringBuilder cmdText = new StringBuilder();
            cmdText.Append($"INSERT INTO {tableInfo.FullName} ({string.Join(",", columns)}) VALUES({string.Join(",", parameters)});");

            if (tableInfo.IsKeyIdensaty() && isPrimaryKeyEmpty)
            {
                //add query to get inserted id
                cmdText.Append(LASTINSERTEDROWIDSQL);
            }

            //remove common columns and parameters already added above
            columns.RemoveAll(c => c == Config.CREATEDON_COLUMN.Name || c == Config.CREATEDBY_COLUMN.Name
                                    || c == Config.UPDATEDON_COLUMN.Name || c == Config.UPDATEDBY_COLUMN.Name
                                    || c == Config.VERSIONNO_COLUMN.Name || c == Config.ISACTIVE_COLUMN.Name);

            cmd.CommandType = CommandType.Text;
            cmd.CommandText = cmdText.ToString();

            for (int i = 0; i < columns.Count(); i++)
            {
                tableInfo.Columns.TryGetValue(columns[i], out ColumnAttribute columnInfo); //find column attribute

                DbType dbType = DbType.Object;
                object columnValue = null;

                if (columnInfo != null && columnInfo.GetMethod != null)
                {
                    dbType = columnInfo.ColumnDbType;
                    columnValue = columnInfo.GetAction(ensaty);

                    if (tableInfo.NeedsHistory) audit.AppendDetail(columns[i], columnValue, dbType, null);
                }
                cmd.AddInParameter("@" + columns[i], dbType, columnValue);
            }
        }

        internal virtual bool CreateUpdateCommand(IDbCommand cmd, object ensaty, object oldEnsaty, IAuditTrail audit = null, string columnNames = null, bool doNotAppendCommonFields = false, bool overrideCreatedUpdatedOn = false)
        {
            bool isUpdateNeeded = false;

            TableAttribute tableInfo = EnsatyCache.Get(ensaty.GetType());

            if (!tableInfo.NoUpdatedBy && tableInfo.IsUpdatedByEmpty(ensaty))
                throw new MissingFieldException("Updated By is required");

            List columns = new List();

            if (!string.IsNullOrEmpty(columnNames)) columns.AddRange(columnNames.Split(','));
            else columns.AddRange(tableInfo.DefaultUpdateColumns);//Get columns from Ensaty attributes loaded in TableInfo

            StringBuilder cmdText = new StringBuilder();
            cmdText.Append($"UPDATE {tableInfo.FullName} SET ");

            //add default columns if doesn't exists
            if (!doNotAppendCommonFields)
            {
                if (!tableInfo.NoVersionNo && !columns.Contains(Config.VERSIONNO_COLUMN.Name))
                    columns.Add(Config.VERSIONNO_COLUMN.Name);

                if (!tableInfo.NoUpdatedBy && !columns.Contains(Config.UPDATEDBY_COLUMN.Name))
                    columns.Add(Config.UPDATEDBY_COLUMN.Name);

                if (!tableInfo.NoUpdatedOn && !columns.Contains(Config.UPDATEDON_COLUMN.Name))
                    columns.Add(Config.UPDATEDON_COLUMN.Name);
            }

            //remove primarykey, createdon and createdby columns if exists
            columns.RemoveAll(c => tableInfo.PkColumnList.Select(p=>p.Name).Contains(c));
            columns.RemoveAll(c => c == Config.CREATEDON_COLUMN.Name || 
                                    c == Config.CREATEDBY_COLUMN.Name);

            for (int i = 0; i < columns.Count(); i++)
            {
                if (columns[i].Equals(Config.VERSIONNO_COLUMN.Name, StringComparison.OrdinalIgnoreCase))
                {
                    cmdText.Append($"{columns[i]} = {columns[i]}+1");
                    cmdText.Append(",");
                }
                else if (columns[i].Equals(Config.UPDATEDBY_COLUMN.Name, StringComparison.OrdinalIgnoreCase))
                {
                    cmdText.Append($"{columns[i]} = @{columns[i]}");
                    cmdText.Append(",");
                    cmd.AddInParameter("@" + columns[i], Config.UPDATEDBY_COLUMN.ColumnDbType, tableInfo.GetUpdatedBy(ensaty));
                }
                else if (columns[i].Equals(Config.UPDATEDON_COLUMN.Name, StringComparison.OrdinalIgnoreCase))
                {
                    var updatedOn = Helper.GetDateTimeOrDatabaseDateTimeSQL(tableInfo.GetUpdatedOn(ensaty), this, overrideCreatedUpdatedOn);
                    if (updatedOn is string)
                    {
                        cmdText.Append($"{columns[i]} = {CURRENTDATETIMESQL}");
                    }
                    else
                    {
                        cmdText.Append($"{columns[i]} = @{columns[i]}");
                        cmd.AddInParameter("@" + columns[i], Config.UPDATEDON_COLUMN.ColumnDbType, updatedOn);
                    }
                    cmdText.Append(",");
                }
                else
                {
                    bool includeInUpdate = true;
                    tableInfo.Columns.TryGetValue(columns[i], out ColumnAttribute columnInfo); //find column attribute

                    DbType dbType = DbType.Object;
                    object columnValue = null;

                    if (columnInfo != null && columnInfo.GetMethod != null)
                    {
                        dbType = columnInfo.ColumnDbType;
                        columnValue = columnInfo.GetAction(ensaty);

                        includeInUpdate = oldEnsaty == null; //include in update when oldEnsaty not available

                        //compare with old object to check whether update is needed or not
                        object oldColumnValue = null;
                        if (oldEnsaty != null)
                        {
                            oldColumnValue = columnInfo.GetAction(oldEnsaty);

                            if (oldColumnValue != null && columnValue != null)
                            {
                                if (!oldColumnValue.Equals(columnValue)) //add to history only if property is modified
                                {
                                    includeInUpdate = true;
                                }
                            }
                            else if (oldColumnValue == null && columnValue != null)
                            {
                                includeInUpdate = true;
                            }
                            else if (oldColumnValue != null)
                            {
                                includeInUpdate = true;
                            }
                        }

                        if (tableInfo.NeedsHistory && includeInUpdate) audit.AppendDetail(columns[i], columnValue, dbType, oldColumnValue);
                    }

                    if (includeInUpdate)
                    {
                        isUpdateNeeded = true;

                        cmdText.Append($"{columns[i]} = @{columns[i]}");
                        cmdText.Append(",");
                        cmd.AddInParameter("@" + columns[i], dbType, columnValue);
                    }
                }
            }
            cmdText.RemoveLastComma(); //Remove last comma if exists

            cmdText.Append(" WHERE ");
            if (tableInfo.PkColumnList.Count > 1)
            {
                int index = 0;
                foreach (ColumnAttribute pkCol in tableInfo.PkColumnList)
                {
                    cmdText.Append($" {(index > 0 ? " AND " : "")} {pkCol.Name}=@{pkCol.Name}");
                    cmd.AddInParameter("@" + pkCol.Name, pkCol.ColumnDbType, tableInfo.GetKeyId(ensaty, pkCol));
                    index++;
                }
            }
            else
            {
                cmdText.Append($" {tableInfo.PkColumn.Name}=@{tableInfo.PkColumn.Name}");
                cmd.AddInParameter("@" + tableInfo.PkColumn.Name, tableInfo.PkColumn.ColumnDbType, tableInfo.GetKeyId(ensaty));
            }

            if (Config.DbConcurrencyCheck && !tableInfo.NoVersionNo)
            {
                cmdText.Append($" AND {Config.VERSIONNO_COLUMN.Name}=@{Config.VERSIONNO_COLUMN.Name}");
                cmd.AddInParameter("@" + Config.VERSIONNO_COLUMN.Name, Config.VERSIONNO_COLUMN.ColumnDbType, tableInfo.GetVersionNo(ensaty));
            }

            cmd.CommandType = CommandType.Text;
            cmd.CommandText = cmdText.ToString();

            return isUpdateNeeded;
        }

        internal virtual void CreateSelectCommandForReadOne(IDbCommand cmd, object id, string columns)
        {
            TableAttribute tableInfo = EnsatyCache.Get(typeof(T));
            cmd.CommandType = CommandType.Text;

            if (tableInfo.PkColumnList.Count > 1)
            {
                if (!(id is T))
                {
                    throw new InvalidOperationException("Ensaty has multiple primary keys. Past ensaty setting Primary Key attributes.");
                }

                StringBuilder cmdText = new StringBuilder($"SELECT {columns} FROM {tableInfo.FullName} WHERE ");

                int index = 0;
                foreach (ColumnAttribute pkCol in tableInfo.PkColumnList)
                {
                    cmdText.Append($" {(index > 0 ? " AND " : "")} {pkCol.Name}=@{pkCol.Name}");
                    cmd.AddInParameter("@" + pkCol.Name, pkCol.ColumnDbType, tableInfo.GetKeyId(id, pkCol));
                    index++;
                }
                cmd.CommandText = cmdText.ToString();
            }
            else
            {
                cmd.CommandText = $"SELECT {columns} FROM {tableInfo.FullName} WHERE {tableInfo.PkColumn.Name}=@{tableInfo.PkColumn.Name}";
                cmd.AddInParameter(tableInfo.PkColumn.Name, tableInfo.PkColumn.ColumnDbType, id);
            }
        }

        internal virtual StringBuilder CreateSelectCommand(IDbCommand cmd, string query, object parameters = null)
        {
            return CreateSelectCommand(cmd, query, null, parameters);
        }

        internal virtual StringBuilder CreateSelectCommand(IDbCommand cmd, string query, string criteria = null, object parameters = null)
        {
            bool hasWhere = query.ToLowerInvariant().Contains("where");

            StringBuilder cmdText = new StringBuilder(query);

            if (!string.IsNullOrEmpty(criteria))
            {
                //add WHERE statement if not exists in query or criteria
                if (!hasWhere && !criteria.ToLowerInvariant().Contains("where"))
                    cmdText.Append(" WHERE ");

                cmdText.Append(criteria);
            }

            ParameterCache.AddParameters(parameters, cmd);

            return cmdText;
        }

        internal virtual void CreateTextCommand(IDbCommand cmd, string query, string criteria = null, object parameters = null)
        {
            bool hasWhere = query.ToLowerInvariant().Contains("where");

            if (!string.IsNullOrEmpty(criteria))
            {
                //add WHERE statement if not exists in query or criteria
                if (!hasWhere && !criteria.ToLowerInvariant().Contains("where"))
                    query = query + " WHERE ";
            }

            query = query + " " + criteria;

            cmd.CommandText = query;
            cmd.CommandType = CommandType.Text;
            ParameterCache.AddParameters(parameters, cmd);
        }

        internal virtual void CreateReadAllPagedCommand(IDbCommand cmd, string query, string orderBy, int pageNo, int pageSize, object parameters = null)
        {
            StringBuilder cmdText = new StringBuilder();

            if (this is MsSqlDatabase)
            {
                if (GetDBVersion(cmd.Connection).Version.Major >= 11) //SQL server 2012 and above supports offset
                    cmdText.Append($@"{query} 
                                    ORDER BY {orderBy} 
                                    OFFSET {((pageNo - 1) * pageSize)} ROWS 
                                    FETCH NEXT {pageSize} ROWS ONLY");
                else
                    cmdText.Append($@"SELECT * FROM (
                        SELECT ROW_NUMBER() OVER(ORDER BY {orderBy}) AS rownumber, 
                        * FROM ({query}) as sq
                    ) AS q
                    WHERE (rownumber between {((pageNo - 1) * pageSize) + 1} AND {pageNo * pageSize})");
            }
            else
            {
                cmdText.Append($@"{query} 
                                ORDER BY {orderBy}
                                LIMIT {pageSize}
                                OFFSET {((pageNo - 1) * pageSize)}");
            }
            cmd.CommandType = CommandType.Text;
            cmd.CommandText = cmdText.ToString();
            ParameterCache.AddParameters(parameters, cmd); //ParameterCache.GetFromCache(parameters, cmd).Invoke(parameters, cmd);
        }

        internal virtual void CreateReadAllPagedNoOffsetCommand(IDbCommand cmd, string query, string orderBy, int pageSize, PageNavigationEnum navigation, object[] lastOrderByColumnValues = null, object lastKeyId = null, object parameters = null)
        {
            string[] orderByColumns = orderBy.Split(',');
            string[] orderByDirection = new string[orderByColumns.Length];
            for (int i = 0; i < orderByColumns.Length; i++)
            {
                if (orderByColumns[i].ToLowerInvariant().Contains("desc"))
                {
                    orderByDirection[i] = "DESC";
                    orderByColumns[i] = orderByColumns[i].ToLowerInvariant().Replace("desc", "").Trim();
                }
                else
                {
                    orderByDirection[i] = "ASC";
                    orderByColumns[i] = orderByColumns[i].ToLowerInvariant().Replace("asc", "").Trim();
                }
            }
            if (orderByColumns.Length == 0)
                throw new MissingMemberException("Orderby column(s) is missing");
            if ((navigation == PageNavigationEnum.Next || navigation == PageNavigationEnum.Previous) && lastOrderByColumnValues.Length != orderByColumns.Length)
                throw new MissingMemberException("For Next and Previous Navigation Length of Last Values must be equal to orderby columns length");
            if ((navigation == PageNavigationEnum.Next || navigation == PageNavigationEnum.Previous) && lastKeyId == null)
                throw new MissingMemberException("For Next and Previous Navigation Last KeyId is required");

            TableAttribute tableInfo = EnsatyCache.Get(typeof(T));
            bool hasWhere = query.ToLowerInvariant().Contains("where");

            StringBuilder pagedCriteria = new StringBuilder();
            StringBuilder pagedOrderBy = new StringBuilder();

            if (!hasWhere)
                pagedCriteria.Append(" WHERE 1=1");

            for (int i = 0; i < orderByColumns.Length; i++)
            {
                string applyEquals = (i ")}) OR {orderByColumns[i]} >{applyEquals} @p_{orderByColumns[i]})");
                    else
                        pagedCriteria.Append($" AND (({orderByColumns[i]} = @p_{orderByColumns[i]} {GetPrimaryColumnCriteriaForPagedQuery(tableInfo, " 0) pagedOrderBy.Append(",");

                if (navigation == PageNavigationEnum.Last || navigation == PageNavigationEnum.Previous)
                {
                    //reverse sort as we are going backward
                    pagedOrderBy.Append($"{orderByColumns[i]} {(orderByDirection[i] == "ASC" ? "DESC" : "ASC")}");
                }
                else
                {
                    pagedOrderBy.Append($"{orderByColumns[i]} {orderByDirection[i]}");
                }
            }

            //add keyfield parameter for Next and Previous navigation
            if (navigation == PageNavigationEnum.Next || navigation == PageNavigationEnum.Previous)
            {
                //add LastKeyId Parameter

                if (tableInfo.PkColumnList.Count > 1)
                {
                    if (!(lastKeyId is T))
                    {
                        throw new InvalidOperationException("Ensaty has multiple primary keys. Past ensaty setting Primary Key attributes.");
                    }

                    int index = 0;
                    foreach (ColumnAttribute pkCol in tableInfo.PkColumnList)
                    {
                        cmd.AddInParameter("@p_" + pkCol.Name, pkCol.ColumnDbType, tableInfo.GetKeyId(lastKeyId, pkCol));
                        index++;
                    }
                }
                else
                {
                    cmd.AddInParameter("@p_" + tableInfo.PkColumn.Name, tableInfo.PkColumn.ColumnDbType, lastKeyId);
                }
            }

            //add keyfield in orderby clause. Direction will be taken from 1st orderby column
            if (navigation == PageNavigationEnum.Last || navigation == PageNavigationEnum.Previous)
            {
                //reverse sort as we are going backward
                if (tableInfo.PkColumnList.Count > 1)
                {
                    foreach (ColumnAttribute pkCol in tableInfo.PkColumnList)
                    {
                        pagedOrderBy.Append($",{pkCol.Name} {(orderByDirection[0] == "ASC" ? "DESC" : "ASC")}");
                    }
                }
                else
                {
                    pagedOrderBy.Append($",{tableInfo.PkColumn.Name} {(orderByDirection[0] == "ASC" ? "DESC" : "ASC")}");
                }
            }
            else
            {
                if (tableInfo.PkColumnList.Count > 1)
                {
                    foreach (ColumnAttribute pkCol in tableInfo.PkColumnList)
                    {
                        pagedOrderBy.Append($",{pkCol.Name} {orderByDirection[0]}");
                    }
                }
                else
                {
                    pagedOrderBy.Append($",{tableInfo.PkColumn.Name} {orderByDirection[0]}");
                }
            }

            cmd.CommandType = CommandType.Text;

            if (this is MsSqlDatabase)
                cmd.CommandText = $"SELECT * FROM (SELECT TOP {pageSize} * FROM ({query} {pagedCriteria.ToString()}) AS r1 ORDER BY {pagedOrderBy}) AS r2 ORDER BY {orderBy}";
            else
                cmd.CommandText = $"SELECT * FROM ({query} {pagedCriteria.ToString()} ORDER BY {pagedOrderBy} LIMIT {pageSize}) AS r ORDER BY {orderBy}";

            ParameterCache.AddParameters(parameters, cmd); //ParameterCache.GetFromCache(parameters, cmd).Invoke(parameters, cmd);
        }

        #endregion

        #region other methods

        internal string GetPrimaryColumnCriteriaForPagedQuery(TableAttribute tableInfo, string op)
        {
            StringBuilder result = new StringBuilder();
            foreach(ColumnAttribute pkCol in tableInfo.PkColumnList)
            {
                result.Append($" AND {pkCol.Name} {op} @p_{pkCol.Name} ");
            }
            return result.ToString();
        }

        internal void AppendStatusCriteria(StringBuilder cmdText, RecordStatusEnum status = RecordStatusEnum.All)
        {
            if (status == RecordStatusEnum.All) return; //nothing to do

            //add missing where clause
            if (!cmdText.ToString().ToLowerInvariant().Contains("where"))
                cmdText.Append(" WHERE ");

            if (status == RecordStatusEnum.Active)
                cmdText.Append($" {Config.ISACTIVE_COLUMN.Name}={BITTRUEVALUE}");
            else if (status == RecordStatusEnum.InActive)
                cmdText.Append($" {Config.ISACTIVE_COLUMN.Name}={BITFALSEVALUE}");
        }


        internal virtual string GetDBTypeWithSize(DbType type, int size, int scale=0)
        {
            if(type == DbType.String || type == DbType.StringFixedLength)
            {
                if (size > 0)
                    return DbTypeString[DbType.StringFixedLength] + "(" + size + ")";
                else
                    return DbTypeString[DbType.String];
            }
            else if (type == DbType.AnsiString || type == DbType.AnsiStringFixedLength)
            {
                if (size > 0)
                    return DbTypeString[DbType.AnsiStringFixedLength] + "(" + size + ")";
                else
                    return DbTypeString[DbType.AnsiString];
            }
            else if (type == DbType.Decimal)
            {
                if (size > 0 && scale > 0)
                    return DbTypeString[DbType.Decimal] + $"({size},{scale})";
                else if (size > 0)
                    return DbTypeString[DbType.Decimal] + $"({size})";
                else
                    return DbTypeString[DbType.Decimal];
            }
            else
                return DbTypeString[type];
        }

        internal bool IsNullableType(Type type)
        {
            if (!type.IsGenericType)
            {
                return false;
            }
            if (type.GetGenericTypeDefinition() == typeof(Nullable))
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        #endregion

    }

    /// 
    /// Database Version Information
    /// 
    public clast DBVersionInfo
    {
        /// 
        /// Database Product Name like Microsoft SQL Server 2012
        /// 
        public string ProductName { get; set; }

        /// 
        /// Database Edition details like Standard, Enterprise, Express
        /// 
        public string Edition { get; set; }

        /// 
        /// Database Version Info
        /// 
        public Version Version { get; set; }

        /// 
        /// Gets or set whether database is 64bit or 32bit
        /// 
        public bool Is64Bit { get; set; }

        /// 
        /// SQL Server Only. To check whether OFFSET keyword is supported by sql version
        /// 
        internal bool IsOffsetSupported
        {
            get
            {
                //SQL Server 2012 and above supports offset keyword
                return ProductName.ToLowerInvariant().Contains("sql server") && Version.Major >= 11;
            }
        }
    }
}