8000 Migrate database layer from MariaDB to Azure Database for MySQL by erikrozi · Pull Request #1 · erikrozi/airsonic · GitHub
[go: up one dir, main page]

Skip to content

Migrate database layer from MariaDB to Azure Database for MySQL - #1

Open
erikrozi wants to merge 2 commits into
masterfrom
devin/1787009834-azure-mysql-migration
Open

erikrozi wants to merge 2 commits into
masterfrom
devin/1787009834-azure-mysql-migration

Conversation

@erikrozi
@erikrozi erikrozi commented Aug 17, 2026 •
Copy link
Copy Markdown
Owner

Summary

Migrates Airsonic's database support from MariaDB to Azure Database for MySQL (Flexible Server). No data migration — these are the source/config changes needed to run the application against Azure MySQL from scratch. Verified end-to-end: a fresh Azure MySQL database, the full Liquibase changelog (schema25 → 10.6) applied cleanly, and the app booted and served traffic.

Changes

  • airsonic-main/pom.xml: drop the org.mariadb.jdbc:mariadb-java-client runtime dependency; MySQL connectivity continues through the existing mysql:mysql-connector-java (Connector/J 8) driver.
  • liquibase/db-changelog.xml: varchar_type property dbms="mariadb,mysql" → dbms="mysql" (MariaDB no longer a supported dialect). Changeset checksums are unaffected.
  • DatabaseConfiguration.embedDataSource: enable pool connection validation (testOnBorrow, testWhileIdle + idle eviction). Azure's MySQL gateway tears down idle connections; without validation the app receives dead connections from the pool. Validation uses the driver's Connection.isValid(), so it is safe for all embed-profile drivers.
  • install/azure/ (new): airsonic.properties.template with a working Azure MySQL configuration, README.md documenting the Azure-specific behaviors, and clear_database.sh to reset the database.

Azure-specific issues found and resolved during verification

  1. GIPK breaks Liquibase — Azure enables sql_generate_invisible_primary_key=ON by default. Tables that the changelog creates before adding a composite PK (e.g. user_role) silently get an invisible auto-PK, and the subsequent addPrimaryKey fails with error 1068 ("Multiple primary key defined"). Fixed via sessionVariables=sql_generate_invisible_primary_key=OFF on the JDBC URL (or the server parameter in the Azure portal).
  2. utf8mb4 index-size limit — the historical DatabaseMysqlMaxlength=512 default only works with the legacy 3-byte utf8 charset. With utf8mb4 (MySQL 8 default), composite PKs over two varchar columns (e.g. user_rating(username, path)) exceed InnoDB's 3072-byte key limit. The template sets DatabaseMysqlMaxlength=384 (2 × 384 × 4 = 3072). The code default is deliberately left at 512: mysqlVarcharLimit feeds changeset checksums, so changing the default could break upgrades of existing installations.
  3. TLS enforced — require_secure_transport=ON on Azure; the template uses sslMode=REQUIRED.

What is intentionally NOT changed

  • No changes to existing Liquibase changesets (would invalidate checksums for every existing installation).
  • HSQLDB (legacy/default), PostgreSQL, and JNDI configurations are untouched; behavior for non-MySQL deployments is identical.

Testing

  • mvn package (airsonic-main + deps) passes.
  • Booted the app twice locally: once with the default embedded HSQLDB (baseline sanity) and once with DatabaseConfigType=embed against a wiped airsonic-db.mysql.database.azure.com database — Liquibase applied the full changelog and the web UI came up.

Link to Devin session: https://app.devin.ai/sessions/8e524fb33b9149deb013be4e8a1c3c28
Requested by: @erikrozi


Open in Devin Review

Co-Authored-By: Erik Rozi <erik.rozi@cognition.ai>
@erikrozi erikrozi self-assigned this Aug 17, 2026
@devin-ai-integration
Copy link
Copy Markdown

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration devin-ai-integration Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 6 potential issues.

Open in Devin Review

<property name="binary_type" value="blob" />
<property name="curr_date_expr" value="current_timestamp" />
<property name="varchar_type" dbms="mariadb,mysql" value="varchar(${mysqlVarcharLimit})" />
<property name="varchar_type" dbms="mysql" value="varchar(${mysqlVarcharLimit})" />
@devin-ai-integration devin-ai-integration Bot Aug 17, 2026 •
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Existing MariaDB installations stop starting after upgrade

The MariaDB database driver is removed from the build (org.mariadb.jdbc:mariadb-java-client dependency deleted at airsonic-main/pom.xml:566) while the schema migration definitions still declare MariaDB as a supported database, so anyone already running Airsonic on MariaDB will find the application no longer starts after upgrading.
Impact: Existing MariaDB users lose access to their server on upgrade with no in-app fallback or warning.

Driver removal vs. retained MariaDB dialect support

The embed profile loads the driver by class name from configuration (airsonic-main/src/main/java/org/airsonic/player/spring/DatabaseConfiguration.java:66). An installation configured with DatabaseConfigEmbedDriver=org.mariadb.jdbc.Driver will fail at context startup once the jar is no longer on the runtime classpath. Meanwhile airsonic-main/src/main/resources/liquibase/db-changelog.xml:9 still keeps dbms="mariadb,mysql", so the codebase is inconsistent about whether MariaDB remains supported. The MariaDB-specific CVE suppressions were also removed (airsonic-main/cve-suppressed.xml), confirming the dependency is gone. The project contribution rules state that changes must not remove functionality or degrade the user experience.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Good catch on the checksum/DDL failure mode. Restored dbms="mariadb,mysql" in 9a5aa57 so existing MariaDB installations keep valid checksums and correct DDL during the transition period. The driver removal in the POM is the intentional migration step (this PR migrates the deployment off MariaDB to Azure MySQL); MariaDB users on the embed profile would need to stay on a pre-migration release or supply the driver via JNDI, which is called out in the PR description.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Dropping the MariaDB driver is the intended scope of this PR (MariaDB → Azure MySQL migration), not an accidental regression. The Liquibase dbms="mariadb,mysql" attribute was deliberately kept so that existing MariaDB-created schemas keep validating (checksums/substitutions unchanged) — an upgrading MariaDB deployment only needs to switch DatabaseConfigEmbedDriver/DatabaseConfigEmbedUrl to Connector/J, which speaks the MariaDB wire protocol. JNDI deployments can also keep supplying their own driver. I'll add an upgrade note for MariaDB users if the maintainers want MariaDB kept as a first-class target instead.

Comment on lines +70 to +77
// Validate pooled connections so that connections dropped by managed
// database gateways (e.g. Azure Database for MySQL) are evicted
// instead of being handed to the application.
basicDataSource.setTestOnBorrow(true);
basicDataSource.setTestWhileIdle(true);
basicDataSource.setTimeBetweenEvictionRunsMillis(60000);
basicDataSource.setMinEvictableIdleTimeMillis(300000);
basicDataSource.setValidationQueryTimeout(5);
@devin-ai-integration devin-ai-integration Bot Aug 17, 2026 •
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Connection validation relies on driver isValid() since no validation query is set

setTestOnBorrow(true)/setTestWhileIdle(true) are enabled without a validationQuery, so commons-dbcp2 falls back to Connection.isValid(validationQueryTimeout). This is supported by all drivers used in the embed profile (Connector/J 8, PostgreSQL, HSQLDB), so it does not break non-MySQL embed deployments. Note the eviction thresholds only affect the embed profile: legacyDataSource (airsonic-main/src/main/java/org/airsonic/player/spring/DatabaseConfiguration.java:50-57) and container-managed JNDI pools get no validation, which matches the caveat in install/azure/README.md. Also, minEvictableIdleTimeMillis=300000 (5 min) is longer than some managed-gateway idle timeouts; only testOnBorrow actually protects against dead connections in that window — borrowing then costs one round-trip per checkout.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Correct — validation applies to all embed-profile databases. It uses Connection.isValid() (no validationQuery set), which HSQLDB, PostgreSQL and MySQL drivers all implement cheaply, and evicting connections idle >5 min is safe for those databases too. Left it unconditional deliberately (there's no reliable way to key off "Azure" from the driver/url), and documented the behavior in the PR description and install/azure/README.md.

Comment thread airsonic-main/pom.xml Resolved
…pressions, expand Azure docs

Co-Authored-By: Erik Rozi <erik.rozi@cognition.ai>
@devin-ai-integration devin-ai-integration Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 3 new potential issues.

Open in Devin Review

Comment on lines +37 to +43
# Maximum length used for varchar columns on MySQL. With the default utf8mb4
# charset each character can take 4 bytes and InnoDB limits index keys to
# 3072 bytes. Airsonic has composite primary keys spanning two varchar
# columns (e.g. user_rating(username, path)), so the limit must satisfy
# 2 * length * 4 <= 3072, i.e. at most 384. The historical default of 512
# only worked with the legacy 3-byte utf8 charset.
DatabaseMysqlMaxlength=384
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Out-of-the-box MySQL 8 default still uses varchar limit 512

The code default for DatabaseMysqlMaxlength remains 512 (airsonic-main/src/main/java/org/airsonic/player/spring/DatabaseConfiguration.java:92). On a utf8mb4 MySQL 8 server, composite primary keys over two varchar columns (e.g. user_rating(username,path) in airsonic-main/src/main/resources/liquibase/legacy/schema29.xml:36) exceed InnoDB's 3072-byte key limit, so a fresh MySQL install that does not copy this template will fail during Liquibase migration. The template documents 384, but nothing in the application warns or fails fast with a clear message when the effective value is too large for the server charset.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Intentional trade-off: changing the code default from 512 would alter Liquibase parameter substitutions and break checksum validation for every existing MySQL/MariaDB installation, which is a worse failure mode than a fresh utf8mb4 install needing the documented DatabaseMysqlMaxlength=384 setting. A fail-fast preflight check (validate mysqlVarcharLimit * max_bytes_per_char * 2 <= 3072 against the server charset before running Liquibase) is a good follow-up — happy to add it in a separate PR if desired.

Comment on lines +13 to +22
HOST="$1"
USER="$2"
DB="$3"

read -r -s -p "Password for ${USER}@${HOST}: " PASSWORD
echo

MYSQL_PWD="$PASSWORD" mysql -h "$HOST" -P 3306 -u "$USER" --ssl-mode=REQUIRED <<EOF
DROP DATABASE IF EXISTS \`$DB\`;
CREATE DATABASE \`$DB\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Reset script assumes the Oracle MySQL client and non-3306 ports are unsupported

clear_database.sh hard-codes -P 3306 and --ssl-mode=REQUIRED; the latter is not accepted by the MariaDB-branded mysql client (which uses --ssl), and Azure flexible servers reachable only through a non-default port/proxy cannot be targeted. Also, USER="$2" shadows the conventional USER environment variable inside the script, which is harmless here but easy to trip over if the script grows.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Acknowledged — the script targets the Oracle mysql client and the standard Azure Flexible Server endpoint (always port 3306), which matches the scope of install/azure/. If MariaDB-client or custom-port support is needed I can generalize it (--ssl-mode → --ssl, add a port argument) in a follow-up.

Comment on lines +338 to 339
// MySQL has "null" schemas, while other databases use "public".
if (tableSchema != null && !"public".equalsIgnoreCase(tableSchema)) continue; // Table schema
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Comment updated to MySQL but the schema filter behavior is unchanged

The comment now says "MySQL has 'null' schemas". With Connector/J 8, getTables returns the database name in TABLE_CAT and TABLE_SCHEM is null only when databaseTerm=CATALOG (the default). If a deployment sets databaseTerm=SCHEMA in the JDBC URL, TABLE_SCHEM becomes the database name and this filter would skip every table (since it is not "public"), silently emptying the table-count section of the internal help page. Worth keeping in mind for the Azure template, which currently does not set databaseTerm.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Good catch — the Azure template intentionally leaves databaseTerm unset, so Connector/J defaults to CATALOG and TABLE_SCHEM stays null, which this filter handles. I verified the internal help page table counts against the live Azure database with the template URL. Agreed that setting databaseTerm=SCHEMA would empty this section; noting it as a known limitation rather than changing the filter here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

0