HandySQL 1.0.0__tar.gz
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.
- handysql-1.0.0/HandySQL/__init__.py +52 -0
- handysql-1.0.0/HandySQL/builder.py +269 -0
- handysql-1.0.0/HandySQL/connection.py +196 -0
- handysql-1.0.0/HandySQL/exceptions.py +40 -0
- handysql-1.0.0/HandySQL/sql_easy.py +1121 -0
- handysql-1.0.0/PKG-INFO +318 -0
- handysql-1.0.0/README.md +309 -0
- handysql-1.0.0/pyproject.toml +16 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HandySQL
|
|
3
|
+
=======
|
|
4
|
+
A simple and powerful SQL library for Python.
|
|
5
|
+
|
|
6
|
+
Supported databases: SQLite · MySQL · PostgreSQL
|
|
7
|
+
|
|
8
|
+
Quick start
|
|
9
|
+
-----------
|
|
10
|
+
>>> from HandySQL import HandySQL
|
|
11
|
+
>>>
|
|
12
|
+
>>> with HandySQL(db_type="sqlite", database="app.db") as db:
|
|
13
|
+
... db.quick_table("users", "name", "email")
|
|
14
|
+
... db.add("users", name="Alice", email="alice@example.com")
|
|
15
|
+
... users = db.get_all("users")
|
|
16
|
+
|
|
17
|
+
Public API
|
|
18
|
+
----------
|
|
19
|
+
Classes:
|
|
20
|
+
HandySQL – main database interface
|
|
21
|
+
SQLBuilder – fluent SQL query builder
|
|
22
|
+
DatabaseError – library exception
|
|
23
|
+
|
|
24
|
+
Functions:
|
|
25
|
+
create_connection(db_type, **kwargs) -> HandySQL
|
|
26
|
+
sql_builder(table) -> SQLBuilder
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from .sql_easy import (
|
|
30
|
+
HandySQL,
|
|
31
|
+
SQLBuilder,
|
|
32
|
+
DatabaseError,
|
|
33
|
+
create_connection,
|
|
34
|
+
sql_builder,
|
|
35
|
+
MYSQL_AVAILABLE,
|
|
36
|
+
POSTGRESQL_AVAILABLE,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"HandySQL",
|
|
41
|
+
"SQLBuilder",
|
|
42
|
+
"DatabaseError",
|
|
43
|
+
"create_connection",
|
|
44
|
+
"sql_builder",
|
|
45
|
+
"MYSQL_AVAILABLE",
|
|
46
|
+
"POSTGRESQL_AVAILABLE",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
__version__ = "1.0.0"
|
|
50
|
+
__author__ = "HandySQL Team"
|
|
51
|
+
__license__ = "MIT"
|
|
52
|
+
__url__ = "https://github.com/HandySQL/HandySQL"
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""
|
|
2
|
+
easysql.builder
|
|
3
|
+
===============
|
|
4
|
+
Fluent SQL query builder.
|
|
5
|
+
|
|
6
|
+
Usage
|
|
7
|
+
-----
|
|
8
|
+
>>> from easysql import sql_builder
|
|
9
|
+
>>>
|
|
10
|
+
>>> q = (sql_builder("users")
|
|
11
|
+
... .select(["name", "email"])
|
|
12
|
+
... .where("age > ?", 18)
|
|
13
|
+
... .order_by("name")
|
|
14
|
+
... .limit(10))
|
|
15
|
+
>>> sql, params = q.build()
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from typing import Any, List, Tuple, Union
|
|
19
|
+
from .exceptions import DatabaseError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SQLBuilder:
|
|
23
|
+
"""
|
|
24
|
+
Fluent builder for SELECT / INSERT / UPDATE / DELETE statements.
|
|
25
|
+
|
|
26
|
+
Every method returns ``self``, so calls can be chained::
|
|
27
|
+
|
|
28
|
+
builder = (SQLBuilder("users")
|
|
29
|
+
.select("*")
|
|
30
|
+
.where("age > ?", 18)
|
|
31
|
+
.order_by("name", "ASC")
|
|
32
|
+
.limit(10))
|
|
33
|
+
sql, params = builder.build()
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, table: str = None):
|
|
37
|
+
self.table = table
|
|
38
|
+
self._select = "*"
|
|
39
|
+
self._where = []
|
|
40
|
+
self._where_params = []
|
|
41
|
+
self._order_by = None
|
|
42
|
+
self._limit = None
|
|
43
|
+
self._offset = None
|
|
44
|
+
self._join = []
|
|
45
|
+
self._set_values = {}
|
|
46
|
+
self._insert_values = []
|
|
47
|
+
self._operation = None
|
|
48
|
+
self._group_by = None
|
|
49
|
+
self._having = None
|
|
50
|
+
self._distinct = False
|
|
51
|
+
|
|
52
|
+
# ------------------------------------------------------------------ #
|
|
53
|
+
# SELECT helpers
|
|
54
|
+
# ------------------------------------------------------------------ #
|
|
55
|
+
|
|
56
|
+
def select(self, columns: Union[str, List[str]] = "*") -> "SQLBuilder":
|
|
57
|
+
"""Set the SELECT column list."""
|
|
58
|
+
self._operation = "SELECT"
|
|
59
|
+
self._select = ", ".join(columns) if isinstance(columns, list) else columns
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
def distinct(self) -> "SQLBuilder":
|
|
63
|
+
"""Add the DISTINCT keyword."""
|
|
64
|
+
self._distinct = True
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
# ------------------------------------------------------------------ #
|
|
68
|
+
# WHERE / AND / OR
|
|
69
|
+
# ------------------------------------------------------------------ #
|
|
70
|
+
|
|
71
|
+
def where(self, condition: str, *params) -> "SQLBuilder":
|
|
72
|
+
"""
|
|
73
|
+
Add a WHERE condition.
|
|
74
|
+
Automatically prepends AND when a condition already exists.
|
|
75
|
+
"""
|
|
76
|
+
if self._where:
|
|
77
|
+
self._where.append(f"AND {condition}")
|
|
78
|
+
else:
|
|
79
|
+
self._where.append(condition)
|
|
80
|
+
self._where_params.extend(params)
|
|
81
|
+
return self
|
|
82
|
+
|
|
83
|
+
def and_where(self, condition: str, *params) -> "SQLBuilder":
|
|
84
|
+
"""Explicitly add an AND condition (alias for where())."""
|
|
85
|
+
return self.where(condition, *params)
|
|
86
|
+
|
|
87
|
+
def or_where(self, condition: str, *params) -> "SQLBuilder":
|
|
88
|
+
"""Add an OR condition."""
|
|
89
|
+
if self._where:
|
|
90
|
+
self._where.append(f"OR {condition}")
|
|
91
|
+
else:
|
|
92
|
+
self._where.append(condition)
|
|
93
|
+
self._where_params.extend(params)
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
def where_in(self, column: str, values: List[Any]) -> "SQLBuilder":
|
|
97
|
+
"""Add a WHERE … IN (…) condition."""
|
|
98
|
+
placeholders = ", ".join("?" for _ in values)
|
|
99
|
+
self._where.append(
|
|
100
|
+
f"AND {column} IN ({placeholders})" if self._where
|
|
101
|
+
else f"{column} IN ({placeholders})"
|
|
102
|
+
)
|
|
103
|
+
self._where_params.extend(values)
|
|
104
|
+
return self
|
|
105
|
+
|
|
106
|
+
def where_between(self, column: str, start: Any, end: Any) -> "SQLBuilder":
|
|
107
|
+
"""Add a WHERE … BETWEEN … AND … condition."""
|
|
108
|
+
prefix = "AND " if self._where else ""
|
|
109
|
+
self._where.append(f"{prefix}{column} BETWEEN ? AND ?")
|
|
110
|
+
self._where_params.extend([start, end])
|
|
111
|
+
return self
|
|
112
|
+
|
|
113
|
+
def where_like(self, column: str, pattern: str) -> "SQLBuilder":
|
|
114
|
+
"""Add a WHERE … LIKE … condition."""
|
|
115
|
+
prefix = "AND " if self._where else ""
|
|
116
|
+
self._where.append(f"{prefix}{column} LIKE ?")
|
|
117
|
+
self._where_params.append(pattern)
|
|
118
|
+
return self
|
|
119
|
+
|
|
120
|
+
def where_null(self, column: str, is_null: bool = True) -> "SQLBuilder":
|
|
121
|
+
"""Add WHERE … IS NULL or IS NOT NULL."""
|
|
122
|
+
prefix = "AND " if self._where else ""
|
|
123
|
+
qualifier = "IS NULL" if is_null else "IS NOT NULL"
|
|
124
|
+
self._where.append(f"{prefix}{column} {qualifier}")
|
|
125
|
+
return self
|
|
126
|
+
|
|
127
|
+
# ------------------------------------------------------------------ #
|
|
128
|
+
# Ordering / grouping / paging
|
|
129
|
+
# ------------------------------------------------------------------ #
|
|
130
|
+
|
|
131
|
+
def order_by(self, column: str, direction: str = "ASC") -> "SQLBuilder":
|
|
132
|
+
"""Set ORDER BY clause."""
|
|
133
|
+
self._order_by = f"{column} {direction}".strip()
|
|
134
|
+
return self
|
|
135
|
+
|
|
136
|
+
def group_by(self, columns: Union[str, List[str]]) -> "SQLBuilder":
|
|
137
|
+
"""Set GROUP BY clause."""
|
|
138
|
+
self._group_by = ", ".join(columns) if isinstance(columns, list) else columns
|
|
139
|
+
return self
|
|
140
|
+
|
|
141
|
+
def having(self, condition: str) -> "SQLBuilder":
|
|
142
|
+
"""Set HAVING clause."""
|
|
143
|
+
self._having = condition
|
|
144
|
+
return self
|
|
145
|
+
|
|
146
|
+
def limit(self, n: int) -> "SQLBuilder":
|
|
147
|
+
"""Set LIMIT."""
|
|
148
|
+
self._limit = n
|
|
149
|
+
return self
|
|
150
|
+
|
|
151
|
+
def offset(self, n: int) -> "SQLBuilder":
|
|
152
|
+
"""Set OFFSET."""
|
|
153
|
+
self._offset = n
|
|
154
|
+
return self
|
|
155
|
+
|
|
156
|
+
# ------------------------------------------------------------------ #
|
|
157
|
+
# JOINs
|
|
158
|
+
# ------------------------------------------------------------------ #
|
|
159
|
+
|
|
160
|
+
def join(self, table: str, on: str, join_type: str = "INNER") -> "SQLBuilder":
|
|
161
|
+
"""Add a JOIN clause."""
|
|
162
|
+
self._join.append(f"{join_type} JOIN {table} ON {on}")
|
|
163
|
+
return self
|
|
164
|
+
|
|
165
|
+
def left_join(self, table: str, on: str) -> "SQLBuilder":
|
|
166
|
+
"""Shorthand for LEFT JOIN."""
|
|
167
|
+
return self.join(table, on, "LEFT")
|
|
168
|
+
|
|
169
|
+
def right_join(self, table: str, on: str) -> "SQLBuilder":
|
|
170
|
+
"""Shorthand for RIGHT JOIN."""
|
|
171
|
+
return self.join(table, on, "RIGHT")
|
|
172
|
+
|
|
173
|
+
# ------------------------------------------------------------------ #
|
|
174
|
+
# DML starters
|
|
175
|
+
# ------------------------------------------------------------------ #
|
|
176
|
+
|
|
177
|
+
def insert(self, table: str) -> "SQLBuilder":
|
|
178
|
+
"""Start an INSERT statement."""
|
|
179
|
+
self._operation = "INSERT"
|
|
180
|
+
self.table = table
|
|
181
|
+
return self
|
|
182
|
+
|
|
183
|
+
def values(self, **kwargs) -> "SQLBuilder":
|
|
184
|
+
"""Provide a row dict for INSERT."""
|
|
185
|
+
self._insert_values.append(kwargs)
|
|
186
|
+
return self
|
|
187
|
+
|
|
188
|
+
def update(self, table: str) -> "SQLBuilder":
|
|
189
|
+
"""Start an UPDATE statement."""
|
|
190
|
+
self._operation = "UPDATE"
|
|
191
|
+
self.table = table
|
|
192
|
+
return self
|
|
193
|
+
|
|
194
|
+
def set(self, **kwargs) -> "SQLBuilder":
|
|
195
|
+
"""Provide SET values for UPDATE."""
|
|
196
|
+
self._set_values.update(kwargs)
|
|
197
|
+
return self
|
|
198
|
+
|
|
199
|
+
def delete(self, table: str) -> "SQLBuilder":
|
|
200
|
+
"""Start a DELETE statement."""
|
|
201
|
+
self._operation = "DELETE"
|
|
202
|
+
self.table = table
|
|
203
|
+
return self
|
|
204
|
+
|
|
205
|
+
# ------------------------------------------------------------------ #
|
|
206
|
+
# Build
|
|
207
|
+
# ------------------------------------------------------------------ #
|
|
208
|
+
|
|
209
|
+
def build(self) -> Tuple[str, List[Any]]:
|
|
210
|
+
"""
|
|
211
|
+
Compile the builder into ``(sql_string, params_list)``.
|
|
212
|
+
|
|
213
|
+
Raises
|
|
214
|
+
------
|
|
215
|
+
DatabaseError
|
|
216
|
+
If no operation has been set, or required data is missing.
|
|
217
|
+
"""
|
|
218
|
+
if not self._operation:
|
|
219
|
+
raise DatabaseError("No SQL operation has been set")
|
|
220
|
+
|
|
221
|
+
sql, params = "", []
|
|
222
|
+
|
|
223
|
+
if self._operation == "SELECT":
|
|
224
|
+
kw = "DISTINCT " if self._distinct else ""
|
|
225
|
+
sql = f"SELECT {kw}{self._select} FROM {self.table}"
|
|
226
|
+
if self._join:
|
|
227
|
+
sql += " " + " ".join(self._join)
|
|
228
|
+
if self._where:
|
|
229
|
+
sql += " WHERE " + " ".join(self._where)
|
|
230
|
+
params.extend(self._where_params)
|
|
231
|
+
if self._group_by:
|
|
232
|
+
sql += f" GROUP BY {self._group_by}"
|
|
233
|
+
if self._having:
|
|
234
|
+
sql += f" HAVING {self._having}"
|
|
235
|
+
if self._order_by:
|
|
236
|
+
sql += f" ORDER BY {self._order_by}"
|
|
237
|
+
if self._limit is not None:
|
|
238
|
+
sql += " LIMIT ?"
|
|
239
|
+
params.append(self._limit)
|
|
240
|
+
if self._offset is not None:
|
|
241
|
+
sql += " OFFSET ?"
|
|
242
|
+
params.append(self._offset)
|
|
243
|
+
|
|
244
|
+
elif self._operation == "UPDATE":
|
|
245
|
+
if not self._set_values:
|
|
246
|
+
raise DatabaseError("No values provided for UPDATE")
|
|
247
|
+
set_clause = ", ".join(f"{k} = ?" for k in self._set_values)
|
|
248
|
+
sql = f"UPDATE {self.table} SET {set_clause}"
|
|
249
|
+
params.extend(self._set_values.values())
|
|
250
|
+
if self._where:
|
|
251
|
+
sql += " WHERE " + " ".join(self._where)
|
|
252
|
+
params.extend(self._where_params)
|
|
253
|
+
|
|
254
|
+
elif self._operation == "INSERT":
|
|
255
|
+
if not self._insert_values:
|
|
256
|
+
raise DatabaseError("No values provided for INSERT")
|
|
257
|
+
cols = list(self._insert_values[0].keys())
|
|
258
|
+
ph = ", ".join("?" for _ in cols)
|
|
259
|
+
rows_ph = ", ".join(f"({ph})" for _ in self._insert_values)
|
|
260
|
+
sql = f"INSERT INTO {self.table} ({', '.join(cols)}) VALUES {rows_ph}"
|
|
261
|
+
params = [v for row in self._insert_values for v in (row[c] for c in cols)]
|
|
262
|
+
|
|
263
|
+
elif self._operation == "DELETE":
|
|
264
|
+
sql = f"DELETE FROM {self.table}"
|
|
265
|
+
if self._where:
|
|
266
|
+
sql += " WHERE " + " ".join(self._where)
|
|
267
|
+
params.extend(self._where_params)
|
|
268
|
+
|
|
269
|
+
return sql, params
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
easysql.connection
|
|
3
|
+
==================
|
|
4
|
+
Low-level connection management for SQLite, MySQL, and PostgreSQL.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sqlite3
|
|
8
|
+
import logging
|
|
9
|
+
from contextlib import contextmanager
|
|
10
|
+
from typing import Any, List, Tuple, Union
|
|
11
|
+
|
|
12
|
+
from .exceptions import DatabaseError, ConnectionError
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
import mysql.connector
|
|
16
|
+
MYSQL_AVAILABLE = True
|
|
17
|
+
except ImportError:
|
|
18
|
+
MYSQL_AVAILABLE = False
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
import psycopg2
|
|
22
|
+
POSTGRESQL_AVAILABLE = True
|
|
23
|
+
except ImportError:
|
|
24
|
+
POSTGRESQL_AVAILABLE = False
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Connection:
|
|
30
|
+
"""
|
|
31
|
+
Manages a single database connection.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
db_type : str
|
|
36
|
+
``"sqlite"``, ``"mysql"``, or ``"postgresql"``
|
|
37
|
+
**kwargs
|
|
38
|
+
Connection parameters (host, user, password, database, port …)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
_ID_TYPES = {
|
|
42
|
+
"sqlite": "INTEGER PRIMARY KEY AUTOINCREMENT",
|
|
43
|
+
"mysql": "INT AUTO_INCREMENT PRIMARY KEY",
|
|
44
|
+
"postgresql": "SERIAL PRIMARY KEY",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
def __init__(self, db_type: str = "sqlite", **kwargs):
|
|
48
|
+
self.db_type = db_type.lower()
|
|
49
|
+
self._conn = None
|
|
50
|
+
self._auto_commit = kwargs.pop("auto_commit", True)
|
|
51
|
+
self._params = kwargs
|
|
52
|
+
|
|
53
|
+
if self.db_type == "sqlite":
|
|
54
|
+
self._db_path = kwargs.get("database", ":memory:")
|
|
55
|
+
elif self.db_type == "mysql":
|
|
56
|
+
if not MYSQL_AVAILABLE:
|
|
57
|
+
raise ConnectionError(
|
|
58
|
+
"mysql-connector-python is not installed.\n"
|
|
59
|
+
"Run: pip install mysql-connector-python"
|
|
60
|
+
)
|
|
61
|
+
elif self.db_type == "postgresql":
|
|
62
|
+
if not POSTGRESQL_AVAILABLE:
|
|
63
|
+
raise ConnectionError(
|
|
64
|
+
"psycopg2 is not installed.\n"
|
|
65
|
+
"Run: pip install psycopg2-binary"
|
|
66
|
+
)
|
|
67
|
+
else:
|
|
68
|
+
raise DatabaseError(
|
|
69
|
+
f"Unsupported database type: '{db_type}'. "
|
|
70
|
+
"Choose 'sqlite', 'mysql', or 'postgresql'."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# ------------------------------------------------------------------ #
|
|
74
|
+
|
|
75
|
+
def open(self):
|
|
76
|
+
"""Open (or return existing) database connection."""
|
|
77
|
+
if self._conn is not None:
|
|
78
|
+
return self._conn
|
|
79
|
+
try:
|
|
80
|
+
p = self._params
|
|
81
|
+
if self.db_type == "sqlite":
|
|
82
|
+
self._conn = sqlite3.connect(self._db_path)
|
|
83
|
+
self._conn.row_factory = sqlite3.Row
|
|
84
|
+
logger.info("SQLite connected: %s", self._db_path)
|
|
85
|
+
elif self.db_type == "mysql":
|
|
86
|
+
self._conn = mysql.connector.connect(
|
|
87
|
+
host=p.get("host", "localhost"),
|
|
88
|
+
user=p.get("user", "root"),
|
|
89
|
+
password=p.get("password", ""),
|
|
90
|
+
database=p.get("database", ""),
|
|
91
|
+
port=p.get("port", 3306),
|
|
92
|
+
)
|
|
93
|
+
logger.info("MySQL connected: %s", p.get("database"))
|
|
94
|
+
elif self.db_type == "postgresql":
|
|
95
|
+
self._conn = psycopg2.connect(
|
|
96
|
+
host=p.get("host", "localhost"),
|
|
97
|
+
user=p.get("user", "postgres"),
|
|
98
|
+
password=p.get("password", ""),
|
|
99
|
+
database=p.get("database", ""),
|
|
100
|
+
port=p.get("port", 5432),
|
|
101
|
+
)
|
|
102
|
+
logger.info("PostgreSQL connected: %s", p.get("database"))
|
|
103
|
+
return self._conn
|
|
104
|
+
except Exception as exc:
|
|
105
|
+
raise ConnectionError(f"Connection failed: {exc}") from exc
|
|
106
|
+
|
|
107
|
+
def close(self):
|
|
108
|
+
"""Close the connection if open."""
|
|
109
|
+
if self._conn:
|
|
110
|
+
self._conn.close()
|
|
111
|
+
self._conn = None
|
|
112
|
+
logger.info("Connection closed")
|
|
113
|
+
|
|
114
|
+
def __enter__(self):
|
|
115
|
+
self.open()
|
|
116
|
+
return self
|
|
117
|
+
|
|
118
|
+
def __exit__(self, *_):
|
|
119
|
+
self.close()
|
|
120
|
+
|
|
121
|
+
# ------------------------------------------------------------------ #
|
|
122
|
+
|
|
123
|
+
@contextmanager
|
|
124
|
+
def cursor(self):
|
|
125
|
+
"""Context manager that yields a cursor and handles commit/rollback."""
|
|
126
|
+
if self._conn is None:
|
|
127
|
+
self.open()
|
|
128
|
+
cur = self._conn.cursor()
|
|
129
|
+
try:
|
|
130
|
+
yield cur
|
|
131
|
+
if self._auto_commit:
|
|
132
|
+
self._conn.commit()
|
|
133
|
+
except Exception as exc:
|
|
134
|
+
self._conn.rollback()
|
|
135
|
+
logger.error("Rolled back: %s", exc)
|
|
136
|
+
raise DatabaseError(f"Database operation error: {exc}") from exc
|
|
137
|
+
finally:
|
|
138
|
+
cur.close()
|
|
139
|
+
|
|
140
|
+
# ------------------------------------------------------------------ #
|
|
141
|
+
|
|
142
|
+
def execute(self, sql: str, params: Union[Tuple, List] = None) -> Any:
|
|
143
|
+
"""
|
|
144
|
+
Run a SQL statement.
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
list[dict] for SELECT
|
|
149
|
+
int (rowcount) for INSERT / UPDATE / DELETE
|
|
150
|
+
"""
|
|
151
|
+
with self.cursor() as cur:
|
|
152
|
+
logger.debug("SQL: %s | params: %s", sql, params)
|
|
153
|
+
cur.execute(sql, params or ())
|
|
154
|
+
if sql.strip().upper().startswith("SELECT"):
|
|
155
|
+
rows = cur.fetchall()
|
|
156
|
+
if self.db_type == "sqlite":
|
|
157
|
+
return [dict(r) for r in rows]
|
|
158
|
+
cols = [c[0] for c in cur.description]
|
|
159
|
+
return [dict(zip(cols, r)) for r in rows]
|
|
160
|
+
return cur.rowcount
|
|
161
|
+
|
|
162
|
+
def execute_many(self, sql: str, params_list: List[Tuple]) -> int:
|
|
163
|
+
"""Run a parameterised statement for each item in params_list."""
|
|
164
|
+
with self.cursor() as cur:
|
|
165
|
+
cur.executemany(sql, params_list)
|
|
166
|
+
return cur.rowcount
|
|
167
|
+
|
|
168
|
+
# ------------------------------------------------------------------ #
|
|
169
|
+
# Transaction helpers
|
|
170
|
+
# ------------------------------------------------------------------ #
|
|
171
|
+
|
|
172
|
+
def begin(self):
|
|
173
|
+
"""Disable auto-commit to start a manual transaction."""
|
|
174
|
+
if self._conn is None:
|
|
175
|
+
self.open()
|
|
176
|
+
self._auto_commit = False
|
|
177
|
+
logger.info("Transaction started")
|
|
178
|
+
|
|
179
|
+
def commit(self):
|
|
180
|
+
"""Commit the current transaction."""
|
|
181
|
+
if self._conn:
|
|
182
|
+
self._conn.commit()
|
|
183
|
+
logger.info("Transaction committed")
|
|
184
|
+
|
|
185
|
+
def rollback(self):
|
|
186
|
+
"""Roll back the current transaction."""
|
|
187
|
+
if self._conn:
|
|
188
|
+
self._conn.rollback()
|
|
189
|
+
logger.info("Transaction rolled back")
|
|
190
|
+
|
|
191
|
+
# ------------------------------------------------------------------ #
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def auto_id_sql(self) -> str:
|
|
195
|
+
"""Return the correct auto-increment primary key definition."""
|
|
196
|
+
return self._ID_TYPES[self.db_type]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
easysql.exceptions
|
|
3
|
+
==================
|
|
4
|
+
All exceptions raised by the EasySQL library.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DatabaseError(Exception):
|
|
9
|
+
"""
|
|
10
|
+
Raised for any EasySQL / database-level error.
|
|
11
|
+
|
|
12
|
+
Examples
|
|
13
|
+
--------
|
|
14
|
+
>>> from easysql import DatabaseError
|
|
15
|
+
>>> try:
|
|
16
|
+
... db.remove("users") # missing filter
|
|
17
|
+
... except DatabaseError as e:
|
|
18
|
+
... print(f"Caught: {e}")
|
|
19
|
+
"""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ConnectionError(DatabaseError):
|
|
24
|
+
"""Raised when a connection to the database cannot be established."""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TableNotFoundError(DatabaseError):
|
|
29
|
+
"""Raised when an operation targets a table that does not exist."""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RecordNotFoundError(DatabaseError):
|
|
34
|
+
"""Raised when a required row is not found (e.g. clone_row with bad ID)."""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ValidationError(DatabaseError):
|
|
39
|
+
"""Raised when input data fails a safety check (e.g. empty filter on delete)."""
|
|
40
|
+
pass
|