public function query($sql, $params = array(), $types = array(), $returnRows = false)
    {
        // A select query, using slave db if configured
        if (!$returnRows && $this->slaveDb) {
            /** @var $slaveDb \Wei\Db */
            $slaveDb = $this->wei->get($this->slaveDb);
            return $slaveDb->query($sql, $params, $types, $returnRows);
        }

        $this->connect();
        $this->queries[] = $sql;
        if ($this->beforeQuery) {
            call_user_func_array($this->beforeQuery, array($sql, $params, $types, $this));
        }

        try {
            if (!$returnRows) {
                if ($params) {
                    $stmt = $this->pdo->prepare($sql);
                    if ($types) {
                        $this->bindParameter($stmt, $params, $types);
                        $stmt->execute();
                    } else {
                        $stmt->execute((array)$params);
                    }
                } else {
                    $stmt = $this->pdo->query($sql);
                }
                $result = $stmt;
            } else {
                if ($params) {
                    $stmt = $this->pdo->prepare($sql);
                    $stmt->execute($params);
                    $result = $stmt->rowCount();
                } else {
                    $result = $this->pdo->exec($sql);
                }
            }
        } catch (\PDOException $e) {
            // Builder exception message
            $msg = sprintf("An exception occurred while executing \"%s\" : \n\n %s", $sql, $e->getMessage());
            $params && $msg .= ', with parameters ' . json_encode($params);

            // Reset exception message
            $message = new \ReflectionProperty($e, 'message');
            $message->setAccessible(true);
            $message->setValue($e, $msg);
            throw $e;
        }

        if ($this->afterQuery) {
            call_user_func_array($this->afterQuery, array($sql, $params, $types, $this));
        }

        return $result;
    }