Триггеры WebAssembly в libSQL
Пользовательские функции WebAssembly в libSQL можно использовать для создания мощных триггеров, упрощающих автоматизацию рабочих процессов.
Первый официальный выпуск libSQL, нашей открытой версии SQLite, принес возможность динамически создавать определяемые пользователем функции на базе WebAssembly. Это интересная функция сама по себе, но в сочетании с триггерами базы данных они становятся мощным строительным блоком для автоматизации ваших рабочих процессов.
Триггеры базы данных
Триггеры базы данных — это древняя функция в мире баз данных, реализованная в большинстве продуктов, и libSQL не является исключением. Триггер — это просто хранимая процедура, которая запускается автоматически, когда в базе данных происходит определенное событие. В libSQL (и SQLite) следующие действия могут активировать триггер:
- ВСТАВЛЯТЬ
- ОБНОВЛЯТЬ
- УДАЛИТЬ
Более того, триггер можно настроить на выполнение до, после или вместо исходного действия с некоторыми ограничениями.
Синтаксис оператора CREATE TRIGGER
очень выразителен:
CREATE [TEMP|TEMPORARY] TRIGGER [IF NOT EXISTS] trigger [BEFORE|AFTER|INSTEAD OF] [INSERT|UPDATE OF [column_name*]|DELETE] ON table_name [FOR EACH ROW] [WHEN condition] BEGIN [statement*]; END;
Но не волнуйтесь, мы рассмотрим несколько простых примеров.
Пользовательские функции WebAssembly
libSQL 0.1.0 позволяет пользователям создавать пользовательские функции не только программно, но и с помощью оператора CREATE FUNCTION
, хорошо известного в мире SQL.
WebAssembly был выбран из-за его надежности, портативности и безопасности, а также производительности. Как объяснялось в нашей предыдущей статье, пользовательские функции WebAssembly можно легко реализовать на Rust с помощью библиотеки libsql_bindgen. Чтобы сделать его еще более эргономичным, мы размещаем интерактивное приложение libSQL bindgen — вы также можете развернуть собственное зеркало из нашего репозитория.
Вариант использования: управление пользователями
В этом примере мы реализуем поток базы данных для создания новых записей пользователей, убедившись, что их пароли не хранятся в открытом виде. Мы будем полагаться на триггеры базы данных, основанные на определяемых пользователем функциях WebAssembly, для автоматизации работы, перенося сложность на уровень базы данных.
Позже мы также представим проверку электронной почты, основанную на отправке пользователям одноразовых токенов.
Вот схема потока, реализованного в этом посте:
Таблица пользователей определяется по следующей схеме:
CREATE TABLE users( user TEXT PRIMARY KEY, email TEXT, pass TEXT );
Шифрование паролей пользователей
Первая часть рабочего процесса заключается в сохранении паролей в зашифрованном виде, а не в виде открытого текста. Создадим таблицу с секретными значениями, используемыми для шифрования паролей вновь созданных пользователей:
CREATE TABLE secrets(secret text); INSERT INTO secrets VALUES ('s3cretk3y');
Далее нам нужна функция шифрования. Вот где пользовательские функции WebAssembly становятся полезными. С помощью libSQL bindgen следующий фрагмент Rust может быть скомпилирован в Wasm и зарегистрирован как определяемая пользователем функция с именем encrypt
:
pub fn encrypt(data: String, key: String) -> String { use magic_crypt::MagicCryptTrait; let mc = magic_crypt::new_magic_crypt!(key, 256); mc.encrypt_str_to_base64(data) }
Сохраните сгенерированный SQL в локальный файл и загрузите его в libsql
оболочке, вызвав .read <path-to-your-file>
и вуаля, функция зарегистрирована! Вы также можете попробовать скопировать и вставить SQL прямо в оболочку libsql
.
С доступными секретами и функцией encrypt()
наш первый триггер можно создать следующим образом:
CREATE TRIGGER encrypt_pass AFTER INSERT ON users BEGIN UPDATE users SET pass = encrypt( new.pass, (SELECT secret FROM secrets LIMIT 1) ) WHERE user = new.user; END;
Сразу же после того, как новая строка вставлена в пользователей, предоставленное значение для pass
заменяется зашифрованным значением с использованием результата функции. Функция шифрования принимает два параметра: пароль и значение сегодняшнего секретного ключа, выбранного из ранее созданной таблицы secrets
.
Проверка триггера
Чтобы проверить, работает ли шифрование, была бы весьма кстати соответствующая функция для выполнения описания. Вот исходный код, который вы снова отправляете в bindgen для генерации кода для UDF:
pub fn decrypt(data: String, key: String) -> String { use magic_crypt::MagicCryptTrait; let mc = magic_crypt::new_magic_crypt!(key, 256); mc.decrypt_base64_to_string(data) .unwrap_or_else(|_| "[ACCESS DENIED]".to_string()) }
Давайте проверим, работает ли триггер, добавив несколько записей в таблицу и проверив ее содержимое:
INSERT INTO users VALUES ( 'peter', '[email protected]', 'roe-deer' ); INSERT INTO users VALUES ( 'iku', '[email protected]', 'turso' ); SELECT user, pass, decrypt( pass, (SELECT secret FROM secrets LIMIT 1) ) AS decrypted FROM users; user pass decrypted ----- ------------------------ --------- peter 1mXVhOvX0YWGHcfEYCvqgg== roe-deer iku GeJmXZujKUJbRai+S/4cBA== turso SELECT user, pass, decrypt( pass, 'incorrect-pass' ) AS decrypted FROM users; user pass decrypted ----- ------------------------ --------------- peter 1mXVhOvX0YWGHcfEYCvqgg== [ACCESS DENIED] iku GeJmXZujKUJbRai+S/4cBA== [ACCESS DENIED]
Идеально — пароли хранятся в зашифрованном виде, но их все равно можно расшифровать с помощью соответствующего ключа.
Подтверждение адреса электронной почты
Но мы еще далеко не закончили изучение возможностей триггеров Wasm! На следующем этапе мы усилим процесс управления пользователями, создав уникальный одноразовый токен для каждого пользователя и поместив его в отдельную таблицу. Позже записи из этой таблицы могут быть обработаны внешней программой, которая отправляет сообщение каждому вновь созданному пользователю с просьбой подтвердить свой адрес электронной почты, отправив токен, сгенерированный специально для него.
Таблица, подготовленная для хранения одноразовых токенов, имеет следующую схему:
CREATE TABLE tokens( user text PRIMARY KEY, date text, token text );
Вот реализация функции single_use_token
на Rust, которая принимает имя пользователя и текущее время в качестве аргументов и возвращает сгенерированный токен:
pub fn single_use_token(user: String, seed: u64) -> String { use rand::{Rng, SeedableRng}; use std::hash::{Hash, Hasher}; let mut hasher = std::collections::hash_map::DefaultHasher::new(); let hash = user.hash(&mut hasher); let token = rand::rngs::StdRng::seed_from_u64(seed + hasher.finish()) .gen::<u64>(); format!("{:x}", token) }
И теперь триггер шифрования паролей, который мы создали в предыдущем абзаце, можно усилить другим действием — обратите внимание, что несколько операций могут выполняться одним триггером, поэтому мы можем просто удалить предыдущий и заменить его новым:
DROP TRIGGER IF EXISTS encrypt_pass; CREATE TRIGGER encrypt_pass_and_generate_token AFTER INSERT ON users BEGIN UPDATE users SET pass = encrypt( new.pass, (SELECT secret FROM secrets LIMIT 1) ) WHERE user = new.user; INSERT INTO tokens VALUES ( new.user, date(), single_use_token(new.user, unixepoch()) ); END;
Проверка на практике
Наконец, давайте проверим, что одноразовый токен действительно был сгенерирован и вставлен в таблицу tokens
.
INSERT INTO users VALUES ( 'wojtek', 'wojtek@ii_corps.pl', 'the bear' ); SELECT * FROM tokens WHERE user = 'wojtek'; user date token ------ ---------- ---------------- wojtek 2022-12-16 48e2873b1c5d861b
Краткое содержание
Интеграция с WebAssembly привнесла переносимые и безопасные пользовательские функции в libSQL в его первом официальном выпуске. Они полезны не только для запросов, но и в качестве строительных блоков для триггеров базы данных. С помощью обеих функций на уровень базы данных можно передать больше бизнес-логики, а вычисления выполнять ближе к данным. Это повышает производительность, безопасность и уровень контроля над вашими данными.
Узнайте больше о пользовательских функциях WebAssembly в нашем предыдущем сообщении в блоге. Если вы хотите начать работу с libSQL, вы можете перейти в наш репозиторий GitHub.