# Подключение, таблица и вставка данных: Connection, CREATE TABLE и INSERT<no value>

## В этом уроке

Урок состоит из двух глав — в том же порядке, что и в презентации:

1. **Глава 1** — открытие соединения, проверка, что соединение работает, и правильное закрытие
2. **Глава 2** — базовый SQL: CREATE TABLE и INSERT

## Глава 1: Открытие соединения, проверка, что оно работает, и правильное закрытие

### Что мы изучим?

**Жизненный цикл** любой работы с базой данных:

```text
1. open a connection  →  2. create a cursor  →  3. run a query  →  4. read the result  →  5. close
```

- **Соединение** (Connection) и **курсор** (Cursor): что такое каждый из них.
- **Проверочный запрос** (Health Check): «база вообще отвечает?»
- **Правильное закрытие**, даже при ошибке.
- **`get_connection()`** — одна функция, которой будет пользоваться весь проект.

### Зачем это нужно?

Каждая функция CRUD и каждый Endpoint сервера (тема API студентов) начинается с открытия соединения и заканчивается его закрытием. **Незакрытое соединение** — классический баг: каждое соединение с MySQL занимает ресурсы — и у нас, и на сервере, — а у MySQL **ограниченное число** одновременных соединений. На сервере, который обрабатывает тысячи запросов, через какое-то время соединения заканчиваются (`Too many connections`), и всё застревает. Это ровно как открытые файлы в уроке о файлах, только в масштабе сервера.

### Объяснение с основ

#### Connection — открытая линия

**Соединение** (Connection) = объект, который представляет **открытый канал** между нашим кодом и сервером MySQL. Через него создают курсор, через него подтверждают изменения (`commit`, позже в этой теме) и через него закрывают. Пока оно открыто, оно занимает ресурсы.

#### Cursor

**Курсор** (Cursor) = объект, который выполняет запрос **и хранит его результаты** и позволяет читать их строка за строкой. Представьте его как палец, который идёт по результатам запроса.

Его создают из соединения и выполняют через него:

```python
cursor = connection.cursor()        # create a cursor
cursor.execute("SELECT 1")          # run a query through it
```

#### Чтение результатов

| Метод курсора | Возвращает | Когда |
|---|---|---|
| `fetchone()` | **Одну строку** (tuple) или `None`, если строки нет | Ожидается одна строка: проверка или поиск по id |
| `fetchall()` | **Список** всех строк (список tuple) | Ожидается несколько строк |

Каждая строка — **tuple** (вспомните кортежи!): `("Dana", 24)`. (Позже в этой теме научимся получать строки в виде dict.)

{{< box "analogy" "Аналогия: звонок в колл-центр" >}}
**Соединение** = сам звонок: вы набрали номер, и линия открыта. **Курсор** = оператор, который ищет для вас информацию и зачитывает результат за результатом. **`fetchone`** = «скажите мне первый». **`fetchall`** = «зачитайте мне все». **`close`** = положить трубку. Если не положить трубку, линия остаётся занятой, и другие не могут дозвониться.
{{< /box >}}

#### Проверочный запрос (Health Check)

Чтобы проверить, что соединение **действительно работает** (а не только что объект создан), выполняют простой запрос, который не зависит ни от одной таблицы:

```sql
SELECT 1
SELECT VERSION()
```

Если ответ пришёл, база жива и отвечает. Именно так работает Endpoint `/health` настоящих серверов (тема бэкенда и FastAPI).

#### Правильное закрытие — try/finally

Что будет, если запрос упадёт **после** того, как мы открыли соединение?

```python
connection = get_connection()
cursor = connection.cursor()
cursor.execute("SELEC 1")          # typo -> exception
connection.close()                 # never reached!
```

Ошибка останавливает функцию, и `close` не выполняется. Решение, которое мы изучили в теме исключений: **`finally`**, который выполняется **всегда**:

```python
connection = get_connection()
try:
    ...                            # work with the database
finally:
    connection.close()             # runs even if an exception happened
```

### Синтаксис

```python
import mysql.connector

def get_connection():
    return mysql.connector.connect(
        host="localhost", user="root", password="kodkod", database="school")

connection = get_connection()
try:
    cursor = connection.cursor()     # create a cursor
    cursor.execute("SELECT 1")       # run a query
    row = cursor.fetchone()          # read one row -> (1,)
finally:
    connection.close()               # always close
```

### Первый пример кода

`week-4/school-db/check_connection.py`:

```python
import mysql.connector

connection = mysql.connector.connect(
    host="localhost", user="root", password="kodkod", database="school")
try:
    cursor = connection.cursor()
    cursor.execute("SELECT VERSION()")
    row = cursor.fetchone()
    print("Row returned:", row)
    print("MySQL version:", row[0])

    cursor.execute("SELECT 1 + 1")
    print("The database can calculate: 1 + 1 =", cursor.fetchone()[0])
    print("Connection works!")
finally:
    connection.close()
    print("Connection closed.")
```

**Вывод:**

```text
Row returned: ('8.4.11',)
MySQL version: 8.4.11
The database can calculate: 1 + 1 = 2
Connection works!
Connection closed.
```

### Разбор кода построчно

| Код | Объяснение |
|---|---|
| `mysql.connector.connect(...)` | **Открытие**. Вы получаете объект соединения (тема ООП: класс и объект!). |
| `try:` | Отсюда любая ошибка всё равно дойдёт до `finally`. |
| `cursor = connection.cursor()` | Создаёт **курсор** — через него выполняют запросы. |
| `cursor.execute("SELECT VERSION()")` | **Выполняет запрос**. Результат хранится в курсоре. |
| `cursor.fetchone()` | Одна строка: **tuple** с одним столбцом: `('8.4.11',)`. Запятая = tuple из одного элемента (вспомните кортежи!). |
| `row[0]` | Первый столбец строки. |
| `cursor.fetchone()[0]` | Первая строка → первый столбец. Распространено для запросов, которые возвращают одно значение. |
| `finally: connection.close()` | **Закрытие**, всегда. Даже если один из запросов упадёт. |

### Ещё один пример

**Ошибка посередине и доказательство, что `finally` закрывает.** А также: что происходит, когда пытаются использовать закрытое соединение.

```python
import mysql.connector

def get_connection():
    return mysql.connector.connect(
        host="localhost", user="root", password="kodkod", database="school")

def run_check(sql):
    connection = get_connection()
    try:
        cursor = connection.cursor()
        cursor.execute(sql)
        value = cursor.fetchone()[0]
        print("OK:", sql, "->", value)
    except mysql.connector.Error as error:
        print("Query failed:", sql, "|", type(error).__name__ + ":", error)
    finally:
        connection.close()
        print("  (connection closed)")
    return connection

run_check("SELECT 40 + 2")
closed = run_check("SELEC 1")

try:
    closed.cursor()
except mysql.connector.Error as error:
    print("Using a closed connection:", error)
```

**Вывод:**

```text
OK: SELECT 40 + 2 -> 42
  (connection closed)
Query failed: SELEC 1 | ProgrammingError: 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'SELEC 1' at line 1
  (connection closed)
Using a closed connection: MySQL Connection not available
```

**Разбор:**

- `mysql.connector.Error` — **базовый класс** всех ошибок драйвера (тема ООП: иерархия исключений!). `ProgrammingError` (ошибка в SQL), `IntegrityError` (нарушение ограничения, позже) и `OperationalError` (проблема с соединением) — его подклассы.
- В запросе с опечаткой (`SELEC`) ошибка была поймана, **и `finally` всё равно закрыл соединение**.
- `1064` — **номер ошибки** MySQL для синтаксической ошибки, а `near 'SELEC 1'` говорит точно, где проблема. Это **синтаксическая ошибка SQL** (та же идея, что в Python, другой язык).
- Использование соединения после `close` → `MySQL Connection not available`. Соединение «мертво», и нужно открыть новое.

#### `get_connection` — одна точка подключения

В настоящем проекте соединение нужно **каждой** функции. Вместо того чтобы писать пять данных для подключения в десятках мест, пишут **одну функцию** (DRY). Это будет основа файла `db.py` во всех этих темах:

```python
import mysql.connector

def get_connection():
    return mysql.connector.connect(
        host="localhost", user="root", password="kodkod", database="school")

def check_database():
    connection = get_connection()
    try:
        cursor = connection.cursor()
        cursor.execute("SELECT VERSION()")
        return {"status": "ok", "mysql_version": cursor.fetchone()[0]}
    except mysql.connector.Error as error:
        return {"status": "error", "message": str(error)}
    finally:
        connection.close()

if __name__ == "__main__":
    print(check_database())
```

```text
{'status': 'ok', 'mysql_version': '8.4.11'}
```

`check_database` возвращает **dict**. В теме API студентов её можно подключить к Endpoint-у `GET /health`, и FastAPI превратит её результат в JSON.

### Типичная ошибка

**Ошибка 1: `close` без `finally`.** Работает на «счастливом пути», но каждая ошибка посередине оставляет соединение открытым. На сервере после нескольких сотен неудачных запросов MySQL отказывает в новых соединениях: `Too many connections`.

**Ошибка 2: забыть `[0]`.**

```python
cursor.execute("SELECT VERSION()")
version = cursor.fetchone()
print("Version: " + version)
```

```text
TypeError: can only concatenate str (not "tuple") to str
```

`fetchone()` возвращает **строку** (tuple), даже когда в ней один столбец. Само значение = `[0]`.

**Ошибка 3: `fetchone()`, когда результатов нет.** `fetchone()` возвращает **`None`**, если строки нет, и тогда `None[0]` → `TypeError: 'NoneType' object is not subscriptable`. Позже в этой теме (поиск по id) мы всегда будем проверять `if row is None`.

**Ошибка 4: новый запрос до того, как прочитаны все результаты.**

```python
cursor.execute("SELECT id FROM students")    # 3 rows
first = cursor.fetchone()                    # read only the first one
cursor.execute("SELECT 1")
```

```text
mysql.connector.errors.InternalError: Unread result found
```

В MySQL результаты приходят с сервера **постепенно**, и новый запрос не может уйти, пока предыдущий не прочитан до конца. **Когда ожидаете несколько строк — `fetchall()`. Когда нужна одна строка — фильтруйте через `WHERE`, чтобы вернулась одна строка.**

### Итоги

- Цикл: **открытие (`connect`) → курсор (`cursor()`) → запрос (`execute`) → чтение (`fetchone`/`fetchall`) → закрытие (`close`)**.
- **Connection** = канал к серверу базы данных. **Cursor** = выполняет запрос и хранит результаты.
- `fetchone()` = одна строка (tuple) или `None`. `fetchall()` = список tuple. Одно значение: `cursor.fetchone()[0]`.
- **Проверочный запрос**: `SELECT 1` / `SELECT VERSION()`. Основа для `/health`.
- **Всегда `try/finally` + `close()`.**
- **Одна `get_connection()`** на весь проект (DRY). Ошибки: `mysql.connector.Error` и её подклассы.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

### Официальная документация

- [Connector/Python — Connecting to MySQL](https://dev.mysql.com/doc/connector-python/en/connector-python-example-connecting.html)
- [MySQLConnection.is_connected()](https://dev.mysql.com/doc/connector-python/en/connector-python-api-mysqlconnection-is-connected.html)
- [MySQLConnection.close()](https://dev.mysql.com/doc/connector-python/en/connector-python-api-mysqlconnection-close.html)

<!-- docs:end -->

## Глава 2: Базовый SQL — CREATE TABLE и INSERT

### Что мы изучим?

Первые две команды SQL:

| Команда | Что делает | CRUD |
|---|---|---|
| **`CREATE TABLE`** | Создаёт **структуру**: таблицу со столбцами | — (подготовка) |
| **`INSERT INTO`** | Добавляет **строку** | **C**reate |

В этом уроке мы сосредоточимся на **самом SQL** и будем выполнять его в **Database Client** в VS Code. В конце увидим, как выполнить тот же SQL из Python. (В следующих уроках научимся работать из Python **правильно** — параметры, commit и результаты.)

### Зачем это нужно?

База `school` работает в контейнере — но она **пуста**. Таблица — это «ящик», где будут лежать данные, а `INSERT` — первое действие в любой системе: регистрация пользователя, добавление товара, оформление заказа. В API, который мы построим в теме API студентов, `POST /students` = `INSERT INTO students`.

### Объяснение с основ

#### Структура команды SQL

- **Ключевые слова** вроде `CREATE`, `TABLE`, `INSERT`, `INTO`, `VALUES` по соглашению пишут **заглавными буквами**, чтобы отличать их от имён. (SQL не различает регистр ключевых слов, но так читается лучше.)
- **Имена** таблиц и столбцов: `snake_case`, строчными буквами, а для таблиц обычно во множественном числе (`students`).
- **Строки** в SQL пишут в **одинарных кавычках**: `'Dana'`.
- Команда заканчивается `;` (обязательно, когда выполняете несколько команд подряд, и всегда рекомендуется).

#### CREATE TABLE

```sql
CREATE TABLE students (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    age INT NOT NULL,
    email VARCHAR(100) NOT NULL UNIQUE
);
```

Каждая строка в скобках = **столбец**: `<name> <TYPE> <constraints>`, через запятую. (После последнего столбца запятой нет!)

#### IF NOT EXISTS

`CREATE TABLE students` **падает**, если таблица уже существует. На сервере, который каждый день запускается заново, нужна команда, которую безопасно выполнять **повторно**:

```sql
CREATE TABLE IF NOT EXISTS students (...);
```

«Создай таблицу, **только если** её ещё нет». Если она есть, ничего не происходит, и ошибки нет. (Обратите внимание: если таблица существует **с другой структурой**, она **не** изменится.)

#### INSERT INTO

```sql
INSERT INTO students (name, age, email)
VALUES ('Dana', 24, 'dana@mail.com');
```

- **`INSERT INTO <table>`** — в какую таблицу.
- **`(name, age, email)`** — **какие столбцы** вы заполняете и в каком порядке.
- **`VALUES (...)`** — значения, **точно в том же порядке**.
- **`id` нет в списке** — его сам заполняет `AUTO_INCREMENT`.

Можно добавить несколько строк одной командой:

```sql
INSERT INTO students (name, age, email) VALUES
    ('Avi', 31, 'avi@mail.com'),
    ('Noa', 22, 'noa@mail.com');
```

{{< box "analogy" "Аналогия: регистрационная анкета" >}}
`CREATE TABLE` = напечатать **пустую анкету**: какие есть поля и какие из них обязательны. `INSERT` = кто-то **заполняет одну анкету** и сдаёт её. Канцелярия (база данных) сама выдаёт номер дела (`id`) и возвращает анкету, в которой не хватает обязательного поля.
{{< /box >}}

### Синтаксис

```sql
CREATE TABLE IF NOT EXISTS table_name (
    id INT AUTO_INCREMENT PRIMARY KEY,
    column_a VARCHAR(100) NOT NULL,
    column_b INT NOT NULL,
    column_c VARCHAR(100) UNIQUE
);

INSERT INTO table_name (column_a, column_b, column_c)
VALUES ('text value', 42, 'unique value');
```

**Как выполнять SQL в Database Client:**

1. Создайте новый файл с расширением `.sql`, например `school.sql`.
2. Вверху файла выберите **соединение** и базу **`school`** (или: правый щелчок по `school` в дереве → **Open Query**).
3. Напишите команды, выделите их и нажмите **Run** (или `Ctrl+Enter`).
4. В дереве сбоку нажмите **Refresh** — и таблица появится. Щелчок по ней открывает строки как электронную таблицу.

### Первый пример кода

`school.sql`:

```sql
CREATE TABLE IF NOT EXISTS students (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    age INT NOT NULL,
    email VARCHAR(100) NOT NULL UNIQUE
);

SHOW TABLES;
```

Выполните в Database Client. Результат `SHOW TABLES`:

```text
Tables_in_school
----------------
students
```

Таблица существует! А в дереве сбоку появилась `students` с четырьмя столбцами.

### Разбор кода построчно

| Строка | Объяснение |
|---|---|
| `CREATE TABLE IF NOT EXISTS students (` | Создать таблицу с именем `students`, если её ещё нет. |
| `id INT AUTO_INCREMENT PRIMARY KEY,` | Идентификатор: целое число, которое база заполняет сама (1, 2, 3...), первичный ключ. MySQL **не переиспользует** номер удалённой строки. |
| `name VARCHAR(100) NOT NULL,` | Текст до 100 символов, обязательный. |
| `age INT NOT NULL,` | Целое число, обязательное. |
| `email VARCHAR(100) NOT NULL UNIQUE` | Текст, обязательный, уникальный. **Без запятой**, потому что это последний столбец. |
| `);` | Конец команды. |
| `SHOW TABLES;` | Команда MySQL: какие таблицы есть в текущей базе. |

Выполните ту же команду снова: ошибки нет. Благодаря `IF NOT EXISTS`.

### Ещё один пример

**Добавляем строки и смотрим, что происходит при нарушении ограничений.** Добавьте в `school.sql` и выполните:

```sql
INSERT INTO students (name, age, email) VALUES ('Dana', 24, 'dana@mail.com');

INSERT INTO students (name, age, email) VALUES
    ('Avi', 31, 'avi@mail.com'),
    ('Noa', 22, 'noa@mail.com');

SELECT * FROM students;
```

```text
id | name | age | email
---+------+-----+--------------
 1 | Dana |  24 | dana@mail.com
 2 | Avi  |  31 | avi@mail.com
 3 | Noa  |  22 | noa@mail.com
```

**База сама выдала id**: 1, 2, 3. (`SELECT *` = «покажи всё» — в следующем уроке изучим его подробно.)

**Теперь нарушим ограничения** — выполняйте **каждую команду отдельно**:

```sql
INSERT INTO students (name, age, email) VALUES ('Tom', 27, 'dana@mail.com');
```

```text
1062 (23000): Duplicate entry 'dana@mail.com' for key 'students.email'
```

```sql
INSERT INTO students (name, email) VALUES ('Rina', 'rina@mail.com');
```

```text
1364 (HY000): Field 'age' doesn't have a default value
```

```sql
INSERT INTO students (name, age, email) VALUES ('Ron', 30);
```

```text
1136 (21S01): Column count doesn't match value count at row 1
```

Мы запросили 3 столбца, но дали только 2 значения. Число значений должно совпадать с числом столбцов.

**Разбор:**

- **`Duplicate entry ... for key 'students.email'`** — email Даны уже существует. **База отказала**, и строка **не** добавилась.
- **`Field 'age' doesn't have a default value`** — мы не заполнили `age`, а он обязательный (`NOT NULL`, без значения по умолчанию).
- Число в начале каждого сообщения (`1062`, `1364`, `1136`) — **код ошибки** MySQL, по нему можно искать в интернете.
- Из Python нарушение `UNIQUE` приходит как **`IntegrityError`** (Integrity = целостность данных). Мы поймаем его позже в этой теме и будем возвращать **409** из API в теме API студентов.
- **Данные остались допустимыми.** В этом сила ограничений: даже если в коде есть баг, база соблюдает правила.

#### Тот же SQL — из Python

На сервере мы не будем выполнять SQL вручную — его будет выполнять **код**. Вот как те же `CREATE TABLE` и `INSERT` выглядят из Python:

```python
import mysql.connector

CREATE_STUDENTS = """
CREATE TABLE IF NOT EXISTS students (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    age INT NOT NULL,
    email VARCHAR(100) NOT NULL UNIQUE
)
"""

connection = mysql.connector.connect(
    host="localhost", user="root", password="kodkod", database="school")
try:
    cursor = connection.cursor()
    cursor.execute(CREATE_STUDENTS)
    cursor.execute(
        "INSERT INTO students (name, age, email) VALUES ('Tom', 27, 'tom@mail.com')")
    connection.commit()
    print("Inserted Tom")
finally:
    connection.close()
```

- `"""..."""` — строка на несколько строк, удобна для длинного SQL.
- `cursor.execute(...)` выполняет **одну команду** за вызов.
- `connection.commit()` — **сохраняет** вставку в базе. Без `commit` строка исчезает при закрытии соединения. (В уроке «запросы из Python» расскажем подробнее.)

### Типичная ошибка

**Ошибка 1: двойные кавычки для строк.**

```sql
INSERT INTO students (name, age, email) VALUES ("Dana", 24, "dana@mail.com");
```

В стандартном SQL **двойные кавычки — для имён** (таблиц и столбцов), а **одинарные — для строк**. MySQL «прощает» и принимает это, но другие базы (PostgreSQL) выдадут ошибку. **Для строк всегда `'...'`.**

**Ошибка 2: запятая после последнего столбца.**

```sql
CREATE TABLE students (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
);
```

```text
1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ')' at line 4
```

**Ошибка 3: забыть список столбцов в INSERT.**

```sql
INSERT INTO students VALUES ('Dana', 24, 'dana@mail.com');
```

```text
1136 (21S01): Column count doesn't match value count at row 1
```

Без списка столбцов SQL ожидает значение для **каждого** столбца, включая `id`, и в порядке таблицы. **Всегда пишите список столбцов явно.** Это ещё и устойчиво к будущим изменениям таблицы.

**Ошибка 4: изменить `CREATE TABLE IF NOT EXISTS` и ожидать, что таблица обновится.** Вы добавили в команду столбец `city` и выполнили её: ничего не произошло, потому что таблица уже существует. На этапе обучения простое решение: удалить таблицу — `DROP TABLE students;` — и выполнить снова. (Это удалит и все строки! В настоящих системах используют **миграции** (Migrations) — инструменты, которые меняют структуру существующей таблицы без потери данных. Тема на будущее.)

### Итоги

- **`CREATE TABLE IF NOT EXISTS name (col TYPE constraints, ...);`** — создаёт структуру. `IF NOT EXISTS` = безопасно запускать повторно (но существующую структуру не обновляет).
- **`INSERT INTO name (cols) VALUES (...);`** — добавляет строку. Всегда со списком столбцов. `id` с `AUTO_INCREMENT` заполняется сам.
- Строки в SQL — в **одинарных кавычках** `'...'`. После последнего столбца запятой нет.
- Нарушение ограничения → ошибка с кодом (`1062` — дубликат, `1364` — отсутствует обязательное поле). Строка не добавляется, и данные остаются допустимыми.
- SQL выполняют в **Database Client**, а из Python — через `cursor.execute(...)` и `commit()`.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

### Официальная документация

- [MySQL — CREATE TABLE](https://dev.mysql.com/doc/refman/8.4/en/create-table.html)
- [MySQL — INSERT](https://dev.mysql.com/doc/refman/8.4/en/insert.html)

<!-- docs:end -->

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## Практика

Упражнения опираются только на то, что пройдено в этом уроке и в предыдущих. Сначала попробуйте сами, и только потом открывайте решение.

### Упражнение 1: Проверка здоровья (Health Check)

Напишите код, который подключается к базе `school`, выполняет два проверочных запроса — `SELECT 1 + 1` и `SELECT 'ok'`, — печатает результаты и **в любом случае** закрывает соединение.

**Ожидаемый вывод:**

```text
The database computed: 2
Status: ok
Connection closed
```

{{< solution >}}
```python
import mysql.connector

connection = mysql.connector.connect(
    host="localhost", user="root", password="kodkod", database="school")
try:
    cursor = connection.cursor()
    cursor.execute("SELECT 1 + 1")
    two = cursor.fetchone()[0]
    print("The database computed:", two)
    cursor.execute("SELECT 'ok'")
    status = cursor.fetchone()[0]
    print("Status:", status)
finally:
    connection.close()
    print("Connection closed")
```

```text
The database computed: 2
Status: ok
Connection closed
```

- `connection.cursor()` создаёт курсор, а `cursor.execute(...)` выполняет через него запрос.
- `fetchone()` возвращает одну строку в виде **tuple**, поэтому `[0]` — первый столбец.
- `finally` гарантирует, что соединение закроется, даже если запрос упадёт.
{{< /solution >}}

### Упражнение 2: Connection и Cursor

1. В чём разница между Connection и Cursor?
2. В чём разница между `fetchone()` и `fetchall()`?

{{< solution >}}
1. **Connection** — открытое соединение с сервером MySQL: через него создают курсор, сохраняют (`commit`) и закрывают. **Cursor** — объект, который выполняет запрос (`execute`) и хранит его результаты.
2. **`fetchone()`** — одна строка (tuple) или `None`, если строки нет. **`fetchall()`** — **список** всех строк или пустой список.
{{< /solution >}}

### Упражнение 3: CREATE TABLE IF NOT EXISTS

Напишите код на Python, который создаёт таблицу `students` (id, name, age, уникальный email) в базе `school`, только если её нет. Выполните создание дважды и покажите, что ошибки нет.

**Ожидаемый вывод:**

```text
table ready
table ready
```

{{< solution >}}
```python
import mysql.connector

CREATE_STUDENTS = """
CREATE TABLE IF NOT EXISTS students (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    age INT NOT NULL,
    email VARCHAR(100) NOT NULL UNIQUE
)
"""

for _ in range(2):
    connection = mysql.connector.connect(
        host="localhost", user="root", password="kodkod", database="school")
    try:
        cursor = connection.cursor()
        cursor.execute(CREATE_STUDENTS)
        print("table ready")
    finally:
        connection.close()
```

```text
table ready
table ready
```

Без `IF NOT EXISTS` второй запуск упал бы: `Table 'students' already exists`. Строка в тройных кавычках (`"""`) позволяет писать SQL на нескольких строках.
{{< /solution >}}

### Упражнение 4: INSERT INTO

Продолжение: добавьте трёх студентов — Дану (24), Ави (31) и Ноа (22) — и сохраните. Затем посчитайте, сколько строк в таблице.

**Ожидаемый вывод:**

```text
Rows: 3
```

{{< solution >}}
```python
import mysql.connector

connection = mysql.connector.connect(
    host="localhost", user="root", password="kodkod", database="school")
try:
    cursor = connection.cursor()
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS students (
            id INT AUTO_INCREMENT PRIMARY KEY,
            name VARCHAR(100) NOT NULL,
            age INT NOT NULL,
            email VARCHAR(100) NOT NULL UNIQUE
        )
    """)
    cursor.execute("""
        INSERT INTO students (name, age, email) VALUES
            ('Dana', 24, 'dana@mail.com'),
            ('Avi', 31, 'avi@mail.com'),
            ('Noa', 22, 'noa@mail.com')
    """)
    connection.commit()
    cursor.execute("SELECT * FROM students")
    print("Rows:", len(cursor.fetchall()))
finally:
    connection.close()
```

```text
Rows: 3
```

`id` вы не пишете — база присваивает его сама. `commit()` сохраняет изменение в базе.
{{< /solution >}}

### Упражнение 5: База отказывает

Вставьте студента в таблицу, а затем попытайтесь вставить другого студента с **тем же email** и студента **с возрастом `NULL`**. Поймайте ошибку каждой попытки и напечатайте её сообщение (`error.msg`).

**Ожидаемый вывод:**

```text
Rejected: Duplicate entry 'dana@mail.com' for key 'students.email'
Rejected: Column 'age' cannot be null
```

{{< solution >}}
```python
import mysql.connector

connection = mysql.connector.connect(
    host="localhost", user="root", password="kodkod", database="school")
try:
    cursor = connection.cursor()
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS students (
            id INT AUTO_INCREMENT PRIMARY KEY,
            name VARCHAR(100) NOT NULL,
            age INT NOT NULL,
            email VARCHAR(100) NOT NULL UNIQUE
        )
    """)
    cursor.execute(
        "INSERT INTO students (name, age, email) VALUES ('Dana', 24, 'dana@mail.com')")
    attempts = [
        "INSERT INTO students (name, age, email) VALUES ('Copy', 20, 'dana@mail.com')",
        "INSERT INTO students (name, age, email) VALUES ('Noa', NULL, 'noa@mail.com')",
    ]
    for sql in attempts:
        try:
            cursor.execute(sql)
        except mysql.connector.IntegrityError as error:
            print("Rejected:", error.msg)
    connection.commit()
finally:
    connection.close()
```

```text
Rejected: Duplicate entry 'dana@mail.com' for key 'students.email'
Rejected: Column 'age' cannot be null
```

**Ограничения**, которые мы определили в таблице, проверяются при каждой вставке — без единой строки проверки в коде Python.
{{< /solution >}}

<!-- exercises:end -->
