Mój HomeAssistant jest uruchomiony na RaspberryPi4 w trybie SuperVised. Postanowiłem zmienić bazę danych trzymającą rekordy z SQLLite na PostgreSQL.
W pierwszej kolejności doinstalowałem PostgreSQL + TimeScaleDB jako Addon:
Ustawienia > Dodatki > Sklep z dodatkami
Przy pomocy trzech kropek w górnym prawym rogu > Repozytoria
Wyszukałem: TimescaleDB i go zainstalowałem
Przed uruchomieniem w zakładce Konfiguracja > Sieć > Zmień porty na hoście, które są udostępniane przez dodatek: ustawiłem port 5432 - to pozwoli dostać się do bazy danych "z zewnątrz"
Uruchomiłem dodatek i zweryfikowałem w logach, że wszystko wystartowało poprawnie
Mój HomeAssistant jest uruchomiony na RaspberryPi4 w trybie SuperVised. Postanowiłem zmienić bazę danych trzymającą rekordy z SQLLite na PostgreSQL.
W pierwszej kolejności doinstalowałem PostgreSQL + TimeScaleDB jako Addon:
Ustawienia > Dodatki > Sklep z dodatkami
Przy pomocy trzech kropek w górnym prawym rogu > Repozytoria
Wyszukałem: TimescaleDB i go zainstalowałem
Przed uruchomieniem w zakładce konfiguracja, blok Sieć, ustawiłem port 5432 - to pozwoli dostać się do bazy danych "z zewnątrz"
Uruchomiłem dodatek i zweryfikowałem w logach, że wszystko wystartowało poprawnie
Kolejnym krokiem było skonfigurowanie samego PostgreSQL. Na swoim komputerze doinstalowałem pakiet postgresql-client-15:
$ sudo apt get install postgresql-client-15
Najpierw:
ciąg homeassistant.local to adres, pod którym nasłuchuje HomeAssistant - czasem mi to działa, lecz lepiej wpisać po prostu adres IP w poniższych komendach
zmieniłem domyślne hasło do postgres (z homeassistant) na XXX
utworzyłem grupę homeassistant, do w której utworzylem dwóch użytkowników:
homeassistant_adm - właściciel bazy (admin) z hasłem YYY - namiary na tego użytkownika podam w sekcji recorder w configuration.yaml
homeassistant_ro - użytkownik "tylko do odczytu" z hasłem ZZZ - mam w planie go wpiąć do dodatków (np. Grafana)
utworzyłem bazę danych homassistant (właściwie zdropowałem starą, której właścicielem był postgres) - z właścicielem homeassistant_adm
$ psql -U postgres -h homeassistant.local -p 5432
Password for user postgres: homeassistant
sql> ALTER USER postgres PASSWORD 'XXX';
sql> CREATE GROUP homeassistant;
sql> CREATE USER homeassistant_adm IN GROUP homeassistant PASSWORD 'YYY';
sql> CREATE USER homeassistant_ro IN GROUP homeassistant PASSWORD 'ZZZ';
sql> DROP DATABASE homeassistant;
sql> CREATE DATABASE homeassistant WITH OWNER homeassistant_adm ENCODING 'utf8';
sql> \q
Potem:
nadałem prawa "tylko do odczytu" użytkownikowi homeassistant_ro
raz pownownie dodałem rozszerzenie timescaledb (w sumie nie wiem co ono daje)
$ psql -U homeassistant_adm -h homeassistant.local -p 5432 homeassistant
sql> Password for user homeassistant_adm: XXX
sql> GRANT USAGE ON SCHEMA public TO homeassistant_ro;
sql> GRANT SELECT ON ALL TABLES IN SCHEMA public TO homeassistant_ro;
sql> ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO homeassistant_ro;
sql> CREATE EXTENSION timescaledb;
Kolejny krok to skonfigurowanie w configuration.yaml i secrets.yaml nowego połaczenia do bazy - użyłem do tego
File editor > secrets.yaml (77b2833f-timescaledb odczytałem z zakładki informującej mnie o stanie dodatku TimeScaleDB, pole "Nazwa hosta"
psql_string: "postgresql://homeassistant_adm:XXX@77b2833f-timescaledb/homeassistant"
File editor > configuration.yaml:
recorder:
auto_purge: false
purge_keep_days: 4000
db_url: !secret psql_string
db_retry_wait: 15
Po wszystkim zrestartowałem HomeAssistant. System się uruchomił i stworzył strukturę bazy danych w PostgreSQL - to był cel.
Następnie przywróciłem starą sekcję recorder (akurat ją miałem, jak nie masz - to wystarczy zakomentować to co dopisano w configuration.yaml) i poownie zrestartowałem HomeAssistant - kolejne kroki zajmą trochę czasu i nie chciałem w tym czasie tracić danych.
Na sam koniec wykonałem zrzut bazy danych - przypuszczałem (i dobrze), że operacja migracji nie jest taka prosta i będę musiał wykonać kilka prób:
pg_dump -h homeassistant.local -U postgres homeassistant -s -c > schema.sql
Do wykonania zrzutu użyłem flag -s (tylko schemat) oraz -c (dodaj DROP).
Na czas "testów" potrzebuje lokalnej kopii (dlaczego: o tym więcej przy pgLoader) bazy danych SQLLite.
W celu posiadania "spójnego" obrazu wstrzymałem generację nowych zdarzeń recorder poprzez wykonanie usługi: recorder.disable w zakładce Narzędzia deweloperskie > Usługi > Recorder: Wyącz.
Następnie skopiowałem z kontenera homeassistant bazę danych na lokalny komputer (user@machine.local) - na obu maszynach wymagane jest włączenie SSH i doinstalowanie rsync:
$ sudo apt install rsync // na lokalnym komputerze
$ ssh homeassistant.local
$ sudo bash
# apt install rsync
# rsync -v --progress --inplace --partial /proc/$(docker inspect -f '{{.State.Pid}}' homeassistant)/root/config/home-assistant_v2.db user@machine.local:home-assistant_v2.db
Dodanie opcji --inplace oraz --partial pozwala szybko dosynchronizować bazę danych, gdybyśmy tą operację musieli powtórzyć.
Na końcu spowrotem właczyłem rejestrowanie zdarzeń poprzez wywołanie usługi: recorder.enable.
Tutaj punkt, który przysporzył mi najwięcej problemów. Po pierwsze, dostarczony pgLoader wraz z miją maszyną (apt install pgloader) działał bardzo niestabilnie. Dlatego zdecydowałem się pobrać najnowszy i ko skompilować - pobrałem najnowszą wersję z https://github.com/dimitri/pgloader/releases:
sudo apt install sbcl
wget https://github.com/dimitri/pgloader/releases/download/v3.6.9/pgloader-bundle-3.6.9.tgz
tar xvfz pgloader-bundle-3.6.9.tgz
cd pgloader-bundle-3.6.9
make
cd ..
ln -s pgloader-bundle-3.6.9/bin/pgloader pgloader
Kolejny problem to pamięć. Na domyślnych parametrach ciągle dostawałem błędy typu: https://github.com/dimitri/pgloader/issues/962
Rozwiązaniem jest dodanie parametru: --dynamic-space-size 262144
I teraz najciekawsze. Migracja 4GB pliku home-assistant_v2.db wymaga około 14GB pamięci. Dla większych baz danych nie wyobrażam sobie używania tego narzędzia.
Acha jest napisane w LISP - aż mi się przypomina: https://xkcd.com/224/ - patrząc na problemy jakie napotkałem, żałuję, że może nie "zhakowałem" wszystkiego w Perlu.
Jak już wszystko mamy czas przejść do migracji. Proces wygląda w skrócie tak.
cat <<EOT>migrate.load
load database
from sqlite://home-assistant_v2.db
into postgresql://homeassistant_adm:YYY@homeassistant.local:5432/homeassistant
with data only, drop indexes, reset sequences, truncate, batch rows = 1000
SET work_mem to '64 MB', maintenance_work_mem to '128 MB';
EOT
$ pgloader --dynamic-space-size 262144 migrate.load
2024-02-10T21:34:19.004000Z LOG pgloader version "3.6.7~devel"
2024-02-10T21:34:19.167999Z LOG Migrating from #<SQLITE-CONNECTION sqlite://home-assistant_v2.db {1006A2ACF3}>
2024-02-10T21:34:19.167999Z LOG Migrating into #<PGSQL-CONNECTION pgsql://homeassistant_adm@homeassistant.local:5432/homeassistant {1006A2AF23}>
2024-02-10T21:34:19.515999Z WARNING Source column "public"."events"."event_id" is casted to type "bigint" which is not the same as "integer", the type of current target database column "public"."events"."event_id".
2024-02-10T21:34:19.515999Z WARNING Source column "public"."events"."event_type" is casted to type "text" which is not the same as "character", the type of current target database column "public"."events"."event_type".
2024-02-10T21:34:19.515999Z WARNING Source column "public"."events"."event_data" is casted to type "text" which is not the same as "character", the type of current target database column "public"."events"."event_data".
2024-02-10T21:34:19.515999Z WARNING Source column "public"."events"."origin" is casted to type "text" which is not the same as "character", the type of current target database column "public"."events"."origin".
2024-02-10T21:34:19.515999Z WARNING Source column "public"."events"."context_id" is casted to type "text" which is not the same as "character", the type of current target database column "public"."events"."context_id".
2024-02-10T21:34:19.515999Z WARNING Source column "public"."events"."context_user_id" is casted to type "text" which is not the same as "character", the type of current target database column "public"."events"."context_user_id".
2024-02-10T21:34:19.515999Z WARNING Source column "public"."events"."context_parent_id" is casted to type "text" which is not the same as "character", the type of current target database column "public"."events"."context_parent_id".
2024-02-10T21:34:19.515999Z WARNING Source column "public"."events"."data_id" is casted to type "bigint" which is not the same as "integer", the type of current target database column "public"."events"."data_id".
2024-02-10T21:34:19.515999Z WARNING Source column "public"."events"."origin_idx" is casted to type "bigint" which is not the same as "smallint", the type of current target database column "public"."events"."origin_idx".
2024-02-10T21:34:19.515999Z WARNING Source column "public"."events"."event_type_id" is casted to type "bigint" which is not the same as "integer", the type of current target database column "public"."events"."event_type_id".
2024-02-10T21:34:19.515999Z WARNING Source column "public"."statistics_meta"."id" is casted to type "bigint" which is not the same as "integer", the type of current target database column "public"."statistics_meta"."id".
2024-02-10T21:34:19.515999Z WARNING Source column "public"."statistics_meta"."statistic_id" is casted to type "text" which is not the same as "character varying", the type of current target database column "public"."statistics_meta"."statistic_id".
2024-02-10T21:34:19.515999Z WARNING Source column "public"."statistics_meta"."source" is casted to type "text" which is not the same as "character varying", the type of current target database column "public"."statistics_meta"."source".
2024-02-10T21:34:19.515999Z WARNING Source column "public"."statistics_meta"."unit_of_measurement" is casted to type "text" which is not the same as "character varying", the type of current target database column "public"."statistics_meta"."unit_of_measurement".
2024-02-10T21:34:19.515999Z WARNING Source column "public"."statistics_meta"."name" is casted to type "text" which is not the same as "character varying", the type of current target database column "public"."statistics_meta"."name".
2024-02-10T21:34:19.515999Z WARNING Source column "public"."recorder_runs"."run_id" is casted to type "bigint" which is not the same as "integer", the type of current target database column "public"."recorder_runs"."run_id".
2024-02-10T21:34:19.515999Z WARNING Source column "public"."schema_changes"."change_id" is casted to type "bigint" which is not the same as "integer", the type of current target database column "public"."schema_changes"."change_id".
2024-02-10T21:34:19.515999Z WARNING Source column "public"."schema_changes"."schema_version" is casted to type "bigint" which is not the same as "integer", the type of current target database column "public"."schema_changes"."schema_version".
2024-02-10T21:34:19.515999Z WARNING Source column "public"."statistics_runs"."run_id" is casted to type "bigint" which is not the same as "integer", the type of current target database column "public"."statistics_runs"."run_id".
2024-02-10T21:34:19.515999Z WARNING Source column "public"."states"."state_id" is casted to type "bigint" which is not the same as "integer", the type of current target database column "public"."states"."state_id".
2024-02-10T21:34:19.515999Z WARNING Source column "public"."states"."domain" is casted to type "text" which is not the same as "character varying", the type of current target database column "public"."states"."domain".
2024-02-10T21:34:19.515999Z WARNING Source column "public"."states"."entity_id" is casted to type "text" which is not the same as "character", the type of current target database column "public"."states"."entity_id".
2024-02-10T21:34:19.515999Z WARNING Source column "public"."states"."state" is casted to type "text" which is not the same as "character varying", the type of current target database column "public"."states"."state".
2024-02-10T21:34:19.515999Z WARNING Source column "public"."states"."attributes" is casted to type "text" which is not the same as "character", the type of current target database column "public"."states"."attributes".
2024-02-10T21:34:19.515999Z WARNING Source column "public"."states"."event_id" is casted to type "bigint" which is not the same as "smallint", the type of current target database column "public"."states"."event_id".
2024-02-10T21:34:19.515999Z WARNING Source column "public"."states"."old_state_id" is casted to type "bigint" which is not the same as "integer", the type of current target database column "public"."states"."old_state_id".
2024-02-10T21:34:19.515999Z WARNING Source column "public"."states"."attributes_id" is casted to type "bigint" which is not the same as "integer", the type of current target database column "public"."states"."attributes_id".
2024-02-10T21:34:19.515999Z WARNING Source column "public"."states"."origin_idx" is casted to type "bigint" which is not the same as "smallint", the type of current target database column "public"."states"."origin_idx".
2024-02-10T21:34:19.515999Z WARNING Source column "public"."states"."context_id" is casted to type "text" which is not the same as "character", the type of current target database column "public"."states"."context_id".
2024-02-10T21:34:19.519999Z WARNING Source column "public"."states"."context_user_id" is casted to type "text" which is not the same as "character", the type of current target database column "public"."states"."context_user_id".
2024-02-10T21:34:19.519999Z WARNING Source column "public"."states"."context_parent_id" is casted to type "text" which is not the same as "character", the type of current target database column "public"."states"."context_parent_id".
2024-02-10T21:34:19.519999Z WARNING Source column "public"."states"."metadata_id" is casted to type "bigint" which is not the same as "integer", the type of current target database column "public"."states"."metadata_id".
2024-02-10T21:34:19.519999Z WARNING Source column "public"."statistics"."id" is casted to type "bigint" which is not the same as "integer", the type of current target database column "public"."statistics"."id".
2024-02-10T21:34:19.519999Z WARNING Source column "public"."statistics"."metadata_id" is casted to type "bigint" which is not the same as "integer", the type of current target database column "public"."statistics"."metadata_id".
2024-02-10T21:34:19.519999Z WARNING Source column "public"."statistics_short_term"."id" is casted to type "bigint" which is not the same as "integer", the type of current target database column "public"."statistics_short_term"."id".
2024-02-10T21:34:19.519999Z WARNING Source column "public"."statistics_short_term"."metadata_id" is casted to type "bigint" which is not the same as "integer", the type of current target database column "public"."statistics_short_term"."metadata_id".
2024-02-10T21:34:19.519999Z WARNING Source column "public"."event_data"."data_id" is casted to type "bigint" which is not the same as "integer", the type of current target database column "public"."event_data"."data_id".
2024-02-10T21:34:19.519999Z WARNING Source column "public"."state_attributes"."attributes_id" is casted to type "bigint" which is not the same as "integer", the type of current target database column "public"."state_attributes"."attributes_id".
2024-02-10T21:34:19.519999Z WARNING Source column "public"."event_types"."event_type_id" is casted to type "bigint" which is not the same as "integer", the type of current target database column "public"."event_types"."event_type_id".
2024-02-10T21:34:19.519999Z WARNING Source column "public"."event_types"."event_type" is casted to type "text" which is not the same as "character varying", the type of current target database column "public"."event_types"."event_type".
2024-02-10T21:34:19.519999Z WARNING Source column "public"."states_meta"."metadata_id" is casted to type "bigint" which is not the same as "integer", the type of current target database column "public"."states_meta"."metadata_id".
2024-02-10T21:34:19.519999Z WARNING Source column "public"."states_meta"."entity_id" is casted to type "text" which is not the same as "character varying", the type of current target database column "public"."states_meta"."entity_id".
2024-02-10T21:34:19.519999Z ERROR pgloader failed to find target table for source NIL."sqlite_stat1" with name "sqlite_stat1" in target catalog
2024-02-10T21:34:19.519999Z LOG Skipping sqlite_stat1
2024-02-10T21:34:19.583999Z WARNING PostgreSQL warning: constraint "statistics_metadata_id_fkey" of relation "statistics" does not exist, skipping
2024-02-10T21:34:19.583999Z WARNING PostgreSQL warning: constraint "statistics_short_term_metadata_id_fkey" of relation "statistics_short_term" does not exist, skipping
2024-02-10T21:34:19.587999Z WARNING PostgreSQL warning: constraint "events_data_id_fkey" of relation "events" does not exist, skipping
2024-02-10T21:34:19.591999Z WARNING PostgreSQL warning: constraint "states_attributes_id_fkey" of relation "states" does not exist, skipping
2024-02-10T21:34:19.591999Z WARNING PostgreSQL warning: constraint "events_event_type_id_fkey" of relation "events" does not exist, skipping
2024-02-10T21:34:19.595999Z WARNING PostgreSQL warning: constraint "states_metadata_id_fkey" of relation "states" does not exist, skipping
2024-02-10T21:43:55.149106Z LOG report summary reset
table name errors rows bytes total time
----------------------- --------- --------- --------- --------------
fetch 0 0 0.000s
fetch meta data 0 50 0.020s
Drop Foreign Keys 0 12 0.080s
Drop Indexes 0 31 0.144s
Truncate 0 12 0.044s
----------------------- --------- --------- --------- --------------
statistics_meta 0 430 22.6 kB 0.116s
events 0 525941 68.6 MB 4.836s
schema_changes 0 20 0.6 kB 0.124s
states 0 17205424 2.2 GB 3m12.711s
recorder_runs 0 10 0.8 kB 1.256s
statistics_runs 0 9147 303.7 kB 1.800s
statistics 0 1535139 123.9 MB 22.384s
event_data 0 10413 4.9 MB 1.856s
event_types 0 22 0.5 kB 1.972s
statistics_short_term 0 1781546 144.9 MB 1m0.824s
state_attributes 0 1142940 442.0 MB 1m38.172s
states_meta 0 1051 38.2 kB 3.360s
----------------------- --------- --------- --------- --------------
COPY Threads Completion 0 4 5m44.255s
Create Indexes 0 31 31m34.781s
Index Build Completion 0 31 3m17.382s
Reset Sequences 0 0 0.196s
Primary Keys 0 11 0.120s
Create Foreign Keys 0 6 33.256s
Install Comments 0 0 0.000s
----------------------- --------- --------- --------- --------------
Na koniec pozostaje naprawienie wartości zwracanych przez sekwencje - niestety pgLoader nie poradził sobie z tym problemem:
SELECT setval(pg_get_serial_sequence('statistics_runs', 'run_id'), coalesce(MAX(run_id)+1, 1)) from statistics_runs;
SELECT setval(pg_get_serial_sequence('statistics_meta', 'id'), coalesce(MAX(id)+1, 1)) from statistics_meta;
SELECT setval(pg_get_serial_sequence('statistics', 'id'), coalesce(MAX(id)+1, 1)) from statistics;
SELECT setval(pg_get_serial_sequence('statistics_short_term', 'id'), coalesce(MAX(id)+1, 1)) from statistics_short_term;
SELECT setval(pg_get_serial_sequence('states', 'state_id'), coalesce(MAX(state_id)+1, 1)) from states;
SELECT setval(pg_get_serial_sequence('state_attributes', 'attributes_id'), coalesce(MAX(attributes_id)+1, 1)) from state_attributes;
SELECT setval(pg_get_serial_sequence('events', 'event_id'), coalesce(MAX(event_id)+1, 1)) from events;
SELECT setval(pg_get_serial_sequence('event_data', 'data_id'), coalesce(MAX(data_id)+1, 1)) from event_data;
SELECT setval(pg_get_serial_sequence('recorder_runs', 'run_id'), coalesce(MAX(run_id)+1, 1)) from recorder_runs;
SELECT setval(pg_get_serial_sequence('schema_changes', 'change_id'), coalesce(MAX(change_id)+1, 1)) from schema_changes;
SELECT setval(pg_get_serial_sequence('event_types', 'event_type_id'), coalesce(MAX(event_type_id)+1, 1)) from event_types;
SELECT setval(pg_get_serial_sequence('states_meta', 'metadata_id'), coalesce(MAX(metadata_id)+1, 1)) from states_meta;
Jeżeli wystąpił Wam "Bład - brak kolumn", to można jeszcze zDROPować te kolumny utworzone tylko na potrzeby migracji:
$ psql -U homeassistant_adm -h homeassistant.local -p 5432 homeassistant
sql> alter table public.states drop created, drop domain;
sql> alter table public.entities drop created;
sql> \q
Pozostaje odkomentować w configuration.yaml sekcję recorder wskazującą na PostgreSQL i restart HomeAssistant.
Jak w Ustawienia > System > Logi nie macie błędów informujących o niemożliwości wstawienia nowych danych (psycopg2) z powodu braku unikatowości - wtedy operacja ustawiania sekwencji nie poszła OK i trzeba jeszcze coś ogarnąć, wykonać "Czyszczenie bazy przed kolejnych podejściem" i ponownie migrować dane.
A jeżeli wszystko działa OK (są dane, historia, energia) - to gratuluje sukcesu.
To powiazane z: https://github.com/dimitri/pgloader/issues/1460 i https://github.com/dimitri/pgloader/issues/1207
Prawdopodobnie baza danych korzysta z:
password_encryption = scram-sha-256
Rozwiązanie to taki ciąg czynności:
$ psql -U postgres -h homeassistant.local -p 5432
sql> SET password_encryption = 'md5';
sql> ALTER USER homeassistant_adm PASSWORD 'YYY';
sql> \q
I powrót od kroku "Wykonanie migracji"
W mojej bazie danych brakowało kolumn states.created, states.domain oraz entities.created - tj. kiedyś były, ale HomeAssistant przestał z nich korzystać i nie usunał (bo może SQLLite dopiero niedawno dorobiło się ALTER TABLE DROP COLUMN). Konieczne było ich utworznie:
$ psql -U homeassistant_adm -h homeassistant.local -p 5432 homeassistant
sql> alter table public.states add created timestamp with time zone null, add domain varchar(64) null;
sql> alter table public.entities add created timestamp with time zone null;
sql> \q
i powrót do kroku "Wykonanie migracji"
Tak informacyjnie, gdybyście potrzebowali w przypadku gdyby import zakończył się w połowie:
echo schema.sql | psql -U postgres -h homeassistant.local -p 5432 homeassistant
Następnie przygotowałem skrypt do migracji:
# cat <<EOT>/root/ha.pgloader
load database
from sqlite://home-assistant_v2.db
into postgresql://homeassistant_adm:XXX@127.0.0.1:5432/homeassistant
with data only, drop indexes, reset sequences, truncate, batch rows = 1000
SET work_mem to '32 MB', maintenance_work_mem to '64 MB';
EOT
I zacząłęm migrować:
# pgloader ha.pgloader
W przypadku błędu XXX
2024-02-09T14:44:37.009000Z LOG pgloader version "3.6.2"
...
2024-02-09T14:44:37.643000Z LOG report summary reset
table name errors rows bytes total time
----------------------- --------- --------- --------- --------------
fetch 0 0 0.000s
fetch meta data 0 51 0.029s
Drop Foreign Keys 0 12 0.009s
Drop Indexes 0 31 0.017s
Truncate 0 12 0.019s
----------------------- --------- --------- --------- --------------
event_data 0 428 31.7 kB 0.071s
event_types 0 11 0.2 kB 0.028s
state_attributes 0 66 11.4 kB 0.078s
statistics_meta 0 2 0.1 kB 0.124s
schema_changes 0 1 0.0 kB 0.138s
events 0 595 54.8 kB 0.209s
statistics 0 2 0.2 kB 0.206s
states_meta 0 25 0.8 kB 0.015s
recorder_runs 0 2 0.1 kB 0.014s
statistics_runs 0 18 0.5 kB 0.103s
states 0 212 23.9 kB 0.116s
statistics_short_term 0 24 1.9 kB 0.175s
----------------------- --------- --------- --------- --------------
COPY Threads Completion 0 4 0.235s
Create Indexes 0 31 0.584s
Index Build Completion 0 31 0.163s
Reset Sequences 0 0 0.013s
Primary Keys 0 11 0.007s
Create Foreign Keys 0 6 0.008s
Install Comments 0 0 0.000s
----------------------- --------- --------- --------- --------------
Total import time ✓ 1386 125.8 kB 1.010s