phpsql-server-2008zend-frameworkzend-db-select

Zend DB fails emulating limit and offset for MS SQL


I'm using Zend Framework 1.12 to access a MSSQL 2008 server. I'm using FreeTDS as the database driver.

I'm using Zend_Db to generate the following query.

 $obj_sel = $obj_db
          -> select ()
          -> from (array ('ps' => 'ProductStock'), array ('PLU', 'stock' => 'SUM(ps.stock)'))
          -> join (array ('pc' => 'ProductCatalogue'), 'ps.PLU = pc.PLU', NULL)
          -> join (array ('gl' => 'Gemini_Location'), 'ps.Location = gl.LocationID', array ('LocationID'))
          -> where ('ps.status = 1')
          -> where ('ps.PLU IS NOT NULL');
           > where ('pc.rootPLU >= ?', $this -> int_start_rootplu);
          -> group ('ps.PLU')
          -> group ('gl.LocationID')
          -> order (array ('ps.PLU', 'gl.LocationID'));

If I run and execute this, I get a query back that seems to work fine and be correct.

SELECT "ps"."PLU", SUM(ps.stock) AS "stock", "gl"."LocationID" FROM "ProductStock" AS "ps"
 INNER JOIN "ProductCatalogue" AS "pc" ON ps.PLU = pc.PLU
 INNER JOIN "Gemini_Location" AS "gl" ON ps.Location = gl.LocationID WHERE (ps.status = 1) AND (ps.PLU IS NOT NULL) AND (pc.rootPLU >= 93838) GROUP BY "ps"."PLU",
    "gl"."LocationID" ORDER BY "ps"."PLU" ASC, "gl"."LocationID" ASC

But when I try to add a limit or an offset to the query like so:

 $obj_sel = $obj_db
          -> select ()
          -> from (array ('ps' => 'ProductStock'), array ('PLU', 'stock' => 'SUM(ps.stock)'))
          -> join (array ('pc' => 'ProductCatalogue'), 'ps.PLU = pc.PLU', NULL)
          -> join (array ('gl' => 'Gemini_Location'), 'ps.Location = gl.LocationID', array ('LocationID'))
          -> where ('ps.status = 1')
          -> where ('ps.PLU IS NOT NULL');
           > where ('pc.rootPLU >= ?', $this -> int_start_rootplu);
          -> group ('ps.PLU')
          -> group ('gl.LocationID')
          -> order (array ('ps.PLU', 'gl.LocationID'))
          -> limit (1000,2000);

I get the following query which SQL server refuses to execute.

SELECT * FROM (SELECT TOP 1000 * FROM (SELECT TOP 3000 "ps"."PLU", SUM(ps.stock) AS "stock", "gl"."LocationID" FROM "ProductStock" AS "ps"
 INNER JOIN "ProductCatalogue" AS "pc" ON ps.PLU = pc.PLU
 INNER JOIN "Gemini_Location" AS "gl" ON ps.Location = gl.LocationID WHERE (ps.status = 1) AND (ps.PLU IS NOT NULL) AND (pc.rootPLU >= 93838) GROUP BY "ps"."PLU",
    "gl"."LocationID" ORDER BY "ps"."PLU" ASC, "gl"."LocationID" ASC) AS inner_tbl ORDER BY "ps"."PLU" , "gl"."LocationID" DESC) AS outer_tbl ORDER BY "ps"."PLU" , "gl"."LocationID" asc

I get the following error:

SQLSTATE[HY000]: General error: 4104 General SQL Server error: Check messages from the SQL Server [4104] (severity 16) [(null)]

I'm not as familiar with MSSQL as I am with MySQL or Postgres so I'm going to have to assume that the TOP technique is right. However, it's pretty clear that whatever SQL Zend DB is generating here isn't right.

Is this a known issue in Zend DB? If so, how can I work around it?


Solution

  • ZF 1 doesn't implement pagination on SQL Server correctly although ZF 2 does.

    I've solved the problem by subclassing Zend_Db_Adapter_Pdo_Mssql:

    class My_Zend_Db_Adapter_Pdo_Mssql extends Zend_Db_Adapter_Pdo_Mssql
    {
        /**
         * @see Zend_Db_Adapter_Pdo_Mssql::limit()
         */
        public function limit($sql, $count, $offset = 0)
        {
            $count = intval($count);
            if ($count <= 0) {
                /** @see Zend_Db_Adapter_Exception */
                require_once 'Zend/Db/Adapter/Exception.php';
                throw new Zend_Db_Adapter_Exception('count parameter invalid: ' . $count);
            }
    
            $offset = intval($offset);
            if ($offset < 0) {
                /** @see Zend_Db_Adapter_Exception */
                require_once 'Zend/Db/Adapter/Exception.php';
                throw new Zend_Db_Adapter_Exception('offset parameter invalid: ' . $count);
            }
    
            if (0 == $offset) {
                $sql = preg_replace('/^SELECT\s+(DISTINCT\s)?/i', 'SELECT $1TOP ' . ($count+$offset) . ' ', $sql);
                return $sql;
            }
    
            $selectStart = stripos($sql, 'SELECT');
            $fromStart = stripos($sql, 'FROM');
    
            $orderby = stristr($sql, 'ORDER BY');
    
            if ($orderby === false) {
                $orderby = 'ORDER BY (SELECT 1)';
            }
    
            $sql = rtrim(str_replace($orderby, '', $sql));
    
            $selectParams = trim(substr($sql, $selectStart + 6, $fromStart - $selectStart - 6));
            $selectParams .= ', ROW_NUMBER() OVER (' . $orderby . ') AS [ZEND_ROW_NUMBER]';
    
            $sql = substr($sql, 0, $selectStart + 6) . ' ' . $selectParams . ' ' . substr($sql, $fromStart);
    
            $outerSql = 'SELECT * FROM (' . $sql . ') AS [ZEND_OFFSET_EMULATION]'
                      . ' WHERE [ZEND_OFFSET_EMULATION].[ZEND_ROW_NUMBER] BETWEEN '
                      . ($offset + 1) . ' AND '
                      . ($offset + $count)
                      . ' ORDER BY [ZEND_ROW_NUMBER] ASC';
    
            return $outerSql;
        }
    }