Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/development-environment/setup.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
* Add a ``local.php`` file in ``app/config``
* Edit the ``local.php`` file using the following template (Mautic adapts to new local settings):

**MySQL/MariaDB configuration:**

.. code-block:: php

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostgreSQL configuration example from PR #15926. Uses pdo_pgsql driver on port 5432, requires PostgreSQL 16+.

Source: mautic/mautic#15926

<?php
Expand All @@ -35,6 +37,27 @@
'db_backup_prefix' => 'bak_',
);

**PostgreSQL configuration:**

.. code-block:: php

<?php
$parameters = array(
'db_driver' => 'pdo_pgsql',
'db_host' => 'localhost',
'db_table_prefix' => null,
'db_port' => '5432',
'db_name' => 'mautic',
'db_user' => 'postgres',
'db_password' => 'postgres_password',
'db_backup_tables' => true,
'db_backup_prefix' => 'bak_',
);

.. note::

PostgreSQL 16 or later is required for PostgreSQL support. Ensure the ``pdo_pgsql`` PHP extension is installed.

Check warning on line 59 in docs/development-environment/setup.rst

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 [Google.Passive] In general, use active voice instead of passive voice ('is installed'). Raw Output: {"message": "[Google.Passive] In general, use active voice instead of passive voice ('is installed').", "location": {"path": "docs/development-environment/setup.rst", "range": {"start": {"line": 59, "column": 103}}}, "severity": "INFO"}

Check warning on line 59 in docs/development-environment/setup.rst

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 [Google.Passive] In general, use active voice instead of passive voice ('is required'). Raw Output: {"message": "[Google.Passive] In general, use active voice instead of passive voice ('is required').", "location": {"path": "docs/development-environment/setup.rst", "range": {"start": {"line": 59, "column": 28}}}, "severity": "INFO"}

* Run the following command and add your own options:

.. code-block:: bash
Expand Down
143 changes: 142 additions & 1 deletion docs/plugins/database.rst
Original file line number Diff line number Diff line change
Expand Up @@ -363,4 +363,145 @@
:param array $columns: Array of columns to included in the index.

:return: ``INDEX {tableName} ($columns...)`` statement
:returntype: string
:returntype: string

Database compatibility
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Database compatibility section based on PR #15926. Key implementation files: CommonRepository.php, GeneratedColumnProvider.php.

Source: mautic/mautic#15926

**********************

Mautic 7.x supports MySQL, MariaDB, and PostgreSQL. Follow these patterns to ensure your queries work across all platforms.

Supported databases
===================

.. list-table::
:widths: 30 30 40
:header-rows: 1

* - Database
- Minimum version
- PHP extension
* - MySQL
- 8.0
- ``pdo_mysql``
* - MariaDB
- 10.6
- ``pdo_mysql``
* - PostgreSQL
- 16
- ``pdo_pgsql``

.. vale off

Case-insensitive string matching
================================

.. vale on

MySQL and MariaDB use case-insensitive ``LIKE`` comparisons by default. PostgreSQL's ``LIKE`` is case-sensitive. Mautic provides helper methods in ``CommonRepository`` to handle this.

**Using the helper methods:**

.. code-block:: php

// In a repository extending CommonRepository
$qb = $this->getEntityManager()->getConnection()->createQueryBuilder();

// Case-insensitive LIKE - uses ILIKE on PostgreSQL, LIKE on MySQL/MariaDB
$qb->andWhere(
$this->getILikeExpression($qb, 'l.email', ':search')
);

// Or wrap the column with LOWER() for case-insensitive comparison
$qb->andWhere(
$this->getLowerLikeExpression($qb, 'l.firstname', ':search')
);

**Available methods in CommonRepository:**

- ``getILikeExpression(QueryBuilder $qb, string $column, string $parameter)``: Returns platform-appropriate case-insensitive ``LIKE`` expression

Check warning on line 421 in docs/plugins/database.rst

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 [Google.Colons] ': R' should be in lowercase. Raw Output: {"message": "[Google.Colons] ': R' should be in lowercase.", "location": {"path": "docs/plugins/database.rst", "range": {"start": {"line": 421, "column": 78}}}, "severity": "WARNING"}
- ``getLowerLikeExpression(QueryBuilder $qb, string $column, string $parameter)``: Wraps the column with ``LOWER()`` for case-insensitive comparison

Check warning on line 422 in docs/plugins/database.rst

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 [Google.Colons] ': W' should be in lowercase. Raw Output: {"message": "[Google.Colons] ': W' should be in lowercase.", "location": {"path": "docs/plugins/database.rst", "range": {"start": {"line": 422, "column": 82}}}, "severity": "WARNING"}
- ``isPostgreSql()``: Returns ``TRUE`` if connected to a PostgreSQL database

.. vale off

Column and alias quoting
========================

.. vale on

PostgreSQL lowercases unquoted identifiers automatically. This causes issues with Mautic's ``camelCase`` column aliases. Always quote identifiers in raw SQL queries that use mixed-case names.

**Use quoted identifiers for camelCase aliases:**

Check failure on line 434 in docs/plugins/database.rst

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 [Vale.Spelling] Did you really mean 'camelCase'? Raw Output: {"message": "[Vale.Spelling] Did you really mean 'camelCase'?", "location": {"path": "docs/plugins/database.rst", "range": {"start": {"line": 434, "column": 30}}}, "severity": "ERROR"}

.. code-block:: php

// Correct - aliases are quoted
$qb->select('l.id, l.first_name AS "firstName", l.date_added AS "dateAdded"');

// Incorrect - PostgreSQL converts to lowercase
$qb->select('l.id, l.first_name AS firstName');

Doctrine's QueryBuilder handles quoting automatically when you use proper field mappings. You only need manual quoting for raw SQL or custom column aliases.

.. vale off

GROUP BY requirements
=====================

.. vale on

PostgreSQL enforces strict ``GROUP BY`` rules, and MySQL 8+ does the same when ``ONLY_FULL_GROUP_BY`` SQL mode is enabled - the default in strict mode. Every column in the ``SELECT`` clause must appear in the ``GROUP BY`` clause or use an aggregate function.

Writing compliant ``GROUP BY`` clauses ensures compatibility across all supported databases and SQL modes.

Mautic's Report Builder corrects ``GROUP BY`` clauses automatically. If you're building custom Reports or queries with aggregates, include all non-aggregated columns in the ``GROUP BY`` clause.

**Correct pattern:**

.. code-block:: php

$qb->select('l.id, l.email, COUNT(e.id) as emailCount')
->from('leads', 'l')
->leftJoin('l', 'emails', 'e', 'e.lead_id = l.id')
->groupBy('l.id, l.email'); // All non-aggregate columns included

**Incorrect pattern:**

.. code-block:: php

// This fails on PostgreSQL - l.email not in GROUP BY
$qb->select('l.id, l.email, COUNT(e.id) as emailCount')
->from('leads', 'l')
->leftJoin('l', 'emails', 'e', 'e.lead_id = l.id')
->groupBy('l.id');

Detecting the database platform
===============================

When you need platform-specific query logic, detect the database type using the connection's platform:

.. code-block:: php

use Doctrine\DBAL\Platforms\PostgreSQLPlatform;

$connection = $this->getEntityManager()->getConnection();
$platform = $connection->getDatabasePlatform();

if ($platform instanceof PostgreSQLPlatform) {
// PostgreSQL-specific logic
} else {
// MySQL/MariaDB logic
}

Repositories extending ``CommonRepository`` can use the ``isPostgreSql()`` helper method.

Best practices
==============

1. **Use Doctrine's ORM and QueryBuilder** - Doctrine abstracts most database differences. Avoid raw SQL when possible.

2. **Test on multiple databases** - Mautic's CI tests against MySQL, MariaDB, and PostgreSQL. Run your Plugin tests against all platforms before release.

3. **Quote mixed-case aliases** - When using custom column aliases with ``camelCase`` names in raw SQL, always quote them.

4. **Use repository helper methods** - ``CommonRepository`` provides cross-platform helpers for common operations like case-insensitive searches.
58 changes: 58 additions & 0 deletions docs/testing/e2e_test_suite.rst
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,64 @@

``Password: secret``

Testing on multiple databases
*****************************

Mautic supports MySQL, MariaDB, and PostgreSQL. The CI pipeline runs tests against all supported databases. Make sure your code works across all databases when developing features or writing tests.

Supported database versions
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi-database testing section based on PR #15926. CI tests run on PostgreSQL 16/18, MariaDB 10.11/11.4, MySQL 8.4/9.4.

Source: mautic/mautic#15926

===========================

.. list-table::
:header-rows: 1

* - Database
- Tested versions
* - PostgreSQL
- 16, 18
* - MariaDB
- 10.11, 11.4
* - MySQL
- 8.4, 9.4

Configuring your test environment for PostgreSQL
================================================

To run tests locally against PostgreSQL:

1. Update your ``.env.test.local`` with PostgreSQL credentials:

.. code-block:: bash

# .env.test.local
DB_DRIVER=pdo_pgsql
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWD=your_password
DB_NAME=mautic_test

2. Ensure the ``pdo_pgsql`` PHP extension is installed and enabled.

Check warning on line 273 in docs/testing/e2e_test_suite.rst

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 [Google.Passive] In general, use active voice instead of passive voice ('is installed'). Raw Output: {"message": "[Google.Passive] In general, use active voice instead of passive voice ('is installed').", "location": {"path": "docs/testing/e2e_test_suite.rst", "range": {"start": {"line": 273, "column": 43}}}, "severity": "INFO"}

3. Run the test suite as normal:

.. code-block:: bash

bin/phpunit

Database-specific test considerations
=====================================

Keep these database differences in mind when writing tests:

- **Case sensitivity**: PostgreSQL ``LIKE`` is case-sensitive; MySQL/MariaDB ``LIKE`` isn't. Use Mautic's helper methods for case-insensitive matching.

Check warning on line 286 in docs/testing/e2e_test_suite.rst

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 [Google.Colons] ': P' should be in lowercase. Raw Output: {"message": "[Google.Colons] ': P' should be in lowercase.", "location": {"path": "docs/testing/e2e_test_suite.rst", "range": {"start": {"line": 286, "column": 23}}}, "severity": "WARNING"}

- **GROUP BY strictness**: PostgreSQL and MySQL 8+ strict mode require all non-aggregated ``SELECT`` columns in ``GROUP BY``.

Check warning on line 288 in docs/testing/e2e_test_suite.rst

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 [Google.Colons] ': P' should be in lowercase. Raw Output: {"message": "[Google.Colons] ': P' should be in lowercase.", "location": {"path": "docs/testing/e2e_test_suite.rst", "range": {"start": {"line": 288, "column": 26}}}, "severity": "WARNING"}

Check warning on line 288 in docs/testing/e2e_test_suite.rst

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 [Google.Acronyms] Spell out 'GROUP', if it's unfamiliar to the audience. Raw Output: {"message": "[Google.Acronyms] Spell out 'GROUP', if it's unfamiliar to the audience.", "location": {"path": "docs/testing/e2e_test_suite.rst", "range": {"start": {"line": 288, "column": 5}}}, "severity": "INFO"}

- **Identifier quoting**: PostgreSQL lowercases unquoted identifiers. Quote ``camelCase`` aliases in raw SQL.

Check warning on line 290 in docs/testing/e2e_test_suite.rst

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 [Google.Colons] ': P' should be in lowercase. Raw Output: {"message": "[Google.Colons] ': P' should be in lowercase.", "location": {"path": "docs/testing/e2e_test_suite.rst", "range": {"start": {"line": 290, "column": 25}}}, "severity": "WARNING"}

For detailed guidance on writing database-agnostic code, refer to the :doc:`/plugins/database` documentation.

Check failure on line 292 in docs/testing/e2e_test_suite.rst

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 [Vale.Terms] Use 'Plugins' instead of 'plugins'. Raw Output: {"message": "[Vale.Terms] Use 'Plugins' instead of 'plugins'.", "location": {"path": "docs/testing/e2e_test_suite.rst", "range": {"start": {"line": 292, "column": 78}}}, "severity": "ERROR"}

Check warning on line 292 in docs/testing/e2e_test_suite.rst

View workflow job for this annotation

GitHub Actions / prose

[vale] reported by reviewdog 🐶 [Mautic.FeatureList] Is this referring to a Mautic feature? If so, use 'Plugins' instead of 'plugins'. Raw Output: {"message": "[Mautic.FeatureList] Is this referring to a Mautic feature? If so, use 'Plugins' instead of 'plugins'.", "location": {"path": "docs/testing/e2e_test_suite.rst", "range": {"start": {"line": 292, "column": 78}}}, "severity": "INFO"}

Contributing
************

Expand Down
Loading