Unicode FAQ – InterBase, Firebird, Delphi, C++Builder – 2009, 2010, XE ... XE7

Составитель: KDV, 12.12.2008, последнее обновление – 15.03.2015.
Информация предоставлена: dimitr, hvlad
Благодарности: shmel, Janex, Таблоид, shaposh
 

Unicode. Что это и зачем это нужно?

Unicode – это способ представления разных национальных символов в универсальном формате. На вопрос "зачем это нужно" можно ответить следующим образом – если вы не планируете переводить ваши приложения на другие национальные языки (немецкий, французский и т. п.), то Unicode вам не нужен. В этом случае вы спокойно используете базу данных в WIN1251, с аналогичным параметром коннекта, и работаете как и раньше.

Если же планируется (даже только в перспективе) создавать приложения, работающие одновременно с несколькими кодовыми страницами, или с многобайтовыми кодировками, то тогда Unicode – ваш вариант. Также крайне рекомендуется для чтения электронные книги, доступные бесплатно покупателям Delphi 2009, 2010 и XE:

 

Какие операционные системы поддерживают Unicode?

Windows – все последние, кроме 95, 98, ME. В этих ОС поддержка unicode неполная, поэтому, в частности, утверждается, что Delphi 2009, 2010, XE и XE2 и приложения, на них написанные, не будут работать в Windows 95, 98 и ME.

Linux – см. Unicode FAQ.
 

Что нужно для работы с Unicode?

Для начала нужна СУБД с поддержкой Unicode. Firebird с версии 2.0 и InterBase с версии 2007 поддерживают кодировку UTF-8, которая является полноценной реализацией формата для хранения Unicode-символов.

Далее, потребуется среда разработки и компоненты или драйверы, поддерживающие Unicode.

Первые среды разработки с полной поддержкой Unicode – Delphi и C++Builder начиная с версии 2009 (и далее 2010, XE-XE7). Если вы пока еще используете предыдущие версии Delphi и BCB (2007 и ниже), то использование unicode будет проблематичным – вам придется не только использовать специальные компоненты (вроде TMS), но и компоненты доступа к БД или драйверы, которые также поддерживают Unicode.
 

С каких версий InterBase и Firebird поддерживают UTF8?

InterBase – с 2007. Firebird – с 2.0.

При этом, разумеется, база данных с поддержкой UTF8 может быть создана только в этих версиях, поскольку UTF8 поддерживается не только кодом сервера, но и таблицей rdb$character_sets в базе данных.

Например, если взять базу данных от Firebird 1.5, и открыть в Firebird 2.x, то поддержки UTF8 в этой базе данных не будет. Чтобы возможность использовать в БД UTF8 появилась, нужно ей сделать backup и restore (при restore будет создана новая БД в новом формате со старыми данными).
 

Поддержка UTF8 в InterBase и Firebird одинаковая?

К сожалению, нет. Причем, неодинаковых аспектов много.

В rdb$character_sets кодировка UTF8 имеет в Firebird идентификатор 4 (это был свободный номер рядом с давно имеющейся в IB и FB кодировкой UNICODE_FSS с ID = 3), а в InterBase – 59. Код 59 в Firebird имеет кодировка WIN1256. То есть, разработчики InterBase не ставили перед собой вопрос обеспечения совместимости с Firebird в этом плане.

Начиная с Firebird 1.5 при получении строковых данных CHAR и VARCHAR с сервера, если это не кодировки NONE или OCTETS, и если кодировка коннекта не NONE, то в sqlsubtype передается: в старшем байте код collate, а в младшем – код character set.

В InterBase также передается код collate в sqlsubtype, однако, с какой именно версии, неизвестно. Возможно, с 2009, т. к. IBX в Delphi 2010 содержит изменения на эту тему (см. в конце FAQ).
 

Чем отличаются кодировки UNICODE_FSS и UTF8?

Это несколько разные реализации поддержки Unicode.

UNICODE_FSS реализует более старую версию стандарта Unicode, которая ограничена 3-мя байтами на символ (поэтому, теоретически, поддерживает не все языки), для нее нет алфавитных наборов сортировок, в версиях ниже Firebird 2.5 для нее не проверяется корректность введенного текста с точки зрения unicode.

UTF8 поддерживает последнюю версию стандарта Unicode, до 4 байт на символ, все вышеперечисленные недостатки отсутствуют.
 

А можно использовать UNICODE_FSS вместо или если нет UTF8?

Только если вы не предполагаете переходить на современные версии InterBase и Firebird, которые поддерживают UTF8. Недостатки UNICODE_FSS перечислены в предыдущих пунктах.
 

Как создать базу в юникоде?

Для этого достаточно при создании БД указать character set UTF8. После этого по умолчанию все создаваемые строковые поля и переменные (varchar, char) таблиц, процедур, триггеров и т.д. будут иметь кодировку UTF8, если вы не укажете другую специально.
 

Как сохранять данные в разных кодировках?

Есть несколько вариантов:
  • Старый и неудобный: можно делать с любыми версиями InterBase и Firebird: создаете столбец в таблице, указывая нужный character set, затем при подсоединении к БД указываете этот же чарсет. Не очень удобно, потому что для каждого языка (чарсета) приложение должно показывать пользователю свой "набор" строковых столбцов.
  • Современный и простой: создать базу в UTF8 и сохранять данные либо в конкретном character set, либо сразу передавать данные в UTF8. Для передачи данных на сервер в UTF8 нужно, чтобы драйверы или компоненты доступа делали это "прозрачно" для приложения. См. пункт.
 

Я могу работать с базой в UTF8 через WIN1251?

Разумеется, для этого достаточно указать чарсет соединения WIN1251. Данные будут идти на сервер в 1251, и автоматически перекодироваться в UTF8 при сохранении (при чтении – перекодироваться обратно в win1251). Это самый легкий вариант начала работы с юникодом. Также это подходящий вариант, если используете Delphi версии 2007 и ниже, и вы не хотите использовать никакие компоненты Unicode (например, tms), но планы перехода на Unicode есть.

Кстати, это не специальная особенность WIN1251 и UTF8. Вы можете использовать любую национальную кодировку точно таким же образом.
 

А говорят, что в Unicode строки занимают больше места?

Да, причем зависит от самих символов, которые находятся в строках.

UTF8 это формат с плавающим размером символа, от 1 до 4 байт. В частности, символы русских букв в 1251 занимают по 2 байта, а "стандартные" английские буквы с кодом от 32 до 127 занимают 1 байт.

Пример. Создадим таблицу со столбцом varchar(30) в win1251, и еще таблицу со столбцом varchar(30) в utf8. Зальем в первую таблицу 100 тысяч записей, со случайными символами в кодировке win1251 от А до я, длиной от 10 до 30 символов. Затем перельем (insert into ... select from) эти данные во вторую таблицу. При этом данные корректно отконвертируются из 1251 в utf8. В итоге, обе таблицы в IBAnalyst будут показаны как:
 
Table Records RecLength VerLen Versions Max Vers Data Pages Size, mb Slots Avg fill% RealFill
X1251 100000 28.86 0.00 0 0 852 6.66 852 66 66
XUTF8 100000 49.01 0.00 0 0 1094 8.55 1094 74 74
Интерпретировать результат можно следующим образом:
  • вторая половина кодовой таблицы символов win1251 сохраняется в utf8 в двух байтах. Первая – в одном. В тесте были использованы символы из второй половины кодовой таблицы.
  • размер таблицы (столбца) в utf8 примерно 2 раза больше, чем размер таблицы (столбца) в win1251. Чуть меньше чем в 2 раза потому, что используется сжатие заголовков записей, и т. п. То есть, будучи взятыми "чистыми", данные в utf8 для русских букв win1251 будут занимать в 2 раза больше места, чем в win1251.
Так что, в зависимости от кодировки, данные могут занимать в БД разный объем.
 

Максимальный размер ключа индекса

Ранее в InterBase и в Firebird размер строкового столбца, по которому можно построить индекс, например для кодировки WIN1251 был ограничен 252 символа без collate, и 84 символа с collate (об этом написано в «О работе с русскими буквами в InterBase/Firebird»).

В Firebird 2.0 и InterBase XE допустимый размер такого столбца был увеличен, и сейчас представляет собой 1/4 размера страницы. Например, при странице 4096 байт максимальный размер индексируемого строкового столбца равен 1024.

Однако, с кодировками, где количество байт на символ выше 1, это не так. Для столбца в кодировке UTF8 и collate unicode_ci при странице 4096 байт максимальный размер составляет 169 символов, поскольку при сортировках ci один символ кодируется 6 байтами. Для страницы 8192 и 16384 байт максимальный размер индексируемого с таким collate столбца будет, соответственно, в 2 и 4 раза больше.
 

Длина строковых столбцов и изменение ее размера

В зависимости от кодировки символы могут занимать в строке 1 и более байт. Для кодировок NONE, WIN1251 и т.п. это действительно 1 байт. Поэтому, если в char(15) или varchar(15) находится слово из 10 букв, то это слово будет занимать 10 байт. Но для некоторых кодировок это может быть и 2, 3, 4 байта на символ. Количество байт на символ в конкретной кодировке записано в системной таблице RDB$CHARACTER_SETS, проще всего посмотреть запросом
select rdb$character_set_id, rdb$character_set_name, rdb$bytes_per_character
from rdb$character_sets

Результат будет примерно таким (примерно потому, что идентификатор кодировки UTF8 и некоторых других, как и набор кодировок, у Firebird и InterBase отличаются, см. этот FAQ далее)
 
RDB$CHARACTER_SET_ID RDB$CHARACTER_SET_NAME RDB$BYTES_PER_CHARACTER
0 NONE 1
1 OCTETS 1
2 ASCII 1
3 UNICODE_FSS 3
4 UTF8 4
48 DOS866 1
51 WIN1250 1
52 WIN1251 1
53 WIN1252 1
54 WIN1253 1
55 WIN1254 1
63 KOI8R 1
64 KOI8U 1
...    
То есть, когда вы создаете в таблице столбец
NAME VARCHAR(10) CHARACTER SET UTF8
Firebird (и InterBase) умножают размер столбца 10 символов на 4 байта на символ, и сохраняют информацию о максимальной длине строки в символах и в байтах в системных таблицах.

Информация о размере столбца хранится в системной таблице RDB$FIELDS (см. полезные запросы к системным таблицам), как для доменов, так и для обычных столбцов:
  • RDB$FIELD_LENGTH – размер столбца в байтах. Т.е. для примера выше в этом столбце будет записано 40, а не 10.
  • RDB$CHARACTER_LENGTH – размер столбца в символах. Для примера выше там будет 10, а не 40.
  • RDB$CHARACTER_SET_ID – идентификатор чарсета, по которому можно выяснить, почему в field_length значение отличается от character_length.
Если вы попытаетесь уменьшить размер столбца командой
alter table ... alter column name type varchar(9),
то будет выдано сообщение об ошибке New size specified for column FIRST_NAME must be at least 40 characters.

Это сообщение на самом деле ошибочно (CORE-8394), потому что в нем говорится о characters, а выводится длина в байтах на символ. Должно быть, разумеется, "at least 10 characters".
 

А как сортируются данные в юникоде?

У InterBase для UTF8 есть только одна сортировка – бинарная UTF8.

В Firebird у кодировки UTF8 есть несколько наборов сортировок (collate) (см. далее). Пользоваться ими можно точно так же, как и с любыми другими наборами сортировок – или указывать collate в объявлении столбца таблицы, или указывать collate в order by, where, и т.д. Примеры есть в FAQ по работе с win1251.

Пример порядка сортировки русских букв столбца UTF8 с умолчательным collate (точно так же как win1251): А, Б, В, ...а, б, в...

Пример порядка сортировки русских букв столбца UTF8 с collate UNICODE (order by ... collate UNICODE – эквивалент order by ... collate pxw_cyrl): а, А, б, Б, в, В, ...

Помните, что при сортировке строк значение также имеет длина строки. Поэтому в последнем случае, не смотря на то что по одной букве б идет перед Б, строка "бб" будет идти после строки "Б".
 

Какие есть сортировки (collate) для UTF8?

В InterBase нет никаких сортировок для UTF8, кроме бинарной UTF8.

В Firebird есть 5 сортировок:
  • UTF8 – по умолчанию, бинарная сортировка
  • UCS_BASIC – то же самое (для соответствия стандарту SQL)
  • UNICODE – алфавитная сортировка
  • UNICODE_CI – алфавитная сортировка для регистронечувствительного поиска (появилась в Firebird 2.1).
  • UNICODE_CI_AI – алфавитная сортировка для регистронечувствительного поиска с игнорированием акцентированных символов (появилась в Firebird 2.5). Для русского языка действует только в отношении букв е и ё, но на их порядок сортировки не влияет.
Пример сортировки смотрите выше.

Пример для unicode_CI_AI. Допустим, есть таблица TEST, в которой в столбце NAME записаны поодиночке все буквы русского алфавита, строчные и прописные.
select * from test where name = 'ё'
будет выдана только строка с ё
select * from test where name collate unicode_ci = 'ё'
будут выданы строки с ё и Ё
select * from test where name collate unicode_ci_ai = 'ё'
будут выданы строки с ё, Ё, е и Е. Тот же результат будет и при
where name collate unicode_ci_ai = 'е'
 
Внимание! В версиях 2.1 и 2.5 есть ряд багов, связанных с unicode_ci, как исправленных, так и нет. Например, 2457, 3239, 1989. Поэтому использовать unicode_ci в версиях ниже 2.5.1 не рекомендуется.
 

Как работает upper в юникоде?

Нормально. Для русских букв таблица перевода прописных в строчные (upper) и обратно (lower) работает как для бинарной сортировки UTF8 так и для любых других имеющихся сортировок. То есть, указывать collate для столбцов при вызове upper/lower не нужно.
 

Регистронезависимый поиск

Можно организовать двумя способами.

По старинке – при помощи upper, даже если столбцы не имеют collate unicode/unicode_ci. Например,
select * from table
where upper(name) = 'СТРОКА'

Естественным для Unicode образом – с collate unicode_ci и unicode_ci_ai (см. пункт выше!) можно проще:
  1. объявляем столбец как
name varchar(30) collate unicode_ci
  1. делаем поиск как
select * from table
where name = 'строка'

При этом, например, если есть записи с 'а' и 'А', то они будут выданы все независимо от того, в строке поиска указано 'а' или 'А'. То есть, поиск получается регистронезависимым. Более того, если есть индекс по столбцу name collate unicode_ci, то он будет использован оптимизатором для поиска.

Если же столбец уже создан и не имеет collate (или имеет collate unicode), то "превратить" его в регистронечувствительный можно указанием collate unicode_ci:
select * from table
where name collate unicode_ci = 'строка'
Однако, если уже есть индекс по name, то он использоваться не будет.

Дополнительно, для сортировки unicode_ci_ai символы е, Е, ё, Ё считаются одинаковыми. Например, запрос
select * from table
where name collate unicode_ci_ai containing 'е'
выдаст все строки, содержащие в столбце name буквы е, Е, ё, Ё. Тот же результат будет и при
where name collate unicode_ci_ai containing 'ё'
 
Внимание! В версиях 2.1 и 2.5 есть ряд багов, связанных с unicode_ci, как исправленных, так и нет. Например, 2457, 3239, 1989. Поэтому использовать unicode_ci в версиях ниже 2.5.1 не рекомендуется.
 

Я создал базу и таблицы в UTF8, подсоединился в UTF8, и получаю ошибку Malformed string

Это значит, что данные, которые передаются на сервер, идут не в юникоде, а в какой-то другой кодировке. То есть виноват драйвер, компоненты доступа, или само приложение.
 

Можно ли использовать при коннекте чарсет NONE?

Да, можно, но при этом нужно учитывать, что кодировка должна быть указана для любых строковых данных, как констант, так и параметров, передаваемых в запросе. Например, если сделать попытку выполнить запрос
select * from table
where name collate unicode_ci = 'строка'
с чарсетом коннекта NONE, то будет выдана ошибка Malformed string. Обратите внимание, что речь идет именно о символах win1251, т.е. тех, которые в однобайтовой кодировке имеют код больше 127, и в unicode кодируются двумя байтами на символ. Если заменить 'строка' на 'string' (т. е. русские на латинские символы), то ошибки не будет.

Поэтому, если константа действительно содержит символы кодировки 1251, то это нужно явно указать в запросе
select * from table
where name collate unicode_ci = _win1251 'строка'

 

А почему IBExpert тоже дает ошибку Malformed sting?

Такое может быть в старых версиях IBExpert, или в новых версиях, где при выборе чарсета коннекта UTF8 рядом не снята галочка с пункта "Do NOT perform conversion from/to UTF8.

Поэтому в новой версии IBExpert перед коннектом к БД в utf8 в Database Registration Info сначала выключите указанную галку, затем делайте соединение. Для старых версий IBExpert есть варианты:

Написать запрос в SQL Editor, затем в меню по правой кнопке мыши вызвать Convert to UNICODE, и только затем выполнить запрос.

Или – перед строковыми константами в запросе нужно указывать явно их кодировку. Например,
select * from table
where name collate unicode_ci = _win1251 'строка'
Также это будет работать, если чарсет коннекта указан как none. При чарсете коннекта win1251 запрос уйдет на сервер в корректной кодировке, и данные будут корректно преобразованы сервером из win1251 в utf8 для сравнения name и строковой константы.
 

Как мне сконвертировать базу WIN1251 в UTF8?

Это можно сделать только копированием данных из одной базы в другую:
  1. создать новую базу в UTF8
  2. извлечь скрипт метаданных из БД win1251, убрать оттуда все упоминания WIN1251 (помните – у вас могут быть проблемы с несоответствием сортировок в WIN1251 и UTF8), и применить этот скрипт на базе в UTF8
  3. перенести все данные каким-нибудь инструментом, вроде IBPump. Такому инструменту не надо уметь поддерживать юникод, т. к. при записи данных в чарсете коннекта win1251 сервер сам преобразует их в UTF8.
 

Какую кодировку использовать в скриптах?

К скриптам есть несколько требований.

В начале скрипта до коннекта к БД должно быть указание SET NAMES ...;, совпадающее с кодировкой символов, которые используются в скрипте.

Например, если текстовый файл содержит символы 1251, то тогда в начале скрипта должно быть
SET NAMES WIN1251;
Если текстовый файл содержит символы в Unicode, то тогда в начале скрипта должно быть написано
SET NAMES UTF8;

При этом, сам файл должен содержать символы именно в юникоде. Например, для "Блокнота" (Notepad) это делается следующим образом – открываете текстовый файл, "Сохранить как" – выбираете кодировку UTF-8 (по умолчанию текстовые файлы Блокнот создает в ansi, если там явно нет символов Unicode).

После этого национальные символы, вводимые при редактировании этого файла в Блокноте, будут в UTF8.
 

Ошибка Division by zero в IBX Delphi 2009 и выше

Баг зарегистрирован в QualityCentral под номером 68103.

Причина – в кривом методе
function TIBXSQLVAR.GetCharsetSize: Integer;
модуля IBSQL.

В IBX, поставляемом с Delphi до версии 2009, до сих пор не было вообще никакой поддержки Unicode. В Delphi 2009 появилась поддержка unicode на уровне рантайма, и также был изменен IBX.

Метод GetCharsetSize появился с целью поддержки unicode, но идентификаторы кодировок InterBase в нем зашиты жестко, что приводит к несовместимости с идентификаторами кодировок в Firebird (см. отличия в поддержке UTF8), а также к некорректной обработке SQLSubtype, где и у InterBase (!) и у Firebird в старшем байте может содержаться код collate столбца. Более правильно было бы определять количество байт на символ обращаясь к столбцу RDB$BYTES_PER_CHARACTER таблицы RDB$CHARACTER_SETS. Это не только унифицировало бы поддержку InterBase и Firebird, но и обеспечило бы совместимость IBX со всеми будущими версиями InterBase, если бы в них появлялись новые кодировки. Однако такая реализация потребовала бы "кэширования" данной информации, чтобы не происходило обращение к этим таблицам каждый раз при вызове GetCharsetSize.

Пример скорректированного для Firebird кода GetCharsetSize (модуль IBSQL.pas):
function TIBXSQLVAR.GetCharsetSize: Integer;
begin
case SQLVar.SQLSubtype and $FF of // здесь and $FF убирает id collate, возвращаемый Firebird
0, 1, 2, 10, 11, 12, 13, 14, 19, 21, 22, 39,
45, 46, 47, 50, 51, 52, 53, 54, 55, 58 : Result := 1;
5, 6, 8, 44, 56, 57, 64 : Result := 2;
3 : Result := 3;
4, 59 : Result := 4; // здесь правильно обрабатывается id UTF8 в Firebird и InterBase
else
Result := 0;
end;
end;
 
Замечание от 28.01.2010. В Delphi 2010 в этой функции есть небольшие исправления, в частности, SQLSubtype and $FF, но идентификатор utf-8 Firebird (код 4) так и не поддерживается. Поэтому Вам все равно придется исправлять строку
59 : Result := 4;
на
4, 59 : Result := 4;
 

Как использовать Unicode (UTF8) в UDF?

При написании UDF на Delphi для работы с UTF8 есть несколько условий:
  • компилировать UDF нужно в Delphi 2009 и выше (2010, XE-XE7). Можно написать такую UDF и в версиях 2007 и ниже, которые не поддерживают Unicode на уровне RTL и VCL, но код будет выглядеть достаточно сложным;
  • во входных и выходных параметрах передаются строки в UTF8, но это строки InterBase или Firebird, поэтому их нужно "приводить" к строкам Delphi;
  • внутри udf в Delphi 2009-XE7 строки можно обрабатывать как UTF8String, или любой другой кодировке, не забывая предыдущий пункт;
  • при аллокировании памяти для результата в функциях с FREE_IT необходимо учитывать, что Unicode-символы в формате UTF-8 могут иметь длину от 1 до 4 байт, поэтому нужно использовать правильные функции для определения длины Unicode-строки.

Вот пример функции, которая принимает UTF8 строку, добавляет к ней текст в unicode, и возвращает UTF8 строку:
DECLARE EXTERNAL FUNCTION utf8ex
CSTRING(80)
RETURNS CSTRING(80) FREE_IT
ENTRY_POINT 'utf8example' MODULE_NAME 'utf8func'
uses ib_util;
...
function utf8example(P1: PAnsiChar): PAnsiChar; cdecl; export;
// параметры объявлены как PAnsiChar, потому что теперь в D2009/2010 обычный
// PChar это PWideChar, что потребовало бы дополнительного приведения типов
var u: UTF8String; // переменная для работы с unicode в формате utf-8 в Delphi
begin
u:=UTF8String(p1) + 'привет'; // в u получаем склеенную строку в UTF8
Result:=ib_util_malloc(Length(u)+1); // Length(u) вычислит правильный размер строки, плюс байт для #0
StrCopy(Result, PAnsiChar(u)); // копируем utf8 строку в переменную результата
end;

Можно было бы использовать и другой тип строки для переменной u, но это потребует дополнительного приведения результата к UTF8String. Этот пример дан как наиболее простой для Delphi 2009-XE7.
 
Примечание. Если мы создали эту функцию в базе данных, у которой чарсет по умолчанию задан UTF8, то она (udf) будет работать корректно даже если ей на вход передать данные в кодировке 1251 или любой другой. Поскольку мы не задавали чарсет параметров функции в "declare external...", он будет идентичен умолчательному, то есть UTF8, и данные в любой другой кодировке будут автоматически перекодированы сервером как перед передачей в виде параметра, так и после получения результата от функции.

Если же создать эту функцию в базе данных с любым другим чарсетом по умолчанию, то эта функция будет работать некорректно, если только при ее объявлении не указать у параметров CHARACTER SET UTF8.
Интересующимся работой с unicode в Delphi рекомендуем книгу Марко Канту Delphi 2009 Handbook, которая доступна в виде pdf бесплатно для зарегистрированных пользователей Delphi 2009 и 2010.
 

Есть проблемы со старыми UDF в базе UTF8

Да, поскольку практически все известные старые библиотеки функций (rfunc, freeudflib и т.п.) обрабатывали строки как однобайтные наборы символов, без учета кодировок вообще. А российские разработчики писали свои udf, предполагая что кодировка базы будет всегда или none или win1251. Поэтому, разумеется, все эти функции не умеют работать с unicode (многобайтными кодировками).

Так что, нужные функции придется переписывать, как в примере выше.

Временно решить проблему, если первое время в базе в utf8-строках будет хранится только информация, полученная из 1251, можно объявив такие udf с указанием для их строковых параметров character set win1251.

В этом случае сервер автоматически перекодирует utf8 в 1251 и обратно при вызове udf. Однако, как только в такую udf будут переданы символы, несовместимые с 1251, сервер сообщит об ошибке.
(c) iBase.ru, 2002-2015.
Запрещается перепечатка, перевод и копирование. Разрешается частичное цитирование с обязательной ссылкой на источник – www.ibase.ru/unicode_faq/

Подпишитесь на новости Firebird в России

Подписаться