Імперативне SQLScript
Імперативне SQLScript
Section titled “Імперативне SQLScript”Імперативне SQLScript — частина мови з управлінням потоком: змінні, цикли, IF/ELSE, винятки, динамічний SQL. На відміну від декларативного коду, тут програміст явно описує послідовність кроків. Це обмежує паралелізацію HANA — тому імперативні конструкції використовують лише там, де декларативно задачу не вирішити.
Сценарії, які потребують імперативного коду:
- Генерація даних, які залежать від попередньої ітерації (рекурсія, накопичувані обчислення).
- Складна error-обробка з cleanup-логікою.
- Виклик kількох DML-statement-ів у транзакції з умовною логікою.
- Робота з масивами і курсорами там, де SET-логіки замало.
Локальні скалярні змінні
Section titled “Локальні скалярні змінні”Декларуються через DECLARE на початку блоку. Опціональне початкове значення:
DO BEGIN DECLARE lv_count INTEGER = 0; DECLARE lv_name NVARCHAR(100); DECLARE lv_today DATE = CURRENT_DATE; DECLARE lv_pi DECIMAL(10,8) = 3.14159265;
lv_count = lv_count + 1; -- запис: без : lv_name = 'Hello'; SELECT :lv_count, :lv_name FROM dummy; -- читання: з :END;Присвоєння через SELECT…INTO
Section titled “Присвоєння через SELECT…INTO”Один або кілька скалярів за раз:
DO BEGIN DECLARE lv_projects INTEGER; DECLARE lv_assignee INTEGER;
SELECT COUNT(DISTINCT project), COUNT(DISTINCT assignee) INTO lv_projects, lv_assignee FROM tasks;
SELECT :lv_projects, :lv_assignee FROM dummy;END;Присвоєння з scalar UDF з кількома вихідними параметрами
Section titled “Присвоєння з scalar UDF з кількома вихідними параметрами”(lv_projects, lv_assignee) = udf_statistic();⚠️ Грабля: SELECT INTO падає на 0 або >1 рядках
SELECT ... INTO scalarочікує рівно один рядок. 0 рядків →SQL_ERROR_CODE = 1299(no data found). 2+ рядки →SQL_ERROR_CODE = 1300(too many rows). Ловити можна тільки черезDECLARE EXIT HANDLER. Якщо не впевнений у кількості — обгортай у TRY/EXIT HANDLER або додавайLIMIT 1.
Локальні табличні змінні
Section titled “Локальні табличні змінні”Декларуються неявно (з присвоєння SELECT-ом) або явно через DECLARE. Деталі — на Декларативному SQLScript.
DO BEGIN -- Неявно lt_x = SELECT * FROM tab1;
-- Явно з типом DECLARE lt_y TABLE LIKE tab1; DECLARE lt_z TABLE (id INT, val NVARCHAR(50));END;DML на table variables
Section titled “DML на table variables”З SAP HANA 2.0 SPS 04 — INSERT/UPDATE/DELETE працюють і на table variables. Префікс : обовʼязковий:
DO BEGIN DECLARE lt_dml TABLE (firstname NVARCHAR(30), lastname NVARCHAR(30));
INSERT INTO :lt_dml SELECT firstname, lastname FROM users; UPDATE :lt_dml SET firstname = UPPER(firstname); DELETE FROM :lt_dml WHERE lastname IS NULL;
SELECT * FROM :lt_dml;END;⚠️ Грабля: DML table variable треба DECLARE
Table variable, на якій плануєш DML, має бути оголошена через явний
DECLARE. Неявно типізована (черезlt_x = SELECT ...) — DML не приймає. Це специфічна технічна вимога HANA.
Допоміжні операції з table variables
Section titled “Допоміжні операції з table variables”RECORD_COUNT(:lt_x) -- к-сть рядків (HANA 2.0+)IS_EMPTY(:lt_x) -- TRUE / FALSE (тільки в imperative, не в WHERE):lt_x.SEARCH((status, assignee), (3, 3)) -- пошук по ключу, повертає індекс:lt_x.DELETE(lv_index) -- видалити рядок за індексом:lt_x.DELETE(lv_index_array) -- видалити кілька:lt_x.DELETE(start..end) -- видалити діапазон:lt_x.DELETE() -- видалити всіСесійні змінні
Section titled “Сесійні змінні”Існують протягом сесії, не блоку. Зберігаються в HANA-сесії, доступні з-під будь-якої частини коду:
SET 'CLIENT' = '100'; -- встановитиSELECT SESSION_CONTEXT('CLIENT') -- прочитати FROM dummy;Спеціальне використання — у AMDP для передачі ABAP-системних полів (sy-mandt, sy-langu) у SQLScript.
Тимчасові таблиці
Section titled “Тимчасові таблиці”Як таблиці БД, але видимі тільки поточній сесії. Корисно для проміжних обчислень великого обʼєму (де table variable не справляється):
-- Локальна (#) — лише для поточної сесіїCREATE LOCAL TEMPORARY TABLE #temp_data (id INT, val NVARCHAR(50));
-- Глобальна (одна метадата для всіх, але дані ізольовані по сесіях)CREATE GLOBAL TEMPORARY TABLE temp_global (id INT, val NVARCHAR(50));
INSERT INTO #temp_data VALUES (1, 'A');SELECT * FROM #temp_data;
DROP TABLE #temp_data;IF / ELSE
Section titled “IF / ELSE”IF :lv_value > 100 THEN ...ELSEIF :lv_value > 50 THEN ...ELSE ...END IF;Predicates EXISTS і IN дозволені в IF тільки з SAP HANA 2.0 SPS 04. CONTAINS і MEMBER OF — заборонені скрізь у IF/WHILE.
FOR loop
Section titled “FOR loop”Лічильниковий цикл — інкремент скалярної цілочислової змінної:
FOR <variable> IN [REVERSE] <initial>..<final> DO <block>END FOR;DO BEGIN DECLARE lv_sum INTEGER = 0; DECLARE lv_indx INTEGER;
FOR lv_indx IN 1..10 DO lv_sum = :lv_sum + :lv_indx; END FOR;
SELECT :lv_indx, :lv_sum FROM dummy; -- 10, 55
FOR lv_indx IN REVERSE 10..1 DO -- зворотний хід: 10, 9, ... ... END FOR;END;REVERSE міняє напрямок ітерації. Змінні, оголошені всередині FOR, не видимі ззовні.
WHILE loop
Section titled “WHILE loop”Перевіряє умову перед кожною ітерацією:
WHILE <condition> DO <block>END WHILE;DO BEGIN DECLARE lv_n INTEGER = 1; DECLARE lv_factorial INTEGER = 1;
WHILE :lv_n <= 10 DO lv_factorial = :lv_factorial * :lv_n; lv_n = :lv_n + 1; END WHILE;
SELECT :lv_factorial FROM dummy; -- 3 628 800END;Контроль ітерацій
Section titled “Контроль ітерацій”| Statement | Призначення |
|---|---|
BREAK; | Вихід з найвнутрішнього циклу |
CONTINUE; | До наступної ітерації |
Курсори
Section titled “Курсори”Особливий цикл для проходу по результату SELECT-у. Декларується через DECLARE CURSOR, потім використовується або через FOR (рекомендовано), або через явні OPEN/FETCH/CLOSE.
FOR loop через курсор (рекомендовано)
Section titled “FOR loop через курсор (рекомендовано)”CREATE PROCEDURE close_projects ASBEGIN DECLARE CURSOR lc_projects (iv_status INT) FOR SELECT id FROM projects WHERE status = :iv_status;
FOR ls_project AS lc_projects(5) DO -- статус 5 = Closed CALL close_tasks(:ls_project.id); END FOR;END;ls_project — рядок з текучими значеннями колонок. Доступ — :ls_project.id. Курсор автоматично відкривається і закривається — без ризику memory leak.
Явні OPEN / FETCH / CLOSE
Section titled “Явні OPEN / FETCH / CLOSE”DECLARE CURSOR lc_x FOR SELECT id, status FROM tasks;
OPEN lc_x;
FETCH lc_x INTO lv_id, lv_status; -- читає поточний рядок, рухає на наступнийIF :lv_id IS NULL THEN ...END IF; -- кінець набору
CLOSE lc_x;⚠️ Грабля: курсор замість SELECT — антипатерн
Курсор у SQLScript — як
LOOPу ABAP. При міграції з ABAP велика спокуса дослівно перекластиLOOP AT itabу курсор. Не роби — у 95% випадків те саме можна зробити одним декларативним SELECT-ом. Курсор виправданий тільки коли логіка реально вимагає послідовної обробки (зміна стану між ітераціями, виклик процедури з side-effect-ами для кожного рядка).
Масиви
Section titled “Масиви”Одновимірні структури, доступ за індексом:
DO BEGIN DECLARE i INTEGER; DECLARE demo NVARCHAR(20) ARRAY; -- порожній масив
-- Заповнення в циклі (1-based індекси) FOR i IN 1..10 DO demo[:i] = CONCAT('Value ', :i); END FOR;
SELECT :demo[10] FROM dummy; -- 'Value 10'
-- Trim до перших 5 елементів demo = TRIM_ARRAY(:demo, 5);
-- Запис у позицію 20 — дірки заповнюються NULL-ами demo[20] = 'Last Element'; SELECT :demo[15] FROM dummy; -- NULL (дірка)END;Конвертації
Section titled “Конвертації”-- ARRAY → tablelt_table = UNNEST(:demo) AS (val);
-- ЛитералDECLARE arr NVARCHAR(10) ARRAY = ARRAY('Z', 'Y', 'B', 'A');
-- Перевірка наявностіIF 'A' MEMBER OF :arr THEN ... END IF;Динамічний SQL
Section titled “Динамічний SQL”Виконання SQL, побудованого як рядок під час runtime. Дві основні форми:
Простіший варіант — без параметрів:
DO BEGIN DECLARE lv_sql NVARCHAR(500); lv_sql = 'INSERT INTO #max_id SELECT ''USERS'' AS table_name, MAX(id) FROM users;'; EXEC :lv_sql;END;EXECUTE IMMEDIATE
Section titled “EXECUTE IMMEDIATE”З параметрами і поверненням:
EXECUTE IMMEDIATE :lv_sql INTO :lt_result -- повернення результату в table variable USING :iv_param1, :iv_param2; -- bind-параметриПараметризований приклад — захищає від SQL injection:
DO BEGIN DECLARE lt_result TABLE (id INT, title NVARCHAR(100)); DECLARE lv_sql NVARCHAR(500) = 'SELECT id, title FROM tasks WHERE status = ? AND assignee = ?';
EXECUTE IMMEDIATE :lv_sql INTO :lt_result USING 3, 42;
SELECT * FROM :lt_result;END;⚠️ Грабля: динамічний SQL = SQL injection
Конкатенація користувацького вводу в SQL-рядок — пряма дорога до injection. Завжди використовуй параметри (
?+USING). Імена таблиць/колонок не можна параметризувати — для них валідуй черезcl_abap_dyn_prgна стороні ABAP, або allow-list імен.
⚠️ Грабля: динамічний SQL і READ-ONLY
До SAP HANA 2.0 SPS 03 динамічний SQL вважався пишучим — не дозволявся в
READ-ONLY-процедурах (тобто і в AMDP). З SPS 04 доданоREADS SQL DATAдляEXEC/EXECUTE IMMEDIATE— позначає динамічний код як read-only.
Транзакції
Section titled “Транзакції”COMMIT — підтверджує всі зміни поточної транзакції. ROLLBACK — скасовує. Транзакція починається автоматично з першим DML, закінчується явним COMMIT/ROLLBACK або кінцем сесії:
DO BEGIN DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; SIGNAL SQL_ERROR_CODE 10001 SET MESSAGE_TEXT = 'Transfer failed'; END;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;END;⚠️ Грабля: COMMIT в AMDP — табу
В AMDP
COMMIT/ROLLBACKне дозволені. Транзакцією керує ABAP-LUW (черезCOMMIT WORKчи implicit на кінці dialog step). Якщо AMDP робитьCOMMIT— рветься узгодженість з ABAP-частиною бізнес-логіки.
Autonomous transactions
Section titled “Autonomous transactions”Окрема транзакція, незалежна від основної. Корисна для error-логу: запис у лог має зберегтися навіть якщо основна транзакція робить ROLLBACK.
CREATE PROCEDURE write_error_log ( IN iv_error_code INT, IN iv_error_message NVARCHAR(500))AS BEGIN BEGIN AUTONOMOUS TRANSACTION INSERT INTO error_log (timestamp, errorcode, message) VALUES (CURRENT_TIMESTAMP, :iv_error_code, :iv_error_message); END;END;⚠️ Грабля: autonomous + спільні таблиці = deadlock
Якщо autonomous transaction торкається тих самих таблиць, що і батьківська, з незакомічених змін — обидві стоять чекати одна одну. Тримай у autonomous тільки окремі таблиці (типово — лог).
Обробка винятків
Section titled “Обробка винятків”Виняток (exception, error) — подія помилки. Або системна (SQL exception, наприклад ділення на 0), або своя власна (user-defined).
Стандартні error-коди
Section titled “Стандартні error-коди”| Код | Опис |
|---|---|
| 257 | sql syntax error |
| 259 | invalid table name |
| 304 | division by zero |
| 328 | invalid name of function or procedure |
| 329 | duplicate name of function or procedure |
| 1299 | no data found |
| 1300 | too many rows |
Повний перелік: SELECT * FROM m_error_codes ORDER BY code;. Свої коди — у діапазоні 10000–19999.
DECLARE CONDITION — імена для кодів
Section titled “DECLARE CONDITION — імена для кодів”DECLARE unknown_task CONDITION; -- без коду — буде випадковийDECLARE division_by_0 CONDITION FOR SQL_ERROR_CODE 304; -- псевдонім стандартногоDECLARE invalid_input CONDITION FOR SQL_ERROR_CODE 10001; -- свій кодSIGNAL — кидаємо виняток
Section titled “SIGNAL — кидаємо виняток”SIGNAL SQL_ERROR_CODE 10000 SET MESSAGE_TEXT = 'My error';
-- або з імʼям з DECLARE CONDITION:SIGNAL my_error SET MESSAGE_TEXT = 'My error';
-- або без коду і тексту (буде випадковий код):SIGNAL my_error;Тільки user-defined exceptions можна кидати через SIGNAL. Системні SQL-помилки кидаються самою БД.
DECLARE EXIT HANDLER — ловимо
Section titled “DECLARE EXIT HANDLER — ловимо”DO BEGIN DECLARE EXIT HANDLER FOR SQL_ERROR_CODE 304 -- конкретний код BEGIN CALL write_error_log(304, 'Division by zero'); ROLLBACK; END;
DECLARE EXIT HANDLER FOR my_error -- по імені BEGIN ... END;
DECLARE EXIT HANDLER FOR SQLEXCEPTION -- усі SQL exceptions BEGIN CALL write_error_log(::SQL_ERROR_CODE, ::SQL_ERROR_MESSAGE); RESIGNAL; -- піднімаємо далі END;
...END;Усередині handler-а доступні ::SQL_ERROR_CODE і ::SQL_ERROR_MESSAGE.
RESIGNAL — підняти далі
Section titled “RESIGNAL — підняти далі”RESIGNAL усередині handler-а перекидає винятку до батьківського блоку. Без RESIGNAL — виняток вважається обробленим, виконання продовжується після блоку з handler-ом.
EXIT HANDLER vs CONDITION HANDLER
Section titled “EXIT HANDLER vs CONDITION HANDLER”В SAP HANA є тільки EXIT HANDLER — після обробки виходить з блоку. На відміну від інших СКБД, тут немає CONTINUE HANDLER. Якщо треба “пропустити помилку і йти далі” — обгортай ризиковану операцію у вкладений блок з власним EXIT HANDLER:
FOR i IN 1..100 DO BEGIN -- вкладений блок DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN CALL write_error_log(::SQL_ERROR_CODE, 'Iteration ' || :i); -- обробили — і EXIT з вкладеного блоку END;
CALL risky_operation(:i); END; -- зовнішній FOR продовжуєтьсяEND FOR;Що далі
Section titled “Що далі”- Чистий код, тестування, продуктивність — як писати швидкий і підтримуваний SQLScript
- Декларативне SQLScript — SELECT, JOIN, CTE
- CRUD у SQLScript — INSERT, UPDATE, MERGE, DELETE
- Основи мови SQLScript — синтаксис, процедури, функції