> ## Documentation Index
> Fetch the complete documentation index at: https://cs-lua.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# mysql

> MySQL/MariaDB: удалённая база, не блокирующая кадр

# mysql

MySQL/MariaDB: удалённая база, не блокирующая кадр.

Для той базы, которая не лежит рядом с сервером — сайт, панель, общий для
нескольких серверов проекта аккаунт. Для локальных данных проще
[`db`](../db/index.md): он синхронный и не тянет за собой внешний процесс.

```lua theme={null}
local site = mysql.connect{ host = "127.0.0.1", user = "root",
    password = "", database = "gamecms" }

site:query("SELECT id, shilings FROM users WHERE steam_id = ?", p:steamid(),
    function(res)
        if not res.ok then return print(res.error) end
        local row = res.rows[1]
        if row then p:chat("баланс: " .. row.shilings) end
    end)
```

Тот же приём, что и [`http`](../http/index.md): запрос уходит на рабочий
поток, ответ возвращается в игровой — тем же проходом, которым тикают
таймеры и ходят HTTP-ответы. Ни одна строчка плагина не касается сокета.
Вызов `:query`/`:exec` ничего не ждёт: он возвращается сразу с id, кадр идёт
дальше, коллбэк приходит когда придёт.

Опциональные хелперы `find` / `create` / `update` / `delete` собирают
безопасный SQL сами: имена таблиц и колонок — только `[A-Za-z0-9_]`,
значения идут через `?`. Для JOIN, подзапросов и сложного WHERE остаётся
сырой `:query` / `:exec`. Миграции схемы — `conn:migrate`.

`mysql.connect` тоже не ждёт: соединение открывается лениво, на рабочем
потоке, при первом запросе — сам вызов `connect` кадр не блокирует и связку
не проверяет.

## Значения через `?`

Как в `db`, но экранирование делает рабочий поток на живом соединении, а не
драйвер напрямую:

| Lua             | SQL                              |
| --------------- | -------------------------------- |
| `nil`           | `NULL`                           |
| `true`, `false` | `1`, `0`                         |
| целое число     | как есть                         |
| дробное         | как есть                         |
| строка          | экранируется и берётся в кавычки |

<Warning>
  Не склеивай запрос из данных игрока — так же, как и с `db`: через `?`
  значение попадёт в базу строкой, чем бы ни было, в склейке — станет частью
  запроса.
</Warning>

## Транспорт

Клиентская библиотека MySQL — не часть модуля: она открывается при первом
запросе, как libcurl у `http`, а не линкуется в сборку.

|         |                                                                             |
| ------- | --------------------------------------------------------------------------- |
| Windows | `libmariadb.dll`, иначе `libmysql.dll`                                      |
| Linux   | `libmariadb.so`, иначе `libmysqlclient.so` — открывается при первом запросе |

Сервер без клиентской библиотеки нормально загрузит модуль: `res.error`
скажет об этом при первом обращении к соединению, а не при старте.

## Объект ответа

Коллбэк получает **одну таблицу**.

| поле                | тип           |                                                                 |
| ------------------- | ------------- | --------------------------------------------------------------- |
| `res.ok`            | boolean       | запрос выполнился                                               |
| `res.rows`          | table         | массив строк; пустой для `INSERT`/`UPDATE`/`DELETE`             |
| `res.affected_rows` | number        | сколько строк изменил `INSERT`/`UPDATE`/`DELETE`                |
| `res.insert_id`     | number        | `LAST_INSERT_ID()` после `INSERT`                               |
| `res.error`         | string \| nil | причина, если `ok` — `false`                                    |
| `res.applied`       | table \| nil  | только у `conn:migrate`: id миграций, применённых в этом вызове |

Строка в `res.rows` — таблица с ключами по именам колонок; `NULL` приходит
как отсутствующий ключ, ровно как в `db`. Тип колонки в SQL решает, придёт
значение числом или строкой:

| SQL-тип колонки                                                    | Lua      |
| ------------------------------------------------------------------ | -------- |
| `INT`, `BIGINT`, `FLOAT`, `DOUBLE`, `DECIMAL`, `YEAR`              | `number` |
| `VARCHAR`, `TEXT`, `DATE`, `DATETIME`, `TIMESTAMP`, `ENUM`, `BLOB` | `string` |

Даты и время остаются строкой не просто так: у них нет единого числового
представления, которое не потребовало бы `parse` на другом конце — так же,
как в исходном `TEXT`-протоколе MySQL, откуда и приходит сырое значение.

## Одно соединение — одна очередь

Два запроса на *разных* соединениях выполняются параллельно (рабочих потоков
несколько). Два запроса на *одном и том же* соединении всегда идут по
очереди — MySQL-соединение нельзя использовать из двух мест одновременно,
поэтому нужен параллелизм — открывай второе соединение, а не жди на одном.

## Обрыв связи

Ошибка запроса закрывает соединение изнутри; следующий вызов
`:query`/`:exec`/`:find`/`:create`/`:update`/`:delete`/`:migrate`
на этом же объекте переподключается заново сам, без явного `:connect()`.
Платится это одним лишним переподключением и на обычную опечатку в SQL —
дёшево по сравнению с тем, чтобы намертво зависнуть на упавшем сайте.

## Перезагрузка и выгрузка

Как и `http`: запросы, оставшиеся на проводе, переживают `lua_reload` и
выгрузку плагина, но их коллбэки не выполняются — ответ выбрасывается.
Перезагрузка не ждёт незавершённые запросы. Соединения при этом не рвутся:
сокет остаётся открытым и простаивает до остановки сервера, чтобы не ждать
рабочий поток на середине запроса.

## Открытие

|                                    |                                       |
| ---------------------------------- | ------------------------------------- |
| [`mysql.connect`](open.md#connect) | Открывает соединение с сайтовой базой |

## Объект соединения

|                                         |                                                                  |
| --------------------------------------- | ---------------------------------------------------------------- |
| [`conn:query`](connection.md#query)     | Выполняет запрос и отдаёт строки в коллбэк                       |
| [`conn:exec`](connection.md#exec)       | То же самое, что conn:query — имя удобнее для запросов без строк |
| [`conn:find`](connection.md#find)       | SELECT по таблице: where / select / order / limit                |
| [`conn:create`](connection.md#create)   | INSERT одной строки из таблицы полей                             |
| [`conn:update`](connection.md#update)   | UPDATE: set + where                                              |
| [`conn:delete`](connection.md#delete)   | DELETE: where                                                    |
| [`conn:migrate`](connection.md#migrate) | Последовательные миграции (SQL или файл в data\_dir)             |
| [`conn:close`](connection.md#close)     | Закрывает соединение                                             |
