# הרצת Query מתוך Python: execute, פרמטרים, commit ו-fetch<no value>

## מה נלמד?

עד עכשיו כתבנו SQL "ידני" ב-Database Client. עכשיו מחברים אותו לקוד, לפי המסלול שהסילבוס מדגיש:

```text
Python  →  Connection  →  Query  →  Database  →  Result  →  Python
(values)   (open)         (SQL +    (runs it)     (rows)     (tuples,
                           params)                            ints, strs)
```

- **`execute`** עם **פרמטרים** (`%s`).
- **SQL Injection**: הסכנה, ולמה פרמטרים פותרים אותה.
- **`commit`**: שמירת שינויים.
- **`lastrowid`**: ה-id שהמסד נתן לשורה החדשה.
- **`fetchone` / `fetchall`** על נתונים אמיתיים.

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

ב-API, הערכים מגיעים **מהמשתמש**: שם, גיל ואימייל מתוך Body של POST. הקוד צריך להכניס אותם לשאילתה **בבטחה**, לשמור, ולהחזיר את מה שנשמר, כולל ה-id החדש (`201 Created` + הסטודנט). כל זה קורה בשיעור הזה, בקוד אמיתי.

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

### `execute(sql, params)`

```python
cursor.execute(
    "INSERT INTO students (name, age, email) VALUES (%s, %s, %s)",
    ("Dana", 24, "dana@mail.com"),
)
```

- ב-SQL כותבים **`%s`** במקום כל ערך (**Placeholder**, "שומר מקום"). ב-MySQL זה תמיד `%s` — **לכל טיפוס**, גם למספרים.
- הערכים עוברים **בנפרד**, כ-**tuple**, **באותו סדר** של ה-Placeholders.
- ה-Driver (`mysql.connector`) מכניס אותם למקום **בבטחה**: מחרוזות כמחרוזות, ומספרים כמספרים. אין צורך בגרש, ואין צורך בהמרה.

### SQL Injection — למה לא לבנות SQL בשרשור

נראה "פשוט" לבנות את השאילתה בעצמנו:

```python
name = input("Student name to search: ")
sql = "SELECT id, name FROM students WHERE name = '" + name + "'"
```

אם המשתמש מקליד `Dana`, מתקבלת השאילתה `... WHERE name = 'Dana'`. בסדר. אבל אם הוא מקליד:

```text
x' OR '1'='1
```

מתקבלת השאילתה:

```sql
SELECT id, name FROM students WHERE name = 'x' OR '1'='1'
```

`'1'='1'` תמיד נכון, ולכן **מוחזרים כל הסטודנטים**. המשתמש **שינה את משמעות השאילתה** דרך הקלט. זה **SQL Injection**, אחת מפרצות האבטחה הנפוצות והמסוכנות ביותר בעולם. באמצעותה פורצים ל-Databases, גונבים סיסמאות, ומוחקים טבלאות.

**עם `%s` ופרמטרים**, ה-Driver מתייחס לקלט **תמיד כערך**, ולעולם לא כחלק מה-SQL. המשתמש יכול להקליד מה שהוא רוצה, והמסד פשוט יחפש סטודנט ששמו הוא המחרוזת המוזרה הזו.

### איך בדיוק `%s` מגן?

בשרשור, ההתקפה עובדת בגלל **גרש אחד**: הגרש שהמשתמש הקליד אחרי `x` **סוגר** את הטקסט שלנו, וכל מה שבא אחריו (`OR '1'='1'`) כבר נקרא כחלק מהפקודה.

עם `%s`, ה-Driver מכין את הערך לפני ששולח אותו: הוא מוסיף **לוכסן הפוך** (`\`) לפני כל גרש בקלט. לוכסן לפני גרש אומר ל-MySQL: "זה גרש רגיל בתוך הטקסט, לא סוף הטקסט".

| שלב | מה רואים |
|---|---|
| המשתמש מקליד | `x' OR '1'='1` |
| ה-Driver מוסיף `\` לפני כל גרש | `x\' OR \'1\'=\'1` |
| מה ש-MySQL מקבל | `SELECT name FROM students WHERE name = 'x\' OR \'1\'=\'1'` |
| איך MySQL קורא | טקסט **אחד**: שם ארוך ומוזר — `x' OR '1'='1` |

הגרש של המשתמש כבר **לא יכול לסגור** את הטקסט, ולכן כל הקלט נשאר בפנים כשם אחד. אין `OR` שהפך לחלק מהפקודה, ו-MySQL מחפש סטודנט בשם הזה — ולא מוצא (`[]`).

**ולמה לא לעשות את זה לבד?** כי קל לפספס מקרה: גרש כפול, לוכסן בקלט, קידוד מיוחד. ה-Driver יודע לטפל בכולם, בכל פעם — לכן תמיד `%s`.

{{< box "warn" "כלל ברזל" >}}
**לעולם** לא בונים SQL עם `+` מערכים שמגיעים מבחוץ. **תמיד `%s` ופרמטרים.** אין יוצאים מן הכלל.
{{< /box >}}

{{< box "analogy" "אנלוגיה: טופס עם תיבות" >}}
שרשור מחרוזות = לתת לזר לכתוב חופשי על הצ'ק שלכם, כולל בשורה של הסכום. פרמטרים = צ'ק מודפס עם תיבה ייעודית לשם, ומה שנכתב בתיבה הוא **רק השם**: אי אפשר "לגלוש" ממנה ולשנות את הסכום.
{{< /box >}}

### `commit` — לשמור באמת

פקודות ש**משנות** נתונים (`INSERT`, `UPDATE`, `DELETE`) נכנסות קודם ל-**Transaction** (עסקה) פתוחה. השינוי נראה בתוך החיבור הנוכחי, אבל **לא נשמר סופית** עד שקוראים ל-**`connection.commit()`**. אם סוגרים את החיבור בלי commit, **השינויים נזרקים**.

- `SELECT` לא צריך commit, כי הוא לא משנה כלום.
- `CREATE TABLE` נשמר ב-MySQL **מיד**, גם בלי commit.

### `lastrowid` ו-`rowcount`

אחרי `execute`, ל-Cursor יש מידע על מה שקרה:

| Attribute | מה הוא מכיל | שימוש |
|---|---|---|
| `cursor.lastrowid` | ה-id שהמסד נתן לשורה **האחרונה שהוכנסה** | להחזיר את הפריט החדש מ-POST |
| `cursor.rowcount` | **כמה שורות השתנו** ב-INSERT/UPDATE/DELETE | לגלות ש"לא נמצא" ב-UPDATE/DELETE (בהמשך הנושא) |

## תחביר

```python
connection = get_connection()
try:
    cursor = connection.cursor()

    # change data: execute + commit
    cursor.execute("INSERT INTO t (a, b) VALUES (%s, %s)", (value_a, value_b))
    connection.commit()
    new_id = cursor.lastrowid

    # read data: execute + fetch
    cursor.execute("SELECT a, b FROM t WHERE b > %s", (10,))
    rows = cursor.fetchall()
    cursor.execute("SELECT a, b FROM t WHERE id = %s", (new_id,))
    row = cursor.fetchone()
finally:
    connection.close()
```

**שימו לב ל-`(10,)`**: tuple של איבר **אחד** חייב פסיק (נושא Python!). `(10)` הוא סתם המספר 10, ו-`execute` תיכשל.

## דוגמת קוד ראשונה

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

```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()
    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 (%s, %s, %s)",
        ("Dana", 24, "dana@mail.com"),
    )
    connection.commit()
    print("Inserted student with id", cursor.lastrowid)

    cursor.execute("SELECT id, name, age, email FROM students")
    print("All rows:", cursor.fetchall())
finally:
    connection.close()
```

**פלט (הרצה ראשונה, על טבלה ריקה):**

```text
Inserted student with id 1
All rows: [(1, 'Dana', 24, 'dana@mail.com')]
```

## פירוק הקוד שורה אחר שורה

| קוד | שלב במסלול |
|---|---|
| `get_connection()` | **Connection**: פתיחה. |
| `cursor = connection.cursor()` | ה-Cursor שדרכו נריץ את כל השאילתות. |
| `"""..."""` | **מחרוזת מרובת שורות** (שלוש מירכאות). נוחה ל-SQL ארוך. |
| `execute(CREATE TABLE ...)` | יצירת הטבלה, אם היא לא קיימת. |
| `execute("INSERT ... VALUES (%s, %s, %s)", ("Dana", 24, "dana@mail.com"))` | **Query** עם 3 Placeholders ו-3 ערכים. **Python → Database.** |
| `connection.commit()` | **שמירה.** בלעדיה, הסטודנטית הייתה נעלמת בסגירה. |
| `cursor.lastrowid` | ה-id שהמסד נתן: 1. |
| `execute("SELECT ...")` + `fetchall()` | **Result → Python**: רשימה של tuples. |
| `finally: connection.close()` | סגירה, תמיד. |

**הריצו שוב:**

```text
Traceback (most recent call last):
  ...
mysql.connector.errors.IntegrityError: 1062 (23000): Duplicate entry 'dana@mail.com' for key 'students.email'
```

בהרצה השנייה, ה-INSERT של דנה נכשל, כי האימייל כבר קיים, והמסד אכף את `UNIQUE`. (ולמרות השגיאה, החיבור **נסגר**, בזכות `finally`.) בהמשך הנושא נטפל בשגיאה הזו כמו שצריך. בינתיים, כדי להתחיל מחדש: ב-Database Client מריצים `DROP TABLE students;`.

## דוגמה נוספת

**הוספה של כמה סטודנטים, חיפושים עם פרמטרים, והדגמה של SQL Injection.**

```python
import mysql.connector

connection = mysql.connector.connect(
    host="localhost", user="root", password="kodkod", database="school")
try:
    cursor = connection.cursor()
    cursor.execute("DROP TABLE IF EXISTS students")
    cursor.execute(
        "CREATE TABLE students (id INT AUTO_INCREMENT PRIMARY KEY, "
        "name VARCHAR(100) NOT NULL, age INT NOT NULL)")
    for name, age in [("Dana", 24), ("Avi", 31), ("Noa", 22)]:
        cursor.execute("INSERT INTO students (name, age) VALUES (%s, %s)", (name, age))
    connection.commit()

    cursor.execute("SELECT id, name, age FROM students WHERE id = %s", (2,))
    print("By id 2:", cursor.fetchone())

    cursor.execute("SELECT name FROM students WHERE age > %s ORDER BY age", (23,))
    print("Older than 23:", cursor.fetchall())

    cursor.execute("SELECT id, name FROM students WHERE id = %s", (99,))
    print("By id 99:", cursor.fetchone())

    attack = "x' OR '1'='1"

    unsafe_sql = "SELECT name FROM students WHERE name = '" + attack + "'"
    print("UNSAFE query:", unsafe_sql)
    cursor.execute(unsafe_sql)
    print("UNSAFE result:", cursor.fetchall())

    cursor.execute("SELECT name FROM students WHERE name = %s", (attack,))
    print("SAFE result:", cursor.fetchall())
finally:
    connection.close()
```

**פלט:**

```text
By id 2: (2, 'Avi', 31)
Older than 23: [('Dana',), ('Avi',)]
By id 99: None
UNSAFE query: SELECT name FROM students WHERE name = 'x' OR '1'='1'
UNSAFE result: [('Dana',), ('Avi',), ('Noa',)]
SAFE result: []
```

**פירוק:**

- `DROP TABLE IF EXISTS students` — מוחק את הטבלה, אם קיימת, כדי שכל הרצה תתחיל נקייה. (נוח להדגמות. **לא** עושים את זה בקוד אמיתי!)
- **INSERT בלולאה, ו-commit אחד בסוף** — שלוש הכנסות נשמרות יחד.
- **`fetchone()` על id קיים** ‏←‏ tuple. **על id שלא קיים** ‏←‏ **`None`**. זה ה"לא נמצא" שיהפוך ל-404.
- **`(2,)`**, **`(23,)`** — tuple של ערך אחד, עם פסיק.
- **הגרסה הלא בטוחה** החזירה **את כל הסטודנטים**, כי הקלט שינה את השאילתה עצמה.
- **הגרסה הבטוחה** חיפשה סטודנט ששמו בדיוק `x' OR '1'='1`, ואין כזה, ולכן `[]`. **ההתקפה נכשלה.**

## טעות נפוצה

**הטעות 1: לשכוח `commit`.**

```python
cursor.execute("INSERT INTO students (name, age) VALUES (%s, %s)", ("Tom", 27))
connection.close()
```

אין שגיאה, ו**Tom לא נשמר**. סגירה בלי commit = ביטול השינויים. זו אחת הטעויות המבלבלות ביותר, כי "הכול עבד".

**הטעות 2: פרמטר בודד בלי פסיק.**

```python
cursor.execute("SELECT name FROM students WHERE id = %s", (2))
```

```text
mysql.connector.errors.ProgrammingError: Could not process parameters: int(2), it must be of type list, tuple or dict
```

`(2)` הוא `int`, ולא tuple. → `(2,)`.

**הטעות 3: מספר פרמטרים לא תואם.**

```python
cursor.execute("INSERT INTO students (name, age) VALUES (%s, %s)", ("Tom",))
```

```text
mysql.connector.errors.ProgrammingError: Not enough parameters for the SQL statement
```

**הטעות 4: `%s` עם גרש.**

```python
cursor.execute("SELECT * FROM students WHERE name = '%s'", ("Dana",))
```

```text
mysql.connector.errors.ProgrammingError: 1064 (42000): You have an error in your SQL syntax; ... near 'Dana''' at line 1
```

ה-Driver כבר מוסיף גרשים בעצמו, ועכשיו יש גרשים כפולים. **`%s` תמיד בלי גרש.**

**הטעות 5: `%s` במקום שם של טבלה או עמודה.** `"SELECT * FROM %s"` לא עובד: Placeholders הם **רק לערכים** — ה-Driver הופך את `students` ל-`'students'` (מחרוזת), וזו שגיאת תחביר. שמות של טבלאות ועמודות כותבים בקוד, קבועים.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## תרגול

התרגילים מבוססים רק על מה שנלמד בשיעור הזה ובשיעורים שלפניו. נסו לבד, ורק אז פתחו את הפתרון.

### תרגיל 1: execute עם פרמטרים

כתבו פונקציה `add_student(connection, name, age)` שמוסיפה סטודנט עם **פרמטרים** (`%s`), שומרת, ומחזירה את ה-`id` החדש.

```python
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)")
print(add_student(connection, "Dana", 24))
print(add_student(connection, "Avi", 31))
cursor.execute("SELECT * FROM students")
print(cursor.fetchall())
```

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

```text
1
2
[(1, 'Dana', 24), (2, 'Avi', 31)]
```

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


def add_student(connection, name, age):
    cursor = connection.cursor()
    cursor.execute(
        "INSERT INTO students (name, age) VALUES (%s, %s)", (name, age))
    connection.commit()
    return cursor.lastrowid


connection = mysql.connector.connect(
    host="localhost", user="root", password="kodkod", database="school")
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)")
print(add_student(connection, "Dana", 24))
print(add_student(connection, "Avi", 31))
cursor.execute("SELECT * FROM students")
print(cursor.fetchall())
connection.close()
```

```text
1
2
[(1, 'Dana', 24), (2, 'Avi', 31)]
```

- `%s` הם **מקומות שמורים**, והערכים עוברים בנפרד כ-tuple. (ב-MySQL כותבים `%s` לכל טיפוס — גם למספרים.)
- `cursor.lastrowid` — ה-`id` שהמסד נתן לשורה החדשה.
{{< /solution >}}

### תרגיל 2: SQL Injection

בטבלה יש שני סטודנטים — דנה ואבי. צרו אותה כך:

```py
import mysql.connector

connection = mysql.connector.connect(
    host="localhost", user="root", password="kodkod", database="school")
cursor = connection.cursor()
cursor.execute("DROP TABLE IF EXISTS students")   # start clean, so ids start from 1
cursor.execute("CREATE TABLE students (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100))")
cursor.execute("INSERT INTO students (name) VALUES ('Dana'), ('Avi')")
connection.commit()
connection.close()
```

קוד שבונה שאילתה מקלט של משתמש:

```text
name = "x' OR '1'='1"
sql = "SELECT name FROM students WHERE name = '" + name + "'"
```

1. איך תיראה הפקודה שנוצרת, ומה היא תחזיר?
2. הראו בקוד את ההבדל: הריצו את השאילתה בשרשור והדפיסו `Unsafe:` ואת התוצאה, ואז את אותה שאילתה עם `%s` והדפיסו `Safe:` ואת התוצאה.

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

```text
Unsafe: [('Dana',), ('Avi',)]
Safe: []
```

{{< solution >}}
1. הפקודה היא `SELECT name FROM students WHERE name = 'x' OR '1'='1'` — והתנאי `'1'='1'` תמיד נכון, ולכן היא מחזירה **את כל הסטודנטים**. הקלט של המשתמש הפך לחלק מה**פקודה**.
2.

```python
import mysql.connector

connection = mysql.connector.connect(
    host="localhost", user="root", password="kodkod", database="school")
cursor = connection.cursor()
cursor.execute("CREATE TABLE IF NOT EXISTS students "
               "(id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100))")
cursor.execute("INSERT INTO students (name) VALUES ('Dana'), ('Avi')")
connection.commit()

name = "x' OR '1'='1"

sql = "SELECT name FROM students WHERE name = '" + name + "'"
cursor.execute(sql)
print("Unsafe:", cursor.fetchall())

cursor.execute("SELECT name FROM students WHERE name = %s", (name,))
print("Safe:", cursor.fetchall())
connection.close()
```

```text
Unsafe: [('Dana',), ('Avi',)]
Safe: []
```

עם `%s` הערך עובר **בנפרד** — ה-Driver מתייחס אליו כאל שם מוזר, לעולם לא כאל פקודה. **לעולם לא בונים SQL בשרשור של קלט.**
{{< /solution >}}

### תרגיל 3: commit שנשכח

```text
connection = mysql.connector.connect(...)
cursor = connection.cursor()
cursor.execute("INSERT INTO students (name, age) VALUES (%s, %s)", ("Noa", 22))
connection.close()
```

התוכנית רצה בלי שגיאה, אבל נועה לא נמצאת במסד. למה?

הראו בקוד: התחילו מטבלת `students` ריקה (עם `name` ו-`age`). הכניסו את נועה **בלי** `commit`, סגרו, ובחיבור חדש ספרו כמה שורות יש בטבלה. אחר כך הכניסו אותה שוב **עם** `commit`, וספרו שוב.

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

```text
Without commit: 0 rows
With commit: 1 rows
```

{{< solution >}}
בלי `commit()` השינוי נשאר **זמני**, ו-`close()` מבטל אותו בשקט.

```python
import mysql.connector


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


def count_rows():
    connection = connect()
    try:
        cursor = connection.cursor()
        cursor.execute("SELECT * FROM students")
        return len(cursor.fetchall())
    finally:
        connection.close()


setup = connect()
setup.cursor().execute("CREATE TABLE IF NOT EXISTS students (name VARCHAR(100), age INT)")
setup.close()

connection = connect()
cursor = connection.cursor()
cursor.execute("INSERT INTO students (name, age) VALUES (%s, %s)", ("Noa", 22))
connection.close()
print("Without commit:", count_rows(), "rows")

connection = connect()
cursor = connection.cursor()
cursor.execute("INSERT INTO students (name, age) VALUES (%s, %s)", ("Noa", 22))
connection.commit()
connection.close()
print("With commit:", count_rows(), "rows")
```

```text
Without commit: 0 rows
With commit: 1 rows
```

`CREATE TABLE` נשמר מיד, אבל כל `INSERT`, `UPDATE` ו-`DELETE` מחכה ל-`commit`.
{{< /solution >}}

### תרגיל 4: חיפוש בטוח לפי id

בטבלה יש סטודנטית אחת — דנה, בת 24. צרו אותה כך:

```py
import mysql.connector

connection = mysql.connector.connect(
    host="localhost", user="root", password="kodkod", database="school")
cursor = connection.cursor()
cursor.execute("DROP TABLE IF EXISTS students")   # start clean, so ids start from 1
cursor.execute("CREATE TABLE students (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100), age INT)")
cursor.execute("INSERT INTO students (name, age) VALUES ('Dana', 24)")
connection.commit()
connection.close()
```

כתבו `find_by_id(student_id)` שפותחת חיבור, מחפשת סטודנט עם פרמטר, וסוגרת תמיד. היא מחזירה את השורה או `None`.

```python
print(find_by_id(1))
print(find_by_id(99))
```

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

```text
(1, 'Dana', 24)
None
```

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


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


def find_by_id(student_id):
    connection = get_connection()
    try:
        cursor = connection.cursor()
        cursor.execute("SELECT * FROM students WHERE id = %s", (student_id,))
        return cursor.fetchone()
    finally:
        connection.close()


setup = get_connection()
cursor = setup.cursor()
cursor.execute("CREATE TABLE IF NOT EXISTS students "
               "(id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100), age INT)")
cursor.execute("INSERT INTO students (name, age) VALUES ('Dana', 24)")
setup.commit()
setup.close()

print(find_by_id(1))
print(find_by_id(99))
```

```text
(1, 'Dana', 24)
None
```

שימו לב ל-`(student_id,)` — tuple עם איבר **אחד** צריך פסיק. בלי הפסיק זה סתם סוגריים סביב מספר.
{{< /solution >}}

### תרגיל 5: המסלול של שאילתה

סדרו את השלבים של הרצת שאילתה מתוך Python:

- (א) `connection.commit()` — רק אם שינינו נתונים
- (ב) `mysql.connector.connect(...)` ואז `connection.cursor()`
- (ג) `connection.close()`
- (ד) `cursor.execute(sql, params)`
- (ה) `cursor.fetchone()` / `fetchall()` — רק אם קראנו נתונים

{{< solution >}}
הסדר: **(ב), (ד), אחר כך (ה) או (א), ובסוף (ג)**

1. **connect + cursor** — פותחים חיבור ויוצרים Cursor.
2. **execute** — מריצים את השאילתה עם הפרמטרים.
3. אם זו קריאה (`SELECT`) — **fetch** את התוצאות. אם זה שינוי (`INSERT`, `UPDATE`, `DELETE`) — **commit**.
4. **close** — סוגרים, בתוך `finally`.
{{< /solution >}}

<!-- exercises:end -->

## סיכום

- המסלול: **Python ‏←‏ Connection ‏←‏ Query (עם `%s` ופרמטרים) ‏←‏ Database ‏←‏ Result ‏←‏ Python**.
- `cursor.execute(sql, params)` — ערכים **רק** דרך `%s` ו-tuple. tuple של ערך אחד: `(x,)`.
- **SQL Injection** = קלט שמשנה את השאילתה. **לעולם** לא לשרשר ערכים מבחוץ לתוך SQL.
- שינוי נתונים (INSERT/UPDATE/DELETE) → **`commit()`**, אחרת השינוי נזרק בסגירה.
- `cursor.lastrowid` = ה-id החדש. `fetchone()` = שורה או `None`. `fetchall()` = רשימת tuples.
- פונקציה לכל פעולה, עם חיבור ו-`try/finally` משלה.

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

## דוקומנטציה רשמית

- [MySQLCursor.execute()](https://dev.mysql.com/doc/connector-python/en/connector-python-api-mysqlcursor-execute.html) — הרצת Query עם %s
- [Inserting Data Using Connector/Python](https://dev.mysql.com/doc/connector-python/en/connector-python-example-cursor-transaction.html) — INSERT ו-commit
- [Querying Data Using Connector/Python](https://dev.mysql.com/doc/connector-python/en/connector-python-example-cursor-select.html) — fetch

<!-- docs:end -->
