Skip to content

Commit c7ed9a7

Browse files
authored
feat: MariaDB/MySQL support (#185)
* fix: make sure the default value of translatable attributes work in MySQL * chore: another way of registering the LocalesType * fix: use Arel.sql instead (in order to support Rails 7.0) * chore(ci): update the test matrix (databases / rails versions) * chore(ci): MySQL/MariaDB doesnt work well with Rails 7.0
1 parent 22b8e53 commit c7ed9a7

35 files changed

+737
-49
lines changed

.github/workflows/verify.yml

Lines changed: 95 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,37 @@ jobs:
1717
# needed because the postgres container does not provide a healthcheck
1818
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
1919

20+
mysql:
21+
image: mysql:8.0
22+
env:
23+
MYSQL_DATABASE: "maglev_engine_test"
24+
MYSQL_ROOT_PASSWORD: "password"
25+
ports:
26+
- 3306:3306
27+
options: >-
28+
--health-cmd "mysqladmin ping"
29+
--health-interval 10s
30+
--health-timeout 5s
31+
--health-retries 5
32+
33+
mariadb:
34+
image: mariadb:11.8
35+
env:
36+
MARIADB_DATABASE: "maglev_engine_test"
37+
MARIADB_ROOT_PASSWORD: "password"
38+
ports:
39+
- 3307:3306
40+
options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=10s --health-timeout=5s --health-retries=3
41+
2042
strategy:
2143
matrix:
22-
node: [20, 23]
23-
gemfile: ["Gemfile.rails_7_0", "Gemfile.rails_7_2", "Gemfile"]
44+
gemfile: ["Gemfile", "Gemfile.rails_7_0", "Gemfile.rails_7_2"]
45+
database: [postgres, sqlite, mysql, mariadb]
46+
exclude:
47+
- gemfile: Gemfile.rails_7_0
48+
database: mysql
49+
- gemfile: Gemfile.rails_7_0
50+
database: mariadb
2451

2552
steps:
2653
- name: Checkout code
@@ -41,17 +68,22 @@ jobs:
4168
run: |
4269
corepack enable
4370
44-
- name: Use Node.js ${{ matrix.node }}
71+
- name: Use Node.js v23
4572
uses: actions/setup-node@v4
4673
with:
47-
node-version: ${{ matrix.node }}
74+
node-version: 23
4875
cache: yarn
4976

5077
- name: Install packages
5178
run: |
5279
yarn install
5380
54-
- name: Setup test database
81+
- name: Run Javascript tests
82+
run: yarn test
83+
84+
# === Postgresql 🐘 ===
85+
- name: Setup test database (Postgresql) 🐘
86+
if: ${{ matrix.database == 'postgres' }}
5587
env:
5688
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
5789
RAILS_ENV: test
@@ -60,14 +92,17 @@ jobs:
6092
run: |
6193
bin/rails db:setup
6294
63-
- name: Run Rails tests
95+
- name: Run Rails tests (Postgresql) 🐘
96+
if: ${{ matrix.database == 'postgres' }}
6497
env:
6598
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
6699
MAGLEV_APP_DATABASE_USERNAME: "maglev"
67100
MAGLEV_APP_DATABASE_PASSWORD: "password"
68-
run: bundle exec rspec
101+
run: bundle exec rspec
69102

70-
- name: Setup test database (SQLite)
103+
# === SQLite 🪽 ===
104+
- name: Setup test database (SQLite) 🪽
105+
if: ${{ matrix.database == 'sqlite' }}
71106
env:
72107
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
73108
RAILS_ENV: test
@@ -79,21 +114,64 @@ jobs:
79114
cp spec/legacy_dummy/db/schema.sqlite.rb spec/legacy_dummy/db/schema.rb
80115
bin/rails db:setup
81116
82-
- name: Run Rails tests (SQLite)
117+
- name: Run Rails tests (SQLite) 🪽
118+
if: ${{ matrix.database == 'sqlite' }}
83119
env:
84120
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
85-
USE_SQLITE: true
121+
USE_SQLITE: 1
86122
run: bundle exec rspec
87123

88-
- name: Cleanup DB schema files
124+
# === MYSQL 🐬 ===
125+
- name: Setup test database (MySQL) 🐬
126+
if: ${{ matrix.database == 'mysql' }}
127+
env:
128+
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
129+
RAILS_ENV: test
130+
USE_MYSQL: 1
131+
MAGLEV_APP_DATABASE_HOST: "127.0.0.1"
132+
MAGLEV_APP_DATABASE_USERNAME: "root"
133+
MAGLEV_APP_DATABASE_PASSWORD: "password"
89134
run: |
90-
cp spec/dummy/db/schema.pg.rb spec/dummy/db/schema.rb
91-
cp spec/legacy_dummy/db/schema.pg.rb spec/legacy_dummy/db/schema.rb
92-
rm -f spec/dummy/db/maglev_engine_test.sqlite3
93-
rm -f spec/legacy_dummy/db/maglev_engine_test.sqlite3
135+
cp spec/dummy/db/schema.mysql.rb spec/dummy/db/schema.rb
136+
cp spec/legacy_dummy/db/schema.mysql.rb spec/legacy_dummy/db/schema.rb
137+
bin/rails db:setup
94138
95-
- name: Run Javascript tests
96-
run: yarn test
139+
- name: Run Rails tests (MySQL) 🐬
140+
if: ${{ matrix.database == 'mysql' }}
141+
env:
142+
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
143+
USE_MYSQL: 1
144+
MAGLEV_APP_DATABASE_HOST: "127.0.0.1"
145+
MAGLEV_APP_DATABASE_USERNAME: "root"
146+
MAGLEV_APP_DATABASE_PASSWORD: "password"
147+
run: bundle exec rspec
148+
149+
# === MariaDB 🦭===
150+
- name: Setup test database (MariaDB) 🦭
151+
if: ${{ matrix.database == 'mariadb' }}
152+
env:
153+
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
154+
RAILS_ENV: test
155+
USE_MYSQL: 1
156+
MAGLEV_APP_DATABASE_HOST: "127.0.0.1"
157+
MAGLEV_APP_DATABASE_PORT: 3307
158+
MAGLEV_APP_DATABASE_USERNAME: "root"
159+
MAGLEV_APP_DATABASE_PASSWORD: "password"
160+
run: |
161+
cp spec/dummy/db/schema.mariadb.rb spec/dummy/db/schema.rb
162+
cp spec/legacy_dummy/db/schema.mariadb.rb spec/legacy_dummy/db/schema.rb
163+
bin/rails db:setup
164+
165+
- name: Run Rails tests (MariaDB) 🦭
166+
if: ${{ matrix.database == 'mariadb' }}
167+
env:
168+
BUNDLE_GEMFILE: ${{ matrix.gemfile }}
169+
USE_MYSQL: 1
170+
MAGLEV_APP_DATABASE_HOST: "127.0.0.1"
171+
MAGLEV_APP_DATABASE_PORT: 3307
172+
MAGLEV_APP_DATABASE_USERNAME: "root"
173+
MAGLEV_APP_DATABASE_PASSWORD: "password"
174+
run: bundle exec rspec
97175

98176
# NOTE: disabled because an error of eslint in the GH env
99177
# - name: Run Javascript linter

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,8 @@ bin/test
4040
spec/dummy/db/*.sqlite3
4141
spec/legacy_dummy/db/*.sqlite3
4242

43+
docker-compose.yml
44+
db/mysql/init/01-init-databases.sql
45+
4346
docs/
4447
TODO.md

Gemfile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ gem 'puma'
3030
# To use a debugger
3131
# gem 'byebug', group: [:development, :test]
3232

33-
# Use SQLite/PostgreSQL for development and test
33+
# Use SQLite/PostgreSQL/MariaDB for development and test
34+
gem 'mysql2'
3435
gem 'pg', '~> 1.5.9'
3536
gem 'sqlite3'
3637

@@ -55,6 +56,8 @@ group :development, :test do
5556
gem 'annotaterb'
5657

5758
gem 'rdoc', '>= 6.6.3.1'
59+
60+
gem 'dotenv'
5861
end
5962

6063
group :test do

Gemfile.lock

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ GEM
9696
date (3.4.1)
9797
diff-lcs (1.5.1)
9898
docile (1.4.1)
99+
dotenv (3.1.8)
99100
drb (2.2.3)
100101
dry-cli (1.2.0)
101102
erubi (1.13.0)
@@ -161,6 +162,7 @@ GEM
161162
mini_portile2 (2.8.9)
162163
minitest (5.25.5)
163164
mutex_m (0.3.0)
165+
mysql2 (0.5.6)
164166
net-imap (0.5.8)
165167
date
166168
net-protocol
@@ -369,10 +371,12 @@ PLATFORMS
369371
DEPENDENCIES
370372
annotaterb
371373
bcrypt
374+
dotenv
372375
factory_bot_rails (~> 6.2.0)
373376
generator_spec
374377
image_processing (~> 1.12.2)
375378
maglevcms!
379+
mysql2
376380
nokogiri (>= 1.15.6)
377381
observer
378382
ostruct

Gemfile.rails_7_0

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ gem 'puma'
3636
# Use SQLite/PostgreSQL for development and test
3737
gem 'pg', '~> 1.5.9'
3838
gem 'sqlite3', '~> 1.4'
39+
gem 'mysql2', '~> 0.5.6'
3940

4041
# Gems no longer be part of the default gems from Ruby 3.5.0
4142
gem 'observer'
@@ -45,6 +46,7 @@ gem 'bigdecimal'
4546
gem 'mutex_m'
4647
gem 'drb'
4748
gem 'fiddle'
49+
gem 'benchmark'
4850

4951
group :development, :test do
5052
# Use SCSS for stylesheets
@@ -60,6 +62,10 @@ group :development, :test do
6062
gem 'generator_spec'
6163

6264
gem 'nokogiri', '>= 1.13.10'
65+
66+
gem 'dotenv'
67+
68+
gem 'database_cleaner-active_record'
6369
end
6470

6571
group :test do

Gemfile.rails_7_0.lock

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,19 @@ GEM
8080
ast (2.4.2)
8181
base64 (0.2.0)
8282
bcrypt (3.1.20)
83+
benchmark (0.4.1)
8384
bigdecimal (3.1.9)
8485
builder (3.3.0)
8586
concurrent-ruby (1.3.4)
8687
crass (1.0.6)
88+
database_cleaner-active_record (2.2.2)
89+
activerecord (>= 5.a)
90+
database_cleaner-core (~> 2.0)
91+
database_cleaner-core (2.0.1)
8792
date (3.4.0)
8893
diff-lcs (1.5.1)
8994
docile (1.4.1)
95+
dotenv (3.1.8)
9096
drb (2.2.1)
9197
dry-cli (1.2.0)
9298
erubi (1.13.0)
@@ -141,6 +147,7 @@ GEM
141147
mini_portile2 (2.8.9)
142148
minitest (5.25.1)
143149
mutex_m (0.3.0)
150+
mysql2 (0.5.6)
144151
net-imap (0.5.1)
145152
date
146153
net-protocol
@@ -302,7 +309,10 @@ PLATFORMS
302309
DEPENDENCIES
303310
base64
304311
bcrypt
312+
benchmark
305313
bigdecimal
314+
database_cleaner-active_record
315+
dotenv
306316
drb
307317
factory_bot_rails (~> 6.2.0)
308318
fiddle
@@ -311,6 +321,7 @@ DEPENDENCIES
311321
maglevcms!
312322
mini_magick (~> 4.11)
313323
mutex_m
324+
mysql2 (~> 0.5.6)
314325
nokogiri (>= 1.13.10)
315326
observer
316327
ostruct

Gemfile.rails_7_2

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ gem 'puma'
3939
# Use SQLite/PostgreSQL for development and test
4040
gem 'pg', '~> 1.5.9'
4141
gem 'sqlite3'
42+
gem 'mysql2', '~> 0.5.6'
4243

4344
# Gems no longer be part of the default gems from Ruby 3.5.0
4445
gem 'observer'
@@ -60,6 +61,10 @@ group :development, :test do
6061
gem 'nokogiri', '>= 1.15.6'
6162

6263
gem 'rdoc', '>= 6.6.3.1'
64+
65+
gem 'dotenv'
66+
67+
gem 'database_cleaner-active_record'
6368
end
6469

6570
group :test do

Gemfile.rails_7_2.lock

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,14 @@ GEM
9292
concurrent-ruby (1.3.4)
9393
connection_pool (2.4.1)
9494
crass (1.0.6)
95+
database_cleaner-active_record (2.2.2)
96+
activerecord (>= 5.a)
97+
database_cleaner-core (~> 2.0)
98+
database_cleaner-core (2.0.1)
9599
date (3.4.0)
96100
diff-lcs (1.5.1)
97101
docile (1.4.1)
102+
dotenv (3.1.8)
98103
drb (2.2.1)
99104
dry-cli (1.2.0)
100105
erubi (1.13.0)
@@ -151,6 +156,7 @@ GEM
151156
mini_portile2 (2.8.9)
152157
minitest (5.25.2)
153158
mutex_m (0.3.0)
159+
mysql2 (0.5.6)
154160
net-imap (0.5.1)
155161
date
156162
net-protocol
@@ -343,10 +349,13 @@ PLATFORMS
343349

344350
DEPENDENCIES
345351
bcrypt
352+
database_cleaner-active_record
353+
dotenv
346354
factory_bot_rails (~> 6.2.0)
347355
generator_spec
348356
image_processing (~> 1.12.2)
349357
maglevcms!
358+
mysql2 (~> 0.5.6)
350359
nokogiri (>= 1.15.6)
351360
observer
352361
ostruct

app/models/concerns/maglev/translatable.rb

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,38 @@ class UnavailableLocaleError < RuntimeError; end
88
extend ActiveSupport::Concern
99

1010
def translations_for(attr)
11-
public_send("#{attr}_translations")
11+
# With MySQL, there is no default value for JSON columns, so we need to check for nil
12+
public_send("#{attr}_translations").presence || {}
1213
end
1314

1415
def translate_attr_in(attr, locale, source_locale)
1516
translations_for(attr)[locale.to_s] ||= translations_for(attr)[source_locale.to_s]
1617
end
1718

19+
# rubocop:disable Metrics/BlockLength
1820
class_methods do
1921
def order_by_translated(attr, direction)
20-
order(Arel.sql("#{attr}_translations->>'#{Maglev::I18n.current_locale}'") => direction)
22+
order(translated_arel_attribute(attr, Maglev::I18n.current_locale) => direction)
23+
end
24+
25+
def translated_arel_attribute(attr, locale)
26+
return Arel.sql("#{attr}_translations->>'#{locale}'") unless mysql?
27+
28+
# MySQL and MariaDB JSON support 🤬🤬🤬
29+
# Note: doesn't work with Rails 7.0.x
30+
json_extract = Arel::Nodes::NamedFunction.new(
31+
'json_extract',
32+
[Arel::Nodes::SqlLiteral.new("#{attr}_translations"), Arel::Nodes.build_quoted("$.#{locale}")]
33+
)
34+
Arel::Nodes::NamedFunction.new('json_unquote', [json_extract])
2135
end
2236

2337
def translates(*attributes, presence: false)
24-
attributes.each { |attr| setup_accessors(attr) }
38+
attributes.each do |attr|
39+
# MariaDB doesn't support native JSON columns (longtext instead), we need to force it.
40+
attribute("#{attr}_translations", :json) if respond_to?(:attribute)
41+
setup_accessors(attr)
42+
end
2543
add_presence_validator(attributes) if presence
2644
end
2745

@@ -45,5 +63,6 @@ def setup_accessors(attr)
4563
define_method("default_#{attr}") { translations_for(attr)[Maglev::I18n.default_locale.to_s] }
4664
end
4765
end
66+
# rubocop:enable Metrics/BlockLength
4867
end
4968
end

0 commit comments

Comments
 (0)