umbot-ydb-adapter 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,457 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.YdbAdapter = void 0;
4
+ const plugins_1 = require("umbot/plugins");
5
+ const umbot_1 = require("umbot");
6
+ const core_1 = require("@ydbjs/core");
7
+ const query_1 = require("@ydbjs/query");
8
+ const anonymous_1 = require("@ydbjs/auth/anonymous");
9
+ const environ_1 = require("@ydbjs/auth/environ");
10
+ const metadata_1 = require("@ydbjs/auth/metadata");
11
+ const static_1 = require("@ydbjs/auth/static");
12
+ const yql_1 = require("./yql");
13
+ const DEFAULT_CONNECT_TIMEOUT = 10000;
14
+ const DEFAULT_QUERY_TIMEOUT = 5000;
15
+ // Создание таблицы или индекса в serverless YDB может идти дольше обычного запроса
16
+ const MIN_DDL_TIMEOUT = 30000;
17
+ const CONNECTION_STRING_ENV = 'YDB_CONNECTION_STRING';
18
+ /**
19
+ * Адаптер umbot для YDB (Yandex Database).
20
+ *
21
+ * Хранит `UsersData`, `ImageTokens`, `SoundTokens` и данные своих моделей. Таблицы
22
+ * встроенных моделей создаёт сам (`ensureSchema`). Все значения уходят в запрос
23
+ * параметрами (`$p0`, `$p1`, ...): конкатенации пользовательских данных в YQL нет.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * import { Bot } from 'umbot';
28
+ * import { YdbAdapter } from 'umbot-ydb-adapter';
29
+ *
30
+ * // Строка подключения — из YDB_CONNECTION_STRING, авторизация — из окружения
31
+ * const bot = new Bot().use(new YdbAdapter());
32
+ * ```
33
+ */
34
+ class YdbAdapter extends plugins_1.BaseDbAdapter {
35
+ dbFormat = 'ydb';
36
+ #driver;
37
+ #sql;
38
+ #tables = new Map();
39
+ /**
40
+ * @param config Параметры подключения (см. {@link IYdbAdapterConfig}). Без них строка подключения
41
+ * берётся из `appConfig.db` или из `YDB_CONNECTION_STRING`.
42
+ */
43
+ constructor(config) {
44
+ // Пустые host/database не мешают: при выборе строки подключения пустое значение пропускается
45
+ super(config
46
+ ? { host: '', database: '', ...config, options: config.options }
47
+ : undefined);
48
+ // Схема встроенных таблиц нужна и без ensureSchema: по ней выбираются типы и индексы
49
+ for (const schema of umbot_1.DB_TABLES_SCHEMA) {
50
+ this.#tables.set(schema.tableName, (0, yql_1.tableInfoFromSchema)(schema));
51
+ }
52
+ }
53
+ /**
54
+ * Опции адаптера из параметров конструктора или из `appConfig.db.options`.
55
+ */
56
+ #getOptions() {
57
+ const options = this._dbOptions?.options ?? this._appContext.appConfig.db?.options;
58
+ return (options ?? {});
59
+ }
60
+ /**
61
+ * Строка подключения: `options.connectionString`, затем `host` + `database`,
62
+ * затем переменная окружения `YDB_CONNECTION_STRING`.
63
+ */
64
+ #getConnectionString() {
65
+ const options = this.#getOptions();
66
+ if (options.connectionString) {
67
+ return options.connectionString;
68
+ }
69
+ const host = this._dbOptions?.host || this._appContext.appConfig.db?.host;
70
+ const database = this._dbOptions?.database || this._appContext.appConfig.db?.database;
71
+ if (host && database) {
72
+ const base = host.replace(/\/+$/, '');
73
+ return `${base}/?database=${encodeURIComponent(database)}`;
74
+ }
75
+ return process.env[CONNECTION_STRING_ENV] || undefined;
76
+ }
77
+ /**
78
+ * Провайдер авторизации и TLS-опции для драйвера.
79
+ */
80
+ #getCredentials(connectionString) {
81
+ const credentials = this.#getOptions().credentials;
82
+ if (credentials && typeof credentials === 'object') {
83
+ return { provider: credentials };
84
+ }
85
+ switch (credentials) {
86
+ case 'metadata':
87
+ return { provider: new metadata_1.MetadataCredentialsProvider() };
88
+ case 'anonymous':
89
+ return { provider: new anonymous_1.AnonymousCredentialsProvider() };
90
+ }
91
+ const user = this._dbOptions?.user || this._appContext.appConfig.db?.user;
92
+ const pass = this._dbOptions?.pass || this._appContext.appConfig.db?.pass;
93
+ if (credentials !== 'environ' && user && pass) {
94
+ // Без TLS-контекста провайдер открыл бы незащищённый канал и для grpcs
95
+ const secureOptions = connectionString.startsWith('grpcs:') ? {} : undefined;
96
+ return {
97
+ provider: new static_1.StaticCredentialsProvider({ username: user, password: pass }, connectionString, secureOptions),
98
+ };
99
+ }
100
+ const environ = new environ_1.EnvironCredentialsProvider(connectionString);
101
+ return { provider: environ, secureOptions: environ.secureOptions };
102
+ }
103
+ /**
104
+ * Подключается к YDB: создаёт драйвер и ждёт discovery не дольше `connectTimeout`.
105
+ * Повторный вызов закрывает предыдущее подключение.
106
+ * @returns true при успешном подключении
107
+ */
108
+ async connect() {
109
+ await this.#closeDriver();
110
+ const connectionString = this.#getConnectionString();
111
+ if (!connectionString) {
112
+ this._saveLog(`Не задана строка подключения: укажите host и database, options.connectionString или переменную ${CONNECTION_STRING_ENV}.`);
113
+ return false;
114
+ }
115
+ const options = this.#getOptions();
116
+ const connectTimeout = options.connectTimeout ?? DEFAULT_CONNECT_TIMEOUT;
117
+ let driver;
118
+ try {
119
+ for (const info of this.#customTables()) {
120
+ this.#tables.set(info.tableName, info);
121
+ }
122
+ const { provider, secureOptions } = this.#getCredentials(connectionString);
123
+ driver = new core_1.Driver(connectionString, {
124
+ credentialsProvider: provider,
125
+ secureOptions,
126
+ 'ydb.sdk.ready_timeout_ms': connectTimeout,
127
+ });
128
+ await driver.ready(AbortSignal.timeout(connectTimeout));
129
+ const sql = options.poolSize === undefined
130
+ ? (0, query_1.query)(driver)
131
+ : (0, query_1.query)(driver, { poolOptions: { maxSize: options.poolSize } });
132
+ this.#driver = driver;
133
+ this.#sql = sql;
134
+ const databaseInfo = (this._appContext.database.databaseInfo ??= {});
135
+ databaseInfo.driver = driver;
136
+ databaseInfo.sql = sql;
137
+ this._appContext.log('YDB: подключение установлено');
138
+ return true;
139
+ }
140
+ catch (error) {
141
+ driver?.close();
142
+ // Строку подключения не логируем целиком: в query-параметрах бывают служебные данные
143
+ this._saveLog('Не удалось подключиться к базе данных', error);
144
+ return false;
145
+ }
146
+ }
147
+ /**
148
+ * Описания своих таблиц из опции `tables`.
149
+ * @returns Описания таблиц; бросает YqlBuildError, если описание некорректно
150
+ */
151
+ #customTables() {
152
+ return (this.#getOptions().tables ?? []).map(yql_1.tableInfoFromDefinition);
153
+ }
154
+ /**
155
+ * Создаёт недостающие таблицы, колонки и индексы встроенных моделей и таблиц из опции `tables`.
156
+ *
157
+ * Сначала выполняется один запрос-проверка (`SELECT ... LIMIT 0` по всем таблицам и индексам).
158
+ * Если схема уже готова — на этом всё: в Cloud Functions метод вызывается на каждом холодном
159
+ * старте, и DDL на каждый старт стоил бы заметного времени.
160
+ * @param tables Описание таблиц (`DB_TABLES_SCHEMA`)
161
+ * @returns true, если схема готова
162
+ */
163
+ async ensureSchema(tables) {
164
+ let infos;
165
+ try {
166
+ infos = [...tables.map(yql_1.tableInfoFromSchema), ...this.#customTables()];
167
+ }
168
+ catch (error) {
169
+ this._saveLog('ensureSchema: некорректное описание таблицы', error);
170
+ return false;
171
+ }
172
+ for (const info of infos) {
173
+ this.#tables.set(info.tableName, info);
174
+ }
175
+ if (this.#getOptions().ensureSchema === false || !infos.length) {
176
+ return true;
177
+ }
178
+ if (!this.#sql) {
179
+ this._saveLog('ensureSchema: нет подключения к базе данных');
180
+ return false;
181
+ }
182
+ const check = infos.map(yql_1.buildSchemaCheck).join('\n');
183
+ if (await this.#tryExecute(check)) {
184
+ return true;
185
+ }
186
+ try {
187
+ for (const info of infos) {
188
+ await this.#prepareTable(info);
189
+ }
190
+ return true;
191
+ }
192
+ catch (error) {
193
+ this._saveLog('ensureSchema: не удалось подготовить таблицы', error);
194
+ return false;
195
+ }
196
+ }
197
+ /**
198
+ * Создаёт таблицу, если её нет, и добавляет недостающие колонки и индексы.
199
+ */
200
+ async #prepareTable(info) {
201
+ await this.#executeDdl((0, yql_1.buildCreateTable)(info));
202
+ for (const [column, type] of info.columns) {
203
+ if (!(await this.#tryExecute((0, yql_1.buildColumnCheck)(info.tableName, column)))) {
204
+ await this.#executeDdl((0, yql_1.buildAddColumn)(info.tableName, column, type));
205
+ }
206
+ }
207
+ for (const index of info.indexes) {
208
+ if (!(await this.#tryExecute((0, yql_1.buildIndexCheck)(info.tableName, index)))) {
209
+ await this.#executeDdl((0, yql_1.buildAddIndex)(info.tableName, index));
210
+ }
211
+ }
212
+ }
213
+ /**
214
+ * Выполняет DDL-запрос с таймаутом не меньше 30 с и без повторов SDK.
215
+ */
216
+ async #executeDdl(text) {
217
+ const timeout = Math.max(this.#getQueryTimeout(), MIN_DDL_TIMEOUT);
218
+ await this.#execute({ text, params: {} }, { idempotent: false, timeout });
219
+ }
220
+ /**
221
+ * Таймаут обычного запроса из опций.
222
+ */
223
+ #getQueryTimeout() {
224
+ return this.#getOptions().queryTimeout ?? DEFAULT_QUERY_TIMEOUT;
225
+ }
226
+ /**
227
+ * Выполняет запрос-проверку схемы.
228
+ * @returns true, если запрос прошёл без ошибок
229
+ */
230
+ async #tryExecute(text) {
231
+ try {
232
+ await this.#execute({ text, params: {} });
233
+ return true;
234
+ }
235
+ catch {
236
+ return false;
237
+ }
238
+ }
239
+ /**
240
+ * Выполняет запрос с ограничением по времени (вместе с повторами SDK).
241
+ * @param statement Текст и параметры
242
+ * @param options `idempotent` — можно ли SDK повторить запрос (по умолчанию да);
243
+ * `timeout` — мс, по умолчанию `queryTimeout`; `write` — изменение данных
244
+ * @returns Наборы строк результата
245
+ */
246
+ async #execute(statement, options = {}) {
247
+ const sql = this.#sql;
248
+ if (!sql) {
249
+ throw new Error('Нет подключения к базе данных');
250
+ }
251
+ let q = sql(statement.text);
252
+ for (const name of Object.keys(statement.params)) {
253
+ q = q.parameter(name, statement.params[name]);
254
+ }
255
+ if (options.write) {
256
+ // YDB меняет таблицы с вторичными индексами только в Serializable-транзакции
257
+ q = q.isolation('serializableReadWrite');
258
+ }
259
+ const timeout = options.timeout ?? this.#getQueryTimeout();
260
+ return (await q.idempotent(options.idempotent ?? true).timeout(timeout));
261
+ }
262
+ /**
263
+ * Контекст сборки запроса: схема таблицы и правила модели.
264
+ */
265
+ #context(data) {
266
+ return { table: this.#tables.get(data.tableName), rules: data.rules };
267
+ }
268
+ /**
269
+ * Выполняет SELECT.
270
+ *
271
+ * Контракт umbot: при `isOne` в `data` — сама запись (не массив); пустая выборка —
272
+ * `{ status: false }` без `error`; сбой — `{ status: false, error }`.
273
+ * @param selectData Информация о таблице
274
+ * @param where Условия выборки
275
+ * @param isOne Вернуть только одну запись
276
+ * @returns Результат выборки
277
+ */
278
+ async _select(selectData, where, isOne) {
279
+ try {
280
+ const statement = (0, yql_1.buildSelect)(selectData.tableName, where, isOne, this.#context(selectData));
281
+ const [rows = []] = await this.#execute(statement);
282
+ if (!rows.length) {
283
+ return { status: false };
284
+ }
285
+ const data = rows.map(yql_1.normalizeRow);
286
+ return { status: true, data: isOne ? data[0] : data };
287
+ }
288
+ catch (error) {
289
+ this._saveLog(`Ошибка SELECT из таблицы "${selectData.tableName}"`, error);
290
+ return { status: false, error: getErrorMessage(error) };
291
+ }
292
+ }
293
+ /**
294
+ * Добавляет запись (UPSERT): повторная запись с тем же ключом перезаписывает строку,
295
+ * поэтому SDK может безопасно повторить запрос после временной ошибки сети.
296
+ * @param insertData Запрос с данными в `data`
297
+ * @returns true при успехе
298
+ */
299
+ async _insert(insertData) {
300
+ try {
301
+ const statement = (0, yql_1.buildUpsert)(insertData.tableName, insertData.data ?? {}, this.#context(insertData));
302
+ await this.#execute(statement, { write: true });
303
+ return true;
304
+ }
305
+ catch (error) {
306
+ this._saveLog(`Ошибка записи в таблицу "${insertData.tableName}"`, error);
307
+ return false;
308
+ }
309
+ }
310
+ /**
311
+ * Обновляет записи по условию `query`. Без условий запрос не выполняется.
312
+ * @param updateData Запрос: условия в `query`, новые значения в `data`
313
+ * @returns true при успехе (в том числе если под условие не попало ни одной записи)
314
+ */
315
+ async _update(updateData) {
316
+ try {
317
+ const statement = (0, yql_1.buildUpdate)(updateData.tableName, updateData.data ?? {}, updateData.query, this.#context(updateData));
318
+ if (statement) {
319
+ await this.#execute(statement, { write: true });
320
+ }
321
+ return true;
322
+ }
323
+ catch (error) {
324
+ this._saveLog(`Ошибка UPDATE таблицы "${updateData.tableName}"`, error);
325
+ return false;
326
+ }
327
+ }
328
+ /**
329
+ * Удаляет записи по условию `query`. Без условий запрос не выполняется.
330
+ * @param removeData Запрос: условия в `query`
331
+ * @returns true при успехе
332
+ */
333
+ async _remove(removeData) {
334
+ try {
335
+ const statement = (0, yql_1.buildDelete)(removeData.tableName, removeData.query, this.#context(removeData));
336
+ await this.#execute(statement, { write: true });
337
+ return true;
338
+ }
339
+ catch (error) {
340
+ this._saveLog(`Ошибка DELETE из таблицы "${removeData.tableName}"`, error);
341
+ return false;
342
+ }
343
+ }
344
+ /**
345
+ * Выполняет произвольный запрос: callback получает клиент запросов и драйвер.
346
+ *
347
+ * @example
348
+ * ```ts
349
+ * const count = await usersData.query(async (sql) => {
350
+ * const [[row]] = await sql`SELECT COUNT(*) AS cnt FROM UsersData`;
351
+ * return { status: true, data: row };
352
+ * });
353
+ * ```
354
+ * @param callback Функция с запросом
355
+ * @returns `data` из результата callback или null при ошибке
356
+ */
357
+ async _query(callback) {
358
+ if (!this.#sql || !this.#driver) {
359
+ this._saveLog('Нет подключения к базе данных');
360
+ return null;
361
+ }
362
+ try {
363
+ const res = await callback(this.#sql, this.#driver);
364
+ if (res.status) {
365
+ return res.data;
366
+ }
367
+ this._saveLog(String(res.error));
368
+ return null;
369
+ }
370
+ catch (error) {
371
+ this._saveLog('Ошибка в query()', error);
372
+ return null;
373
+ }
374
+ }
375
+ /**
376
+ * Выполняет произвольный запрос (см. `_query`).
377
+ * @param callback Функция с запросом
378
+ * @returns `data` из результата callback или null при ошибке
379
+ */
380
+ query(callback) {
381
+ return this._query(callback);
382
+ }
383
+ /**
384
+ * Экранирует строку для своего запроса: результат можно поставить внутрь `'...'` в YQL.
385
+ * Адаптер этим методом не пользуется — значения уходят параметрами; для своих запросов
386
+ * тоже лучше параметры (`` sql`... ${value}` `` в `query()`).
387
+ * @param str Значение
388
+ * @returns Экранированная строка
389
+ */
390
+ escapeString(str) {
391
+ return (0, yql_1.escapeYqlString)(str);
392
+ }
393
+ /**
394
+ * Проверяет соединение запросом `SELECT 1`.
395
+ * @returns true, если база отвечает
396
+ */
397
+ async isConnected() {
398
+ if (!this.#sql) {
399
+ return false;
400
+ }
401
+ try {
402
+ await this.#execute({ text: 'SELECT 1', params: {} });
403
+ return true;
404
+ }
405
+ catch {
406
+ return false;
407
+ }
408
+ }
409
+ /**
410
+ * Закрывает драйвер и очищает `databaseInfo`. Безопасен к повторному вызову.
411
+ */
412
+ async destroy() {
413
+ await super.destroy();
414
+ await this.#closeDriver();
415
+ }
416
+ /**
417
+ * Закрывает пул сессий и драйвер, убирает ссылки на них из контекста приложения.
418
+ */
419
+ async #closeDriver() {
420
+ const driver = this.#driver;
421
+ const sql = this.#sql;
422
+ this.#driver = undefined;
423
+ this.#sql = undefined;
424
+ const databaseInfo = this._appContext?.database?.databaseInfo;
425
+ if (databaseInfo) {
426
+ delete databaseInfo.driver;
427
+ delete databaseInfo.sql;
428
+ }
429
+ try {
430
+ // Пул сессий закрывается до драйвера: иначе сессии на сервере живут до своего таймаута
431
+ await sql?.[Symbol.asyncDispose]();
432
+ }
433
+ catch (error) {
434
+ this._saveLog('Ошибка при закрытии пула сессий', error);
435
+ }
436
+ driver?.close();
437
+ }
438
+ /**
439
+ * Пишет ошибку в лог umbot (секреты маскирует логгер фреймворка).
440
+ * @param message Текст ошибки
441
+ * @param error Исключение
442
+ */
443
+ _saveLog(message, error) {
444
+ const text = error === undefined ? message : `${message}: ${getErrorMessage(error)}`;
445
+ this._appContext?.logError(`YDB: ${text}`, error === undefined ? {} : { error });
446
+ }
447
+ }
448
+ exports.YdbAdapter = YdbAdapter;
449
+ /**
450
+ * Текст ошибки для лога и поля `error` результата.
451
+ */
452
+ function getErrorMessage(error) {
453
+ if (error instanceof Error) {
454
+ return error.message;
455
+ }
456
+ return String(error);
457
+ }
@@ -0,0 +1,4 @@
1
+ export { YdbAdapter } from './YdbAdapter';
2
+ export type { IYdbAdapterConfig, IYdbDbInfo, IYdbOptions, TYdbCredentials } from './YdbAdapter';
3
+ export { SUPPORTED_OPERATORS, YDB_COLUMN_TYPES } from './yql';
4
+ export type { IYdbTableDefinition, TYdbColumnType } from './yql';
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.YDB_COLUMN_TYPES = exports.SUPPORTED_OPERATORS = exports.YdbAdapter = void 0;
4
+ var YdbAdapter_1 = require("./YdbAdapter");
5
+ Object.defineProperty(exports, "YdbAdapter", { enumerable: true, get: function () { return YdbAdapter_1.YdbAdapter; } });
6
+ var yql_1 = require("./yql");
7
+ Object.defineProperty(exports, "SUPPORTED_OPERATORS", { enumerable: true, get: function () { return yql_1.SUPPORTED_OPERATORS; } });
8
+ Object.defineProperty(exports, "YDB_COLUMN_TYPES", { enumerable: true, get: function () { return yql_1.YDB_COLUMN_TYPES; } });
package/dist/yql.d.ts ADDED
@@ -0,0 +1,196 @@
1
+ import type { IDbTableSchema, IModelRules, IQueryData } from 'umbot';
2
+ import type { Value } from '@ydbjs/value';
3
+ /**
4
+ * Типы колонок YDB, которые понимает адаптер.
5
+ */
6
+ export declare const YDB_COLUMN_TYPES: readonly ["Utf8", "Int64", "Double", "Bool"];
7
+ /**
8
+ * Тип колонки YDB: в него адаптер приводит значение параметра.
9
+ */
10
+ export type TYdbColumnType = (typeof YDB_COLUMN_TYPES)[number];
11
+ /**
12
+ * Вторичный индекс таблицы.
13
+ */
14
+ export interface IYdbIndexInfo {
15
+ /** Имя индекса (`idx_<поля>`) */
16
+ name: string;
17
+ /** Поля индекса в порядке объявления */
18
+ fields: readonly string[];
19
+ }
20
+ /**
21
+ * Что адаптер знает о таблице: ключ, типы колонок и индексы.
22
+ */
23
+ export interface IYdbTableInfo {
24
+ /** Имя таблицы */
25
+ tableName: string;
26
+ /** Колонки первичного ключа: `primaryKeyName` и `uniqueKeys` из схемы umbot */
27
+ primaryKey: readonly string[];
28
+ /** Типы колонок */
29
+ columns: ReadonlyMap<string, TYdbColumnType>;
30
+ /** Вторичные индексы (кроме тех, что совпадают с началом первичного ключа) */
31
+ indexes: readonly IYdbIndexInfo[];
32
+ }
33
+ /**
34
+ * Готовый к выполнению запрос: текст YQL и параметры `$pN`.
35
+ */
36
+ export interface IYqlStatement {
37
+ /** Текст запроса без `DECLARE`: SDK добавляет их сам по типам параметров */
38
+ text: string;
39
+ /** Параметры запроса */
40
+ params: Record<string, Value>;
41
+ }
42
+ /**
43
+ * Ошибка сборки запроса: некорректное имя, условие или значение.
44
+ * Адаптер не отправляет такой запрос в базу, а возвращает ошибку вызывающему коду.
45
+ */
46
+ export declare class YqlBuildError extends Error {
47
+ constructor(message: string);
48
+ }
49
+ /**
50
+ * Контекст сборки запроса: схема таблицы (если известна) и правила модели.
51
+ */
52
+ export interface IYqlContext {
53
+ /** Схема таблицы, если таблица встроенная или передана в `ensureSchema` */
54
+ table?: IYdbTableInfo | undefined;
55
+ /** Правила валидации модели (`IQuery.rules`) */
56
+ rules?: readonly IModelRules[] | undefined;
57
+ }
58
+ /**
59
+ * Операторы условий, которые понимает адаптер.
60
+ */
61
+ export declare const SUPPORTED_OPERATORS: readonly ["$gt", "$gte", "$lt", "$lte", "$ne", "$in", "$nin", "$like", "$null"];
62
+ /**
63
+ * Описание своей таблицы для опции `tables`: адаптер создаст её вместе со встроенными
64
+ * и будет искать по её индексам через `VIEW`.
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * const orders: IYdbTableDefinition = {
69
+ * tableName: 'Orders',
70
+ * primaryKey: ['orderId'],
71
+ * columns: { orderId: 'Utf8', userId: 'Utf8', total: 'Double', paid: 'Bool' },
72
+ * indexes: [['userId']],
73
+ * };
74
+ * ```
75
+ */
76
+ export interface IYdbTableDefinition {
77
+ /** Имя таблицы (можно с директорией: `bots/Orders`) */
78
+ tableName: string;
79
+ /** Колонки первичного ключа; каждая должна быть в `columns` */
80
+ primaryKey: readonly string[];
81
+ /** Колонки и их типы */
82
+ columns: Readonly<Record<string, TYdbColumnType>>;
83
+ /** Наборы колонок, по которым нужен вторичный индекс */
84
+ indexes?: readonly (readonly string[])[];
85
+ }
86
+ /**
87
+ * Собирает описание таблицы из схемы umbot (`DB_TABLES_SCHEMA`).
88
+ * Все поля встроенных таблиц строковые, поэтому колонки получают тип `Utf8`.
89
+ * @param schema Описание таблицы
90
+ * @returns Описание таблицы для адаптера
91
+ */
92
+ export declare function tableInfoFromSchema(schema: IDbTableSchema): IYdbTableInfo;
93
+ /**
94
+ * Собирает описание своей таблицы из опции `tables` и проверяет его.
95
+ * @param definition Описание таблицы
96
+ * @returns Описание таблицы для адаптера
97
+ */
98
+ export declare function tableInfoFromDefinition(definition: IYdbTableDefinition): IYdbTableInfo;
99
+ /**
100
+ * Проверяет имя таблицы и возвращает его в обратных кавычках.
101
+ * @param name Имя таблицы
102
+ * @returns Имя для подстановки в YQL
103
+ */
104
+ export declare function quoteTable(name: string): string;
105
+ /**
106
+ * Проверяет имя колонки (или индекса) и возвращает его в обратных кавычках.
107
+ * @param name Имя колонки
108
+ * @returns Имя для подстановки в YQL
109
+ */
110
+ export declare function quoteColumn(name: string): string;
111
+ /**
112
+ * Выбирает вторичный индекс для SELECT. YDB не использует индекс без явного `VIEW`,
113
+ * поэтому поиск по (platform, path) в таблицах токенов без него шёл бы полным перебором.
114
+ * @returns Имя индекса или undefined, если поиск идёт по первичному ключу или индекса нет
115
+ */
116
+ export declare function chooseIndex(table: IYdbTableInfo | undefined, where: IQueryData | null): string | undefined;
117
+ /**
118
+ * Собирает SELECT.
119
+ * @param tableName Имя таблицы
120
+ * @param where Условия выборки
121
+ * @param isOne Нужна только одна запись
122
+ * @param ctx Контекст запроса
123
+ * @returns Запрос
124
+ */
125
+ export declare function buildSelect(tableName: string, where: IQueryData | null, isOne: boolean, ctx: IYqlContext): IYqlStatement;
126
+ /**
127
+ * Собирает UPSERT одной записи. Поля со значением undefined пропускаются,
128
+ * null записывается как NULL.
129
+ * @param tableName Имя таблицы
130
+ * @param data Данные записи
131
+ * @param ctx Контекст запроса
132
+ * @returns Запрос
133
+ */
134
+ export declare function buildUpsert(tableName: string, data: IQueryData, ctx: IYqlContext): IYqlStatement;
135
+ /**
136
+ * Собирает UPDATE. Колонки первичного ключа из SET убираются: YDB не меняет ключ
137
+ * записи, а модель umbot передаёт `uniqueKeys` и в данных, и в условии.
138
+ * @param tableName Имя таблицы
139
+ * @param data Новые значения
140
+ * @param where Условия (обязательны)
141
+ * @param ctx Контекст запроса
142
+ * @returns Запрос или null, если менять нечего
143
+ */
144
+ export declare function buildUpdate(tableName: string, data: IQueryData, where: IQueryData | null, ctx: IYqlContext): IYqlStatement | null;
145
+ /**
146
+ * Собирает DELETE.
147
+ * @param tableName Имя таблицы
148
+ * @param where Условия (обязательны: удаление всей таблицы этим методом запрещено)
149
+ * @param ctx Контекст запроса
150
+ * @returns Запрос
151
+ */
152
+ export declare function buildDelete(tableName: string, where: IQueryData | null, ctx: IYqlContext): IYqlStatement;
153
+ /**
154
+ * Собирает CREATE TABLE IF NOT EXISTS. Колонки ключа объявляются NOT NULL.
155
+ * @param table Описание таблицы
156
+ * @returns Текст запроса
157
+ */
158
+ export declare function buildCreateTable(table: IYdbTableInfo): string;
159
+ /**
160
+ * Собирает запрос-проверку: выполняется без ошибок, только если таблица, все её колонки
161
+ * и индексы уже есть. Данные не читаются (`LIMIT 0`).
162
+ * @param table Описание таблицы
163
+ * @returns Текст запроса (несколько SELECT через `;`)
164
+ */
165
+ export declare function buildSchemaCheck(table: IYdbTableInfo): string;
166
+ /**
167
+ * Собирает проверку наличия одной колонки.
168
+ */
169
+ export declare function buildColumnCheck(tableName: string, column: string): string;
170
+ /**
171
+ * Собирает ALTER TABLE ... ADD COLUMN.
172
+ */
173
+ export declare function buildAddColumn(tableName: string, column: string, type: TYdbColumnType): string;
174
+ /**
175
+ * Собирает проверку наличия индекса.
176
+ */
177
+ export declare function buildIndexCheck(tableName: string, index: IYdbIndexInfo): string;
178
+ /**
179
+ * Собирает ALTER TABLE ... ADD INDEX.
180
+ */
181
+ export declare function buildAddIndex(tableName: string, index: IYdbIndexInfo): string;
182
+ /**
183
+ * Экранирует строку для подстановки внутрь строкового литерала YQL (`'...'` или `"..."`).
184
+ * Адаптер сам так не делает — все значения уходят параметрами; функция нужна тем,
185
+ * кто собирает свой запрос через `model.escapeString()`.
186
+ * @param value Значение
187
+ * @returns Строка с экранированными обратным слешем, кавычками и управляющими символами
188
+ */
189
+ export declare function escapeYqlString(value: string | number): string;
190
+ /**
191
+ * Приводит значение из ответа YDB к виду, который понимают модели umbot:
192
+ * Int64 приходит как bigint, а JSON.stringify и сравнения с числами на нём ломаются.
193
+ * @param row Строка результата
194
+ * @returns Строка с bigint, переведёнными в number (или в строку, если не помещаются)
195
+ */
196
+ export declare function normalizeRow(row: Record<string, unknown>): Record<string, unknown>;