https://qiita.com/iisaka51/items/45f23df873ea5be433c7
@iisaka51 (Гоічі (Іісака) Юкава)
Іржа
База даних
RocksDB
TiKV
surrealdb
Останнє оновлення 15 травня 2023 року
Опубліковано 21 січня 2023 року
Вступ.
Цей документ підсумовує інформацію про SurrealDB, яка була опублікована у липні 2022 року.
Історія SurrealDB
Вона розробляється з 2016 року, хоча публічно доступна лише протягом короткого періоду часу
Лютий 2016 р. Розпочато розробку на GoLang
2017 Липень Розпочато роботу як SaaS-бек-енд БД
2021 Жовтень Вирішено випустити як Open Source, перебудовано на Rust
2022 Липень Випущено Beta.1
2022 Серпень Випущено Beta.5
2022 Жовтень Випущено Beta.8
SurrealDB Inc.
Листопад 2021 Засновано компанію SurrealDB Ltd. у Лондоні
Січень 2023 Залучено $6 млн для DBaaS
Історія створення SurrealDB
Основні тенденції
Абстракція баз даних, хмарні, безсерверні
Все більше компаній переходять на DBaaS
61% розробників/операторів завершили або збираються завершити повну > міграцію на DBaaS, згідно з опитуванням MariaDB
Зростаючий розмір ринку DBaaS
До $24,8 млрд до 2025 року
Багатий інвестиційний клімат для DBaaS
SingleStore залучає $30 млн (2022/жовтень)
EdgeDB залучає $15 млн (2022/листопад)
SurrealDB залучає $6 млн (січень 2023)
Профіль SurrealDB
Головна сторінка новин Hacke's
No. 4 2022/Серпень
No. 2 2022/Вересень
Потрапили до списку улюблених репозиторіїв GitHub
2022/Серпень
2022/грудень
Зірка GitHub
Від 180 зірок до 1500 зірок за 48 годин
5,000 зірок за 3 тижні після публікації
10 000 зірок за 4 тижні після публікації
1 у гарячому списку Reddit у розділах "Програмування" та "Rust
Ліцензія SurrealDB
Вихідний код SurrealDB поширюється за ліцензією Business Software License 1.1
SDK та бібліотеки/драйвери від MIT
SurrealDB BSL дозволяє використовувати SurrealDB на необмеженій кількості вузлів, якщо ви не пропонуєте його як комерційний DBaaS.
Можна вбудовувати в продукти
SurrealDB BSL дійсний протягом 4 років
1 січня 2026 року це обмеження закінчується, і код стає відкритим за поточною ліцензією Apache License 2.0
Вільне використання для будь-яких цілей
Можливості SurrealDB
Реалізовано на Rust
Сегментація Стійкість до збоїв
Крос-компілюється
Відносно швидка порівняно з іншими мовами
Легка: розмір двійкового файлу Linux 24 МБ, macOS: 44 МБ
Сервер і клієнт REPL в одному бінарному файлі
Просте встановлення
Підтримує HTTP/Restful API
Підтримує WebSockets
Внутрішні БД: EchoDB, RocksDB, TiKV, FoundationDB, IndexedDB
Функції як база даних
Без схем: немає проблем з визначенням схеми
Різні формати зберігання: таблиці, документи, графіки тощо
Багаторядкові, багатотабличні ACID-транзакції
Записуйте посилання на записи та з'єднання у вигляді спрямованих графів
Не потрібно JOIN, розумне уникнення проблеми N+1
Заздалегідь визначені запити на аналіз
Відбирайте, агрегуйте, групуйте та впорядковуйте дані так, як вони записані
Розширюйте запити за допомогою вбудованого JavaScript
Можна писати регулярні вирази в запитах (/regex/)
Підтримує GeoJSON
Може виконувати CRUD операції паралельно
Переваги SurrealDB
Одночасна диференціація функцій
Користувачі можуть отримати доступ до неї безпосередньо з фронтенду
База даних може бути належним чином налаштована для автентифікації та авторизації
Синхронізація даних в режимі реального часу
Мало конкурентів, таких як Google Firestore
Єдина в своєму роді БД, якщо використовується локально
Аутентифікація та авторизація на стороні бази даних ...
Може забезпечити контроль доступу на основі ролей
Контроль доступу на основі визначених ролей, таких як адміністратор, редактор, глядач тощо.
Слабкі сторони SurrealDB
Ще не опублікована (липень 2022).
Можуть існувати потенційні вразливості та помилки безпеки
PostgreSQL вперше опублікована в 1997 році, попередник Postgress в 1989 році)
Перша версія MySQL була опублікована в 1995 році
MariaDB вперше опублікована в 2009 році
MongoDB вперше опублікована у 2009 році
Доступно мало інформації.
Офіційна документація знаходиться у стадії розробки
Якщо є сумніви, прочитайте вихідний код
Не всі функції були реалізовані
SurrealDB наразі не є прибутковою: сервіс DBaaS буде доступний у 2023 році
Функції, що знаходяться в стадії розробки (все ще не завершені в beta.9)
Підтримка мульти-вузлів у розподіленому режимі
Реплікація
Перевірка здоров'я
GraphQL
FULLTEXT - повнотекстове індексування
LEARN поля
Автоматична конфігурація на основі аналізу заданих полів за допомогою машинного навчання
Версійні тимчасові таблиці
Можливість "повернутися в минуле" при зверненні до даних
Підсвічування коду IDE (Atom, VSCode, Vim...)
Реліз додатку для введення/виведення даних користувачем планується як 1.x
Огляд дворівневої архітектури SurrealDB
surrealdb_arch.png
Огляд роботи SurrealDB
surrealdb_dataflow.png
Вихідний код.
Числа показують кількість рядків разом з коментарями (SurrealDB 1.0.0-beta.8).
До речі, MariaDB 10.9 має понад 1.3 мільйони рядків (понад 12,000 рядків лише для клієнта).
|-- src // рівень API 4172
|-- net 1981
|-- cli 895
|-- rpc 174
...
|-- lib/src // рівень BL 32261
|-- sql 19030
|-- fnc 3524
...
|-- kvs 3286
|-- indexdb 220
|-- rocksdb 316
|-- tikv 269
...
SurrealDB реалізує напрочуд мало функцій
Використовуйте nom для розбору запитів
Функції можна об'єднувати в ланцюжки для побудови парсерів інкрементно
Використання echodb для зберігання в пам'яті (Tobie)
In-Memory KVS DB з підтримкою багатоверсійного паралелізму
Використання storekey для збереження у KVS (Tobie)
Двійкове кодування зі збереженням порядку словника
Корисно для створення відсортованих KVS-ключів
Використання MsgPack та serde для серіалізації/десеріалізації (rmp-serde)
Використовуйте geo для розбору GeoJSON
Використовуйте RocksDB для використання локальних файлів як сховища даних
Використовуйте TiKV та FoundationDB для функціональності розподіленої БД
Елементи, які SurrealDB зберігає у KVS
Метадані
Таблиці, індекси, діапазони та інші структури
Дані.
Значення об'єктів, що зберігаються в SurrealDB
Як SurrealDB зберігає дані у KVS
Структури Map Rust та MsgPack з прив'язкою до serde
Економія місця у сховищі
Ефективність серіалізації/десеріалізації
Два способи доступу до даних у KVS
За ключем: вкажіть конкретний ключ і отримайте значення (швидко)
Сканування: вкажіть діапазон ключів і отримайте всі значення (повільний)
У ключах зберігається ієрархічна структура
Простір імен -> База даних -> Таблиця -> Ідентифікатор
SurrealDB перетворює ієрархічну структуру ключів у діапазони для сканування
Встановлення (просте і швидке)
Linux.
$ curl -sSf https://install.surrealdb.com | sh
macOS
$ brew install surrealdb/tap/surreal
Windows.
PS C:\> iwr https://windows.surrealdb.com -useb | iex
Docker/Podmandocker
$ docker run --rm -p 8000:8000 surrealdb/surrealdb:latest start
$ podman run --rm -p 8000:8000 surrealdb/surrealdb:latest start
Запуск SurrealDB
Першим аргументом підкоманди start є місце, куди будуть записані дані.
За замовчуванням це пам'ять
$ surreal start --user root --pass root memory
Інші напрямки експорту
файлова система file:///path/to/data.db (RocksDB)
rocksdb:///path/to/data.db RocksDB
tikv://endpoiint TiKV TiKV
fdb:[///path/to/clusterfile] FoundationDB (потрібна перезбірка)
Режим STRICT.
--strict Почніть із зазначення параметрів.
NAMESPACE,, DATABASE має бути визначено, інакше виникне помилка
Помилка, якщо не визначено TABLE
Дамп/Відновлення
Дамп до файлу за допомогою підкоманди export
Відновлення з файлу за допомогою підкоманди import
$ surreal export --conn http://dev00:8000 --ns test --db test dump.db
$ surreal import --conn http://dev00:8000 --ns test --db test dump.db
Підключіться з CLI-клієнта
Виконайте підкоманду sql
$ surreal sql --conn http://dev00:8000 --ns test --db test --pretty
--user і --pass аутентифікація користувача/пароля ROOT
--pretty для форматування та виведення JSON виводу
--ns NAMESPACE --db instruct DATABASE
Коли SurrealDB працює в режимі STRICT, опції -ns і -db ігноруються
HTTP RESTful API
ТИП ШЛЯХУ Опис.
/key/:table GET Отримати всі записи в таблиці з бази даних.
/key/:table/:id GET Отримати конкретний запис з бази даних
/key/:table POST Створити запис у таблиці в базі даних
/key/:table/:id POST Створити певний запис у таблиці в базі даних
/key/:table/:table DELETE Видалити всі записи таблиці з бази даних
/key/:table/:id PUT Оновити вказаний запис у базі даних
/key/:table/:id PATCH Змінює вказаний запис у базі даних
/key/:table/:id DELETE Видаляє вказаний запис з бази даних
HTTP RESTful API (продовження)
ТИП ШЛЯХУ Опис.
/version GET Повертає версію SurrealDB
/signup POST Реєстрація для аутентифікації SCOPE
/signin POST Вхід з аутентифікацією SCOPE
/rpc POST Запит до WebSocket з JON-RPC
/sql POST Дозволити запити до SurQL
/export GET Дамп вмісту бази даних
/import POST Застосувати вміст запиту до бази даних (відновлення)
HTTP RESTful API (неповний)
ТИП ШЛЯХУ Опис.
/sync GET Реплікація
/health GET Перевірка стану бази даних
/status GET Повернення статусу
Визначення таблиць в загальному SQL
CREATE TABLE human (
id int, nickname text, nickname
текст ніка,
вік int,
PRIMARY KEY(id)
);
SurealDB не має схеми
Не потрібно визначати таблиці або поля
Не потрібно вносити зміни при додаванні полів
Якщо сервер запускається в режимі STRICT, спочатку потрібно визначити таблиці
CREATE human:freddie SET nickname="freddie", age=99 ;
CREATE human:brian SET nickname="brian", age=75, sex=true ;
ID == TableName:UniqID Зверніть увагу, що ID містить назву таблиці.
Вказати тип поля без схеми
CREATE human:freddie SET
nickname = <string> "freddie",.
age = <int> 99 ;
Типи та приведення
bool, int, float, string, number, decimal, datetime, duration
Рядки дати/часу перетворюються до стандарту ISO 8601: так само, як і приведення до <datetime>.
Якщо ви хочете обробляти рядок з датою/часом як звичайний рядок, використовуйте приведення до <string>.
SELECT * FROM "2023-01-01";.
SELECT * FROM <datetime> "2023-01-01";
SELECT * FROM <string> "2023-01-01T02:03:00Z" + "-test";
Визначте схему
DEFINE TABLE human SCHEMAFULL;
ОБМЕЖИМО поле nickname на рядок типу human TYPE;
ВИЗНАЧИМО ПОЛЕ вік за типом human TYPE int;
[перевизначити] таблицю як SCHMALESS
DEFINE TABLE human SCHEMALESS ;
DEFINE TABLE human SCHMAFULL; [пере]визначити таблицю як SCHMAFULL
DEFINE TABLE human SCHEMAFULL ;
SCHEMAFULL
Зберігаються лише дані, дозволені у визначених полях.
Можна обмежити певними типами даних
DEFINE FIELD може встановити значення за замовчуванням, якщо не введено жодних даних
Значення, що встановлюється, задається в $value
DEFINE TABLE person SCHEMAFULL;.
DEFINE FIELD name ON person TYPE string VALUE $value OR 'guest';.
Додавання даних
Якщо поле -id не вказано, ідентифікатор встановлюється автоматично
INSERT INTO людина (нік, вік)
VALUES ('brian', 75);
INSERT INTO human (id, нік, вік)
VALUES ('human:freddie', 'freddie', -1);
CREATE human:robert SET nickname='robert', age=30;
CREATE human SET
id = human:jack, nickname='jack', age=30;
CREATE human CONTENT
{ id: 'human:john', nickname: 'john', age: 99 };
INSERT
Може ОНОВЛЮВАТИ, якщо є дублікати ідентифікаторів
INSERT INTO test (id, test, something)
VALUES ('tester', true, 'other');
INSERT INTO test (id, test, something)
VALUES ('tester', true, 'other' )
ON DUPLICATE KEY UPDATE something = 'other' ;
Вкладені поля
Поля в таблиці можуть бути вкладеними.
На вкладені поля можна посилатися за допомогою крапкових позначень
UPDATE person:test CONTENT {
settings: {
nested: {
object: {
thing: 'test'
}
}
}
};
SELECT settings.nested.object FROM person ;.
NONE та NULL
Поле може мати значення NONE та NULL
NONE: значення не задано
NULL: задається пусте значення
CREATE person:test1 SET email = 'info@example.com';
CREATE person:test2 SET email = NONE;
CREATE person:test3 SET email = NULL;
ВИКОРИСТАННЯ.
Вказати простір імен та базу даних для використання
Ефективно при доступі з автентифікацією ROOT.
USE NAMESPACE test ;
USE NAMESPACE test DATABASE db1 ;
USE NS test тест БД db1 ;
Відношення в загальному SQL
CREATE TABLE armor (
id int,
ім'я текст,
опір текст,
PRIMARY KEY(id)
);
INSERT INTO armor VALUES
(0, "leather", 3);
(1, "platemail", 30),
(2, "кольчуга", 20),.
CREATE TABLE player (
ім'я текст,
strength int,
armor_id int,
PRIMARY KEY((ім'я),.
CONSTRAIN fk_armor
FOREIGN KEY(armor_id)
ПОСИЛАННЯ armor(id)
);
Відношення в SurrealQL (SurQL)
CREATE armor:leather SET registance = 3;
CREATE armor:кольчуга SET registance = 20;
CREATE armor:platemail SET registance = 30;
CREATE player:jack SET strength = 22, armor = armor:platemail;
CREATE player:brian SET strength = 20, armor = armor:leather;
Використання імен таблиць в ідентифікаторах
SurQL: Відношення з визначеною схемою
DEFINE TABLE armor SCHEMAFULL;.
DEFINE FIELD resistance ON armor TYPE int;.
CREATE armor:leather SET resistance = 3;
CREATE armor:кольчуга SET resistance = 20;
CREATE armor:platemail ВСТАНОВИТИ опір = 30;
DEFINE TABLE player SCHEMAFULL;.
DEFINE FIELD strength ON player TYPE int;
DEFINE FIELD armor ON player TYPE record(armor);
CREATE player:jack SET strength = 22, armor = armor:platemail;
CREATE player:brian SET strength = 20, armor = armor:leather;
Загальні SQL-зв'язки: JOIN
SELECT
ім'я гравця,
player.strength,.
armor.name AS armor_name,.
опір броні AS опір_броні
FROM player
JOIN armor
ON armor.id = player.armor_id
Відношення в SurQL: не потрібно JOIN
FETCH розширює вказане поле
SELECT * FROM player FETCH armor;.
Посилання на запис
CREATE armor:leather SET registance = 3;
CREATE armor:chainmail SET registance = 20;
CREATE armor:platemail SET registance = 30;
CREATE player:jack SET strength = 22, armor = armor:platemail;
CREATE player:brian SET strength = 20, armor = armor:leather;
Зовнішній ключ == Посилання на запис
Відношення: ONE-TO-ONE
CREATE human:freddie SET nickname="freddie", age=99 ;
CREATE human:brian SET nickname="brian", age=75
UPDATE human:brian SET bff = human:freddie;
SELECT bff.nickname, bff.age FROM human:brian
Вкажіть поля в зовнішній таблиці, з'єднавши їх крапками
Зв'язок: один-до-багатьох
CREATE car:tesla SET model='Model S', ev=True, price=99000;
CREATE car:mustang SET model='Mustang Cobra', ev=False, price=60000;
UPDATE human:brian SET cars=["car:tesla"];
UPDATE human:freddie SET cars=["car:mustang"];
UPDATE car:tesla SET owner = human:brian;
UPDATE car:mustang SET owner = human:freddie;
CREATE parts:tire SET brand='Michelin', size=5;
CREATE parts:gastank SET brand='Tanksy', size=10;
CREATE parts:battery SET brand='Xi Ping', size=20;
UPDATE car:mustang SET parts = ['parts:tire', 'parts:gastank'];
UPDATE car:tesla SET parts = ['parts:tire', 'parts:battery'];
ВІДНОШЕННЯ: ОДИН-ДО-БАГАТЬОХ
SELECT parts FROM car:mustang
SELECT cars.parts.brand FROM human:brian ;.
Вказуйте поля в зовнішніх таблицях, з'єднуючи їх крапками
Графові з'єднання
RELATE player:jack -> wants_to_buy -> armor:dragon;
RELATE player:jack -> wants_to_buy -> armor:platemail;
SELECT * FROM wants_to_buy;
SELECT id, -> wants_to_buy -> armor AS wtb FROM player;
SELECT id, <- wants_to_buy <- player AS players FROM armor:dragon
Вказуйте поля в зовнішніх таблицях, з'єднуючи їх за допомогою "->" або "<-".
LIMIT.
Вказує кількість результатів, які повертає SELECT
На даний момент не дуже добре працює вказівка FETCH
CREATE tag:rs SET name = 'Rust';.
CREATE tag:go SET name = 'Golang';
CREATE tag:js SET name = 'JavaScript';
CREATE person:tobie SET tags = [tag:rs, tag:go, tag:js];
CREATE person:jaime SET tags = [tag:js];
SELECT * FROM person LIMIT 1;
SELECT * FROM (SELECT * FROM person FETCH tags) LIMIT 1;
// SELECT * FROM person LIMIT 1 FETCH tags;
START.
Вказує початкову позицію результату, що повертається SELECT (з нуля).
Добре працює, коли вказано FETCH
SELECT * FROM person START AT 1;
SELECT * FROM (SELECT * FROM person FETCH теги) START 1;
SELECT * FROM person START 1 FETCH теги;
SurQL: WHERE, ORDER BY, GROUP BY
SELECT * FROM armor ;
SELECT * armor WHERE resistance >= 30 ;
SELECT math::sum(strength) FROM player GROUP BY ALL ;
SELECT * FROM armor ORDER BY RAND();
SELECT * FROM armor ORDER RAND();
SELECT * FROM armor ORDER resistance NUMERIC ASC ;
SELECT * FROM armor ORDER resistance NUMERIC DESC ;
SurQL: BEFORE, AFTER, DIFF
CREATE, UPDATE і DELETE можуть повертати до, після і різницю запитів
UPDATE human:freddie SET email = 'freddie@example.com';
UPDATE human:freddie SET email = 'freddie@dummy.com' RETURN DIFF ;.
Визначення аналітичних запитів
Після того, як дані записані, виконайте відбір, агрегування, групування, впорядкування тощо.
DEFINE TABLE person SCHEMALESS;.
DEFINE TABLE person_by_age AS
SELECT
count(),.
вік,.
math::sum(age) AS total, math::mean(age) AS average
math::mean(age) AS average
FROM person
GROUP BY age ;
EVENT.
Вміст таблиці, вказаний в ON TABLE: $before, $after
Ідентифікатор, при якому відбулася подія: $this
Подія, що відбулася: $event
UPDATE human:freddie SET email = 'freddie@example.com';
UPDATE human:brian SET email = 'brian@example.com';
DEFINE EVENT changelog ON TABLE human
WHEN $before.email ! = $after.email
THEN ( CREATE changelog SET
time = time::now(),.
email = $after.email );
RANGE Повторювати стільки разів, скільки вказано через двокрапку
> CREATE |test:10| SET time = time::now();
[
{
"result": [
{
"id": "test:g9hq0dowz77us5yvxnst",.
"time": "2022-12-20T04:10:19.282031670Z"
},
{
"id": "test:nk45tn46dy2bn1hd6zj9", { "time".
"time": "2022-12-20T04:10:19.282450969Z"
},.
RANGE Повторити із зазначенням початкового та кінцевого значень.
> CREATE |test:1..10| SET time = time::now();
[
{
"result": [
{
"id": "test:1",.
"time": "2022-12-20T04:12:21.477667592Z"
},
{
"id": "test:2", "time".
"time": "2022-12-20T04:12:21.478289880Z"
},.
Регулярний вираз.
> SELECT * FROM test WHERE id = /. *[24]. */
[
{
"result": [
{
"id": "test:2",.
"time": "2022-12-20T04:12:21.478289880Z"
},
{
"id": "test:4", "time".
"time": "2022-12-20T04:12:21.478332436Z"
}
],.
"status": "OK",.
IF THEN ELSE
UPDATE person SET classtype =
IF age <= 10 THEN
'junior'.
ELSE IF age <= 21 THEN
'студент'
ELSE IF age >= 65 THEN
'старший'
ELSE
NULL
END
MERGE.
Об'єднання полів таблиці: додавання, видалення
UPDATE person:test SET
name.initials = 'TMH',.
name.first = 'Tobie',.
name.last = 'Morgan Hitchcock';.
UPDATE person:test MERGE {
ім'я: {
title: 'Mr',.
ініціали: NONE,.
суфікс: ['BSc', 'MSc', }
}
};
Обмеження таблиці ASSERT
Кожне визначене поле може визначати обмеження на дані в ASSERT
DEFINE FIELD countrycode ON user TYPE рядок
// Переконатися, що код країни відповідає ISO-3166
ASSERT $value ! = NONE AND $value = /[A-Z]{3}/
// Встановити значення за замовчуванням, якщо пусто
VALUE $value OR 'GBR'
;.
Функція FUTURE
- Визначити поля таблиці за значеннями, які будуть встановлені пізніше
UPDATE person:test SET
can_drive = <future> {
birthday && time::now() > birthday + 18y };
UPDATE person:test SET birthday = <datetime> '2007-06-22';
UPDATE person:test SET birthday = <datetime> '2001-06-22';
ДОЗВОЛИ.
Обмеження CRUD-операцій на TABLE і FIELD
ВИЗНАЧИТИ користувацьку TABLE БЕЗСХЕМНО
ДОВОЛЕЖЕННЯ
FOR select, create, update
WHERE id = $auth.id
FOR delete
WHERE id = $auth.id OR $auth.admin = true ;.
ACID-транзакція
BEGIN TRANSACTION;.
UPDATE coin:one SET balance += -23.00 ;
UPDATE coin:two SET balance -= 23.00 ;
ЗАФІКСУВАТИ ТРАНЗАКЦІЮ;.
АБО
СКАСУВАТИ ТРАНЗАКЦІЮ;.
Якщо для таблиці зі встановленим DROP вказується нова транзакція, всі транзакції, які не були завершені, знищуються.
Якщо встановлено DROP і для таблиці вказано нову транзакцію, всі транзакції, які не були завершені, відкидаються.
За замовчуванням нові транзакції доступні лише для читання (якщо це можливо).
WebSocket
Вказати URL на зразок ws://dev00:8000/rpc (ws://, wss://)
POST-передача з наступним JSON в якості тіла повідомлення.
{
"id": <ID для ідентифікації>,.
"method": <команда>,.
"params": <масив параметрів, необхідних команді>.
}
Аутентифікація в SurrealDB
Аутентифікація ROOT
Користувач/пароль, вказаний при запуску сервера
-pass Відсутність опції вимикає автентифікацію ROOT
Автентифікація користувача.
Користувач/пароль, створений за допомогою DEFINE LOGIN
Автентифікація за допомогою токена
Автентифікація за допомогою JSON Web Token (JWT) (RFC7519, RFC8725)
Автентифікація за допомогою сторонніх OAuth-сервісів
Аутентифікація SCOPE
SIGNUP та SIGNIN попередньо визначені
Періоди доступу можуть бути обмежені
LOGIN
Доступ може бути обмежено до ПРОСТОРУ ІМЕН та БАЗИ ДАНИХ
Користувачі без авторизації NAMESPACE не можуть створювати/видаляти БД
DEFINE LOGIN admin НА NAMESPACE PASSWORD "admin.admin";.
DEFINE LOGIN guest ON DATABASE PASSWORD "guest.guest";.
Зображення для входу.
surrealdb_login.png
TOKEN.
Дозволяти доступ тільки для запитів з певним токеном в заголовку
Може бути встановлений для ПРОСТОРУ ІМЕН, БАЗИ ДАНИХ і СФЕРИ
DEFINE TOKEN my_token ON DATABASE
TYPE HS512 VALUE '1234567890';.
SCOPE.
Всі поля JSON-RPC встановлюються як змінні
SCOPE дає можливість доступу до бази даних
Доступ до таблиць і полів залежить від ДОПУСКУ.
SCOPE з TOKEN не може створювати/модифікувати/видаляти таблиці або відображати інформацію
DEFINE FIELD email ON TABLE user TYPE string ASSERT is::email($value);
ВИЗНАЧАЄТЬСЯ ІНДЕКС email НА стовпчиках таблиці user COLUMNS email UNIQUE;
DEFINE SCOPE account SESSION 24h
SIGNUP ( CREATE user SET
email = $email, pass = crypto::argon
pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user
WHERE email = $email
AND crypto::argon2::compare(pass, $pass) );.
Зображення SCOPE
surrealdb_scope.png
SIGNUP для аутентифікації SCOPE
let jwt = fetch('https://api.surrealdb.com/signup', {
method: 'POST', {
headers: {
'Accept': 'application/json',.
'NS': 'google', // Вказати простір імен
'DB': 'gmail', // Вказати базу даних
}, }
body: JSON.stringify({
'NS': 'google', // Вказати простір імен
'SC': 'account',.
email: 'tobie@surrealdb.com',.
pass: 'a85b19*1@jnta0$b&!'
}),.
});
Посилання на токени та дані аутентифікації із запитів.
$session, $scope, $token та $auth встановлюються на спеціальну інформацію, релевантну для клієнта
При використанні NAMESPACE, DATABASE або TOKEN встановлюються $session і $token
$toekn встановлюється у всі поля JWT-токену
$scope встановлюється на ім'я SCOPE в автентифікації SCOPE
$auth встановлюється, якщо JWT має поле id в аутентифікації SCOPE і дані, вказані в id, існують в таблиці
SELECT * FROM $session;.
SELECT * FROM $token;.
SELECT * FROM $scope;.
SELECT * FROM $auth;.
LIVE запит
Діє при доступі через WebScoket
Зміни даних передаються в реальному часі клієнтам, додаткам, пристроям кінцевих користувачів і серверу
Передача в реальному часі до сторонніх бібліотек
Всі клієнтські пристрої синхронізуються
Щоб завершити LIVE-запит, вкажіть його за допомогою KILL
LIVE SELECT * FROM user WHERE age > 18 ;.
PARALLEL.
PARALLEL додається до команд CREATE, DELETE, UPDATE та SELECT
Виконання запиту обробляється паралельно
SELECT * FROM test PARALLEL ;
TIMEOUT
TIMEOUT додається в CREATE, DELETE, UPDATE і SELECT.
Очікування часу, зазначеного при виконанні запиту
SELECT * FROM
http::get('https://ipinfo.io')
TIMEOUT 10s;.
GeoJSON
Pont, Line, Polygon, MultiPoint, MultiLine, MultiPolygon, Collection
UPDATE university:oxford SET area = {{}.
координати: [
[[ [10.0, 11.2],[10.5, 11.9],[10.8, 12.0],[10.0, 11.2]],]
[[ [9.0, 11.2], [10.5, 11.9],[10.3, 13.0], [9.0, 11.2]]
]
};
SELECT * FROM university:oxford;.
Вбудовані функції SurQL
array::xxxx()
combine, complement, concat, difference, disinc, intersect, len, sort::asc, sort::desc, sort, union, all, any, add, append, insert, prepend, remove add, append, insert, prepend, remove, reverse, group, push, pop
count()
crypto::xxxx()
argon2::compare, argon2::generate, bcrypt::compare, bcrypt::generate, md5, pdkdf2::compare, pdkdf2::generate, scrypt::compare, scrypt:: generate, sha1, sha25, sha512
Вбудовані функції SurQL (продовження)
duration::xxxx()
дні, години, хвилини, секунди, тижні, роки
geo::xxxx()
площа, пеленг, центроїд, відстань, hash::decode, hash::encode
http::xxxx()
head, get, put, post, patch, delete
is::xxxx()
alphanum, alpha, domain, email, hexadecimal, latitude, longitude, numeric, semver, url, uuid, url, datetime
Вбудовані функції SurQL (продовження)
math::xxxx()
abs, bottom, ceil, fixed, floor, interquartile, max, mean, midhinge, min, mode, nearestrank, percentile, round, spread, sqrt, stddev, sum, top, trimean, variance, pow
not()
parse::email()
хост, користувач
parse::url::xxxx)
домен, фрагмент, хост, порт, шлях, запит, схема
Вбудовані функції SurQL (продовження)
rand()
rand::xxxx()
bool, enum, float, guid, int, string, time, uuuid:v4, uuid:v7, uuid
session::xxxxxx()
db, id, ip, ns, origin, sc, sd, token,.
Вбудовані функції SurQL (продовження)
string::xxxx()
concat, endsWith, join, length, lowercase, repeat, replace, reverse, slice, slug, split, startsWith, trim, uppercase, words
час::xxxx()
день, поверх, формат, група, година, хвилина, місяць, нано, зараз, коло, секунда, unix, wday, yday, рік, часовий пояс
type::xxxxxx()
bool, datetime, decimal, duration, float, int, number, point, regex, table, thin
Вбудовані в SurQL звичайні числа
math::xxxx
E, FRAC_1_PI, FRAC_1_SQORT_2, FRAC_2_PI, FRAC_2_SQRT_PI, FRAC_PI_2, FRAC_PI_3, FRAC_PI_4, FRAC_PI_6, FRAC_PI_8, LN_10, LN_2, LOGO10_2, LOG10_E,. LOG2_10, LOG2_E, PI, SQRT_2, TAU
Налаштування параметрів LET
Числові, рядкові та інші об'єкти можна задавати як змінні
Можна посилатися на них як на $змінну із запиту
LET $test = { some: 'thing', other: true };
SELECT * FROM $test WHERE some = 'thing';
Розширення SurQL за допомогою JavaScript
Всі значення з SurrealDB автоматично перетворюються в типи JavaScript
Значення, що повертаються з функцій JavaScript, автоматично конвертуються в значення SurrealDB
Булеві типи, цілі числа, числа з плаваючою точкою, рядки, масиви, об'єкти та об'єкти дати
автоматично конвертуються у значення SurrealDB або з них.
CREATE user:test SET created_at = function() {
return new Date();
};
Приклад JavaScript розширення 1
CREATE platform:test SET version = function() {
const { platform } = await import('os');
return platform();
};
Приклад розширення на JavaScript 2
LET $value = 'SurrealDB';.
LET $words = ['awesome', 'advanced', 'cool'];
CREATE article:test SET summary = function($value, $words) {
return `${аргументи[0]} is ${аргументи[1].join(', ')}`;
};
Приклад JavaScript-розширення 3
CREATE film:test SET
ratings = [
{ ratings: 6.3 }
{ рейтинг: 8.7 }, }
],.
display = function() {
return this.ratings.filter(r => {
return r.rating >= 7;
}).map(r => {
return { ... .r,.
rating: Math.round(r.rating * 10) }; }
});
};
Простий приклад реалізації RETful API на Python
from urllib.request import Request, urlopen
import base64
import json
from pprint import pprint as pp
_BASE_URL = "http://dev00:8000"
_NAMESPACE = "test"
_DATABASE = "test"
_USER = "root"
_PASS = "root"
auth_str = f"{_USER}:{_PASS}".encode("utf-8")
credential = base64.b64encode(auth_str)
auth = "Basic " + credential.decode("utf-8")
headers = {{ "Accept": "application.js"; "accept": "application.js
"Accept": "application/json",.
"Авторизація": auth,.
"NS": _NAMESPACE,.
"DB": _DATABASE,.
}
url = _BASE_URL + "/key/human"
request = Request(url, headers=headers)
with urlopen(request) as res:.
data = res.read()
pp(json.loads(data)[0]['result'])
Приклад виконання запиту через RETful API на Python
import requests
from requests.auth import HTTPBasicAuth
from pprint import pprint as pp
_URL = "http://dev00:8000/sql"
_NAMESPACE = "test"
_DATABASE = "test"
_USER = "root"
_PASS = "root"
_HEADERS = {}.
'Content-Type': 'application/json',.
'Accept':'application/json',.
'ns': _NAMESPACE,.
'db': _DATABASE'
}
_auth = HTTPBasicAuth(_USER, _PASS)
def db(query):.
res = requests.post( _URL,.
headers=_headers,.
auth = _auth,
data=query )
if "code" в res.json():.
згенерувати виключення (res.json())
return res.json()
if __name__ == '__main__':.
while True:.
sql = input('SQL> ')
if sql.upper() == 'Q':
break
val = db(sql)
pp(val)
Приклад запиту через WebSocket на Python
import asyncio
from surrealdb import WebsocketClient
from pprint import pprint as pp
_URL = "ws://dev00:8000/rpc"
_NAMESPACE = "test"
_DATABASE = "test"
_USER = "root"
_PASS = "root"
async def main():.
async with WebsocketClient( url=_URL,.
namespace=_NAMESPACE, database=_DATABASE,.
username=_USER, password=_PASS,.
) as session:.
while True:.
sql = input('SQL> ')
if sql.upper() == 'Q': break
res = await session.query(sql)
pp(res).
PyPI погано працює з WebSocket
Виконується запит на витягування.
Цей репозиторій поки що працюватиме.
GitHub - iisaka51/surrealdb.py на develop
Інші приклади реалізації
Flask
GitHub - santiagodonoso/python_flask_surrealdb
FastAPI
GitHub - santiagodonoso/fastapi_surrealdb_v_1
PHP
GitHub - santiagodonoso/php_surrealdb
React
GitHub - rvdende/surrealreact: інтерфейс дослідника SurrealDB, написаний на реакті.
Deno
https://github.com/officialrajdeepsingh/surrealDb-deno
Типоскрипт.
Пояснення SurrealDB за допомогою Express.js, Node.js та TypeScript
На що слід звернути увагу
Внутрішні БД доступні в surrealdb-1.0.0-beat8
RocksDB, TiKV
FoundationDB, IndexedDB потребують перезбірки
IndexedDB можна використовувати для вбудованих цілей
Для доступу до БАЗИ ДАНИХ потрібен простір імен (NAMESPACE)
Різні простори імен призведуть до різних БД з однаковими іменами.
В одному просторі імен можна створити будь-яку кількість БАЗ ДАНИХ.
Однак, вам потрібен дозвіл на доступ до NAMESPACE/.
Це, ймовірно, баг!
Деякі підкоманди SurQL погано працюють в CLI
Підкоманда SurQL - це доступ до HTTP RestfulAPI з натисканням клавіші повернення
Крапка з комою не розпізнається як один запит
У файлах, наданих підкомандою import, все працює нормально.
Копіювання та вставка є нормальним, якщо натиснуто клавішу Return.
Коментарі приймаються, але тільки коментарі є помилкою.
Порожній символ ('') вважається виконаним.
Змінні, визначені за допомогою LET, втрачаються при натисканні клавіші Return.
Транзакція вважається NG, якщо клавіша Return натиснута в середині транзакції.
Оголошення USE також втрачається при натисканні клавіші Return.
Висновок.
Напрочуд легко встановити і налаштувати
Дуже, дуже, дуже сумісна з веб-додатками
Потужні запити, що розширюються за допомогою JavaScript
Бізнес-логіка та автентифікація користувачів можуть оброблятися безпосередньо в базі даних
Спрощений стек внутрішніх технологій → скорочення часу розробки → зниження витрат
Все ще в процесі розробки, але дуже багатообіцяюче
Посилання
Офіційний сайт SurrealDB
GitHub - surrealdb/surrealdb: Масштабована, розподілена, спільна, документ-графова база даних для Інтернету в режимі реального часу
StackOverflow
Помилка аутентифікації при використанні зовнішнього токену JWT в SurrealDB
Обхід відношень по висхідній в SurrealDB
Як увійти в область видимості в SurrealDB?
Як вказати простір імен при використанні веб-сокетів з SurrealDB
Рекомендовані REST-клієнти
Surrealist
Розділяє результати декількох запитів на вкладки.
Чудовий приклад реалізації SurrealDB
GitHub - StarlaneStudios/Surrealist: ⚡ Блискавично швидкий графічний майданчик запитів до SurrealDB для робочого столу
Postman з вкладками - REST-клієнт
Розширення для Chrome
Запити можна зберігати як колекції
Колекції можна експортувати/імпортувати
Postman з вкладками - REST-клієнт
Thunder Client
Розширення для VSCode
Thunder Client - Розширення клієнта REST API для VS Code
Додаток.
Параметри сервера та змінні оточення
DB_PATH: Місце зберігання даних (пам'ять)
USER Ім'я користувача для автентифікації ROOT (root), --user/-u
PASS Пароль користувача для автентифікації ROOT, --pass/-p
ADDR Підмережа, у якій дозволено автентифікацію ROOT (127.0.0.1/32), --addr
BIND Ім'я хоста/IP-адреса для прослуховування з'єднань (0.0.0.0:8000), --bind/-b
KEY Секретний ключ для шифрування ON-DISK, --key/-k
KVS_CA Файл сертифікатів для KVS-з'єднання, --kvs-ca
KVS_CRT CERT-файл для KVS-з'єднання, --kvs-crt
KVS_KEY Приватний ключ для KVS-з'єднань, --kvs-key
WEB_CRT CERT-файл для прослуховування SSL-з'єднань, --web-crt
WEB_KEY Закритий ключ для прослуховування SSL-з'єднань, --web-key
STRICT Запуск у режимі STRICT, якщо встановлено, --strict
LOG Рівень журналу "warn", ["info"], "debug", "trace", "full", --log
ACID: чотири характеристики, які визначають транзакцію
Атомарність.
Кожен оператор у транзакції розглядається як одиниця.
Послідовність.
Гарантує, що транзакція вносить зміни в таблицю тільки в заздалегідь визначений і передбачуваний спосіб.
Забезпечує, що транзакція вносить зміни в таблицю тільки у заздалегідь визначений та передбачуваний спосіб.
Ізоляція
Забезпечує обробку кожного запиту так, ніби він відбувається незалежно, навіть якщо кілька користувачів одночасно читають і записують в одну і ту ж таблицю.
Кожен запит обробляється так, ніби він виник незалежно, навіть якщо кілька користувачів одночасно читають і пишуть в одну і ту ж таблицю.
Довговічність
Гарантує, що зміни даних від успішно виконаних транзакцій будуть збережені у разі збою системи.
Гарантує, що зміни даних від успішно виконаних транзакцій будуть збережені навіть у разі збою системи.
Проблема N+1
Проблема, коли доступ до бази даних призводить до того, що запити виконуються загалом N+1 раз.
SELECT виконується один раз для отримання N записів
Виконання SELECT N разів для отримання даних, пов'язаних з N записами
Схильні до прихованої роботи при використанні ORM
Може спричинити важку (повільну) роботу програми
RocksDB
Популярна, високопродуктивна вбудована база даних KVS
Форк LevelDB, розроблений компанією Meta (Facebook)
Використовується у виробництві різними веб-сервісами, такими як Facebook, Yahoo!
Забезпечує персистентність даних, одночасно підвищуючи продуктивність та безпеку
Продуктивність лінійно зростає з кількістю процесорів
На продуктивність RocksDB сильно впливає налаштування платформи
Непросто через складність багатьох параметрів, що налаштовуються
TiKV
База даних KVS, що працює на бекенді TiDB
Персистентність даних (з RocksDB)
Гарантії цілісності даних для розподілених баз даних
MVCC (Multi-Version Concurrency Control)
Реалізація розподілених транзакцій
Google Parcorator / 2PC (2 Phase Commit)
Співпроцесор
Зауважте, що за замовчуванням TiKV конфігурується як 3-вузловий кластер.
Зберігається у тимчасових файлах, поки кластер не буде сконфігурований з 3-ма вузлами
Тимчасові файли автоматично видаляються при запуску.
Посилання: https://docs.pingcap.com/tidb/dev/tikv-configuration-file
Конфігурація кластера: https://tikv.org/docs/5.1/deploy/install/production/
Архітектура TiKV
tikv_stack.png
IndexedDB
Вбудована база даних KVS на основі браузера
Постійно зберігає дані в браузері користувача
Дозволяє створювати веб-додатки з розширеними можливостями запитів незалежно від умов мережі
Без схем
Підтримує ACID-транзакції
Асинхронна обробка
Багатоверсійний паралелізм
Користувачі можуть видаляти дані так само легко, як стирати файли cookie
Вибір браузера
Дані не можуть бути зареєстровані в IndexedDB, якщо їх обсяг перевищує певний рівень.
Обсяг даних, які можна зареєструвати в IndexedDB, залежить від середовища.
FoundationDB
NoSQL з підтримкою ACID-транзакцій
Можна працювати з SQL.
Ключі сортуються.
Пропускна здатність 20 000 записів/сек на одне ядро при використанні SSD
Лінійно масштабується до 500 ядер
Читання за 1 мс, запис за 5 мс
Можна розподіляти/резервувати в кластерній конфігурації
Може бути сконфігурований принаймні з одним вузлом (резервування/вузли не можуть бути додані пізніше)
Конфігурація FDB за замовчуванням не працює належним чином, тому потрібна конфігурація
$ fdbcli --exec 'configure ssd'
$ fdbcli --exec 'writemode on'
$ fdbcli --exec 'getrangekeys \x00 \xff'
Посилання: https://apple.github.io/foundationdb/command-line-interface.html
У бінарних випусках FoundationDB відключено
Залежить від версії FoundationDB, вказаної у функціях
$ cargo feature surreal
Доступні функції для ``surreal``.
default = ["storage-rocksdb", "scripting", "http"].
http = ["surrealdb/http"].
scripting = ["surrealdb/scripting"].
storage-fdb = ["surrealdb/kv-fdb-6_3"].
storage-rocksdb = ["surrealdb/kv-rocksdb"]]
storage-tikv = ["surrealdb/kv-tikv"].
$ grep kv-fdb- lib/Cargo.toml
kv-fdb-5_1 = ["foundationdb/fdb-5_1", "kv-fdb"].
kv-fdb-5_2 = ["foundationdb/fdb-5_2", "kv-fdb"].
kv-fdb-6_0 = ["foundationdb/fdb-6_0", "kv-fdb"].
kv-fdb-6_1 = ["foundationdb/fdb-6_1", "kv-fdb"].
kv-fdb-6_2 = ["foundationdb/fdb-6_2", "kv-fdb"].
kv-fdb-6_3 = ["foundationdb/fdb-6_3", "kv-fdb"].
kv-fdb-7_0 = ["foundationdb/fdb-7_0", "kv-fdb"].
kv-fdb-7_1 = ["foundationdb/fdb-7_1", "kv-fdb"].
Доступна нотація є вадою у вантажній функції. Видано запити на вилучення.
Перезбірка SurrealDB.
Налаштуйте середовище розробки rust (за необхідності).
$ curl -sSf https://sh.rustup.rs | sh
$ source $HOME/.cargo/env
$ rustup install stable
Клонуйте репозиторій SurrealDB
$ git clone https://github.com/surrealdb/surrealdb.git
$ cd surrealdb
Зберіть заново.
$ carrgo build -release -all-features # Не вдається на 2 ГБ пам'яті.
# або
$ cargo build -release -features storage-fdb # TiKV буде вимкнено
TiKV проти FoundationDB
Функціонал моніторингу FDB слабкий
$ fdbcli --exec 'status json'
TiKV з Prometheus для посилань на дані та Grafana для моніторингу стану
FDB чутлива до версій
За замовчуванням FDB зберігається у пам'яті і потребує зміни конфігурації
FDB реалізована на C++, TiKV - на Rust
FDB може обслуговуватися принаймні з одним вузлом, TiKV вимагає щонайменше трьох вузлів
Реплікація RocksDB
Реплікація у реальному часі можлива за допомогою rocksplicator
https://github.com/pinterest/rocksplicator
Зауважте, однак, що Rocksplicator - це застарілий проект, який не підтримується і не обслуговується Pinterest.
Про зберігання бінарних файлів.
SurrealDB не призначена для такого використання
Слід розглянути об'єктне сховище
GitHub - minio/minio: Мультихмарне сховище об'єктів
GitHub - scality/Zenko: Zenko - мультихмарний контролер даних з відкритим вихідним кодом: володійте та контролюйте свої дані в будь-якій хмарі.
Синтаксис SurQL
INFO
INFO FOR [
KV
| NS | ПРОСТІР ІМЕН
| БАЗА ДАНИХ
| БАЗА ДАНИХ
| TABLE @table
];
ВИЗНАЧИТИ ПРОСТІР ІМЕН | БАЗУ ДАНИХ ...
DEFINE [
ПРОСТІР ІМЕН @name
DEFINE [ NAMESPACE | DATABASE @name
| LOGIN @name ON [ NAMESPACE | DATABASE ]
[ PASSWORD @pass | PASSHASH @hash ]
| TOKEN @name ON [ NAMESPACE | DATABASE | SCOPE ]
TYPE @алгоритм VALUE @значення
| SCOPE @name [ SESSION @duration ]
[ SIGNUP @вираз ] [ SIGNIN @вираз ]
| EVENT @name ON [ TABLE ] @table
WHEN @вираз THEN @вираз
@алгоритм
EDDSA, ES256, ES384, ES512, HS256, HS384, HS512,.
PS256, PS384, PS512, RS256, RS384, RS51
ОЗНАЧИТИ ТАБЛИЦЮ
DEFINE [
| TABLE @name
[DROP ]
[ SCHEMAFULL | SCHEMALESS ]
[ AS SELECT @projections
FROM @tables
[ WHERE @condition ]
[ GROUP [ BY ] @groups ]
]
[ PERMISSIONS [ NONE | FULL
| FOR select @expression
| FOR create @expression
| FOR update @expression
| FOR delete @expression
] ]
; ;
ВИЗНАЧИТИ ПОЛЕ | ІНДЕКС
DEFINE [
| FIELD @name ON [ TABLE ] @table
[ TYPE @type ]
[ VALUE @вираз ]
[ ASSERT @вираз ]
[ PERMISSIONS [ NONE | FULL
| FOR select @expression
| FOR create @expression
| FOR update @expression
| FOR delete @expression
]
| INDEX @name ON [ TABLE ] @table [ FIELDS | COLUMNS ] @fields [ UNIQUE ]
] ;
CREATE
CREATE @targets
[ CONTENT @value
| SET @field = @value ...
]
[ RETURN [ NONE | BEFORE | AFTER | DIFF | @projections ... ]
[ TIMEOUT @duration ]
[ PARALLEL ]
;
ВИДАЛИТИ
REMOVE [
NAMESPACE @name
| DATABASE @name
| LOGIN @name ON [ NAMESPACE | DATABASE ]
| TOKEN @name ON [ NAMESPACE | DATABASE ]
| SCOPE @name
| TABLE @name
| TABLE @name
| FIELD @name ON [ TABLE ] @table
| INDEX @name ON [ TABLE ] @table
] ;
INSERT
INSERT [ IGNORE ] INTO @what
[ @value
| (@fields) VALUES (@values)
[ ON DUPLICATE KEY UPDATE @field = @value ... ]
]
;
ОНОВЛЕННЯ
UPDATE @targets.
[ CONTENT @value
| MERGE @value
| PATCH @value
| SET @field = @value ...
]
[ WHERE @умова ]
[ RETURN [ NONE | BEFORE | AFTER | DIFF | @projections ... ]
[ TIMEOUT @duration ]
[ PARALLEL ]
;
ВИДАЛИТИ
DELETE @targets ]
[ WHERE @condition ]
[ RETURN [ NONE | BEFORE | AFTER | DIFF | @projections ... ]
[ TIMEOUT @duration ]
[ PARALLEL ]
;
SELECT
SELECT @projections
FROM @targets
[ WHERE @condition ]
[ SPLIT [ AT ] @field ... ]
[ GROUP [ BY ] @field ... ]
[ ORDER [ BY ]
@field [RAND()| COLLATE| NUMERIC ] [ ASC | DESC ] ...
]
[ LIMIT [ BY ] @limit ]
[ START [ AT ] @start ]
[ FETCH @field ... ]
[ TIMEOUT @duration ]
[ PARALLEL ]
;
ВИКОРИСТОВУВАТИ
ВИКОРИСТОВУВАТИ.
[ NAMESPACE | NS ] @namespace ]
[[ DATABASE | DB ] @database ] ;
поділитися
Зареєструйтесь зараз і почніть користуватися Qiita зручніше!
Ми надішлемо вам статті, які відповідають вашим потребам.
Ви зможете ефективно перечитувати корисну інформацію пізніше.
Про функції, які ви можете використовувати при вході в систему
iisaka51
@iisaka51 (Гоічі (Іісака) Юкава)
https://github.com/iisaka51
посилання
rss
0 件のコメント:
コメントを投稿