pyflashdb 1.0.1__py3-none-any.whl
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.
- flash/__init__.py +10 -0
- flash/adapters/__init__.py +0 -0
- flash/adapters/mongo_adapter.py +247 -0
- flash/adapters/mysql_adapter.py +217 -0
- flash/adapters/postgres_adapter.py +226 -0
- flash/core.py +306 -0
- flash/exceptions.py +33 -0
- flash/filters.py +140 -0
- flash/triggers.py +148 -0
- pyflashdb-1.0.1.dist-info/METADATA +34 -0
- pyflashdb-1.0.1.dist-info/RECORD +13 -0
- pyflashdb-1.0.1.dist-info/WHEEL +5 -0
- pyflashdb-1.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PostgreSQL adapter for Flash.
|
|
3
|
+
Requires: pip install psycopg2-binary
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
from ..filters import filters_to_sql, build_update_sql, python_type_to_sql
|
|
8
|
+
from ..exceptions import ConnectionError, QueryError, TransactionError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PostgreSQLAdapter:
|
|
12
|
+
"""
|
|
13
|
+
PostgreSQL backend adapter for FlashDB.
|
|
14
|
+
Uses psycopg2 under the hood.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, config: dict):
|
|
18
|
+
self.config = config
|
|
19
|
+
self.conn = None
|
|
20
|
+
self.cursor = None
|
|
21
|
+
self._in_transaction = False
|
|
22
|
+
self._connect()
|
|
23
|
+
|
|
24
|
+
def _connect(self):
|
|
25
|
+
try:
|
|
26
|
+
import psycopg2
|
|
27
|
+
import psycopg2.extras
|
|
28
|
+
self.conn = psycopg2.connect(
|
|
29
|
+
host=self.config.get("host", "localhost"),
|
|
30
|
+
port=self.config.get("port", 5432),
|
|
31
|
+
user=self.config.get("user", "postgres"),
|
|
32
|
+
password=self.config.get("password", ""),
|
|
33
|
+
dbname=self.config.get("database"),
|
|
34
|
+
)
|
|
35
|
+
self.conn.autocommit = True
|
|
36
|
+
self.cursor = self.conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
|
37
|
+
except ImportError:
|
|
38
|
+
raise ConnectionError(
|
|
39
|
+
"psycopg2 is not installed. Run: pip install psycopg2-binary"
|
|
40
|
+
)
|
|
41
|
+
except Exception as e:
|
|
42
|
+
raise ConnectionError(f"PostgreSQL connection failed: {e}")
|
|
43
|
+
|
|
44
|
+
def _execute(self, sql: str, params: list = None) -> Any:
|
|
45
|
+
# psycopg2 uses %s but doesn't need None as []
|
|
46
|
+
try:
|
|
47
|
+
self.cursor.execute(sql, params if params else None)
|
|
48
|
+
return self.cursor
|
|
49
|
+
except Exception as e:
|
|
50
|
+
if not self._in_transaction:
|
|
51
|
+
self.conn.rollback()
|
|
52
|
+
raise QueryError(f"PostgreSQL query failed: {e}\nSQL: {sql}\nParams: {params}")
|
|
53
|
+
|
|
54
|
+
def _quote(self, name: str) -> str:
|
|
55
|
+
return f'"{name}"'
|
|
56
|
+
|
|
57
|
+
# ── Core CRUD ──────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
def all(self, table: str) -> List[dict]:
|
|
60
|
+
self._execute(f"SELECT * FROM {self._quote(table)}")
|
|
61
|
+
return [dict(row) for row in self.cursor.fetchall()]
|
|
62
|
+
|
|
63
|
+
def select(self, table: str, fields: List[str] = None, filters: dict = None,
|
|
64
|
+
limit: int = None, offset: int = None, order_by: str = None) -> List[dict]:
|
|
65
|
+
cols = ", ".join(self._quote(f) for f in fields) if fields else "*"
|
|
66
|
+
sql = f"SELECT {cols} FROM {self._quote(table)}"
|
|
67
|
+
params = []
|
|
68
|
+
|
|
69
|
+
if filters:
|
|
70
|
+
clause, params = filters_to_sql(filters)
|
|
71
|
+
sql += f" WHERE {clause}"
|
|
72
|
+
|
|
73
|
+
if order_by:
|
|
74
|
+
sql += f" ORDER BY {order_by}"
|
|
75
|
+
|
|
76
|
+
if limit is not None:
|
|
77
|
+
sql += f" LIMIT {int(limit)}"
|
|
78
|
+
if offset is not None:
|
|
79
|
+
sql += f" OFFSET {int(offset)}"
|
|
80
|
+
|
|
81
|
+
self._execute(sql, params)
|
|
82
|
+
return [dict(row) for row in self.cursor.fetchall()]
|
|
83
|
+
|
|
84
|
+
def add(self, table: str, data: dict) -> Any:
|
|
85
|
+
fields = ", ".join(self._quote(k) for k in data.keys())
|
|
86
|
+
placeholders = ", ".join(["%s"] * len(data))
|
|
87
|
+
sql = f"INSERT INTO {self._quote(table)} ({fields}) VALUES ({placeholders}) RETURNING *"
|
|
88
|
+
self._execute(sql, list(data.values()))
|
|
89
|
+
row = self.cursor.fetchone()
|
|
90
|
+
return dict(row) if row else None
|
|
91
|
+
|
|
92
|
+
def bulk_insert(self, table: str, records: List[dict]) -> int:
|
|
93
|
+
if not records:
|
|
94
|
+
return 0
|
|
95
|
+
fields = ", ".join(self._quote(k) for k in records[0].keys())
|
|
96
|
+
placeholders = ", ".join(["%s"] * len(records[0]))
|
|
97
|
+
sql = f"INSERT INTO {self._quote(table)} ({fields}) VALUES ({placeholders})"
|
|
98
|
+
rows = [list(r.values()) for r in records]
|
|
99
|
+
try:
|
|
100
|
+
self.cursor.executemany(sql, rows)
|
|
101
|
+
return self.cursor.rowcount
|
|
102
|
+
except Exception as e:
|
|
103
|
+
raise QueryError(f"PostgreSQL bulk insert failed: {e}")
|
|
104
|
+
|
|
105
|
+
def update(self, table: str, filters: dict, data: dict) -> int:
|
|
106
|
+
set_clause, set_params = build_update_sql(data)
|
|
107
|
+
where_clause, where_params = filters_to_sql(filters)
|
|
108
|
+
sql = f"UPDATE {self._quote(table)} SET {set_clause}"
|
|
109
|
+
if where_clause:
|
|
110
|
+
sql += f" WHERE {where_clause}"
|
|
111
|
+
self._execute(sql, set_params + where_params)
|
|
112
|
+
return self.cursor.rowcount
|
|
113
|
+
|
|
114
|
+
def delete(self, table: str, filters: dict = None) -> int:
|
|
115
|
+
sql = f"DELETE FROM {self._quote(table)}"
|
|
116
|
+
params = []
|
|
117
|
+
if filters:
|
|
118
|
+
clause, params = filters_to_sql(filters)
|
|
119
|
+
sql += f" WHERE {clause}"
|
|
120
|
+
self._execute(sql, params)
|
|
121
|
+
return self.cursor.rowcount
|
|
122
|
+
|
|
123
|
+
# ── Schema Operations ──────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
def create_table(self, table: str, schema: dict, primary_key: str = "id") -> bool:
|
|
126
|
+
col_defs = []
|
|
127
|
+
has_pk = False
|
|
128
|
+
for col, typ in schema.items():
|
|
129
|
+
sql_type = python_type_to_sql(typ, "postgres")
|
|
130
|
+
if col == primary_key:
|
|
131
|
+
col_defs.append(f"{self._quote(col)} SERIAL PRIMARY KEY")
|
|
132
|
+
has_pk = True
|
|
133
|
+
else:
|
|
134
|
+
col_defs.append(f"{self._quote(col)} {sql_type}")
|
|
135
|
+
if not has_pk:
|
|
136
|
+
col_defs.insert(0, f"{self._quote(primary_key)} SERIAL PRIMARY KEY")
|
|
137
|
+
|
|
138
|
+
sql = f"CREATE TABLE IF NOT EXISTS {self._quote(table)} ({', '.join(col_defs)})"
|
|
139
|
+
self._execute(sql)
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
def drop_table(self, table: str) -> bool:
|
|
143
|
+
self._execute(f"DROP TABLE IF EXISTS {self._quote(table)}")
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
def truncate(self, table: str) -> bool:
|
|
147
|
+
self._execute(f"TRUNCATE TABLE {self._quote(table)} RESTART IDENTITY CASCADE")
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
def show_tables(self) -> List[str]:
|
|
151
|
+
sql = """
|
|
152
|
+
SELECT table_name FROM information_schema.tables
|
|
153
|
+
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
|
|
154
|
+
"""
|
|
155
|
+
self._execute(sql)
|
|
156
|
+
return [row["table_name"] for row in self.cursor.fetchall()]
|
|
157
|
+
|
|
158
|
+
def describe(self, table: str) -> List[dict]:
|
|
159
|
+
sql = """
|
|
160
|
+
SELECT column_name, data_type, is_nullable, column_default
|
|
161
|
+
FROM information_schema.columns
|
|
162
|
+
WHERE table_name = %s
|
|
163
|
+
ORDER BY ordinal_position
|
|
164
|
+
"""
|
|
165
|
+
self._execute(sql, [table])
|
|
166
|
+
return [dict(row) for row in self.cursor.fetchall()]
|
|
167
|
+
|
|
168
|
+
# ── Transactions ───────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
def begin(self):
|
|
171
|
+
self.conn.autocommit = False
|
|
172
|
+
self._in_transaction = True
|
|
173
|
+
|
|
174
|
+
def commit(self):
|
|
175
|
+
try:
|
|
176
|
+
self.conn.commit()
|
|
177
|
+
except Exception as e:
|
|
178
|
+
raise TransactionError(f"Commit failed: {e}")
|
|
179
|
+
finally:
|
|
180
|
+
self.conn.autocommit = True
|
|
181
|
+
self._in_transaction = False
|
|
182
|
+
|
|
183
|
+
def rollback(self):
|
|
184
|
+
try:
|
|
185
|
+
self.conn.rollback()
|
|
186
|
+
except Exception as e:
|
|
187
|
+
raise TransactionError(f"Rollback failed: {e}")
|
|
188
|
+
finally:
|
|
189
|
+
self.conn.autocommit = True
|
|
190
|
+
self._in_transaction = False
|
|
191
|
+
|
|
192
|
+
# ── Raw Query ──────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
def raw(self, sql: str, params: list = None) -> Any:
|
|
195
|
+
self._execute(sql, params)
|
|
196
|
+
try:
|
|
197
|
+
return [dict(row) for row in self.cursor.fetchall()]
|
|
198
|
+
except Exception:
|
|
199
|
+
return self.cursor.rowcount
|
|
200
|
+
|
|
201
|
+
# ── Count ──────────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
def count(self, table: str, filters: dict = None) -> int:
|
|
204
|
+
sql = f"SELECT COUNT(*) as cnt FROM {self._quote(table)}"
|
|
205
|
+
params = []
|
|
206
|
+
if filters:
|
|
207
|
+
clause, params = filters_to_sql(filters)
|
|
208
|
+
sql += f" WHERE {clause}"
|
|
209
|
+
self._execute(sql, params)
|
|
210
|
+
row = self.cursor.fetchone()
|
|
211
|
+
return row["cnt"] if row else 0
|
|
212
|
+
|
|
213
|
+
# ── Lifecycle ──────────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
def close(self):
|
|
216
|
+
if self.cursor:
|
|
217
|
+
self.cursor.close()
|
|
218
|
+
if self.conn:
|
|
219
|
+
self.conn.close()
|
|
220
|
+
|
|
221
|
+
def ping(self) -> bool:
|
|
222
|
+
try:
|
|
223
|
+
self._execute("SELECT 1")
|
|
224
|
+
return True
|
|
225
|
+
except Exception:
|
|
226
|
+
return False
|
flash/core.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Flash Core — FlashDB unified database interface.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
from flash import FlashDB
|
|
6
|
+
|
|
7
|
+
flash = FlashDB("mysql", config)
|
|
8
|
+
flash.add("users", {"name": "John", "age": 25})
|
|
9
|
+
flash.all("users")
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Any, Dict, List, Optional, Callable
|
|
13
|
+
from .exceptions import UnsupportedDatabaseError, FlashError
|
|
14
|
+
from .triggers import make_trigger_mixin
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FlashDB(make_trigger_mixin()):
|
|
18
|
+
"""
|
|
19
|
+
FlashDB — a unified, trigger-aware database interface.
|
|
20
|
+
Supports MySQL, PostgreSQL, and MongoDB with a single API.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
db_type (str): One of 'mysql', 'postgres', 'mongodb'
|
|
24
|
+
config (dict): Connection configuration dict.
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
flash = FlashDB("mysql", {
|
|
28
|
+
"host": "localhost",
|
|
29
|
+
"user": "root",
|
|
30
|
+
"password": "1234",
|
|
31
|
+
"database": "mydb"
|
|
32
|
+
})
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
SUPPORTED = ("mysql", "postgres", "postgresql", "mongodb", "mongo")
|
|
36
|
+
|
|
37
|
+
def __init__(self, db_type: str, config: dict):
|
|
38
|
+
self.db_type = db_type.lower().strip()
|
|
39
|
+
self.config = config
|
|
40
|
+
self._init_triggers()
|
|
41
|
+
self._adapter = self._load_adapter()
|
|
42
|
+
|
|
43
|
+
def _load_adapter(self):
|
|
44
|
+
if self.db_type == "mysql":
|
|
45
|
+
from .adapters.mysql_adapter import MySQLAdapter
|
|
46
|
+
return MySQLAdapter(self.config)
|
|
47
|
+
|
|
48
|
+
elif self.db_type in ("postgres", "postgresql"):
|
|
49
|
+
from .adapters.postgres_adapter import PostgreSQLAdapter
|
|
50
|
+
return PostgreSQLAdapter(self.config)
|
|
51
|
+
|
|
52
|
+
elif self.db_type in ("mongodb", "mongo"):
|
|
53
|
+
from .adapters.mongo_adapter import MongoAdapter
|
|
54
|
+
return MongoAdapter(self.config)
|
|
55
|
+
|
|
56
|
+
else:
|
|
57
|
+
raise UnsupportedDatabaseError(
|
|
58
|
+
f"'{self.db_type}' is not supported. Choose from: mysql, postgres, mongodb"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# ── Read Operations ────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
def all(self, table: str) -> List[dict]:
|
|
64
|
+
"""Fetch all records from a table/collection."""
|
|
65
|
+
self._triggers.fire("before", "select", table, {})
|
|
66
|
+
result = self._adapter.all(table)
|
|
67
|
+
self._triggers.fire("after", "select", table, {}, result)
|
|
68
|
+
return result
|
|
69
|
+
|
|
70
|
+
def select(
|
|
71
|
+
self,
|
|
72
|
+
table: str,
|
|
73
|
+
fields: List[str] = None,
|
|
74
|
+
filters: dict = None,
|
|
75
|
+
limit: int = None,
|
|
76
|
+
offset: int = None,
|
|
77
|
+
order_by: str = None,
|
|
78
|
+
) -> List[dict]:
|
|
79
|
+
"""
|
|
80
|
+
Select records with optional field projection, filters, sorting, and pagination.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
table: Table/collection name.
|
|
84
|
+
fields: List of field names to return.
|
|
85
|
+
filters: Flash filter dict (e.g. {"age": {">": 18}}).
|
|
86
|
+
limit: Maximum number of records.
|
|
87
|
+
offset: Number of records to skip.
|
|
88
|
+
order_by: Field name to sort by (prefix '-' for descending in MongoDB).
|
|
89
|
+
|
|
90
|
+
Example:
|
|
91
|
+
flash.select("users", fields=["name", "age"], filters={"age": {">": 18}}, limit=10)
|
|
92
|
+
"""
|
|
93
|
+
payload = {"fields": fields, "filters": filters}
|
|
94
|
+
self._triggers.fire("before", "select", table, payload)
|
|
95
|
+
result = self._adapter.select(
|
|
96
|
+
table, fields=fields, filters=filters,
|
|
97
|
+
limit=limit, offset=offset, order_by=order_by
|
|
98
|
+
)
|
|
99
|
+
self._triggers.fire("after", "select", table, payload, result)
|
|
100
|
+
return result
|
|
101
|
+
|
|
102
|
+
def where(self, table: str, filters: dict, fields: List[str] = None) -> List[dict]:
|
|
103
|
+
"""
|
|
104
|
+
Shorthand for filtered select.
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
flash.where("users", {"age": {">": 18}})
|
|
108
|
+
flash.where("users", {"name": "John"})
|
|
109
|
+
"""
|
|
110
|
+
return self.select(table, fields=fields, filters=filters)
|
|
111
|
+
|
|
112
|
+
def find_one(self, table: str, filters: dict) -> Optional[dict]:
|
|
113
|
+
"""Return the first matching record, or None."""
|
|
114
|
+
results = self.select(table, filters=filters, limit=1)
|
|
115
|
+
return results[0] if results else None
|
|
116
|
+
|
|
117
|
+
def count(self, table: str, filters: dict = None) -> int:
|
|
118
|
+
"""Count records in a table/collection, optionally filtered."""
|
|
119
|
+
return self._adapter.count(table, filters)
|
|
120
|
+
|
|
121
|
+
def limit(self, table: str, n: int) -> List[dict]:
|
|
122
|
+
"""Fetch the first N records."""
|
|
123
|
+
return self.select(table, limit=n)
|
|
124
|
+
|
|
125
|
+
def paginate(self, table: str, page: int = 1, size: int = 10) -> dict:
|
|
126
|
+
"""
|
|
127
|
+
Paginate records.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
{
|
|
131
|
+
"data": [...],
|
|
132
|
+
"page": 1,
|
|
133
|
+
"size": 10,
|
|
134
|
+
"total": 42
|
|
135
|
+
}
|
|
136
|
+
"""
|
|
137
|
+
offset = (page - 1) * size
|
|
138
|
+
data = self.select(table, limit=size, offset=offset)
|
|
139
|
+
total = self.count(table)
|
|
140
|
+
return {"data": data, "page": page, "size": size, "total": total}
|
|
141
|
+
|
|
142
|
+
# ── Write Operations ───────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
def add(self, table: str, data: dict) -> Any:
|
|
145
|
+
"""
|
|
146
|
+
Insert a single record.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Inserted ID (int for SQL, str for MongoDB).
|
|
150
|
+
|
|
151
|
+
Example:
|
|
152
|
+
flash.add("users", {"name": "John", "age": 25})
|
|
153
|
+
"""
|
|
154
|
+
self._triggers.fire("before", "insert", table, data)
|
|
155
|
+
result = self._adapter.add(table, data)
|
|
156
|
+
self._triggers.fire("after", "insert", table, data, result)
|
|
157
|
+
return result
|
|
158
|
+
|
|
159
|
+
def bulk_insert(self, table: str, records: List[dict]) -> int:
|
|
160
|
+
"""
|
|
161
|
+
Insert multiple records at once.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Number of inserted records.
|
|
165
|
+
|
|
166
|
+
Example:
|
|
167
|
+
flash.bulk_insert("users", [{"name": "A"}, {"name": "B"}])
|
|
168
|
+
"""
|
|
169
|
+
self._triggers.fire("before", "insert", table, records)
|
|
170
|
+
result = self._adapter.bulk_insert(table, records)
|
|
171
|
+
self._triggers.fire("after", "insert", table, records, result)
|
|
172
|
+
return result
|
|
173
|
+
|
|
174
|
+
def update(self, table: str, filters: dict, data: dict) -> int:
|
|
175
|
+
"""
|
|
176
|
+
Update records matching filters with new data.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Number of updated records.
|
|
180
|
+
|
|
181
|
+
Example:
|
|
182
|
+
flash.update("users", {"name": "John"}, {"age": 26})
|
|
183
|
+
"""
|
|
184
|
+
payload = {"filters": filters, "data": data}
|
|
185
|
+
self._triggers.fire("before", "update", table, payload)
|
|
186
|
+
result = self._adapter.update(table, filters, data)
|
|
187
|
+
self._triggers.fire("after", "update", table, payload, result)
|
|
188
|
+
return result
|
|
189
|
+
|
|
190
|
+
def delete(self, table: str, filters: dict = None) -> int:
|
|
191
|
+
"""
|
|
192
|
+
Delete records matching filters. If no filters given, deletes ALL records.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Number of deleted records.
|
|
196
|
+
|
|
197
|
+
Example:
|
|
198
|
+
flash.delete("users", {"name": "John"})
|
|
199
|
+
"""
|
|
200
|
+
self._triggers.fire("before", "delete", table, filters or {})
|
|
201
|
+
result = self._adapter.delete(table, filters)
|
|
202
|
+
self._triggers.fire("after", "delete", table, filters or {}, result)
|
|
203
|
+
return result
|
|
204
|
+
|
|
205
|
+
# ── Schema Operations ──────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
def create_table(self, table: str, schema: dict, primary_key: str = "id") -> bool:
|
|
208
|
+
"""
|
|
209
|
+
Create a table/collection with the given schema.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
table: Table name.
|
|
213
|
+
schema: Dict of {column_name: type_string}.
|
|
214
|
+
primary_key: Name of the primary key column (SQL only).
|
|
215
|
+
|
|
216
|
+
Example:
|
|
217
|
+
flash.create_table("users", {
|
|
218
|
+
"id": "int",
|
|
219
|
+
"name": "str",
|
|
220
|
+
"email": "str",
|
|
221
|
+
"age": "int"
|
|
222
|
+
})
|
|
223
|
+
"""
|
|
224
|
+
return self._adapter.create_table(table, schema, primary_key)
|
|
225
|
+
|
|
226
|
+
def drop_table(self, table: str) -> bool:
|
|
227
|
+
"""Drop a table/collection entirely."""
|
|
228
|
+
return self._adapter.drop_table(table)
|
|
229
|
+
|
|
230
|
+
def truncate(self, table: str) -> bool:
|
|
231
|
+
"""Remove all records from a table/collection without dropping it."""
|
|
232
|
+
return self._adapter.truncate(table)
|
|
233
|
+
|
|
234
|
+
def show_tables(self) -> List[str]:
|
|
235
|
+
"""List all tables/collections in the database."""
|
|
236
|
+
return self._adapter.show_tables()
|
|
237
|
+
|
|
238
|
+
def describe(self, table: str):
|
|
239
|
+
"""Return schema/column information for a table/collection."""
|
|
240
|
+
return self._adapter.describe(table)
|
|
241
|
+
|
|
242
|
+
# ── Transactions ───────────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
def begin(self):
|
|
245
|
+
"""Begin a transaction (SQL) or session (MongoDB 4+ replica set)."""
|
|
246
|
+
self._adapter.begin()
|
|
247
|
+
|
|
248
|
+
def commit(self):
|
|
249
|
+
"""Commit the current transaction."""
|
|
250
|
+
self._adapter.commit()
|
|
251
|
+
|
|
252
|
+
def rollback(self):
|
|
253
|
+
"""Roll back the current transaction."""
|
|
254
|
+
self._adapter.rollback()
|
|
255
|
+
|
|
256
|
+
# ── Raw Query Escape Hatch ─────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
def raw(self, query, params: list = None, table: str = None) -> Any:
|
|
259
|
+
"""
|
|
260
|
+
Execute a raw query/command.
|
|
261
|
+
|
|
262
|
+
SQL: flash.raw("SELECT * FROM users WHERE age > %s", [18])
|
|
263
|
+
Mongo: flash.raw({"name": "John"}, table="users")
|
|
264
|
+
"""
|
|
265
|
+
return self._adapter.raw(query, params) if params is not None else self._adapter.raw(query)
|
|
266
|
+
|
|
267
|
+
# ── MongoDB-Specific ───────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
def aggregate(self, table: str, pipeline: list) -> List[dict]:
|
|
270
|
+
"""
|
|
271
|
+
Run a MongoDB aggregation pipeline (MongoDB only).
|
|
272
|
+
|
|
273
|
+
Example:
|
|
274
|
+
flash.aggregate("orders", [
|
|
275
|
+
{"$group": {"_id": "$user_id", "total": {"$sum": "$amount"}}}
|
|
276
|
+
])
|
|
277
|
+
"""
|
|
278
|
+
if not hasattr(self._adapter, "aggregate"):
|
|
279
|
+
raise FlashError("aggregate() is only available for MongoDB.")
|
|
280
|
+
return self._adapter.aggregate(table, pipeline)
|
|
281
|
+
|
|
282
|
+
def create_index(self, table: str, field: str, unique: bool = False) -> str:
|
|
283
|
+
"""Create an index on a MongoDB collection field."""
|
|
284
|
+
if not hasattr(self._adapter, "create_index"):
|
|
285
|
+
raise FlashError("create_index() is only available for MongoDB.")
|
|
286
|
+
return self._adapter.create_index(table, field, unique)
|
|
287
|
+
|
|
288
|
+
# ── Utility ────────────────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
def ping(self) -> bool:
|
|
291
|
+
"""Test if the database connection is alive."""
|
|
292
|
+
return self._adapter.ping()
|
|
293
|
+
|
|
294
|
+
def close(self):
|
|
295
|
+
"""Close the database connection."""
|
|
296
|
+
self._adapter.close()
|
|
297
|
+
|
|
298
|
+
def __enter__(self):
|
|
299
|
+
return self
|
|
300
|
+
|
|
301
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
302
|
+
self.close()
|
|
303
|
+
return False
|
|
304
|
+
|
|
305
|
+
def __repr__(self):
|
|
306
|
+
return f"<FlashDB type={self.db_type} db={self.config.get('database', '?')}>"
|
flash/exceptions.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom exceptions for the Flash library.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FlashError(Exception):
|
|
7
|
+
"""Base exception for all Flash errors."""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ConnectionError(FlashError):
|
|
12
|
+
"""Raised when a database connection fails."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class QueryError(FlashError):
|
|
17
|
+
"""Raised when a query fails to execute."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class UnsupportedDatabaseError(FlashError):
|
|
22
|
+
"""Raised when an unsupported database type is used."""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TransactionError(FlashError):
|
|
27
|
+
"""Raised when a transaction operation fails."""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SchemaError(FlashError):
|
|
32
|
+
"""Raised when a schema operation fails."""
|
|
33
|
+
pass
|