Перейти до вмісту

Імперативне 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;

З 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() -- видалити всі

Існують протягом сесії, не блоку. Зберігаються в HANA-сесії, доступні з-під будь-якої частини коду:

SET 'CLIENT' = '100'; -- встановити
SELECT SESSION_CONTEXT('CLIENT') -- прочитати
FROM dummy;

Спеціальне використання — у AMDP для передачі ABAP-системних полів (sy-mandt, sy-langu) у SQLScript.

Як таблиці БД, але видимі тільки поточній сесії. Корисно для проміжних обчислень великого обʼєму (де 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 :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 <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 <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 800
END;
StatementПризначення
BREAK;Вихід з найвнутрішнього циклу
CONTINUE;До наступної ітерації

Особливий цикл для проходу по результату SELECT-у. Декларується через DECLARE CURSOR, потім використовується або через FOR (рекомендовано), або через явні OPEN/FETCH/CLOSE.

FOR loop через курсор (рекомендовано)

Section titled “FOR loop через курсор (рекомендовано)”
CREATE PROCEDURE close_projects AS
BEGIN
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.

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-ами для кожного рядка).

Одновимірні структури, доступ за індексом:

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;
-- ARRAY → table
lt_table = UNNEST(:demo) AS (val);
-- Литерал
DECLARE arr NVARCHAR(10) ARRAY = ARRAY('Z', 'Y', 'B', 'A');
-- Перевірка наявності
IF 'A' MEMBER OF :arr THEN ... END IF;

Виконання 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 :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.

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-частиною бізнес-логіки.

Окрема транзакція, незалежна від основної. Корисна для 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 тільки окремі таблиці (типово — лог).

Виняток (exception, error) — подія помилки. Або системна (SQL exception, наприклад ділення на 0), або своя власна (user-defined).

КодОпис
257sql syntax error
259invalid table name
304division by zero
328invalid name of function or procedure
329duplicate name of function or procedure
1299no data found
1300too 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 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-помилки кидаються самою БД.

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 усередині handler-а перекидає винятку до батьківського блоку. Без RESIGNAL — виняток вважається обробленим, виконання продовжується після блоку з 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;