Доступ до таблиць з Сі розширень для Postgres

привіт!
В цей раз я розповім не про використання Python або черговий трюк з CSS/HTML і, на жаль, не про те, як я 5 років портувати Вангеры, а про один важливий аспект написання розширень для чудової СУБД PostgresSQL.

Насправді, вже є досить багато статей про те, як написати розширення для Postgres на Сі (наприклад, ), в тому числі і на російською мовою. Але, як правило, в них описуються досить прості випадки. У цих статтях і інструкціях автори реалізують функції, які отримують на вхід дані, як-то їх обробляють, а потім повертають одне число, рядок або користувальницький тип. У них немає пояснень, що робити, якщо з Сі коду потрібно пробігтися по звичайній таблиці, існуючої в базі, або індексу.

До таблиць з Сі можна отримати доступ через добре описаний але повільний SPI (Server Programming Interface), також є дуже складний спосіб, через буфери, а я розповім про компромісний варіант. Під катом я постарався дати приклади коду з докладними поясненнями.


Основи
Припускаю, що свої простенькі функції ви вже писали, і бачили, що вони оголошуються хитрим способом:
Datum to_regclass(PG_FUNCTION_ARGS);

і просто так викликати таку функцію не можна. Давайте краще відразу розберемо на прикладі функції to_regclass:
Datum my_index_oid_datum = DirectFunctionCall1(to_regclass, CStringGetDatum("my_index"));
Oid my_index_oid = DatumGetObjectId(my_index_oid_datum);

У цьому коді я викликаю функцію to_regclass, з допомогою макросу, щоб перетворити ім'я об'єкта бази даних індексу, таблиці тощо) в його Oid (унікальний номер у каталозі). У цій функції тільки один аргумент, тому у макросу з промовистою назвою DirectFunctionCall1 у кінці стоїть одиниця. У файлі include/fmgr.h оголошені такі макроси аж до 9 аргументів. Самі аргументи завжди представлені універсальним типом Datum, саме тому Сі рядок «my_index» наводиться до Datum за допомогою функції CStringGetDatum. Постгресовые функції в принципі спілкуються за допомогою Datum, тому і результатом роботи нашого макросу буде значення типу Datum. Після цього потрібно перетворити його до типу Oid за допомогою макросу DatumGetObjectId. Всі можливі варіанти конвертації потрібно дивитися тут: include/postgres.h.
Також поясню ще одну річ: в Сі прийнято оголошувати змінні на початку блоку, але я для наочності оголошую їх там, де починаю використовувати. На практиці так не пишуть.

Доступ до таблиці
Відразу поясню, чому SPI повільний. Справа в тому, що запит, виконаний з допомогою SPI, проходить всі етапи аналізу і планування. Крім того, йти простим шляхом, де немає магії, мені здається нецікавим.
Наступне, про що хочеться сказати, це назви — в Postgres вони плутають! Із-за довгої історії проекту в коді залишилося багато дивних назв типів, методів та функцій.
Перш, ніж читати далі, бажано мати базові уявлення про MVCC Postgres. Всі наведені нижче приклади працюють тільки в рамках вже створеної транзакції, і, якщо ви раптом залізли туди, де її ще немає, вам знадобиться значно більше магії.

Отже, припустимо, ми хочемо просто пробігтися по таблиці, яка містить два поля: int id text nickname, і вивести їх в лог. Для початку нам треба відкрити heap (таблицю) з певною блокуванням:
RangeVar *table_rv = makeRangeVar("public", "my_table", -1);
Relation table_heap = heap_openrv(table_rv, AccessShareLock);

Замість функції heap_openrv heap_open, у якій перший аргумент це Oid таблиці можна отримати за допомогою функції у першій частині статті). Думаю, призначення RangeVar інтуїтивно зрозуміло, а от на блокування зупинимося докладніше. Типи блокувань оголошені у файлі include/storage/lockdefs.h з досить зрозумілими коментарями. Ви можете побачити цю інформацію у таблиці:
AccessShareLock SELECT
RowShareLock SELECT FOR UPDATE/FOR SHARE
RowExclusiveLock INSERT, UPDATE, DELETE
ShareUpdateExclusiveLock VACUUM (non-FULL),ANALYZE, CREATE INDEX CONCURRENTLY
ShareLock CREATE INDEX (WITHOUT CONCURRENTLY)
ShareRowExclusiveLock like EXCLUSIVE MODE, but allows ROW SHARE
ExclusiveLock blocks ROW SHARE/SELECT...FOR UPDATE
AccessExclusiveLock ALTER TABLE, DROP TABLE, VACUUM FULL and unqualified LOCK TABLE
Так як ми хотіли лише пробігтися по табличці, тобто виконати, SeqScan, вибираємо AccessShareLock. Після того, як ми відкрили heap, нам необхідно ініціалізувати процес сканування таблиці:
HeapScanDesc heapScan = heap_beginscan(sr_plans_heap, SnapshotSelf, 0, (ScanKey) NULL);

Як і очікувалося, першим аргументом цієї функції передаємо наш heap, а от SnapshotSelf вимагає пояснень. Робота MVCC в Postgres припускає, що в кожен момент часу може існувати кілька версій одного рядка таблиці, і саме в snapshot (моментальний знімок транзакції) записано, які ми можемо бачити, а які ні. Крім SnapshotSelf, тобто поточного snapshot транзакції, є, наприклад, SnapshotAny, підставивши який, ми б змогли побачити всі видалені або змінені tuples (рядки таблиці). Інші види можете глянути в include/utils/tqual.h. Такі аргументи у heap_beginscan — це кількість ключів пошуку (ScanKey) і власне самі ключі. Ключі пошуку (ScanKey) — це по суті умови, тобто те, що ви пишете у WHERE. Для роботи з heap ключі пошуку не дуже потрібні, тому що ви завжди самі в своєму коді можете зробити перевірку умов. А ось при пошуку за індексом ми від їх ініціалізації та використання нікуди не підемо.

І тепер найголовніше, явися цикл:
Datum values[2];
bool nulls[2];

for (;;)
{
HeapTuple local_tuple;
local_tuple = heap_getnext(heapScan, ForwardScanDirection);
if (local_tuple == NULL)
break;
heap_deform_tuple(local_tuple, table_heap->rd_att, values, nulls);
elog(WARNING,
"Test id:%i nick:%s",
DatumGetInt32(values[0]),
TextDatumGetCString(values[1])
);
}

У цьому циклі ми викликаємо функцію heap_getnext, з допомогою якої отримуємо наступний tuple, поки нам не повернеться нульовий покажчик. Функція heap_getnext отримує наш HeapScanDesc і напрямків сканування, для нас буде актуально два: це пряме сканування — ForwardScanDirection і зворотне — BackwardScanDirection. Тепер нам залишилося розпакувати tuple і отримати доступ до його полях, для цього ми викликаємо heap_deform_tuple, куди передаємо наш tuple, після чого його опис (яке беремо з heap) і два масиву (один для значень, а інший для визначення NULL значень). Далі, користуючись уже знайомими для нас функціями, перетворимо елементи масиву values (складається з Datum) до звичайних Сі типами.

А тепер не забудемо закрити наше сканування heap (таблиці) і закрити сам heap:
heap_endscan(heapScan);
heap_close(sr_plans_heap, AccessShareLock);

Закриваємо heap ми з таким же типом блокування, з яким і відкривали його.

Робота з індексом
API пошуку за індексом буде аналогічним пошуку по heap, але тільки вимагатиме більше рядків коду для ініціалізації. У коді постараємося виводити повідомлення тільки для рядків, де перший аргумент дає відповідь на головне питання життя, всесвіту і всього такого. Як і для heap, для початку наведемо шматок коду з усіма підготовчими роботами:
RangeVar *table_rv = makeRangeVar("public", "my_table", -1);
Relation table_heap = heap_openrv(table_rv, AccessShareLock);
table_idx_oid = DatumGetObjectId(DirectFunctionCall1(
to_regclass,
StringGetDatum("my_table_idx")
));
Relation table_idx_rel = index_open(table_idx_oid, AccessShareLock);
indexScan = index_beginscan(table_heap, table_idx_rel, SnapshotSelf, 1, 0);
ScanKeyData key;
ScanKeyInit(&key, 1, BTEqualStrategyNumber, F_INT4EQ, Int32GetDatum(42));
index_rescan(indexScan, &key, 1, NULL, 0);

Отже, для пошуку за індексом нам так само, як і в минулому прикладі, треба відкрити heap (таблицю), з якою пов'язаний цей індекс. За допомогою to_regclass ми знаходимо oid для нашого індексу my_table_idx, після відкриваємо його за допомогою index_open, знаходячи потрібний нам Relation. Після чого ініціалізуємо процес сканування індексу index_beginscan, тут головне відміну від heap_beginscan в тому, що у нас буде 1 ключ пошуку (ScanKey).
ScanKeyInit, як зрозуміло з назви, ініціалізує ключ для пошуку. Першим аргументом йде сам ключ (тип ScanKeyData), після ми вказуємо порядковий номер аргументу, за яким буде вестись пошук (нумерація з 1), далі йде стратегія пошуку. По факту це схоже на оператор в умові (інші стратегії можна подивитися тут include/access/startnum.h), після ми вказуємо безпосередньо oid функції, яка буде проводити нашу операцію порівняння (ці oid оголошені у файлі include/utils/fmgroids.h). І нарешті, останній наш аргумент — це Datum, який має містити значення, за яким повинен проводиться пошук.

Далі йде ще одна нова функція index_rescan і служить вона для того, щоб запустити пошук за індексом. Одного index_beginscan тут недостатньо. На вхід ця функція отримує список ключів (у нас тільки один), кількість цих ключів, після чого ключі для сортування та кількість ключів для сортування (вони використовуються для умови з ORDER BY, якого в цьому прикладі немає). Начебто усі підготовки пройшли, і можна показувати основний цикл, правда, він буде дуже схожий на той що було з heap:
for (;;)
{
HeapTuple local_tuple;
ItemPointer tid = index_getnext_tid(indexScan, ForwardScanDirection);
if (tid == NULL)
break;
local_tuple = index_fetch_heap(indexScan);
heap_deform_tuple(local_tuple, table_heap->rd_att, values, nulls);
elog(WARNING,
"Test id:%i nick:%s",
DatumGetInt32(values[0]),
DatumGetCString(PG_DETOAST_DATUM(values[1]))
);
}

Так як тепер ми біжимо по індексу, а не самому heap, то отримуємо ми ItemPointer, спеціальний вказівник на запис в індексі, (якщо вас цікавлять подробиці, звертайтеся до відповідної документації www.postgresql.org/docs/9.5/static/storage-page-layout.html або відразу до файлу include/storage/bufpage.h) використовуючи який ми ще повинні отримати tuple з heap. В даному циклі index_getnext_tid функціонально схожий на heap_getnext і додається тільки index_fetch_heap, а решта повністю аналогічно.

Для закінчення нашої операції, як можна здогадатися, нам треба буде закрити пошук за індексом, сам індекс і відкритий heap:
index_endscan(indexScan);
index_close(table_idx_rel, heap_lock);
heap_close(table_heap, heap_lock);

Зміна даних
Отже, ми навчилися робити SeqScan і IndexScan, тобто шукати по нашій табличці і навіть використовувати для цього індекс, але як тепер в неї щось додати? Для цього нам потрібні будуть функції simple_heap_insert та index_insert.

Перш, ніж змінювати щось в таблиці і в пов'язаних індексах, їх треба відкрити з потрібними блокуваннями, способом, показаним раніше, після чого можна виконувати вставку:
values[0] = Int32GetDatum(42);
values[1] = CStringGetDatum("First q");
tuple = heap_form_tuple(table_heap->rd_att, values, nulls);
simple_heap_insert(table_heap, tuple);
index_insert(
table_idx_rel,
values,
nulls,
&(tuple->t_self),
table_heap,
UNIQUE_CHECK_NO
);

Тут ми робимо зворотну операцію, тобто з масивів values і nulls формуємо tuple,
після чого додаємо його в heap і потім додаємо відповідну запис в індекс. Після попередніх пояснень цей код для вас повинен бути зрозумілий.
Для оновлення tuple потрібно зробити наступне:
values[0] = Int32GetDatum(42);
replaces[0] = true;

newtuple = heap_modify_tuple(
local_tuple,
RelationGetDescr(table_heap),
values,
nulls,
replaces
);
simple_heap_update(table_heap, &newtuple->t_self, newtuple);

У нас з'являється масив булевих змінних replaces, де зберігається інформація, яка поле змінилося. Після чого ми формуємо новий tuple на базі старого, але з нашими правками за допомогою heap_modify_tuple. І в самому кінці виконуємо саме оновлення simple_heap_update. Так як у нас з'явився новий tuple, а старий позначений як віддалений, то потрібно також додати запис в індекс для нового tuple способом, що був показаний раніше.

Тепер видалити tuple не складе праці з функцією simple_heap_delete, і вона не вимагає пояснень. Хочеться відзначити, що відповідний запис в індексі видаляти не потрібно, це відбудеться автоматично при виконанні очищення операцією VACUUM.

Підсумок
Ми навчилися отримувати доступ до таблиці з Сі коду, в тому числі за індексом. Я постарався максимально докладно описати кожну функцію та її призначення, але, якщо що-то виявилося незрозуміло, запитуйте в коментарях, я постараюся відповісти і доповнити статтю.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.