Моят личен и професионален живот

Показват се публикациите с етикет Разработка. Показване на всички публикации
Показват се публикациите с етикет Разработка. Показване на всички публикации

2020-02-29

Миграциите към Git и GitHub продължават

Миналата година започнах миграция от CVS към Git и GitHub (виж Първи миграции от CVS към Git), което е нещо което исках да започна още през 2018-та (виж Миграция към Git и GitHub), а планирах дори от по-отдавна. Е, оказа се трудна задача, защото все още мигрирам, тъй като много от CVS хранилищата ми изискваха поправки, така че историята да може да бъде правилно прехвърлена в Git. По време на миграциите бях изненадан колко небрежен съм бил към кода си, защото открих непродадени промени от преди чак 10 години, повредени файлове с ревизии и различни несъответствия в историята. След като мигрирах някои проекти веднага започнах да работя по обновявания и поправки, така че вече продължавам работата си в Git. Също така пуснах непрекъсната интеграция за някои проекти използвайки Travis CI и GitHub Actions.

В момента мигрирам Slackware скриптовете си за изграждане на пакети, които са колекция от над 300 шел скрипта и свързаните с тях файлове организирани като отделни хранилища в отделни директории, но под един корен. В CVS това беше напълно в реда на нещата, но не произвежда добра история в Git, защото етикетите за версии или клонове (напр. FFmpeg-3_4_7 или MySQL-5_5) са специфични само за определен скрипт и използвани само от файловете в съответната му директория. Затова реших да ги мигрирам отделно и да ги обединя в друго хранилище с под модули. Има доста проблеми и с тази миграция.

Това са някои от проблемите:
  • етикети или клонове с подобни имена (напр. TEST-123 и TEST_123). Тези са лесни за оправяне - просто трия грешния етикет/клон;
  • неправилно поставени етикети или клонове. Тъй като в CVS поставях етикети и започвах клонове както е нужно някои етикети и клонове бяха поставени на различни файлове в различно време, което в Git историята се отразява като подавания със съобщение "This commit was manufactured by cvs2git to create tag 'TEST-123'" или "This commit was manufactured by cvs2git to create branch 'TEST-123'" с файлове които са добавени, премахнати или променени, за да се нагласи историята за съответния етикет или клон. Оправям такива проблеми с пренареждане на проблемните подавания, което е лесно само със смяна на датата във файла с ревизии в хранилището, но отнема време да се прегледа и разбере причината;
  • неизползвани файлове, които не са изтрити в историята. Това също предизвиква подавания със съобщения като споменатите в предишната точка. Тези проблеми оправям с изтриване на неизползваните файлове с минала дата, така че те да не се взимат в предвид от следващи подавания;
  • файлове принадлежащи на клон, но подадени в ствола вместо това. Подобно на предходното. В някои случаи, има файлове, които трябва да съществуват само по някакъв клон (напр. като кръпки за поправка на проблеми с конкретна версия на софтуера), но са били подадени в ствола. Тъй като в CVS няма голяма разлика между етикети и клонове е било достатъчно да има етикет на промените, за да се изваждат правилно. Премествам такива от ствола в клон, което означава изтриване на ревизиите от ствола и преместването им върху клона;
  • правописни грешки. Тъй като най-вече подавах към CVS от команден ред и не използвах проверка на правописа имам много съобщения към подавания с правописни грешки, които дразнят очите ми. Поправям ги след първоначална миграция към Git, за да мога да проверя всички съобщения наведнъж.
Два инструмента се оказа от голяма помощ след миграция - licensee и github-linguist. Използвам първия, за да проверя дали лиценза се открива правилно. Имах някои проблеми (виж бъгове 361 и 392) с някои от проектите ми, затова сега проверявам преди да бутна към GitHub. Втория е полезен за фина настройка на откриването на езиците (напр. искам да виждам просто SQL, а не PLSQL, PLpgSQL, SQLPL или TSQL, които лингвист би открил, въпреки че всичките ми публикувани SQL изходни кодове са за MySQL. И за мен SQL е код, не данни).

Както и да е публикувам това, за да отбележа, че миграциите ми към Git продължават. Постигнах голям напредък миналата година мигрирайки 28 хранилища. А тази година досега мигрирах още 114. Така надявам се до края на годината ще бъда свободен от ползване на CVS (или поне само по изключение). Първоначално планувах да мигрирам всичко, но вече смятам да пропусна някои проекти и примери с по-ниска стойност, които ако е нужно, мога да мигрирам по-късно.

2019-07-31

Нови възможности в MySQL 8.0.17

Миналия Вторник (27-ми Юли), Oracle пуснаха MySQL 8.0.7 следвайки тримесечния цикъл на нови версии въведен миналата година с 8 серията. Това е следващата версия по "поддръжката" въвеждаща някои нови възможности, както и обезценявайки някои нестандартни такива, така че ето това, което забелязах започвайки с тези, които смятам важни за разработчици.

Индекси върху множество стойности

С помощта на създавани колони (въведени с MySQL 5.7.6) и функционални индекси (въведени с MySQL 8.0.13 за което писах преди) стана възможно да се индексират данни в сложни стойности на колони като JSON. Но в JSON можете да имате скалари както и масиви, така че търсенето в масиви не беше възможно с помощта на индекс. Индексите върху множество стойности идват, за да решат това позволявайки множество записи в индекса да сочат към един и същ запис с данни. Такива индекси се създават като всеки друго функционален индекс и се използват автоматично от оптимизатора когато е възможно. Да видим един пример - регистър на преводачи с говоримите от тях езици.

CREATE TABLE translators (
  id INT AUTO_INCREMENT,
  jdata JSON,

  PRIMARY KEY(id)
);

  • Случай 1 - масив от низове

Да създадем малко данни:

INSERT INTO translators (jdata)
VALUES ('{"name": "T1", "langs": ["English", "French", "Spanish"]}'),
       ('{"name": "T2", "langs": ["English", "Spanish"]}'),
       ('{"name": "T3", "langs": ["French", "Spanish"]}');


След това, да потърсим преводачи, които говорят Английски използвайки новия MEMBER OF оператор:

SELECT id, jdata->>'$.name', jdata->'$.langs'
  FROM translators
 WHERE 'English' MEMBER OF (jdata->'$.langs');


Или новата функция JSON_OVERLAPS ето така:

SELECT id, jdata->>'$.name', jdata->'$.langs'
  FROM translators
 WHERE JSON_OVERLAPS(jdata->'$.langs', '["English"]');


И двете заявки водят до един и същ резултат:

+----+------------------+----------------------------------+
| id | jdata->>'$.name' | jdata->'$.langs'                 |
+----+------------------+----------------------------------+
|  1 | T1               | ["English", "French", "Spanish"] |
|  2 | T2               | ["English", "Spanish"]           |
+----+------------------+----------------------------------+
2 rows in set (0.00 sec)


Като се имат в предвид данните очаквано заявките връщат T1 и T2, но не T3. Обаче, тези заявки правят пълно сканиране на таблицата, така че производителността им ще деградира с натрупването на данни в таблицата.

Execution plan without index

За щастие, вече е възможно да се добави индекс върху множество стойности ето така:

ALTER TABLE translators
  ADD INDEX idx_langs_arr ((CAST(jdata->'$.langs' AS CHAR(8) ARRAY)));

Това е функционален индекс, в който е необходимо да се използва функцията CAST в новата ключова дума ARRAY. С индекса плана за изпълнение на SELECT заявките отгоре става съответно:
Execution plan of MEMBER OF with indexExecution plan of JSON OVERLAPS with index

  • Случай 2 - масив от обекти
Малко по-различно е за масив от обекти, но само за JSONPath израза. Нека създадем малко данни (след като почистим Случай 1):

INSERT INTO translators (jdata)
VALUES ('{"name": "T1", "langs": [{"lang": "English"}, {"lang": "French"}, {"lang": "Spanish"}]}'),
       ('{"name": "T2", "langs": [{"lang": "English"}, {"lang": "Spanish"}]}'),
       ('{"name": "T3", "langs": [{"lang": "French"}, {"lang": "Spanish"}]}');


След това, нека да потърсим преводачи, които говорят Английски по същите два начина:

SELECT id, jdata->>'$.name', jdata->'$.langs[*].lang'
  FROM translators
 WHERE 'English' MEMBER OF (jdata->'$.langs[*].lang');


SELECT id, jdata->>'$.name', jdata->'$.langs[*].lang'
  FROM translators
 WHERE JSON_OVERLAPS(jdata->'$.langs[*].lang', '["English"]');


Точно както в първия случай заявките правят пълно сканиране на таблицата, което вече лесно може да бъде променено с добавянето на индекс върху множество стойности ето така:

ALTER TABLE translators
  ADD INDEX idx_langs_obj ((CAST(jdata->'$.langs[*].lang' AS CHAR(8) ARRAY)));


Забележете леко различния синтаксис на JSONPath израза. За да работи индекса разбира се е необходимо да се използва същия израз в WHERE клаузата както в определението на индкеса. Разработчиците, които предпочитат да пазят данни направо в JSON колони трябва да бъдат щастливи от тази нова възможност, тък като тя прави възможно индексирането не само на скаларни стойности, но също така масиви.

JSON

Освен вече споменатите нов стандартен оператор MEMBER OF за търсене на стойности в JSON масиви има три нови функции JSON_OVERLAPS, JSON_SCHEMA_VALID и JSON_SCHEMA_VALIDATION_REPORT.
  • Функцията JSON_OVERLAPS сравнява два JSON документа и връща истина "ако двата документа имат някоя обща ключ-стойност двойка или елементи на масив". Както MEMBER OF и JSON_CONTAINS функцията JSON_OVERLAPS може да се облагодетелства от индекси върху множество стойности.
  • Функциите JSON_SCHEMA_VALID и JSON_SCHEMA_VALIDATION_REPORT са добавени във връзка с поддръжката на JSON Schema. Първата проверява JSON документ спрямо JSON схема и връща истина ако е верен иначе лъжа, така че да може да се ползва като CHECK ограничение. Втората ще предостави подробности по грешките от проверката под формата на JSON документ.

Преобразуване към FLOAT, DOUBLE и REAL

Функциите CAST и CONVERT вече могат да преобразуват към типове данни с плаваща запетая като FLOAT, DOUBLE и REAL. Нека да пробваме с неправилна стойност:

SELECT CAST('1.23.34' AS FLOAT) cast_res, CONVERT('1.23.34', FLOAT) conv_res;

+--------------------+--------------------+
| cast_res           | conv_res           |
+--------------------+--------------------+
| 1.2300000190734863 | 1.2300000190734863 |
+--------------------+--------------------+
1 row in set, 2 warnings (0.00 sec)

Има две предупреждения, така че нека ги видим с show warnings:

+---------+------+---------------------------------------------+
| Level   | Code | Message                                     |
+---------+------+---------------------------------------------+
| Warning | 1292 | Truncated incorrect DOUBLE value: '1.23.34' |
| Warning | 1292 | Truncated incorrect DOUBLE value: '1.23.34' |
+---------+------+---------------------------------------------+
2 rows in set (0.00 sec)

Преобразуването към DOUBLE и REAL произвежда различен резултат и същите предупреждения:

SELECT CAST('1.23.34' AS DOUBLE) cast_res, CONVERT('1.23.34', DOUBLE) conv_res;
SELECT CAST('1.23.34' AS REAL) cast_res, CONVERT('1.23.34', REAL) conv_res;


+----------+----------+
| cast_res | conv_res |
+----------+----------+
|     1.23 |     1.23 |
+----------+----------+
1 row in set, 2 warnings (0.00 sec)

CLONE команда

В продължение на много години MySQL администратори трябваше да разтоварват основни сървъри, прехвърлят архива по мрежата и го зареждат в реплики, за да инициализират състоянието им (виж Copy a Database from one Server to Another). Аз нямам много отношение към администрация на бази данни, но това беше тромава и досадна процедура особенно в случаите на невъзобновими грешки при репликация когато трябваше да инициализирам отново състоянието на репликите, така че го смятах за тежест. Защо не мога просто да "клонирам" основния сървър след почистване на данните в репликата? Е, в новата версия  е възможно лесно да се създават нови инстанции или инициализират наново същестуващи с разработената естесвена поддръжка на провизиране в сървъра с командата CLONE. Това става възможно с новия MySQL Clone Plugin. Можете да откриете повече за това като прочетете следните статии:
Това е фокуса на тази версии и съм сигурен, че ще промени значително работата на организации използващи обширано MySQL репликация.

Обезценяавния

Това е списък на възможносите, които биват обезценени с тази версия и ще бъдат премахнатеи в бъдещи версии:
  • Функцията FOUND_ROWS и модификатора на заявки SQL_CALC_FOUND_ROWS. Документацията предлага използването на COUNT(*) за да се намери броя редове.
  • Атрибути на числови типове данни:
    • Дължина за показване на целочислени типове данни. Вече се показва предупрежение "1681 Integer display width is deprecated and will be removed in a future release." ако се опитате да създадете таблици с INT(11) например. Аз имам доста такива определения както са били предоставени от mysqldump и MySQL Workbench, така че ще трябва да ги махна всички преди изразите да бъдат отхвърлени с грешка в бъдеще.
    • Атрибута ZEROFILL. Така или иначе никога не съм го ползвал.
    • Атрибута UNSIGNED за FLOAT, DOUBLE, и DECIMAL типове данни. Също не съм го ползвал никога.
    • AUTO_INCREMENT поддръжката за FLOAT и DOUBLE типове данни. Някой?
  • Синтаксиса  FLOAT(M,D) и DOUBLE(M,D) за указването на броя цифри за типове с плаваща запетая.
  • Логически оператори && (двоен амперсанд), който е синоним на AND, || (двоен пайп), който е синоним на OR и ! (удивителна), която е синоним на NOT. Не съм сигурен, че някога съм ги ползвал дори в ежедневни заявки, защото намирам ползването на AND,  OR и NOT за доста по-изразително.
  • Ключовата дума BINARY за указване на _bin колации. Никога не съм я ползвал също така.
Разработчиците трябва да вземат в предвид отърваването от тези нестандартни възможности, за да предотвратят неприятни изненади в бъдеще. Аз ще прегледам и поправя проектите си възможно най-скоро.

Оправени бъгове

Във връзка с CHECK ограничения открих два бъга в предходата версия и един от тях беше оправен (виж бъг #95189 CHECK constraint comparing columns is not always enforced with UPDATE queries). За пълен пример моля, виж bug_95189_test_case.sql скрипта, така че нека просто проверим:

SET binlog_format = 'STATEMENT';
SET binlog_row_image = 'minimal';


UPDATE tst SET end_date = '2019-04-20' WHERE id = 1;
/* Error Code: 3819. Check constraint 'chk_dat' is violated. */

Значи е оправен, защото очаквано UPDATE заявката пропада дори със специфичние настройки за двоичен журнал. Другият (виж bug #95192 CHECK constraint comparing column with default value is not enforced) ще трябва да почака.

Това завършва моя преглед на новите възможности в MySQL 8.0.17 версия по поддръжка. Надявам се новите версии да донесат повече възможноти за разработчици.

2019-04-30

Нови възможности в MySQL 8.0.16 екосистемата

Миналия Четвъртък (2019-04-25), точно преди почивните дни за Православния Великден Оракъл пусна версия 8.0.16 на продуктите в MySQL екосистемата (включваща сървър, рутер, шел, ксъединители и workbench ГПИ). Новите версии на сървъра продължиха традицията установена с MySQL 8 на въвеждане на нови възможности, въпреки че все още са обозначавани като "Издание по поддръжката" ("Maintenance Release"). На последния Pre-FOSDEM MySQL Day Giuseppe Maxia предложи да се ползват семантични версии и аз заставам зад това, защото разбирам и подкрепям семантичните версии.

Това ще бъде наистина полезно, защото версия 8.0.15 на сървъра беше истинско издание по поддръжката оправящо само важен проблем с групова репликация. Следвайки новата схема на номериране на версиите (виж MySQL 8.0: It Goes to 11!) всички други продукти от екосистемата бяха бутнати до 8.0.15 дори без каквито и да било промени. Наистина не съм сигурен, че това е нужно, защото докато се ползва същата голяма (и малка) версия не би трябвало да има проблем със съвместимостта. А и същите номера на версии не осигуряват наистина пълна съвместимост между продуктите както моите бъгове за MySQL Workbench (напр. 90620, 92900, 92908 и 94012) показват.

MySQL Server

E, какво е новото в новата версия на сървъра. Ето това което забелязах.

CHECK ограничения

Лично аз чакам тази възможност от MySQL 4 когато започнаха да добавят големи нови възможности в сървъра и той започна да става повече като по-напредналите бази с данни. Бъг #3465 беше отворен през Април 2004 (и беше обещано да бъде оправен в MySQL 5.1), така че тази SQL възможност най-накрая е направена след повече от 15 години.

До сега (както може би сте забелязали) ключовата дума CHECK в CREATE TABLE беше приемана, но безшумно пренебрегвана. В миналото ползвахме заобиколни решения. Например можеше да се осигурят само неотрицателни стойности в колона с числов тип (цели числа и типове с фиксирана/плаваща запетая) с използването на нестандартния атрибут UNSIGNED (виж Numeric Type Attributes). По-сложни ограничения можеха да бъдат направени с тригери и вдигане на състояния (виж SIGNAL syntax) както показах в представянето ми MySQL 8 for developers на Пролетния семинар на БГПО 2018 г. и подобни все още ще са нужни.

MySQL 8.0.16 вече поддържа и двете ограничения на ниво таблица и колона (виж CHECK constraints) за всички машини за съхранение. Ограничения на ниво таблица се поставят навсякъде в CREATE TABLE извън определенията на колони и могат да се обръщат към една или повече колони дори с предварително споменаване (т.е. на колони определени по-късно в израза). Ограниченията на ниво колона се поставят в определението на колоната и могат да се обръщат само към нея. За съжаление, израза който определя условието за проверка може да ползва само литерали, детерминистични вградени функции и оператори (т.е. вградени процедури, ОПФ/и, променливи и подзаявки не са разрешение). Не могат също да се ползват CHECK ограничения на колони с референтни действия от външни ключове (т.е. ON UPDATE, ON DELETE).

Нещото което трябва да се има в предвид с CHECK ограниченията е, че грешка има само ако условието се изчисли като FALSE. Ако са изчисли UNKNOWN поради NULL стойности грешка няма да има. Това е покрито много добре от Markus Winand в статията му The Three-Valued Logic of SQL, така че горещо ви препоръчвам да я прочетете цялата.

CHECK ограниченията имат име до 64 символа и ако не бъде указано сървъра ще създаде име като [име на таблица]_chk_[пореден номер], защото имената трябва да са уникални за схемата. Полезна възможност е да се създаде, но да не се налага ограничението (т.е. с NOT ENFORCED клауза), което е като разрешаване/забраняване за тригери което е възможност която искам да видя направена в бъдещи версии.

За основна употреба и прости примери за CHECK ограничения в MySQL, моля вижте наръчника и статията MySQL 8.0.16 Introducing CHECK constraint. По-интересен пример за CHECK ограничение върху JSON данни има в статията MySQL 8.0.16: how to validate JSON values in NoSQL with check constraint.

Ето първия ми пример. Мисля, че доста често има по две дати в таблица (напр. начална и крайна дата), които трябва да представляват началото и края на нещо, така че трябва да се в хронологичен ред. Нека да го направим с таблицата emp от примерната DEPT и EMP схема на Оракъл, която съм пригодил за MySQL.

ALTER TABLE emp
  ADD COLUMN retdate DATE AFTER hiredate,
  ADD CONSTRAINT ret_after_hire CHECK (
retdate > hiredate);

Обаче, когато се опитах да обновя ред на служител, който вече има стойност за дата на наемане със следната заявка (датата на пенсиониране предполагаемо идва от грешен потребителски вход и/или лошо приложение):

UPDATE emp
   SET retdate = STR_TO_DATE('1019-04-25', '%Y-%m-%d')
 WHERE empno = 7369;

1 row(s) affected Rows matched: 1  Changed: 1  Warnings: 0

заявката неочаквано успя и нямаше грешка за "нарушено ограничение". След обсъждане с Frédéric Deschamps в Slack подадох бъг #95189. Изглежда е свързано с репликация, защото на моя самостоятелен MySQL 8.0.16 сървър под Windows проблема не се възпроизведе и заявката работи както се очаква, но пробата която направих беше на MySQL 8.0.16 сървъра ми работещ под Slackware64 -current, който репликира от моя основен MySQL 5.7.26 сървър.

Ето друг пример. Ограничение на ниво таблица, което осигурява дата на наемане днес или в бъдеще и заплата, която е положително число.

ALTER TABLE emp
  ADD CONSTRAINT emp_chks CHECK (hiredate >= CURDATE() AND sal > 0);

Такова ограничение може да изглежда разумно (ако вземате в предвид само въвеждане на данни), но разбира се резултата е:

Error Code: 3814. An expression of a check constraint 'emp_chks' contains disallowed function: curdate.

защото CURDATE е недетерминистична функция. Такова ограничение е възможно в PostgreSQL (която също поддържа съхранени процедури и ОПФ/и), но не и в Oracle (която поддържа съхранени процедури) и MariaDB (която има горе-долу същите ограничения като MySQL, въпреки че не са ясно описани). Проблемите с това са как ще валидирате ограничението за съществуващите редове и как ще обновявате редове, защото стойността на CURDATE се сменя всеки ден. Решението е да се създаде допълнителна колона запазваща текущата дата при създаването на реда и да се ползва тя за проверка на ограничението.

Ако се опитате директно да създадете колоната и наложите ограничението (т.е. с ALTER TABLE) разбира се ще получите:

Error Code: 3819. Check constraint 'emp_chks' is violated.

защото CHECK ограничението, както другите ограничения (напр. първични и външни ключове, уникални индекси, NOT NULL) се валидират за всички редове при създаване и трябва винаги да остават валидни. Така, че нека пробвам другояче.

Първо, зареждам новата колона от съществуващите данни (напр. въз основа на hiredate колоната тъй като изрази са възможни като стойности по подразбиране от 8.0.13):

ALTER TABLE emp
  ADD COLUMN created DATE DEFAULT (hiredate);

След това, промяна на колоната и добавяне на CHECK ограничение:

ALTER TABLE emp
  MODIFY COLUMN created DATE NOT NULL DEFAULT (CURDATE()),
  ADD CONSTRAINT emp_chks CHECK (hiredate >= created AND sal > 0);

И сега да пробваме заявки:

UPDATE emp SET hiredate = '1979-04-25' WHERE empno = 7369;

INSERT INTO emp
  (empno, ename, job, mgr, hiredate, sal)
VALUES
  (9999, 'MULDER', 'INVESTIG.', 7839, '2019-04-25', 4242);

Очаквано UPDATE заявката предизвиква грешка:

Error Code: 3819. Check constraint 'emp_chks' is violated.

но INSERT заявката минава, което е неочаквано. Явно, стойността на колона created не е заредена със стойността по подразбиране когато CHECK ограничението се валидира. Пробвах същото (само, че с малко по-различен синтаксис) на Oracle XE 18 и работи както се очаква - и UPDATE и INSERT заявките нарушаваха CHECK ограничението.

Има нова таблица CHECK_CONSTRAINTS в INFORMATION_SCHEMA, която предоставя информация за създадените CHECK ограничения във всички схеми. Допълнителна информация за името на таблицата и дали CHECK ограничението е наложено или не може да се получи от таблицата TABLE_CONSTRAINTS чрез прецеждане по новата стойност CHECK на колоната CONSTRAINT_TYPE.

Пространствени данни

След ST_Distance от 8.0.14, сега ST_Length функцията също поддържа незадължителен втори параметър unit, така че е възможно да се изчисляват дължини в различните поддържани единици както са определени в INFORMATION_SCHEMA.ST_UNITS_OF_MEASURE таблицата.

Сървъра прави цялостно надграждане

Сървъра вече може да надгражда mysql схемата, речника на данните и системните таблици както и PERFORMANCE_SCHEMA, INFORMATION_SCHEMA, sys и потребителски схеми (виж What the MySQL Upgrade Process Upgrades), така че mysql_upgrade командата се пенсионира. Това е важна административна възможност, защото тя ще направи надгражданията по-лесни и по-удобни. В миналото постоянно забравях да пусна командата, което водеше до странни проблеми по-късно.

Във връзка с надграждането имах странен проблем изразяващ се в това, че сървъра не можеше да стартира и печаташе съобщение [ERROR] [MY-013384] [Server] Could not create server upgrade info file at '/var/lib/mysql/data/' в лога въпреки, че правата бяха наред. Успях да намеря този gist mysql will not start after 8.0.15 to 8.0.16 update on Ubuntu 16.04 LTS с Google и след като създадох файла mysql_upgrade_info в /var/lib/mysql/data/ и след като прехвърлих собствеността му на mysql:mysql сървъра успя да запали успешно. Надграждах от 8.0.14 всъщност, но мисля, че може да е бъг. Вероятно сървъра очаква файла да е бил създаден.

Друга интересна нова възможност е избора --validate-config за проверка на конфигурацията на сървъра както администраторите са свикнали с други сървъри (напр. Apache). Това е наистина важно особено за среди в продукция, където неочаквания престой може да е крайно неприятен. Прочетете повече в Server Configuration Validation.

Системни потребители

MySQL сметки вече се категоризират и така разграничават като системни (които притежават SYSTEM_USER привилегия) и обикновени потребители (които не притежават). Това допринася за по-добро разделение на ролите, тъй като само системни потребители могат да извършват определени административни операции върху системни сметки. Преди това всеки потребител с подходящите привилегии можеше например да изтрие всяка сметка или убие всяка връзка принадлежаща на всеки потребител. Възможно е също така да се отнемат глобални привилегии частично за определени схеми (т.е. като изключения) чрез новата системна променлива partial_revokes, което преди това извикваше задаването на права поотделно за всички съществуващи схеми и добавяне на пава за всяка нова схема.

Сигурност

Новите неща са поддръжка за TLS 1.3, възможност за обновяване на SSL сертификатите без рестартиране на сървъра (виж ALTER INSTANCE RELOAD TLS) и информация за сертификатите в таблицата keyring_keys на PERFORMANCE_SCHEMA.

MySQL Router

Рутера вече има HTTP съставка, която му позволява да излага просто web-интерфейс и REST ППИта с цел да осигури по-добра наблюдателност чрез интеграция с външни инструменти за наблюдение и управление. Други значими промени са динамичната смяна между режими с един и много господари и подобрен журнал. Сега има WITH_ROUTER CMake избор за изграждане на рутер заедно с MySQL сървъра, която по подразбиране е ON и която реших да сменя на OFF, защото планирам да продължа да изграждам рутер като отделен пакет.

MySQL Shell

Шела идва с новия Shell Reporting Framework (виж Reporting with MySQL Shell), който позволява записването, показването и наблюдението на потребителски доклади. Нетърпелив съм да я пробвам и ще напиша отделна публикация по-късно. Вижте статията MySQL Shell 8.0.16: User Defined Reports от Jesper Wisborg Krogh. Също така сега вече е възможно да се изпълнява SQL без смяна на режима, тъй като \sql командата вече не сменя режима ако ѝ е подаден SQL израз, което мисля е доста полезно.

MySQL Workbench

MySQL Workbench вече поддържа прозоречни функции в SELECT (виж бъг #90620), изрази в DEFAULT (виж бъг #92900) и като части на ключове (виж бъг #92908) както и ключовата дума LATERAL (виж бъг #94012) - всички те достъпни от предходните 8.0.x версии на сървъра. За съжаление, не виждам каквато и да е поддръжка за CHECK ограничения (виж бъг #95143, който вече беше потвърден), така че отново еднаквите номера на версии не значат нищо.

2019-01-23

Нови възможности за разработчци в MySQL 8.0.14

С пускането на MySQL 8.0.14 Oracle запазва вече установената практика да въвежда нови възможности за разработчици дори с версии за поддръжка, които обикновено съдържат само малки подобрения и най-вече поправки на бъгове. Разгледах бележките към версията на 8.0.14, публикацията The MySQL 8.0.14 Maintenance Release is Generally Available на и разбира се наръчника, експериментирах и тук отдолу е моя избор на нови възможности свързани с разработка.

Латерални производни таблици (Lateral derived tables)

Преди MySQL 8.0.14 не беше възможно за производни таблици да се обръщат към (зависят от) колони на предходните таблици в FROM клаузата. Сега това ограничение е премахнато с добавянето на ключова дума LATERAL (виж Lateral derived tables). Ключовата дума LATERAL означава, че производната таблица зависи от предходната таблица от ляво. Можете да имате повече от една LATERAL производна таблица в заявка и всяка ще зависи само от предходната таблица или производна таблица. Латералните производни таблици са така наречения "for each" цикъл на SQL и това прави възможни някои операции, които иначе не са възможни или са по-малко ефикасни.
Ето един пример. Да кажем, че искате да изчислите минималната, средната и максималната заплата за всеки отдел в организацията. Преди трябваше да го напишете така:
План за изпълнение на заявката с производна таблица

SELECT D.dname, DT.min_sal, DT.avg_sal, DT.max_sal
  FROM dept D

       LEFT JOIN
       (SELECT E.deptno, MIN(E.sal) min_sal, AVG(E.sal) avg_sal, MAX(E.sal) max_sal
          FROM emp E
         GROUP BY E.deptno
       ) AS DT
       ON DT.deptno = D.deptno;


И така да използвате производна таблица DT, за да изчислите мин/сред/макс заплата за всички отдели от таблицата emp и тогава съедините с таблица dept получавайки следния резултат:

+------------+---------+-------------+---------+
| dname      | min_sal | avg_sal     | max_sal |
+------------+---------+-------------+---------+
| ACCOUNTING | 1300.00 | 2916.666667 | 5000.00 |
| RESEARCH   |  800.00 | 2175.000000 | 3000.00 |
| SALES      |  950.00 | 1566.666667 | 2850.00 |

| OPERATIONS |         |             |         |
+------------+---------+-------------+---------+
4 rows in set (0.0014 sec)


Производната таблица е напълно независима от другата съединена таблица, тъй като може да произведе резултат сама (т.е. не зависи от стойностите на колоните на другата таблица). Плана за изпълнение на тази заявка е даден от дясно и той потвърждава, че резултата на производната таблица първо бива материализиран, за да може да бъде съединен с другата таблица.
Друг подход би бил с използване на подзаявки в SELECT клаузата ето така:

SELECT D.dname,
       (SELECT MIN(E.sal) FROM emp E WHERE E.deptno = D.deptno) AS min_sal,
       (SELECT AVG(E.sal) FROM emp E WHERE E.deptno = D.deptno) AS avg_sal,
       (SELECT MAX(E.sal) FROM emp E WHERE E.deptno = D.deptno) AS max_sal
  FROM dept D;


което няма да бъде ефикасно (представете си таблица с продажби и хиляди търговци ако искате да оцените техните продажби), защото три заявки ще трябва да вършат работата на една. Не е възможно да се ползва само една подзаявка за изчисляване на всички необходими стойности в SELECT, защото такива подзаявки трябва да са скаларни. Подобна заявка ще предизвика грешка Error Code: 1241. Operand should contain 1 column(s) ако пробвате.
Ако се опитате да свържете производната таблица към другата таблица с заявка като следната:

SELECT D.dname, DT.min_sal, DT.avg_sal, DT.max_sal
  FROM dept D,
       (SELECT MIN(E.sal) min_sal, AVG(E.sal) avg_sal, MAX(E.sal) max_sal
          FROM emp E
         WHERE E.deptno = D.deptno
       ) AS DT;


ще получите грешка Error Code: 1054. Unknown column 'D.deptno' in 'where clause', защото таблица D не е позната на производната таблица. Заявката е незаконна в SQL-92, но в SQL-1999 става законна ако производната таблица се предшества от ключовата дума LATERAL:

SELECT D.dname, LDT.min_sal, LDT.avg_sal, LDT.max_sal
  FROM dept D,
       LATERAL
       (SELECT MIN(E.sal) min_sal, AVG(E.sal) avg_sal, MAX(E.sal) max_sal
          FROM emp E
         WHERE E.deptno = D.deptno
       ) AS LDT;

План за изпълнение на заявката с латерална производна таблица

и създава следния резултат:

+------------+---------+-------------+---------+
| dname      | min_sal | avg_sal     | max_sal |
+------------+---------+-------------+---------+
| ACCOUNTING | 1300.00 | 2916.666667 | 5000.00 |
| RESEARCH   |  800.00 | 2175.000000 | 3000.00 |
| SALES      |  950.00 | 1566.666667 | 2850.00 |
| OPERATIONS |    NULL |        NULL |    NULL |
+------------+---------+-------------+---------+
4 rows in set (0.1182 sec)


Както се вижда от графиката на плана за изпълнение отдясно в този случай няма групиране, но MySQL дава по-висока цена, защото достъпа до производната таблица е чрез пълно сканиране на таблица. По-интересната информация обаче е в табличния план за изпълнение (колона partitions е преднамерено скрита):

+----+-------------------+------------++------+---------------+-----------+---------+-------------------+------+----------+----------------------------+
| id | select_type       | table      || type | possible_keys | key       | key_len | ref               | rows | filtered | Extra                      |
+----+-------------------+------------++------+---------------+-----------+---------+-------------------+------+----------+----------------------------+
|  1 | PRIMARY           | D          || ALL  | NULL          | NULL      | NULL    | NULL              |    4 |      100 | Rematerialize (<derived2>) |
|  1 | PRIMARY           | <derived2> || ALL  | NULL          | NULL      | NULL    | NULL              |    2 |      100 | NULL                       |
|  2 | DEPENDENT DERIVED | E          || ref  | fk_deptno     | fk_deptno | 5       | dept_emp.D.deptno |    4 |      100 | NULL                       |
+----+-------------------+------------++------+---------------+-----------+---------+-------------------+------+----------+----------------------------+
3 rows in set, 2 warnings (0.0010 sec)
Note (code 1276): Field or reference 'dept_emp.D.deptno' of SELECT #2 was resolved in SELECT #1
Note (code 1003): /* select#1 */ select `dept_emp`.`d`.`dname` AS `dname`,`ldt`.`min_sal` AS `min_sal`,`ldt`.`avg_sal` AS `avg_sal`,`ldt`.`max_sal` AS `max_sal` from `dept_emp`.`dept` `d` join lateral (/* select#2 */ select min(`dept_emp`.`e`.`sal`) AS `min_sal`,avg(`dept_emp`.`e`.`sal`) AS `avg_sal`,max(`dept_emp`.`e`.`sal`) AS `max_sal` from `dept_emp`.`emp` `e` where (`dept_emp`.`e`.`deptno` = `dept_emp`.`d`.`deptno`)) `ldt`

Има две нови информация и допълнителна бележка. Плана ясно показва, че производната таблица E (derived2) е зависима (DEPENDENT) от другата таблица и че тя се материализира отново за всеки ред от D (виж EXPLAIN extra information). Това е причината поради която латералните производни таблици са познати също като "for each" цикъла на SQL. Бележката дава информация за това как външното позоваване в производната таблица е разрешено.

Разбира се MySQL Workbench (дори надграден до 8.0.14 също) отново не е запознат с новия синтаксис (виж предишната ми публикация Нови възможности за разработчици в MySQL 8.0.13), защото не оцветява правилно новата ключова дума и показва грешка в SQL редактора точно след нея. Докладвах това като бъг 94012, но нямам много надежда, тъй като 90620, 92900 и 92908 бяха потвърдени, но са все още отворени. Номерата на версиите нямат голямо значение в днешни дни :-)

Моля, обърнете внимание, че е възможно да се направи връзка с външната таблица ако производната таблица е в подзаявка (виж пример в WL#461).

Агрегатни JSON функции вече може да се ползват като прозоречни функции

Вече е възможно да се ползват агрегатни функции JSON_ARRAYAGG и JSON_OBJECTAGG като прозоречни функции с използването на OVER клауза (виж Window Function Concepts and Syntax). Това прави всички (освен COUNT(DISTINCT) и GROUP_CONCAT) от агрегатните функции възможни за употреба като прозоречни функции след като побитовите AND/OR/XOR функции бяха направени такива с MySQL 8.0.12. Ето един пример:

SELECT E.ename, E.sal,
       AVG(E.sal) OVER dw AS avg_sal,
       JSON_OBJECTAGG(D.dname, E.sal) OVER dw AS dept_sal
  FROM emp  E,
       dept D
 WHERE E.deptno = D.deptno
WINDOW dw AS (PARTITION BY D.deptno);


+--------+---------+-------------+------------------------+
| ename  | sal     | avg_sal     | dept_sal               |
+--------+---------+-------------+------------------------+
| CLARK  | 2450.00 | 2916.666667 | {"ACCOUNTING": 1300.0} |
| KING   | 5000.00 | 2916.666667 | {"ACCOUNTING": 1300.0} |
| MILLER | 1300.00 | 2916.666667 | {"ACCOUNTING": 1300.0} |
| SMITH  |  800.00 | 2175.000000 | {"RESEARCH": 3000.0}   |
| JONES  | 2975.00 | 2175.000000 | {"RESEARCH": 3000.0}   |
| SCOTT  | 3000.00 | 2175.000000 | {"RESEARCH": 3000.0}   |
| ADAMS  | 1100.00 | 2175.000000 | {"RESEARCH": 3000.0}   |
| FORD   | 3000.00 | 2175.000000 | {"RESEARCH": 3000.0}   |
| ALLEN  | 1600.00 | 1566.666667 | {"SALES": 950.0}       |
| WARD   | 1250.00 | 1566.666667 | {"SALES": 950.0}       |
| MARTIN | 1250.00 | 1566.666667 | {"SALES": 950.0}       |
| BLAKE  | 2850.00 | 1566.666667 | {"SALES": 950.0}       |
| TURNER | 1500.00 | 1566.666667 | {"SALES": 950.0}       |
| JAMES  |  950.00 | 1566.666667 | {"SALES": 950.0}       |
+--------+---------+-------------+------------------------+
14 rows in set (0.0021 sec)


Важно е да се отбележи, че MySQL не разрешава повторение на ключове в JSON типа данни, така че в прозорец без подредба функцията JSON_OBJECTAGG ще върне последната стойност за ключа, което може да е неопределено.

Подобрения по X протокол

Според бележките към версията данните вече винаги се обръщат в символния набор utf8mb4 (с използване на utf8mb4_general_ci колация). Другото забележително подобрение е поддръжката на функционалност за подготвяне на заявки. Бележките към версията не предоставят препратка към тази нова функционалност, но е споменал WL#9270 в своята статия, така че вярвам става въпрос за подготвяне на CRUD операции (виж Preparing CRUD Statements). Един прост пример на JavaScript ще бъде следното:

MySQL Shell 8.0.14
Copyright (c) 2016, 2019, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its affiliates.
Other names may be trademarks of their respective owners.
 MySQL  JS > \connect user@localhost
Creating a session to 'user@localhost'
Your MySQL connection id is 22 (X protocol)
Server version: 8.0.14 MySQL Community Server - GPL
 MySQL  localhost:33060+ ssl  JS > \use test
Default schema `test` accessible through db.
 MySQL  localhost:33060+ ssl  test  JS > var usr = db.createCollection('users')
 MySQL  localhost:33060+ ssl  test  JS > usr.add({name:"User1",age:15})
Query OK, 1 item affected (0.0108 sec)
 MySQL  localhost:33060+ ssl  test  JS > usr.add({name:"User2",age:17})
Query OK, 1 item affected (0.0138 sec)
 MySQL  localhost:33060+ ssl  test  JS > usr.add({name:"User3",age:20})
Query OK, 1 item affected (0.0105 sec)
 MySQL  localhost:33060+ ssl  test  JS > usr.add({name:"User4",age:19})
Query OK, 1 item affected (0.0137 sec)
 MySQL  localhost:33060+ ssl  test  JS > usr.add({name:"User5",age:16})
Query OK, 1 item affected (0.0118 sec)
 MySQL  localhost:33060+ ssl  test  JS > usr.find()
[
    {"_id": "00005c46e7e5000000000000000a","age": 15,"name": "User1"},
    {"_id": "00005c46e7e5000000000000000e","age": 17,"name": "User2"},
    {"_id": "00005c46e7e5000000000000000f","age": 20,"name": "User3"},
    {"_id": "00005c46e7e50000000000000010","age": 19,"name": "User4"},
    {"_id": "00005c46e7e50000000000000011","age": 16,"name": "User5"}
]
5 documents in set (0.0006 sec)
 MySQL  localhost:33060+ ssl  test  JS > var fcmd = usr.find('age >= :page')
 MySQL  localhost:33060+ ssl  test  JS > fcmd.bind('page', 18)
[
    {"_id": "00005c46e7e5000000000000000f","age": 20,"name": "User3"},
    {"_id": "00005c46e7e50000000000000010","age": 19,"name": "User4"}
]
2 documents in set (0.0006 sec)
 MySQL  localhost:33060+ ssl  test  JS > fcmd.bind('page', 16)
[
    {"_id": "00005c46e7e5000000000000000e","age": 17,"name": "User2"},
    {"_id": "00005c46e7e5000000000000000f","age": 20,"name": "User3"},
    {"_id": "00005c46e7e50000000000000010","age": 19,"name": "User4"},
    {"_id": "00005c46e7e50000000000000011","age": 16,"name": "User5"}
]
4 documents in set (0.0003 sec)

 MySQL  localhost:33060+ ssl  test  JS > fcmd.bind('page', 17)
[
    {"_id": "00005c46e7e5000000000000000e","age": 17,"name": "User2"},
    {"_id": "00005c46e7e5000000000000000f","age": 20,"name": "User3"},
    {"_id": "00005c46e7e50000000000000010","age": 19,"name": "User4"}
]
3 documents in set (0.0004 sec)


римера създава колекция от потребители с техните имена и възраст, после печата цялата колекция. Интересната част започва с реда подчертан в жълто. Той подготвя израз използвайки именуван параметър (анонимни параметри с ? не се поддържат от X протокола), но не го изпълнява. Изпълнението се случва след като се свърже стойност към параметъра и това може да се прави много пъти произвеждайки различни резултати. Интересно е, че в общия журнал първото свързване всъщност изпълнява заявка, след това има подготовка и след това има изпълнения:

Query    SELECT doc FROM `test`.`users` -> usr.find()
Query    SELECT doc FROM `test`.`users` WHERE (JSON_EXTRACT(doc,'$.age') >= 18) -> fcmd.bind('page', 18)
Prepare    SELECT doc FROM `test`.`users` WHERE (JSON_EXTRACT(doc,'$.age') >= ?)
Execute    SELECT doc FROM `test`.`users` WHERE (JSON_EXTRACT(doc,'$.age') >= 16) -> fcmd.bind('page', 16)
Execute    SELECT doc FROM `test`.`users` WHERE (JSON_EXTRACT(doc,'$.age') >= 17) -> fcmd.bind('page', 17)


Използването на подготвени изразни за многократно изпълнявани изрази може да доведе до подобрения в производителността заради спестеното време за синтактичен разбор на заявката, затова това е нещо което трябва да имате в предвид ако трябва да подобрите производителността на приложенията и скриптовете си.

Подобрения в работа с пространствени данни

Функцията ST_Distance вече приема като незадължителен трети параметър мерната единица за връщаната стойност. Възможните стойности са определени в таблица INFORMATION_SCHEMA.ST_UNITS_OF_MEASURE заедно с коефициент за превръщане към основната единица метър (metre), което е и стойността по подразбиране. Ето един пример за изчисляване на разстоянието между София и Сидни в километри и морски мили в SRID 4326:

SELECT ST_Distance(ST_PointFromText('POINT( 42.69751 23.32415)', 4326),
                   ST_PointFromText('POINT(-33.86667 151.20000)', 4326)) / 1000 dist_km;
+--------------------+
| dist_km            |
+--------------------+
| 15431.933058990671 |
+--------------------+
1 row in set (0.0023 sec)


SELECT ST_Distance(ST_PointFromText('POINT( 42.69751 23.32415)', 4326),
                   ST_PointFromText('POINT(-33.86667 151.20000)', 4326), 'nautical mile') dist_nm;


+------------------+
| dist_nm          |
+------------------+
| 8332.57724567531 |
+------------------+
1 row in set (0.0008 sec)

Това завършва моя преглед. Има разбира се много повече в MySQL 8.0.14 не само за разработчици, така че ви насърчавам да изследвате и откриете повече. Препратките в началото на тази статия са добри като начало.

2019-01-10

Нещата които научих за себе си през 2018

За мен 2018-та беше година на надежди, някои от които не се осъществиха. Надявах се на някои промени, но в крайна сметка те не се случиха, в което разбира се няма нищо трагично, тъй като успях да науча някои неща за себе си. В тази статия поглеждам назад към това, което научих за себе си през изминалата година.

Нямам достатъчно опит с отворен код

Това ми беше казано някъде в началото на Март и въобще не възразявам. Допринесъл съм малки кръпки, преводи и буболечки към различни проекти с отворен код в изминалите около 20 години, публикувал съм някои от личните си проекти под отворени лицензи, но ми се иска да бях направил повече. Вярно е, че можех да допринеса повече, но за съжаление просто не намерих времето за това. Все още силно вярвам в софтуера със свободен и отворен код (FOSS), който използвам от университетските ми години, все още използвам и ще продължавам да използвам и за напред. Обаче, в днешни дни не съм толкова придирчив за това, че FOSS трябва също така да е безплатен като в "безплатна бира", което ме приближава към философията на GNU.

Не чета достатъчно/вашите книги

Беше ми предложено от някои да чета повече книги. Да, вероятно мога да чета повече, но все пак чета. И може да не чета същото като вас и вие не сте тези, които ще ми кажете какво да чета както и аз не казвам на вас. Освен книги чета също и някои списания както съм го правил от тийнейджърските си години. И някои неща в мрежата. С тази публикация искам да благодаря на семейството си и приятелите, които ми подариха книги миналата година, така че сега имам към 10 книги в списъка си за 2019. Съжалявам, списъка е лична информация ;-)

Нямам достатъчно опит с тази или онази технология

Научих това през лятото. Все още се смятам за любопитен човек относно технологията, но просто няма как да имам опит с всичко там и не вярвам, че някой може. За мен е по-интересно как човек възприема технологиите и как се справя с промените. Преди две десетилетия софтуера беше доста различен както и инструментите и технологиите, с които се правеше. Темпото на промените се увеличи както и хората заети в разработката на софтуер. Има много хора започнали да разработват софтуер преди едно, две, три десетилетия и повече, които лесно се приспособяват към нови инструменти и технологии, защото имат основни разбирания за това как се прави и работи софтуера. И което е по-важно те знаят, че трябва да учат постоянно и да се включат. Това означава да четат наръчниците, да следят и се възползват от промените, да обсъждат, да докладват буболечки, да правят предложения за нови функционалности, да допринасят колкото им е възможно и като цяло да се включат в общността.

Не съм гъвкав

Това е нещо което ми беше казано преди Коледните празници. Да, вероятно, не съм толкова гъвкав все пак. Запознат съм и разбирам ценностите и принципите на гъвкавата методология, но в същото време не ги споделям напълно.
  • Съгласен съм, че хората и взаимодействията се по-важни от процесите и инструментите, но само второто може да направи първото лесно и ефективно. Без ясни процеси и правилните инструменти дори най-добрите ще се провалят да взаимодействат по начин, който облагодетелства разработката на софтуер. Вярвам, че такива хора първо ще създадат необходимите процеси и инструменти.
  • Съгласен съм, че работещия софтуер е по-важен от подробната документация, но съм виждал софтуер написан с оскъдна или въобще никаква документация. След известно време (напр. 5, 10, 15 години) когато такъв софтуер спре да работи, трябва да бъде надграден или да бъде обяснен на клиент, той първо трябва да мине през процес на обратно инженерство, тъй като най-вероятно първоначалните разработчици са напуснали отдавна компанията. Затова в краткосрочен план и за малки проекти напълно поддържам това, но в дългосрочен план и за големи проекти не мисля, че помага на някого. Проблемът е, че ако не се напише навреме документацията най-вероятно никога няма да бъде написана.
  • Съгласен съм, че сътрудничеството с клиента е по-важно от прегорите по договора, но не всички клиенти могат да сътрудничат ефективно. Някои клиенти дори не знаят какво точно искат, не четат документи (и дори отказват да го правят когато са помолени) съответно не познават софтуера, който ползват и не прехвърлят правилно знанието през времето. Това са само моите скромни наблюдения, но вярвам, че сътрудничеството с клиента трябва да бъде отворено в смисъл, че трябва да бъде достъпно до всички заинтересовани страни сега и в бъдеще. Виждал съм случаи, в които софтуера е разработен в "тясно сътрудничество" между разработчик и човек от страна на клиента чрез лични съобщения, за който след това никой не знае нищо и не иска да знае.
  • Съгласен съм, че реакцията на промените е по-важно от следването на план, но реакцията на постоянни промени означава, че няма план и обхват на проекта въобще. Проектите, които започват като мехурче и постоянно се променят лесно се деформират и излизат извън контрол. Такива проекти обикновено се провалят. Виждал съм някои никога не свършващи проекти в практиката си и това е едно от нещата, които наистина не харесвам.
Тези и вероятно други причини карат организациите да избират хибридни подходи в разработката на софтуер, защото намират гъвкавата методология за твърде крайна и неефективна в големи организации, а аз съм работил в такива в последните повече от 15 години, така че вероятно възгледите ми са изкривени от тази перспектива.

Аз съм 'напреднал потребител'

Това ми беше писано от представител на поддръжката. Случи се след като поисках (и настоях) за някои прости (според мен) подобрения в софтуера предоставян и поддържан от голям доставчик, за който той работеше. То го написа в смисъл, че само аз искам тези подобрения, от което автоматично ми стана ясно, че те няма да бъдат направени. Е, ако това ме определя като "напреднал потребител", така да бъде, но съм доста разочарован, защото очаквах повече от SOHO устройство използващо софтуер със свободен и отворен код. Може да напиша отделна статия за това по-късно.

Аз съм себе си

Накрая, но не на последно място аз все още съм себе си с всичките си предимства и недостатъци като човек и професионалист. Нямам претенции да съм всичко за всеки, нямам претенции, че знам всичко и нямам претенции да съм най-опитния там. Всичко, което научих за себе си през 2018-та, беше основано на някакъв опит и съм сигурен, че ще ми помогне да стана по-добър човек и професионалист в бъдеще.