divconq.sql.SqlManager.java Source code

Java tutorial

Introduction

Here is the source code for divconq.sql.SqlManager.java

Source

/* ************************************************************************
#
#  DivConq
#
#  http://divconq.com/
#
#  Copyright:
#    Copyright 2014 eTimeline, LLC. All rights reserved.
#
#  License:
#    See the license.txt file in the project's top-level directory for details.
#
#  Authors:
#    * Andy White
#
************************************************************************ */
package divconq.sql;

import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Types;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

import divconq.hub.Hub;
import divconq.lang.op.FuncResult;
import divconq.lang.op.OperationContext;
import divconq.lang.op.OperationResult;
import divconq.struct.ListStruct;
import divconq.struct.RecordStruct;
import divconq.struct.Struct;
import divconq.struct.scalar.NullStruct;
import divconq.util.StringUtil;
import divconq.xml.XElement;

// TODO there is much much more to do to make this easier to support many different db engines
public class SqlManager {
    public final DateTimeFormatter stampFmt = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSS");

    protected Map<String, SqlDatabase> databases = new HashMap<String, SqlManager.SqlDatabase>();

    public String getNowAsString() {
        return this.stampFmt.print(new DateTime(DateTimeZone.UTC));
    }

    public String getDateAsString(DateTime dt) {
        return this.stampFmt.print(dt.toDateTime(DateTimeZone.UTC));
    }

    public String getDateAsString(long dt) {
        return this.stampFmt.print(new DateTime(dt, DateTimeZone.UTC));
    }

    public SqlDatabase getDatabase(String name) {
        return this.databases.get(name);
    }

    public void init(OperationResult or, XElement config) {
        if (config == null)
            return;

        for (XElement del : config.selectAll("Database")) {
            String name = del.getAttribute("Name", "default");

            SqlDatabase db = new SqlDatabase();
            db.name = name;
            db.init(or, del);

            this.databases.put(name, db);
        }
    }

    public void stop() {
        for (SqlDatabase db : this.databases.values())
            db.stop();
    }

    public class SqlDatabase {
        protected String connstring = null;
        protected String name = null;

        protected SqlEngine engine = null;

        // single connection style engines
        protected Connection conn = null;
        protected Semaphore lock = new Semaphore(0);

        protected boolean poolmode = false;
        protected ConcurrentLinkedQueue<Connection> pool = new ConcurrentLinkedQueue<>();

        public void init(OperationResult or, XElement del) {
            if (del == null)
                return;

            try {
                String driver = del.getAttribute("Driver");

                Class.forName(driver);

                this.connstring = Hub.instance.getClock().getObfuscator()
                        .decryptHexToString(del.getAttribute("Connection"));

                // if null then try unencrypted
                if (this.connstring == null)
                    this.connstring = del.getAttribute("Connection");

                if (this.connstring.startsWith("jdbc:h2:"))
                    this.engine = SqlEngine.H2;
                else if (this.connstring.startsWith("jdbc:sqlserver:"))
                    this.engine = SqlEngine.SqlServer;
                else if (this.connstring.startsWith("jdbc:mariadb:"))
                    this.engine = SqlEngine.MariaDb;
                else if (this.connstring.startsWith("jdbc:mysql:"))
                    this.engine = SqlEngine.MySQL;
                else {
                    or.errorTr(189, this.connstring.substring(0, Math.min(this.connstring.length(), 15)));
                    return;
                }

                this.poolmode = "Pooled".equals(del.getAttribute("Mode"));

                if (this.engine == SqlEngine.SqlServer || this.engine == SqlEngine.MariaDb
                        || this.engine == SqlEngine.MySQL) {
                    or.info(0, "Using database " + this.name + " with multiple connections.");
                } else {
                    this.conn = DriverManager.getConnection(this.connstring);

                    or.info(0, "Connected to database " + this.name + " single connection.");

                    this.releaseConnection(this.conn);
                }
            } catch (Exception x) {
                or.errorTr(190, this.name, x);
                return;
            }
        }

        public void stop() {
            try {
                if (this.engine == SqlEngine.SqlServer || this.engine == SqlEngine.MariaDb
                        || this.engine == SqlEngine.MySQL) {
                    if (this.poolmode) {
                        Connection conn = this.pool.poll();

                        while (conn != null) {
                            try {
                                conn.close();
                            } catch (Exception x) {
                                // unimportant
                            }

                            conn = this.pool.poll();
                        }
                    }
                } else if (this.engine == SqlEngine.H2)
                    this.conn.close();
            } catch (Exception x) {
                // unimportant
            }
        }

        public Connection acquireConnection() {
            Hub.instance.getCountManager().allocateNumberCounter("dcSqlAcquireConnection").increment();

            if (this.engine == SqlEngine.SqlServer || this.engine == SqlEngine.MariaDb
                    || this.engine == SqlEngine.MySQL) {
                try {
                    Connection conn = null;

                    if (this.poolmode) {
                        conn = this.pool.poll();

                        if ((conn != null) && !conn.isValid(2))
                            conn = null;
                    }

                    if (conn == null) {
                        conn = DriverManager.getConnection(this.connstring);
                        Hub.instance.getCountManager().allocateNumberCounter("dcSqlConnectionCreate").increment();
                    }

                    return conn;
                } catch (SQLException x) {
                }
            } else if (this.engine == SqlEngine.H2) {
                try {
                    this.lock.acquire();
                    return this.conn;
                } catch (InterruptedException e) {
                }
            }

            Hub.instance.getCountManager().allocateNumberCounter("dcSqlAcquireConnectionFail").increment();

            return null;
        }

        public void releaseConnection(Connection conn) {
            Hub.instance.getCountManager().allocateNumberCounter("dcSqlReleaseConnection").increment();

            if (this.engine == SqlEngine.SqlServer || this.engine == SqlEngine.MariaDb
                    || this.engine == SqlEngine.MySQL) {
                if (this.poolmode) {
                    this.pool.add(conn);
                    return;
                }

                try {
                    conn.close();
                } catch (SQLException x) {
                }
            } else if (this.engine == SqlEngine.H2) {
                this.lock.release();
            }
        }

        public SqlEngine getEngine() {
            return this.engine;
        }

        public boolean testConnection() {
            Connection conn = this.acquireConnection();

            if (conn == null)
                return false;

            this.releaseConnection(conn);

            return true;
        }

        // warning - may not use same connection between calls
        public String getLastIdSql() {
            if (this.engine == SqlEngine.H2)
                return "SELECT IDENTITY() AS lid";

            if (this.engine == SqlEngine.SqlServer)
                return "SELECT @@IDENTITY AS lid";

            if (this.engine == SqlEngine.MariaDb || this.engine == SqlEngine.MySQL)
                return "SELECT LAST_INSERT_ID() AS lid";

            return null;
        }

        /*
         * We should always talk in UTC...
         *  
         * @return
         */
        public String nowFunc() {
            if (this.getEngine() == SqlEngine.SqlServer)
                return "GETUTCDATE()";

            if (this.getEngine() == SqlEngine.MariaDb || this.getEngine() == SqlEngine.MySQL)
                return "UTC_TIMESTAMP()";

            return "NOW()";
        }

        // TODO
        // only support MINUTES at present
        public String timeUnit(TimeUnit unit) {
            if (unit == TimeUnit.MINUTES)
                return "MINUTE";

            return null;
        }

        public String modNowFunc(TimeUnit unit, int amt) {
            return this.modTimeFunc(this.nowFunc(), unit, amt);
        }

        public String modTimeFunc(String time, TimeUnit unit, int amt) {
            String unitname = this.timeUnit(unit);

            // h2 syntax
            String expr = "DATEADD('" + unitname + "', " + amt + ", " + time + ") ";

            if (this.getEngine() == SqlEngine.SqlServer)
                expr = "DATEADD(" + unitname + ", " + amt + ", " + time + ") ";

            if (this.getEngine() == SqlEngine.MariaDb || this.getEngine() == SqlEngine.MySQL)
                expr = "DATE_ADD(" + time + ", INTERVAL " + amt + " " + unitname + ") ";

            return expr;
        }

        public String formatColumn(String name) {
            if (this.getEngine() == SqlEngine.SqlServer)
                return "[" + name + "]";

            if (this.getEngine() == SqlEngine.MariaDb || this.getEngine() == SqlEngine.MySQL)
                return "`" + name + "`";

            // TODO check what H2 uses...

            return name;
        }

        public void processException(Exception x, OperationResult or) {
            if (x instanceof SQLException) {
                SQLException sx = (SQLException) x;

                if (this.getEngine() == SqlEngine.MariaDb || this.getEngine() == SqlEngine.MySQL) {
                    // duplicate id error - this is not always an error for the caller
                    // we treat it as an error return code but log it as an Info
                    if (sx.getErrorCode() == 1062) {
                        or.exit(194, OperationContext.get().tr("_code_194", this.name, x));
                        return;
                    }
                } else if (this.getEngine() == SqlEngine.SqlServer) {
                    if (sx.getErrorCode() == 2627) {
                        or.exit(194, OperationContext.get().tr("_code_194", this.name, x));
                        return;
                    }
                }

                // TODO add other databases

                or.errorTr(195, this.name, ((SQLException) x).getErrorCode(), x);
                return;
            }

            or.errorTr(186, this.name, x);
        }

        public FuncResult<Integer> executeFreestyle(String sql, Object... params) {
            FuncResult<Integer> res = new FuncResult<Integer>();

            Connection conn = this.acquireConnection();

            // if connection is bad/missing then just try again later
            if (conn == null) {
                res.errorTr(185, this.name);
                return res;
            }

            try {
                FuncResult<PreparedStatement> psres = this.prepStatement(conn, sql, params);

                if (res.hasErrors())
                    return res;

                PreparedStatement pstmt = psres.getResult();

                Hub.instance.getCountManager().countObjects("dcSqlExecuteCount", pstmt);

                res.setResult(pstmt.executeUpdate());

                pstmt.close();
            } catch (Exception x) {
                this.processException(x, res);

                Hub.instance.getCountManager().countObjects("dcSqlExecuteFail", sql);
            } finally {
                this.releaseConnection(conn);
            }

            return res;
        }

        // return a list of records where each row is a record in this collection
        // -- NOTE: column names are all lower case
        public FuncResult<ListStruct> executeQueryFreestyle(String sql, Object... params) {
            FuncResult<ListStruct> res = new FuncResult<ListStruct>();
            ListStruct list = new ListStruct();

            res.setResult(list);

            Connection conn = this.acquireConnection();

            // if connection is bad/missing then just try again later
            if (conn == null) {
                res.errorTr(185, this.name);
                return res;
            }

            ResultSet rs = null;

            try {
                FuncResult<PreparedStatement> psres = this.prepStatement(conn, sql, params);

                if (res.hasErrors())
                    return res;

                PreparedStatement pstmt = psres.getResult();

                Hub.instance.getCountManager().countObjects("dcSqlQueryCount", pstmt);

                // MariaDB hint that this turns on streaming... review TODO
                //pstmt.setFetchSize(Integer.MIN_VALUE);

                rs = pstmt.executeQuery();

                ResultSetMetaData md = rs.getMetaData();
                int columns = md.getColumnCount();

                while (rs.next()) {
                    RecordStruct rec = new RecordStruct();

                    for (int i = 1; i <= columns; i++)
                        rec.setField(md.getColumnLabel(i).toLowerCase(), rs.getObject(i));

                    list.addItem(rec);
                }

                pstmt.close();
            } catch (Exception x) {
                this.processException(x, res);

                Hub.instance.getCountManager().countObjects("dcSqlQueryFail", sql);
            } finally {
                try {
                    if (rs != null)
                        rs.close();
                } catch (SQLException x) {
                }

                this.releaseConnection(conn);
            }

            return res;
        }

        // return a list of records where each row is a record in this collection
        public FuncResult<ListStruct> executeQueryPage(SqlSelect[] select, String from, String where,
                String groupby, String orderby, int offset, int pagesize, Object... params) {
            FuncResult<ListStruct> res = new FuncResult<ListStruct>();

            // acquire
            Connection conn = this.acquireConnection();

            // if connection is bad/missing then just try again later
            if (conn == null) {
                res.errorTr(185, this.name);
                return res;
            }

            // prepare
            FuncResult<PreparedStatement> psres = this.prepPage(conn, select, from, where, groupby, orderby, offset,
                    pagesize, params);

            if (res.hasErrors())
                return res;

            PreparedStatement pstmt = psres.getResult();

            // execute
            res = callAndFormat(select, pstmt);

            try {
                if (pstmt != null)
                    pstmt.close();
            } catch (SQLException x) {
            }

            // release
            this.releaseConnection(conn);

            return res;
        }

        // return a list of records where each row is a record in this collection
        public FuncResult<ListStruct> executeQueryLimit(SqlSelect[] select, String from, String where,
                String groupby, String orderby, int limit, boolean distinct, Object... params) {
            FuncResult<ListStruct> res = new FuncResult<ListStruct>();

            // acquire
            Connection conn = this.acquireConnection();

            // if connection is bad/missing then just try again later
            if (conn == null) {
                res.errorTr(185, this.name);
                return res;
            }

            // prepare
            FuncResult<PreparedStatement> psres = this.prepLimit(conn, select, from, where, groupby, orderby, limit,
                    distinct, params);

            if (res.hasErrors())
                return res;

            PreparedStatement pstmt = psres.getResult();

            // execute
            res = callAndFormat(select, pstmt);

            try {
                if (pstmt != null)
                    pstmt.close();
            } catch (SQLException x) {
            }

            // release
            this.releaseConnection(conn);

            return res;
        }

        // return a list of records where each row is a record in this collection
        public FuncResult<ListStruct> executeQuery(SqlSelect[] select, String from, String where, String groupby,
                String orderby, Object... params) {
            FuncResult<ListStruct> res = new FuncResult<ListStruct>();

            // acquire
            Connection conn = this.acquireConnection();

            // if connection is bad/missing then just try again later
            if (conn == null) {
                res.errorTr(185, this.name);
                return res;
            }

            // prepare
            FuncResult<PreparedStatement> psres = this.prep(conn, select, from, where, groupby, orderby, params);

            if (res.hasErrors())
                return res;

            PreparedStatement pstmt = psres.getResult();

            // execute
            res = this.callAndFormat(select, pstmt);

            try {
                if (pstmt != null)
                    pstmt.close();
            } catch (SQLException x) {
            }

            // release
            this.releaseConnection(conn);

            return res;
        }

        // return a single value (row/column) from table 
        public FuncResult<Struct> executeQueryScalar(SqlSelect select, String from, String where, String orderby,
                Object... params) {
            FuncResult<Struct> res = new FuncResult<Struct>();
            res.setResult(NullStruct.instance);

            // acquire
            Connection conn = this.acquireConnection();

            // if connection is bad/missing then just try again later
            if (conn == null) {
                res.errorTr(185, this.name);
                return res;
            }

            SqlSelect[] selects = new SqlSelect[] { select };

            // prepare
            FuncResult<PreparedStatement> psres = this.prep(conn, selects, from, where, null, orderby, params);

            if (res.hasErrors())
                return res;

            PreparedStatement pstmt = psres.getResult();

            // execute
            FuncResult<ListStruct> res2 = this.callAndFormat(selects, pstmt);

            ListStruct lrs = res2.getResult();

            if (lrs.getSize() > 0) {
                RecordStruct rec = lrs.getItemAsRecord(0);

                res.setResult(rec.getField(select.name));
            }

            try {
                if (pstmt != null)
                    pstmt.close();
            } catch (SQLException x) {
            }

            // release
            this.releaseConnection(conn);

            return res;
        }

        // return a single String value (row/column) from table 
        public String executeQueryString(String col, String from, String where, String orderby, Object... params) {
            FuncResult<Struct> rsres = this.executeQueryScalar(new SqlSelectString(col), from, where, orderby,
                    params);

            if (rsres.hasErrors() || rsres.isEmptyResult())
                return null;

            return Struct.objectToString(rsres.getResult());
        }

        // return a single Integer value (row/column) from table 
        public Long executeQueryInteger(String col, String from, String where, String orderby, Object... params) {
            FuncResult<Struct> rsres = this.executeQueryScalar(new SqlSelectInteger(col), from, where, orderby,
                    params);

            if (rsres.hasErrors() || rsres.isEmptyResult())
                return null;

            return Struct.objectToInteger(rsres.getResult());
        }

        // return a single Boolean value (row/column) from table 
        public Boolean executeQueryBoolean(String col, String from, String where, String orderby,
                Object... params) {
            FuncResult<Struct> rsres = this.executeQueryScalar(new SqlSelectBoolean(col), from, where, orderby,
                    params);

            if (rsres.hasErrors() || rsres.isEmptyResult())
                return null;

            return Struct.objectToBoolean(rsres.getResult());
        }

        // return a single row from table 
        public FuncResult<RecordStruct> executeQueryRecord(SqlSelect[] selects, String from, String where,
                Object... params) {
            FuncResult<RecordStruct> res = new FuncResult<RecordStruct>();

            // acquire
            Connection conn = this.acquireConnection();

            // if connection is bad/missing then just try again later
            if (conn == null) {
                res.errorTr(185, this.name);
                return res;
            }

            // prepare
            FuncResult<PreparedStatement> psres = this.prep(conn, selects, from, where, null, null, params);

            if (res.hasErrors())
                return res;

            PreparedStatement pstmt = psres.getResult();

            // execute
            FuncResult<ListStruct> res2 = this.callAndFormat(selects, pstmt);

            ListStruct lrs = res2.getResult();

            if (lrs.getSize() > 0)
                res.setResult(lrs.getItemAsRecord(0));
            else
                res.setResult(new RecordStruct()); // always return something - caller should do an empty check

            try {
                if (pstmt != null)
                    pstmt.close();
            } catch (SQLException x) {
            }

            // release
            this.releaseConnection(conn);

            return res;
        }

        // return a single row (the first) from table 
        public FuncResult<RecordStruct> executeQueryRecordFirst(SqlSelect[] selects, String from, String where,
                String orderby, Object... params) {
            FuncResult<RecordStruct> res = new FuncResult<RecordStruct>();

            // acquire
            Connection conn = this.acquireConnection();

            // if connection is bad/missing then just try again later
            if (conn == null) {
                res.errorTr(185, this.name);
                return res;
            }

            // prepare
            FuncResult<PreparedStatement> psres = this.prep(conn, selects, from, where, null, orderby, params);

            if (res.hasErrors())
                return res;

            PreparedStatement pstmt = psres.getResult();

            // execute
            FuncResult<ListStruct> res2 = this.callAndFormat(selects, pstmt);

            ListStruct lrs = res2.getResult();

            if (lrs.getSize() > 0)
                res.setResult(lrs.getItemAsRecord(0));
            else
                res.setResult(new RecordStruct()); // always return something - caller should do an empty check

            try {
                if (pstmt != null)
                    pstmt.close();
            } catch (SQLException x) {
            }

            // release
            this.releaseConnection(conn);

            return res;
        }

        // return a list of records where each row is a record in this collection
        public FuncResult<ListStruct> callAndFormat(SqlSelect[] select, PreparedStatement pstmt) {
            FuncResult<ListStruct> res = new FuncResult<ListStruct>();
            ListStruct list = new ListStruct();

            res.setResult(list);

            ResultSet rs = null;

            try {
                Hub.instance.getCountManager().countObjects("dcSqlQueryCount", pstmt);

                // MariaDB hint that this turns on streaming... review TODO
                //pstmt.setFetchSize(Integer.MIN_VALUE);

                rs = pstmt.executeQuery();

                ResultSetMetaData md = rs.getMetaData();
                int columns = md.getColumnCount();

                if (columns > select.length) {
                    res.error(1, "Mismatched column name list"); // TODO code tr
                    return res;
                }

                while (rs.next()) {
                    RecordStruct rec = new RecordStruct();

                    for (int i = 1; i <= columns; i++) {
                        String name = select[i - 1].name;

                        rec.setField(name, select[i - 1].format(rs.getObject(i)));
                    }

                    list.addItem(rec);
                }
            } catch (Exception x) {
                this.processException(x, res);

                Hub.instance.getCountManager().countObjects("dcSqlQueryFail", pstmt.toString());
            } finally {
                try {
                    if (rs != null)
                        rs.close();
                } catch (SQLException x) {
                }
            }

            return res;
        }

        // caller needs to close statememt
        // will not return open statement and errors
        public FuncResult<PreparedStatement> prepStatement(Connection conn, String sql, Object... params) {
            FuncResult<PreparedStatement> res = new FuncResult<PreparedStatement>();

            // if connection is bad/missing then just try again later
            if (conn == null) {
                res.errorTr(185, this.name);
                return res;
            }

            PreparedStatement pstmt = null;

            try {
                pstmt = conn.prepareStatement(sql);

                for (int i = 0; i < params.length; i++) {
                    Object param = params[i];

                    // null params are intentionally not supported - allows us to optionally add params to a complex query
                    // for NULL support see SqlNull enum
                    if (param == null)
                        continue;

                    if (param instanceof DateTime)
                        param = SqlManager.this.getDateAsString((DateTime) param);

                    if (param instanceof String) {
                        if (this.engine == SqlEngine.MariaDb || this.engine == SqlEngine.MySQL)
                            pstmt.setString(i + 1, (String) param);
                        else if (this.engine == SqlEngine.SqlServer)
                            pstmt.setNString(i + 1, (String) param);
                        else if (this.engine == SqlEngine.H2)
                            pstmt.setNString(i + 1, (String) param);

                        continue;
                    }

                    if (param instanceof BigDecimal) {
                        pstmt.setBigDecimal(i + 1, (BigDecimal) param);
                        continue;
                    }

                    if (param instanceof Double) {
                        pstmt.setDouble(i + 1, (double) param);
                        continue;
                    }

                    if (param instanceof Integer) {
                        pstmt.setInt(i + 1, (int) param);
                        continue;
                    }

                    if (param instanceof Long) {
                        pstmt.setLong(i + 1, (long) param);
                        continue;
                    }

                    if (param instanceof SqlNull) {
                        if (param == SqlNull.DateTime)
                            pstmt.setNull(i + 1, Types.DATE);
                        else if (param == SqlNull.VarChar) {
                            if (this.engine == SqlEngine.MariaDb || this.engine == SqlEngine.MySQL)
                                pstmt.setNull(i + 1, Types.VARCHAR);
                            else if (this.engine == SqlEngine.SqlServer)
                                pstmt.setNull(i + 1, Types.NVARCHAR);
                            else if (this.engine == SqlEngine.H2)
                                pstmt.setNull(i + 1, Types.NVARCHAR);
                        } else if (param == SqlNull.BigDecimal)
                            pstmt.setNull(i + 1, Types.DECIMAL);
                        else if (param == SqlNull.Double)
                            pstmt.setNull(i + 1, Types.FLOAT);
                        else if (param == SqlNull.Int)
                            pstmt.setNull(i + 1, Types.INTEGER);
                        else if (param == SqlNull.Long)
                            pstmt.setNull(i + 1, Types.BIGINT);
                        else if (param == SqlNull.Text)
                            pstmt.setNull(i + 1, Types.CLOB); // TODO test

                        continue;
                    }
                }

                res.setResult(pstmt);
            } catch (Exception x) {
                this.processException(x, res);

                Hub.instance.getCountManager().countObjects("dcSqlPrepFail", sql);

                try {
                    if (pstmt != null)
                        pstmt.close();
                } catch (SQLException x2) {
                }
            }

            return res;
        }

        // caller needs to close statememt
        public FuncResult<PreparedStatement> prepPage(Connection conn, SqlSelect[] select, String from,
                String where, String groupby, String orderby, int offset, int pagesize, Object... params) {
            // if connection is bad/missing then just try again later
            if (conn == null) {
                FuncResult<PreparedStatement> res = new FuncResult<PreparedStatement>();
                res.errorTr(185, this.name);
                return res;
            }

            String sql = "SELECT ";

            for (int i = 0; i < select.length; i++) {
                if (i > 0)
                    sql += ", ";

                sql += select[i].toSql(this);
            }

            sql += " FROM " + from;

            if (StringUtil.isNotEmpty(where))
                sql += " WHERE " + where;

            if (StringUtil.isNotEmpty(groupby))
                sql += " GROUP BY " + groupby;

            if (StringUtil.isEmpty(orderby)) {
                FuncResult<PreparedStatement> res = new FuncResult<PreparedStatement>();
                res.error(1, "Order By required with paging");
                return res;
            }

            if (this.engine == SqlEngine.MariaDb || this.engine == SqlEngine.MySQL) {
                sql = "SELECT * FROM ( " + sql + " ) AS recset ORDER BY " + orderby + " LIMIT " + offset + ","
                        + pagesize + ";";
            } else if (this.engine == SqlEngine.SqlServer) {
                sql = "WITH RecordPager AS ( " + "SELECT *, ROW_NUMBER() OVER (ORDER BY " + orderby
                        + ") AS RowNumber " + "FROM ( " + sql + " ) AS recset " + ") "
                        + "SELECT * FROM RecordPager WHERE RowNumber BETWEEN " + (offset + 1) + " AND "
                        + (offset + pagesize);
            } else if (this.engine == SqlEngine.H2) {
                // TODO
            }

            // TODO support for other dbms
            // http://en.wikipedia.org/wiki/Select_(SQL)
            // http://stackoverflow.com/questions/2771439/jdbc-pagination
            // http://stackoverflow.com/questions/1986998/resultset-to-pagination
            // http://stackoverflow.com/questions/971964/limit-10-20-in-sqlserver

            return this.prepStatement(conn, sql, params);
        }

        // caller needs to close statememt
        public FuncResult<PreparedStatement> prepLimit(Connection conn, SqlSelect[] select, String from,
                String where, String groupby, String orderby, int limit, boolean distinct, Object... params) {
            // if connection is bad/missing then just try again later
            if (conn == null) {
                FuncResult<PreparedStatement> res = new FuncResult<PreparedStatement>();
                res.errorTr(185, this.name);
                return res;
            }

            String sql = "SELECT ";

            if (distinct)
                sql += "DISTINCT ";

            for (int i = 0; i < select.length; i++) {
                if (i > 0)
                    sql += ", ";

                sql += select[i].toSql(this);
            }

            sql += " FROM " + from;

            if (StringUtil.isNotEmpty(where))
                sql += " WHERE " + where;

            if (StringUtil.isNotEmpty(groupby))
                sql += " GROUP BY " + groupby;

            if (StringUtil.isEmpty(orderby)) {
                FuncResult<PreparedStatement> res = new FuncResult<PreparedStatement>();
                res.error(1, "Order By required with limit");
                return res;
            }

            if (this.engine == SqlEngine.MariaDb || this.engine == SqlEngine.MySQL) {
                sql = "SELECT * FROM ( " + sql + " ) AS unset ORDER BY " + orderby + " LIMIT " + limit + ";";
            } else if (this.engine == SqlEngine.SqlServer) {
                sql = "SELECT TOP " + limit + " * FROM ( " + sql + " ) AS unset ORDER BY " + orderby;
            } else if (this.engine == SqlEngine.H2) {
                // TODO
            }

            return this.prepStatement(conn, sql, params);
        }

        // caller needs to close statememt
        public FuncResult<PreparedStatement> prep(Connection conn, SqlSelect[] select, String from, String where,
                String groupby, String orderby, Object... params) {
            // if connection is bad/missing then just try again later
            if (conn == null) {
                FuncResult<PreparedStatement> res = new FuncResult<PreparedStatement>();
                res.errorTr(185, this.name);
                return res;
            }

            String sql = "SELECT ";

            for (int i = 0; i < select.length; i++) {
                if (i > 0)
                    sql += ", ";

                sql += select[i].toSql(this);
            }

            sql += " FROM " + from;

            if (StringUtil.isNotEmpty(where))
                sql += " WHERE " + where;

            if (StringUtil.isNotEmpty(groupby))
                sql += " GROUP BY " + groupby;

            if (StringUtil.isNotEmpty(orderby))
                sql += " ORDER BY " + orderby;

            return this.prepStatement(conn, sql, params);
        }

        public FuncResult<Integer> executeUpdate(String sql, Object... params) {
            FuncResult<Integer> res = new FuncResult<Integer>();
            res.setResult(0);

            // acquire
            Connection conn = this.acquireConnection();

            // if connection is bad/missing then just try again later
            if (conn == null) {
                res.errorTr(185, this.name);
                return res;
            }

            // prep
            FuncResult<PreparedStatement> psres = this.prepStatement(conn, sql, params);

            if (res.hasErrors())
                return res;

            PreparedStatement pstmt = psres.getResult();

            try {
                Hub.instance.getCountManager().countObjects("dcSqlUpdateCount", pstmt);

                // execute
                int cnt = pstmt.executeUpdate();

                res.setResult(cnt);
            } catch (Exception x) {
                this.processException(x, res);

                Hub.instance.getCountManager().countObjects("dcSqlUpdateFail", sql);
            } finally {
                try {
                    if (pstmt != null)
                        pstmt.close();
                } catch (SQLException x) {
                }

                // release
                this.releaseConnection(conn);
            }

            return res;
        }

        public FuncResult<Integer> executeDelete(String sql, Object... params) {
            FuncResult<Integer> res = new FuncResult<Integer>();
            res.setResult(0);

            // acquire
            Connection conn = this.acquireConnection();

            // if connection is bad/missing then just try again later
            if (conn == null) {
                res.errorTr(185, this.name);
                return res;
            }

            // prep
            FuncResult<PreparedStatement> psres = this.prepStatement(conn, sql, params);

            if (res.hasErrors())
                return res;

            PreparedStatement pstmt = psres.getResult();

            try {
                Hub.instance.getCountManager().countObjects("dcSqlDeleteCount", pstmt);

                // execute
                int cnt = pstmt.executeUpdate();

                res.setResult(cnt);
            } catch (Exception x) {
                this.processException(x, res);

                Hub.instance.getCountManager().countObjects("dcSqlDeleteFail", sql);
            } finally {
                try {
                    if (pstmt != null)
                        pstmt.close();
                } catch (SQLException x) {
                }

                // release
                this.releaseConnection(conn);
            }

            return res;
        }

        public FuncResult<Long> executeInsertReturnId(String sql, Object... params) {
            FuncResult<Long> res = new FuncResult<Long>();
            res.setResult(0L);

            // acquire
            Connection conn = this.acquireConnection();

            // if connection is bad/missing then just try again later
            if (conn == null) {
                res.errorTr(185, this.name);
                return res;
            }

            // prep
            FuncResult<PreparedStatement> psres = this.prepStatement(conn, sql, params);

            if (res.hasErrors())
                return res;

            PreparedStatement pstmt = psres.getResult();

            try {
                Hub.instance.getCountManager().countObjects("dcSqlInsertCount", pstmt);

                // execute
                int cnt = pstmt.executeUpdate();

                pstmt.close();

                if (cnt == 1) {
                    pstmt = conn.prepareStatement(this.getLastIdSql());

                    ResultSet rs = pstmt.executeQuery();

                    if (rs.next())
                        res.setResult(rs.getLong("lid"));
                }
            } catch (Exception x) {
                this.processException(x, res);

                Hub.instance.getCountManager().countObjects("dcSqlInsertFail", sql);
            } finally {
                try {
                    if (pstmt != null)
                        pstmt.close();
                } catch (SQLException x) {
                }

                // release
                this.releaseConnection(conn);
            }

            return res;
        }

        // TODO look into "get generated keys"
        public FuncResult<Integer> executeInsert(String sql, Object... params) {
            FuncResult<Integer> res = new FuncResult<Integer>();
            res.setResult(0);

            // acquire
            Connection conn = this.acquireConnection();

            // if connection is bad/missing then just try again later
            if (conn == null) {
                res.errorTr(185, this.name);
                return res;
            }

            // prep
            FuncResult<PreparedStatement> psres = this.prepStatement(conn, sql, params);

            if (res.hasErrors())
                return res;

            PreparedStatement pstmt = psres.getResult();

            try {
                Hub.instance.getCountManager().countObjects("dcSqlInsertCount", pstmt);

                // execute
                int cnt = pstmt.executeUpdate();

                pstmt.close();

                res.setResult(cnt);
            } catch (Exception x) {
                this.processException(x, res);

                Hub.instance.getCountManager().countObjects("dcSqlInsertFail", sql);
            } finally {
                try {
                    if (pstmt != null)
                        pstmt.close();
                } catch (SQLException x) {
                }

                // release
                this.releaseConnection(conn);
            }

            return res;
        }
    }
}