# חיבור, טבלה והכנסת נתונים: Connection, CREATE TABLE ו-INSERT<no value>

## מה בשיעור

השיעור הזה בנוי משני פרקים, באותו סדר כמו במצגת:

1. **פרק ראשון** — פתיחת Connection, בדיקה שהחיבור עובד וסגירה נכונה
2. **פרק שני** — SQL בסיסי: CREATE TABLE ו-INSERT

## פרק ראשון: פתיחת Connection, בדיקה שהחיבור עובד וסגירה נכונה

### מה נלמד?

**מחזור החיים** של כל עבודה מול מסד נתונים:

```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 בשרת (נושא Students API – שרת עם מסד נתונים) מתחילה בפתיחת חיבור ומסתיימת בסגירה. **חיבור שלא נסגר** הוא באג קלאסי: כל חיבור ל-MySQL תופס משאבים — גם אצלנו וגם בשרת — ול-MySQL יש **מספר מוגבל** של חיבורים בבת אחת. בשרת שמטפל באלפי בקשות, אחרי זמן מה נגמרים החיבורים (`Too many connections`) והכול נתקע. זה בדיוק כמו קבצים פתוחים בנושא Python, רק בקנה מידה של שרת.

### הסבר מהבסיס

#### Connection — הקו הפתוח

**Connection** (חיבור) = אובייקט שמייצג **ערוץ פתוח** בין הקוד שלנו לשרת ה-MySQL. דרכו יוצרים Cursor, דרכו מאשרים שינויים (`commit`, בהמשך הנושא), ודרכו סוגרים. כל עוד הוא פתוח, הוא תופס משאבים.

#### Cursor — הסמן

**Cursor** (סמן) = אובייקט שמריץ שאילתה **ומחזיק את התוצאות** שלה, ומאפשר לקרוא אותן שורה אחר שורה. דמיינו אותו כאצבע שעוברת על תוצאות השאילתה.

יוצרים אותו מה-Connection, ומריצים דרכו:

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

#### קריאת תוצאות

| מתודה של Cursor | מחזירה | מתי |
|---|---|---|
| `fetchone()` | **שורה אחת** (tuple), או `None` אם אין | מצפים לשורה אחת: בדיקה, או חיפוש לפי id |
| `fetchall()` | **רשימה** של כל השורות (רשימת tuples) | מצפים לכמה שורות |

כל שורה היא **tuple** (נושא Python!): `("Dana", 24)`. (בהמשך הנושא נלמד לקבל שורות כ-dict.)

{{< box "analogy" "אנלוגיה: שיחת טלפון למוקד" >}}
**Connection** = השיחה עצמה: חייגתם, והקו פתוח. **Cursor** = הנציג שמחפש לכם מידע וקורא לכם תוצאה אחר תוצאה. **`fetchone`** = "תגיד לי את הראשון". **`fetchall`** = "תקריא לי את כולם". **`close`** = לנתק. אם לא מנתקים, הקו נשאר תפוס, ואחרים לא יכולים להתקשר.
{{< /box >}}

#### שאילתת בדיקה (Health Check)

כדי לבדוק שהחיבור **באמת עובד** (ולא רק שהאובייקט נוצר), מריצים שאילתה פשוטה שלא תלויה באף טבלה:

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

אם חזרה תשובה, המסד חי ועונה. כך בדיוק עובד ה-`/health` Endpoint של שרתים אמיתיים (נושא Backend ו-FastAPI).

#### סגירה נכונה — try/finally

מה קורה אם השאילתה נכשלת **אחרי** שפתחנו חיבור?

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

השגיאה עוצרת את הפונקציה, ו-`close` לא רץ. הפתרון שלמדנו בנושא Python: **`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(...)` | **פתיחה**. מתקבל אובייקט Connection (נושא OOP: Class ואובייקט!). |
| `try:` | מכאן, כל שגיאה עדיין תגיע ל-`finally`. |
| `cursor = connection.cursor()` | יוצר **Cursor** — דרכו מריצים שאילתות. |
| `cursor.execute("SELECT VERSION()")` | **מריץ שאילתה**. התוצאה נשמרת ב-Cursor. |
| `cursor.fetchone()` | שורה אחת: **tuple** עם עמודה אחת: `('8.4.11',)`. הפסיק = tuple של איבר אחד (נושא Python!). |
| `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` — **מחלקת הבסיס** לכל השגיאות של ה-Driver (נושא OOP: היררכיית Exceptions!). `ProgrammingError` (שגיאה ב-SQL), `IntegrityError` (הפרת אילוץ, בהמשך) ו-`OperationalError` (בעיה בחיבור) הן תתי-מחלקות שלה.
- בשאילתה עם שגיאת הקלדה (`SELEC`), השגיאה נתפסה, **ו-`finally` עדיין סגר את החיבור**.
- `1064` הוא **מספר השגיאה** של MySQL לשגיאת תחביר, ו-`near 'SELEC 1'` אומר בדיוק איפה הבעיה. זו **Syntax Error של SQL** (נושא Python: אותו רעיון, שפה אחרת).
- שימוש בחיבור אחרי `close` → `MySQL Connection not available`. החיבור "מת", וצריך לפתוח חדש.

#### `get_connection` — נקודת חיבור אחת

בפרויקט אמיתי, **כל** פונקציה צריכה חיבור. במקום לכתוב את חמשת פרטי החיבור בעשרות מקומות, כותבים **פונקציה אחת** (DRY, נושא Python). זה יהיה הבסיס של קובץ `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**. בנושא Students 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 (`cursor()`) ‏←‏ שאילתה (`execute`) ‏←‏ קריאה (`fetchone`/`fetchall`) ‏←‏ סגירה (`close`)**.
- **Connection** = הערוץ לשרת המסד. **Cursor** = מריץ שאילתה ומחזיק תוצאות.
- `fetchone()` = שורה אחת (tuple) או `None`. `fetchall()` = רשימת tuples. ערך יחיד: `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) — mysql.connector.connect()
- [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 -->

## פרק שני: SQL בסיסי — CREATE TABLE ו-INSERT

### מה נלמד?

שתי הפקודות הראשונות ב-SQL:

| פקודה | מה עושה | CRUD |
|---|---|---|
| **`CREATE TABLE`** | יוצרת **מבנה**: טבלה עם עמודות | — (הכנה) |
| **`INSERT INTO`** | מוסיפה **שורה** | **C**reate |

בשיעור הזה מתמקדים ב-**SQL עצמו**, ומריצים אותו ב-**Database Client** שב-VS Code. בסוף נראה גם איך מריצים את אותו SQL מתוך Python. (בשיעורים הבאים נלמד לעבוד מ-Python **כמו שצריך** — פרמטרים, commit ותוצאות.)

### למה צריך את זה?

המסד `school` רץ בקונטיינר — אבל הוא **ריק**. טבלה היא ה"מגירה" שבה הנתונים יישבו, ו-`INSERT` הוא הפעולה הראשונה בכל מערכת: הרשמת משתמש, הוספת מוצר, ביצוע הזמנה. ב-API שנבנה בנושא Students API – שרת עם מסד נתונים, `POST /students` = `INSERT INTO students`.

### הסבר מהבסיס

#### מבנה של פקודת SQL

- **מילות מפתח** (Keywords) כמו `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'`** — האימייל של דנה כבר קיים. **המסד סירב**, והשורה **לא** נוספה.
- **`Field 'age' doesn't have a default value`** — לא מילאנו `age`, והוא חובה (`NOT NULL`, בלי ברירת מחדל).
- המספר בתחילת כל הודעה (`1062`, `1364`, `1136`) הוא **קוד השגיאה** של MySQL — אפשר לחפש אותו באינטרנט.
- מ-Python, הפרת `UNIQUE` מגיעה כ-**`IntegrityError`** (Integrity = שלמות הנתונים). נתפוס אותה בהמשך הנושא, ונחזיר **409** מה-API בנושא Students 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()
```

- `"""..."""` — מחרוזת על כמה שורות (נושא Python), נוחה ל-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, ו-`cursor.execute(...)` מריץ דרכו שאילתה.
- `fetchone()` מחזיר שורה אחת כ-**tuple**, ולכן `[0]` — העמודה הראשונה.
- `finally` מבטיח שהחיבור ייסגר גם אם השאילתה נכשלת.
{{< /solution >}}

### תרגיל 2: Connection ו-Cursor

1. מה ההבדל בין Connection לבין Cursor?
2. מה ההבדל בין `fetchone()` ל-`fetchall()`?

{{< solution >}}
1. **Connection** — החיבור הפתוח לשרת ה-MySQL: דרכו יוצרים Cursor, שומרים (`commit`) וסוגרים. **Cursor** — האובייקט שמריץ שאילתה (`execute`) ומחזיק את התוצאות שלה.
2. **`fetchone()`** — שורה אחת (tuple), או `None` אם אין. **`fetchall()`** — **רשימה** של כל השורות, או רשימה ריקה.
{{< /solution >}}

### תרגיל 3: CREATE TABLE IF NOT EXISTS

כתבו קוד Python שיוצר במסד `school` את טבלת `students` (id, name, age, email ייחודי) רק אם היא לא קיימת. הריצו את היצירה פעמיים, והראו שאין שגיאה.

**פלט צפוי:**

```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: המסד מסרב

הכניסו לטבלה סטודנט, ואז נסו להכניס סטודנט נוסף עם **אותו אימייל**, וסטודנט **עם גיל `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 -->
