diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml
index a297100d4..5e534db55 100644
--- a/.github/workflows/test-unit.yml
+++ b/.github/workflows/test-unit.yml
@@ -111,9 +111,9 @@ jobs:
           ACCEPT_EULA: Y
           SA_PASSWORD: atk4_pass
       oracle:
-        image: ghcr.io/mvorisek/docker-oracle-xe-11g
+        image: gvenzl/oracle-xe:18
         env:
-          ORACLE_ALLOW_REMOTE: true
+          ORACLE_PASSWORD: atk4_pass
     steps:
       - name: Checkout
         uses: actions/checkout@v2
@@ -218,11 +218,10 @@ jobs:
         env:
           DB_DSN: "pdo_oci:dbname=oracle/xe"
           DB_USER: system
-          DB_PASSWORD: oracle
+          DB_PASSWORD: atk4_pass
           NLS_LANG: AMERICAN_AMERICA.AL32UTF8
         run: |
-          php -d opcache.enable_cli=1 vendor/bin/phpunit --exclude-group none $(if [ -n "$LOG_COVERAGE" ]; then echo --coverage-text; else echo --no-coverage; fi) -v \
-          || php -d opcache.enable_cli=1 vendor/bin/phpunit --exclude-group none $(if [ -n "$LOG_COVERAGE" ]; then echo --coverage-text; else echo --no-coverage; fi) -v
+          php -d opcache.enable_cli=1 vendor/bin/phpunit --exclude-group none $(if [ -n "$LOG_COVERAGE" ]; then echo --coverage-text; else echo --no-coverage; fi) -v
           if [ -n "$LOG_COVERAGE" ]; then mv coverage/phpunit.cov coverage/phpunit-oracle-pdo.cov; fi
 
       - name: "Run tests: Oracle - OCI8 (only for coverage or cron)"
@@ -230,11 +229,10 @@ jobs:
         env:
           DB_DSN: "oci8:dbname=oracle/xe"
           DB_USER: system
-          DB_PASSWORD: oracle
+          DB_PASSWORD: atk4_pass
           NLS_LANG: AMERICAN_AMERICA.AL32UTF8
         run: |
-          php -d opcache.enable_cli=1 vendor/bin/phpunit --exclude-group none $(if [ -n "$LOG_COVERAGE" ]; then echo --coverage-text; else echo --no-coverage; fi) -v \
-          || php -d opcache.enable_cli=1 vendor/bin/phpunit --exclude-group none $(if [ -n "$LOG_COVERAGE" ]; then echo --coverage-text; else echo --no-coverage; fi) -v
+          php -d opcache.enable_cli=1 vendor/bin/phpunit --exclude-group none $(if [ -n "$LOG_COVERAGE" ]; then echo --coverage-text; else echo --no-coverage; fi) -v
           if [ -n "$LOG_COVERAGE" ]; then mv coverage/phpunit.cov coverage/phpunit-oracle-oci8.cov; fi
 
       - name: Upload coverage logs 1/2 (only for latest Phpunit)
diff --git a/composer.json b/composer.json
index e6317ee7f..45dd4b641 100644
--- a/composer.json
+++ b/composer.json
@@ -42,7 +42,7 @@
     "require-release": {
         "php": ">=7.4 <8.2",
         "atk4/core": "~3.2.0",
-        "doctrine/dbal": "^3.ลก",
+        "doctrine/dbal": "^3.3",
         "mvorisek/atk4-hintable": "~1.7.1"
     },
     "require-dev": {
diff --git a/src/Persistence/Sql/Connection.php b/src/Persistence/Sql/Connection.php
index 157b8392f..68f7b6235 100644
--- a/src/Persistence/Sql/Connection.php
+++ b/src/Persistence/Sql/Connection.php
@@ -316,7 +316,7 @@ public function connection()
     public function execute(Expression $expr): DbalResult
     {
         if ($this->connection === null) {
-            throw new Exception('Queries cannot be executed through this connection');
+            throw new Exception('DBAL connection is not set');
         }
 
         return $expr->execute($this->connection);
diff --git a/src/Persistence/Sql/Oracle/Query.php b/src/Persistence/Sql/Oracle/Query.php
index 5c8ebf855..37b658edf 100644
--- a/src/Persistence/Sql/Oracle/Query.php
+++ b/src/Persistence/Sql/Oracle/Query.php
@@ -97,70 +97,24 @@ protected function _sub_render_condition(array $row): string
         return parent::_sub_render_condition($row);
     }
 
-    public function groupConcat($field, string $delimiter = ',')
-    {
-        return $this->expr('listagg({field}, []) within group (order by {field})', ['field' => $field, $delimiter]);
-    }
-
-    // {{{ for Oracle 11 and lower to support LIMIT with OFFSET
-
-    protected $template_select = '[with]select[option] [field] [from] [table][join][where][group][having][order]';
-    /** @var string */
-    protected $template_select_limit = 'select * from (select "__t".*, rownum "__dsql_rownum" [from] ([with]select[option] [field] [from] [table][join][where][group][having][order]) "__t") where "__dsql_rownum" > [limit_start][and_limit_end]';
-
-    public function limit($cnt, $shift = null)
-    {
-        $this->template_select = $this->template_select_limit;
-
-        return parent::limit($cnt, $shift);
-    }
-
-    public function _render_limit_start(): string
-    {
-        return (string) ($this->args['limit']['shift'] ?? 0);
-    }
-
-    public function _render_and_limit_end(): ?string
+    public function _render_limit(): ?string
     {
-        if (!$this->args['limit']['cnt']) {
-            return '';
+        if (!isset($this->args['limit'])) {
+            return null;
         }
 
-        return ' and "__dsql_rownum" <= '
-            . max((int) ($this->args['limit']['cnt'] + $this->args['limit']['shift']), (int) $this->args['limit']['cnt']);
-    }
-
-    public function getRowsIterator(): \Traversable
-    {
-        foreach (parent::getRowsIterator() as $row) {
-            unset($row['__dsql_rownum']);
-
-            yield $row;
-        }
-    }
-
-    public function getRows(): array
-    {
-        return array_map(function ($row) {
-            unset($row['__dsql_rownum']);
+        $cnt = (int) $this->args['limit']['cnt'];
+        $shift = (int) $this->args['limit']['shift'];
 
-            return $row;
-        }, parent::getRows());
+        return ($shift ? ' offset ' . $shift . ' rows' : '')
+            . ($cnt ? ' fetch next ' . $cnt . ' rows only' : '');
     }
 
-    public function getRow(): ?array
+    public function groupConcat($field, string $delimiter = ',')
     {
-        $row = parent::getRow();
-
-        if ($row !== null) {
-            unset($row['__dsql_rownum']);
-        }
-
-        return $row;
+        return $this->expr('listagg({field}, []) within group (order by {field})', ['field' => $field, $delimiter]);
     }
 
-    /// }}}
-
     public function exists()
     {
         return $this->dsql()->mode('select')->field(