3 grudnia 2024
W pracy nadszedł czas na Continuous integration (CI) w projektach nieco większych niż zwykła strona www. Zwykle w swojej pracy jako repozytorium kodu używam Bitbucketa więc postanowiłem daleko nie szukać narzędzi do Continuous integration (CI). Zamiast poznawać TravisCI czy CircleCI, więc mój wybór padł na bitbucketowe pipelines. Niestety zaraz na starcie, przy samych migracjach pojawił się problem wraz z błędem
django.db.utils.OperationalError: (1366, "Incorrect string value: '\\xC5\\xBCytko...' for column 'name' at row 1")
Jasnym było, że polski projekt to i polskie znaki diakrytyczne były problemem (pole ‘użytkownik’). Baza tworzy się domyślnie w latin1_swedish_ci. Długo walczyłem szukając informacji jak utworzyć bazę danych z odpowiednim zestawem znaków utf8. Oczywiście zacząłem od szukania informacji jak zrobić to domyślnie, coś ponad CHARSET: UTF8 w opcjach bazy danych, bo to i tak nic nie dawało – baza i tak była tworzona w szwedzkim kodowaniu:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'test_pipelines',
'USER': 'test_user',
'PASSWORD': 'test_user_password',
'HOST': '127.0.0.1',
'PORT': '3306',
'OPTIONS': {
"init_command": "SET default_storage_engine=MyISAM",
},
'TEST': {
'NAME': 'test_pipelines',
'CHARSET': 'UTF8',
},
}
}
Nie chciało mi się wierzyć, że nie ma czegoś ponad porady z linku https://confluence.atlassian.com/bitbucket/test-with-databases-in-bitbucket-pipelines-856697462.html:
definitions:
services:
mysql:
image: mysql
environment:
MYSQL_DATABASE: 'pipelines'
MYSQL_RANDOM_ROOT_PASSWORD: 'yes'
MYSQL_USER: 'test_user'
MYSQL_PASSWORD: 'test_user_password'
Znalazłem w końcu dwa rozwiązania problemu. Uruchomienie mysqld z parametrami jak poniżej, zgodnie z instrukcjami pod adresem https://kierenpitts.com/blog/2017/05/testing-django-applications-with-bitbucket-pipelines-and-mysql/
definitions:
services:
mysql:
image: mysql
command: mysqld --character-set-server=utf8 --collation-server=utf8_polish_ci --default-storage-engine=MyISAM
environment:
MYSQL_DATABASE: 'test_pipelines'
MYSQL_RANDOM_ROOT_PASSWORD: 'yes'
MYSQL_USER: 'test_user'
MYSQL_PASSWORD: 'test_user_password'
Niestety to rozwiązanie nie zadziałało. W ‘buildzie’ nie widać, żeby taka baza z takim kodowaniem się tworzyła.
Pozostało mi rozwiązanie drugie. Utworzyć bazę i skryptem ustawić odpowiednie kodowanie, zgodnie z tym co zostało opisane tu (https://josefottosson.se/change-collation-to-utf-8-on-all-tables-with-django-mysql/). Skrypty muszą się uruchomić zanim zostanie uruchomiana migracja. Tak więc powstał skrypt zmieniający bazę na utf8_polish_ci:
import sys
from project import settings
from django.db import connection
import os
sys.path.append(settings.BASE_DIR)
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings_pipelines")
def init():
cursor = connection.cursor()
tables = connection.introspection.table_names()
sql = "ALTER DATABASE pipelines CHARACTER SET utf8 COLLATE utf8_polish_ci;"
cursor.execute(sql)
for table in tables:
print "Fixing table: %s" %table
sql = "ALTER TABLE %s CONVERT TO CHARACTER SET utf8;" %(table)
cursor.execute(sql)
print "Table %s set to utf8"%table
print "DONE!"
init()
Ostatecznie mój plik bitbucket-pipelines.yml wygląda mniej więcej tak:
image: python:2.7
pipelines:
default:
- step:
caches:
- pip
services:
- mysql
script: # Modify the commands below to build your repository.
- pip install --upgrade pip
- pip install six
- pip install -r requirements.txt
- export DJANGO_SETTINGS_MODULE=project.settings_pipelines
- python pipeline_database_conversion.py
- python manage.py migrate --settings=project.settings_pipelines
- python manage.py migrate --database=historical --settings=project.settings_pipelines
definitions:
services:
mysql:
image: mysql
environment:
MYSQL_DATABASE: 'pipelines'
MYSQL_RANDOM_ROOT_PASSWORD: 'yes'
MYSQL_USER: 'test_user'
MYSQL_PASSWORD: 'test_user_password'
MYSQL_DEFAULT_CHARACTER_SET: 'utf8'
Proszę się nie dziwić dwóm migracjom, ponieważ ten projekt korzysta z 2 dwóch różnych baz danych (na devie czy produkcji), jednak na potrzeby testów utworzyłem tylko jedną bazę która zawiera wszystkie tabele. Ostatecznie testy z użyciem pytest działają aż miło.
Co do skryptu zmieniający kodowanie w bazie, prawdopodobnie większości osobom wystarczy część bez pętli iterującej po tabelach:
def init():
cursor = connection.cursor()
tables = connection.introspection.table_names()
sql = "ALTER DATABASE pipelines CHARACTER SET utf8 COLLATE utf8_polish_ci;"
cursor.execute(sql)
Dojście do momentu, kiedy wszystko działa zajęło mi bardzo dużo czasu, dlatego postanowiłem to opisać. Mam nadzieję, że pomoże to innym django/python developerom w łatwiejszej integracji bitbucketa i piplines, zwłaszcza tym polskiego pochodzenia.