velocity-python 0.0.92__py3-none-any.whl → 0.0.94__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.
Potentially problematic release.
This version of velocity-python might be problematic. Click here for more details.
- velocity/__init__.py +1 -1
- velocity_python-0.0.94.dist-info/METADATA +977 -0
- {velocity_python-0.0.92.dist-info → velocity_python-0.0.94.dist-info}/RECORD +6 -6
- velocity_python-0.0.92.dist-info/METADATA +0 -409
- {velocity_python-0.0.92.dist-info → velocity_python-0.0.94.dist-info}/WHEEL +0 -0
- {velocity_python-0.0.92.dist-info → velocity_python-0.0.94.dist-info}/licenses/LICENSE +0 -0
- {velocity_python-0.0.92.dist-info → velocity_python-0.0.94.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,977 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: velocity-python
|
|
3
|
+
Version: 0.0.94
|
|
4
|
+
Summary: A rapid application development library for interfacing with data storage
|
|
5
|
+
Author-email: Velocity Team <contact@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://codeclubs.org/projects/velocity
|
|
8
|
+
Keywords: database,orm,sql,rapid-development,data-storage
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Topic :: Database
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Requires-Python: >=3.7
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: boto3>=1.26.0
|
|
25
|
+
Requires-Dist: requests>=2.25.0
|
|
26
|
+
Requires-Dist: jinja2>=3.0.0
|
|
27
|
+
Requires-Dist: xlrd>=2.0.0
|
|
28
|
+
Requires-Dist: openpyxl>=3.0.0
|
|
29
|
+
Requires-Dist: sqlparse>=0.4.0
|
|
30
|
+
Provides-Extra: mysql
|
|
31
|
+
Requires-Dist: mysql-connector-python>=8.0.0; extra == "mysql"
|
|
32
|
+
Provides-Extra: sqlserver
|
|
33
|
+
Requires-Dist: python-tds>=1.10.0; extra == "sqlserver"
|
|
34
|
+
Provides-Extra: postgres
|
|
35
|
+
Requires-Dist: psycopg2-binary>=2.9.0; extra == "postgres"
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
38
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
39
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
40
|
+
Requires-Dist: flake8>=6.0.0; extra == "dev"
|
|
41
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
42
|
+
Requires-Dist: pre-commit>=3.0.0; extra == "dev"
|
|
43
|
+
Provides-Extra: test
|
|
44
|
+
Requires-Dist: pytest>=7.0.0; extra == "test"
|
|
45
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "test"
|
|
46
|
+
Requires-Dist: pytest-mock>=3.10.0; extra == "test"
|
|
47
|
+
Provides-Extra: docs
|
|
48
|
+
Requires-Dist: sphinx>=5.0.0; extra == "docs"
|
|
49
|
+
Requires-Dist: sphinx-rtd-theme>=1.2.0; extra == "docs"
|
|
50
|
+
Dynamic: license-file
|
|
51
|
+
|
|
52
|
+
# Velocity.DB
|
|
53
|
+
|
|
54
|
+
A modern Python database abstraction library that simplifies database operations across multiple database engines. Velocity.DB provides a unified interface for PostgreSQL, MySQL, SQLite, and SQL Server, with features like transaction management, automatic connection pooling, and database-agnostic query building.
|
|
55
|
+
|
|
56
|
+
## Core Design Philosophy
|
|
57
|
+
|
|
58
|
+
Velocity.DB is built around two fundamental concepts that make database programming intuitive and safe:
|
|
59
|
+
|
|
60
|
+
### 1. One Transaction Per Function Block
|
|
61
|
+
|
|
62
|
+
Every database operation must be wrapped in a single transaction using the `@engine.transaction` decorator. This ensures:
|
|
63
|
+
|
|
64
|
+
- **Atomicity**: All operations in a function either succeed together or fail together
|
|
65
|
+
- **Consistency**: Database state remains valid even if errors occur
|
|
66
|
+
- **Isolation**: Concurrent operations don't interfere with each other
|
|
67
|
+
- **Automatic cleanup**: Transactions commit on success, rollback on any exception
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
@engine.transaction # This entire function is one atomic operation
|
|
71
|
+
def transfer_money(tx, from_account_id, to_account_id, amount):
|
|
72
|
+
# If ANY operation fails, ALL changes are automatically rolled back
|
|
73
|
+
from_account = tx.table('accounts').find(from_account_id)
|
|
74
|
+
to_account = tx.table('accounts').find(to_account_id)
|
|
75
|
+
|
|
76
|
+
from_account['balance'] -= amount # This change...
|
|
77
|
+
to_account['balance'] += amount # ...and this change happen together or not at all
|
|
78
|
+
|
|
79
|
+
# No need to manually commit - happens automatically when function completes
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 2. Rows as Python Dictionaries
|
|
83
|
+
|
|
84
|
+
Database rows behave exactly like Python dictionaries, using familiar syntax:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
@engine.transaction
|
|
88
|
+
def work_with_user(tx):
|
|
89
|
+
user = tx.table('users').find(123)
|
|
90
|
+
|
|
91
|
+
# Read like a dictionary
|
|
92
|
+
name = user['name']
|
|
93
|
+
email = user['email']
|
|
94
|
+
|
|
95
|
+
# Update like a dictionary
|
|
96
|
+
user['name'] = 'New Name'
|
|
97
|
+
user['status'] = 'active'
|
|
98
|
+
|
|
99
|
+
# Check existence like a dictionary
|
|
100
|
+
if 'phone' in user:
|
|
101
|
+
phone = user['phone']
|
|
102
|
+
|
|
103
|
+
# Get all data like a dictionary
|
|
104
|
+
user_data = dict(user) # or user.to_dict()
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
This design eliminates the need to learn ORM-specific syntax while maintaining the power and flexibility of direct database access.
|
|
108
|
+
|
|
109
|
+
## Features
|
|
110
|
+
|
|
111
|
+
- **Multi-database support**: PostgreSQL, MySQL, SQLite, SQL Server
|
|
112
|
+
- **Transaction management**: Decorator-based transaction handling with automatic rollback
|
|
113
|
+
- **Query builder**: Database-agnostic SQL generation with foreign key expansion
|
|
114
|
+
- **Connection pooling**: Automatic connection management and pooling
|
|
115
|
+
- **Type safety**: Comprehensive type hints and validation
|
|
116
|
+
- **Modern Python**: Built for Python 3.8+ with modern packaging
|
|
117
|
+
|
|
118
|
+
## Supported Databases
|
|
119
|
+
|
|
120
|
+
- **PostgreSQL** (via psycopg2)
|
|
121
|
+
- **MySQL** (via mysqlclient)
|
|
122
|
+
- **SQLite** (built-in sqlite3)
|
|
123
|
+
- **SQL Server** (via pytds)
|
|
124
|
+
|
|
125
|
+
## Installation
|
|
126
|
+
|
|
127
|
+
Install the base package:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
pip install velocity-python
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Install with database-specific dependencies:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
# For PostgreSQL
|
|
137
|
+
pip install velocity-python[postgres]
|
|
138
|
+
|
|
139
|
+
# For MySQL
|
|
140
|
+
pip install velocity-python[mysql]
|
|
141
|
+
|
|
142
|
+
# For SQL Server
|
|
143
|
+
pip install velocity-python[sqlserver]
|
|
144
|
+
|
|
145
|
+
# For all databases
|
|
146
|
+
pip install velocity-python[all]
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Quick Start
|
|
150
|
+
|
|
151
|
+
### Database Connection
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
import velocity.db
|
|
155
|
+
|
|
156
|
+
# PostgreSQL
|
|
157
|
+
engine = velocity.db.postgres(
|
|
158
|
+
host="localhost",
|
|
159
|
+
port=5432,
|
|
160
|
+
database="mydb",
|
|
161
|
+
user="username",
|
|
162
|
+
password="password"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# MySQL
|
|
166
|
+
engine = velocity.db.mysql(
|
|
167
|
+
host="localhost",
|
|
168
|
+
port=3306,
|
|
169
|
+
database="mydb",
|
|
170
|
+
user="username",
|
|
171
|
+
password="password"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# SQLite
|
|
175
|
+
engine = velocity.db.sqlite("path/to/database.db")
|
|
176
|
+
|
|
177
|
+
# SQL Server
|
|
178
|
+
engine = velocity.db.sqlserver(
|
|
179
|
+
host="localhost",
|
|
180
|
+
port=1433,
|
|
181
|
+
database="mydb",
|
|
182
|
+
user="username",
|
|
183
|
+
password="password"
|
|
184
|
+
### Transaction Management
|
|
185
|
+
|
|
186
|
+
Velocity.DB enforces a "one transaction per function" pattern using the `@engine.transaction` decorator. The decorator intelligently handles transaction injection:
|
|
187
|
+
|
|
188
|
+
#### How Transaction Injection Works
|
|
189
|
+
|
|
190
|
+
The `@engine.transaction` decorator automatically provides a transaction object, but **you must declare `tx` as a parameter** in your function signature:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
@engine.transaction
|
|
194
|
+
def create_user_with_profile(tx): # ← You MUST declare 'tx' parameter
|
|
195
|
+
# The engine automatically creates and injects a Transaction object here
|
|
196
|
+
# 'tx' is provided by the decorator, not by the caller
|
|
197
|
+
|
|
198
|
+
user = tx.table('users').new()
|
|
199
|
+
user['name'] = 'John Doe'
|
|
200
|
+
user['email'] = 'john@example.com'
|
|
201
|
+
|
|
202
|
+
profile = tx.table('profiles').new()
|
|
203
|
+
profile['user_id'] = user['sys_id']
|
|
204
|
+
profile['bio'] = 'Software developer'
|
|
205
|
+
|
|
206
|
+
return user['sys_id']
|
|
207
|
+
|
|
208
|
+
# When you call the function, you DON'T pass the tx argument:
|
|
209
|
+
user_id = create_user_with_profile() # ← No 'tx' argument needed
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
#### The Magic Behind the Scenes
|
|
213
|
+
|
|
214
|
+
The decorator uses Python's `inspect` module to:
|
|
215
|
+
|
|
216
|
+
1. **Check the function signature** - Looks for a parameter named `tx`
|
|
217
|
+
2. **Automatic injection** - If `tx` is declared but not provided by caller, creates a new Transaction
|
|
218
|
+
3. **Parameter positioning** - Inserts the transaction object at the correct position in the argument list
|
|
219
|
+
4. **Transaction lifecycle** - Automatically commits on success or rolls back on exceptions
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
@engine.transaction
|
|
223
|
+
def update_user_settings(tx, user_id, settings): # ← 'tx' must be declared
|
|
224
|
+
# Engine finds 'tx' in position 0, creates Transaction, and injects it
|
|
225
|
+
user = tx.table('users').find(user_id)
|
|
226
|
+
user['settings'] = settings
|
|
227
|
+
user['last_updated'] = datetime.now()
|
|
228
|
+
|
|
229
|
+
# Call without providing 'tx' - the decorator handles it:
|
|
230
|
+
update_user_settings(123, {'theme': 'dark'}) # ← Only pass your parameters
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
#### Advanced: Transaction Reuse
|
|
234
|
+
|
|
235
|
+
If you want multiple function calls to be part of the same transaction, **explicitly pass the `tx` object** to chain operations together:
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
@engine.transaction
|
|
239
|
+
def create_user(tx, name, email):
|
|
240
|
+
user = tx.table('users').new()
|
|
241
|
+
user['name'] = name
|
|
242
|
+
user['email'] = email
|
|
243
|
+
return user['sys_id']
|
|
244
|
+
|
|
245
|
+
@engine.transaction
|
|
246
|
+
def create_profile(tx, user_id, bio):
|
|
247
|
+
profile = tx.table('profiles').new()
|
|
248
|
+
profile['user_id'] = user_id
|
|
249
|
+
profile['bio'] = bio
|
|
250
|
+
return profile['sys_id']
|
|
251
|
+
|
|
252
|
+
@engine.transaction
|
|
253
|
+
def create_user_with_profile(tx, name, email, bio):
|
|
254
|
+
# All operations in this function use the SAME transaction
|
|
255
|
+
|
|
256
|
+
# Pass 'tx' to keep this call in the same transaction
|
|
257
|
+
user_id = create_user(tx, name, email) # ← Pass 'tx' explicitly
|
|
258
|
+
|
|
259
|
+
# Pass 'tx' to keep this call in the same transaction too
|
|
260
|
+
profile_id = create_profile(tx, user_id, bio) # ← Pass 'tx' explicitly
|
|
261
|
+
|
|
262
|
+
# If ANY operation fails, ALL changes are rolled back together
|
|
263
|
+
return user_id
|
|
264
|
+
|
|
265
|
+
# When you call the main function, don't pass tx - let the decorator provide it:
|
|
266
|
+
user_id = create_user_with_profile('John', 'john@example.com', 'Developer')
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
#### Two Different Transaction Behaviors
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
# Scenario 1: SAME transaction (pass tx through)
|
|
273
|
+
@engine.transaction
|
|
274
|
+
def atomic_operation(tx):
|
|
275
|
+
create_user(tx, 'John', 'john@example.com') # ← Part of same transaction
|
|
276
|
+
create_profile(tx, user_id, 'Developer') # ← Part of same transaction
|
|
277
|
+
# If profile creation fails, user creation is also rolled back
|
|
278
|
+
|
|
279
|
+
# Scenario 2: SEPARATE transactions (don't pass tx)
|
|
280
|
+
@engine.transaction
|
|
281
|
+
def separate_operations(tx):
|
|
282
|
+
create_user('John', 'john@example.com') # ← Creates its own transaction
|
|
283
|
+
create_profile(user_id, 'Developer') # ← Creates its own transaction
|
|
284
|
+
# If profile creation fails, user creation is NOT rolled back
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Key Rule**: To include function calls in the same transaction, **always pass the `tx` parameter explicitly**. If you don't pass `tx`, each decorated function creates its own separate transaction.
|
|
288
|
+
|
|
289
|
+
#### Class-Level Transaction Decoration
|
|
290
|
+
|
|
291
|
+
You can also apply `@engine.transaction` to an entire class, which automatically wraps **all methods** that have `tx` in their signature:
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
@engine.transaction
|
|
295
|
+
class UserService:
|
|
296
|
+
"""All methods with 'tx' parameter get automatic transaction injection"""
|
|
297
|
+
|
|
298
|
+
def create_user(self, tx, name, email):
|
|
299
|
+
# This method gets automatic transaction injection
|
|
300
|
+
user = tx.table('users').new()
|
|
301
|
+
user['name'] = name
|
|
302
|
+
user['email'] = email
|
|
303
|
+
return user['sys_id']
|
|
304
|
+
|
|
305
|
+
def update_user(self, tx, user_id, **kwargs):
|
|
306
|
+
# This method also gets automatic transaction injection
|
|
307
|
+
user = tx.table('users').find(user_id)
|
|
308
|
+
for key, value in kwargs.items():
|
|
309
|
+
user[key] = value
|
|
310
|
+
return user.to_dict()
|
|
311
|
+
|
|
312
|
+
def get_user_count(self):
|
|
313
|
+
# This method is NOT wrapped (no 'tx' parameter)
|
|
314
|
+
return "This method runs normally without transaction injection"
|
|
315
|
+
|
|
316
|
+
def some_utility_method(self, data):
|
|
317
|
+
# This method is NOT wrapped (no 'tx' parameter)
|
|
318
|
+
return data.upper()
|
|
319
|
+
|
|
320
|
+
# Usage - each method call gets its own transaction automatically:
|
|
321
|
+
service = UserService()
|
|
322
|
+
|
|
323
|
+
# Each call creates its own transaction:
|
|
324
|
+
user_id = service.create_user('John', 'john@example.com') # ← Own transaction
|
|
325
|
+
user_data = service.update_user(user_id, status='active') # ← Own transaction
|
|
326
|
+
|
|
327
|
+
# Methods without 'tx' work normally:
|
|
328
|
+
count = service.get_user_count() # ← No transaction injection
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
#### Combining Class and Method Transactions
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
@engine.transaction
|
|
335
|
+
class UserService:
|
|
336
|
+
|
|
337
|
+
def create_user(self, tx, name, email):
|
|
338
|
+
user = tx.table('users').new()
|
|
339
|
+
user['name'] = name
|
|
340
|
+
user['email'] = email
|
|
341
|
+
return user['sys_id']
|
|
342
|
+
|
|
343
|
+
def create_profile(self, tx, user_id, bio):
|
|
344
|
+
profile = tx.table('profiles').new()
|
|
345
|
+
profile['user_id'] = user_id
|
|
346
|
+
profile['bio'] = bio
|
|
347
|
+
return profile['sys_id']
|
|
348
|
+
|
|
349
|
+
def create_user_with_profile(self, tx, name, email, bio):
|
|
350
|
+
# Share transaction across method calls within the same class
|
|
351
|
+
user_id = self.create_user(tx, name, email) # ← Pass tx to share transaction
|
|
352
|
+
profile_id = self.create_profile(tx, user_id, bio) # ← Pass tx to share transaction
|
|
353
|
+
return user_id
|
|
354
|
+
|
|
355
|
+
# Usage:
|
|
356
|
+
service = UserService()
|
|
357
|
+
# This creates ONE transaction for all operations:
|
|
358
|
+
user_id = service.create_user_with_profile('John', 'john@example.com', 'Developer')
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
**Key Benefits:**
|
|
362
|
+
- **Automatic transaction management**: No need to call `begin()`, `commit()`, or `rollback()`
|
|
363
|
+
- **Intelligent injection**: Engine inspects your function and provides `tx` automatically
|
|
364
|
+
- **Parameter flexibility**: `tx` can be in any position in your function signature
|
|
365
|
+
- **Transaction reuse**: Pass existing transactions to chain operations together
|
|
366
|
+
- **Clear boundaries**: Each function represents a complete business operation
|
|
367
|
+
- **Testable**: Easy to test since each function is a complete unit of work
|
|
368
|
+
|
|
369
|
+
**Important Rules:**
|
|
370
|
+
- **Must declare `tx` parameter**: The function signature must include `tx` as a parameter
|
|
371
|
+
- **Don't pass `tx` when calling from outside**: Let the decorator provide it automatically for new transactions
|
|
372
|
+
- **DO pass `tx` for same transaction**: To include function calls in the same transaction, explicitly pass the `tx` parameter
|
|
373
|
+
- **Class decoration**: `@engine.transaction` on a class wraps all methods that have `tx` in their signature
|
|
374
|
+
- **Selective wrapping**: Methods without `tx` parameter are not affected by class-level decoration
|
|
375
|
+
- **No `_tx` parameter**: Using `_tx` as a parameter name is forbidden (reserved)
|
|
376
|
+
- **Position matters**: The decorator injects `tx` at the exact position declared in your signature
|
|
377
|
+
|
|
378
|
+
### Table Operations
|
|
379
|
+
|
|
380
|
+
#### Creating Tables
|
|
381
|
+
|
|
382
|
+
```python
|
|
383
|
+
@engine.transaction
|
|
384
|
+
def create_tables(tx):
|
|
385
|
+
# Create a users table
|
|
386
|
+
users = tx.table('users')
|
|
387
|
+
users.create()
|
|
388
|
+
|
|
389
|
+
# Add columns by treating the row like a dictionary
|
|
390
|
+
user = users.new() # Creates a new row object
|
|
391
|
+
user['name'] = 'Sample User' # Sets column values using dict syntax
|
|
392
|
+
user['email'] = 'user@example.com' # No need for setters/getters
|
|
393
|
+
user['created_at'] = datetime.now() # Python types automatically handled
|
|
394
|
+
|
|
395
|
+
# The row is automatically saved when the transaction completes
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
#### Selecting Data
|
|
399
|
+
|
|
400
|
+
```python
|
|
401
|
+
@engine.transaction
|
|
402
|
+
def query_users(tx):
|
|
403
|
+
users = tx.table('users')
|
|
404
|
+
|
|
405
|
+
# Select all users - returns list of dict-like row objects
|
|
406
|
+
all_users = users.select().all()
|
|
407
|
+
for user in all_users:
|
|
408
|
+
print(f"User: {user['name']} ({user['email']})") # Dict syntax
|
|
409
|
+
|
|
410
|
+
# Select with conditions
|
|
411
|
+
active_users = users.select(where={'status': 'active'}).all()
|
|
412
|
+
|
|
413
|
+
# Select specific columns
|
|
414
|
+
names = users.select(columns=['name', 'email']).all()
|
|
415
|
+
|
|
416
|
+
# Select with ordering and limits
|
|
417
|
+
recent = users.select(
|
|
418
|
+
orderby='created_at DESC',
|
|
419
|
+
qty=10
|
|
420
|
+
).all()
|
|
421
|
+
|
|
422
|
+
# Find single record - returns dict-like row object
|
|
423
|
+
user = users.find({'email': 'john@example.com'})
|
|
424
|
+
if user:
|
|
425
|
+
# Access like dictionary
|
|
426
|
+
user_name = user['name']
|
|
427
|
+
user_id = user['sys_id']
|
|
428
|
+
|
|
429
|
+
# Check existence like dictionary
|
|
430
|
+
has_phone = 'phone' in user
|
|
431
|
+
|
|
432
|
+
# Convert to regular dict if needed
|
|
433
|
+
user_dict = user.to_dict()
|
|
434
|
+
|
|
435
|
+
# Get by primary key
|
|
436
|
+
user = users.find(123) # Returns dict-like row object or None
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
#### Updating Data
|
|
440
|
+
|
|
441
|
+
```python
|
|
442
|
+
@engine.transaction
|
|
443
|
+
def update_user(tx):
|
|
444
|
+
users = tx.table('users')
|
|
445
|
+
|
|
446
|
+
# Find and update using dictionary syntax
|
|
447
|
+
user = users.find(123) # Returns a row that behaves like a dict
|
|
448
|
+
user['name'] = 'Updated Name' # Direct assignment like a dict
|
|
449
|
+
user['updated_at'] = datetime.now() # No special methods needed
|
|
450
|
+
|
|
451
|
+
# Check if columns exist before updating
|
|
452
|
+
if 'phone' in user:
|
|
453
|
+
user['phone'] = '+1-555-0123'
|
|
454
|
+
|
|
455
|
+
# Get current values like a dictionary
|
|
456
|
+
current_status = user.get('status', 'unknown')
|
|
457
|
+
|
|
458
|
+
# Bulk update using where conditions
|
|
459
|
+
users.update(
|
|
460
|
+
{'status': 'inactive'}, # What to update (dict format)
|
|
461
|
+
where={'<last_login': '2023-01-01'} # Condition using operator prefix
|
|
462
|
+
)
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
#### Inserting Data
|
|
466
|
+
|
|
467
|
+
```python
|
|
468
|
+
@engine.transaction
|
|
469
|
+
def create_users(tx):
|
|
470
|
+
users = tx.table('users')
|
|
471
|
+
|
|
472
|
+
# Method 1: Create new row and populate like a dictionary
|
|
473
|
+
user = users.new() # Creates empty row object
|
|
474
|
+
user['name'] = 'New User' # Assign values using dict syntax
|
|
475
|
+
user['email'] = 'new@example.com' #
|
|
476
|
+
# Row automatically saved when transaction completes
|
|
477
|
+
|
|
478
|
+
# Method 2: Insert with dictionary data directly
|
|
479
|
+
user_id = users.insert({
|
|
480
|
+
'name': 'Another User',
|
|
481
|
+
'email': 'another@example.com'
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
# Method 3: Upsert (insert or update) using dictionary syntax
|
|
485
|
+
users.upsert(
|
|
486
|
+
{'name': 'John Doe', 'status': 'active'}, # Data to insert/update
|
|
487
|
+
{'email': 'john@example.com'} # Matching condition
|
|
488
|
+
)
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
#### Deleting Data
|
|
492
|
+
|
|
493
|
+
```python
|
|
494
|
+
@engine.transaction
|
|
495
|
+
def delete_users(tx):
|
|
496
|
+
users = tx.table('users')
|
|
497
|
+
|
|
498
|
+
# Delete single record
|
|
499
|
+
user = users.find(123)
|
|
500
|
+
user.delete()
|
|
501
|
+
|
|
502
|
+
# Delete with conditions
|
|
503
|
+
users.delete(where={'status': 'inactive'})
|
|
504
|
+
|
|
505
|
+
# Truncate table
|
|
506
|
+
users.truncate()
|
|
507
|
+
|
|
508
|
+
# Drop table
|
|
509
|
+
users.drop()
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### Advanced Queries
|
|
513
|
+
|
|
514
|
+
#### Foreign Key Navigation
|
|
515
|
+
|
|
516
|
+
Velocity.DB supports automatic foreign key expansion using pointer syntax:
|
|
517
|
+
|
|
518
|
+
```python
|
|
519
|
+
@engine.transaction
|
|
520
|
+
def get_user_with_profile(tx):
|
|
521
|
+
users = tx.table('users')
|
|
522
|
+
|
|
523
|
+
# Automatic join via foreign key
|
|
524
|
+
users_with_profiles = users.select(
|
|
525
|
+
columns=['name', 'email', 'profile_id>bio', 'profile_id>avatar_url'],
|
|
526
|
+
where={'status': 'active'}
|
|
527
|
+
).all()
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
#### Complex Conditions
|
|
531
|
+
|
|
532
|
+
Velocity.DB supports various where clause formats:
|
|
533
|
+
|
|
534
|
+
```python
|
|
535
|
+
@engine.transaction
|
|
536
|
+
def complex_queries(tx):
|
|
537
|
+
users = tx.table('users')
|
|
538
|
+
|
|
539
|
+
# Dictionary format with operator prefixes
|
|
540
|
+
results = users.select(where={
|
|
541
|
+
'status': 'active', # Equals (default)
|
|
542
|
+
'>=created_at': '2023-01-01', # Greater than or equal
|
|
543
|
+
'><age': [18, 65], # Between
|
|
544
|
+
'%email': '@company.com', # Like
|
|
545
|
+
'!status': 'deleted' # Not equal
|
|
546
|
+
}).all()
|
|
547
|
+
|
|
548
|
+
# List of tuples format for complex predicates
|
|
549
|
+
results = users.select(where=[
|
|
550
|
+
('status = %s', 'active'),
|
|
551
|
+
('priority = %s OR urgency = %s', ('high', 'critical'))
|
|
552
|
+
]).all()
|
|
553
|
+
|
|
554
|
+
# Raw string format
|
|
555
|
+
results = users.select(where="status = 'active' AND age >= 18").all()
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
**Available Operators:**
|
|
559
|
+
|
|
560
|
+
| Operator | SQL Equivalent | Example Usage | Description |
|
|
561
|
+
|----------|----------------|---------------|-------------|
|
|
562
|
+
| `=` (default) | `=` | `{'name': 'John'}` | Equals (default when no operator specified) |
|
|
563
|
+
| `>` | `>` | `{'>age': 18}` | Greater than |
|
|
564
|
+
| `<` | `<` | `{'<score': 100}` | Less than |
|
|
565
|
+
| `>=` | `>=` | `{'>=created_at': '2023-01-01'}` | Greater than or equal |
|
|
566
|
+
| `<=` | `<=` | `{'<=updated_at': '2023-12-31'}` | Less than or equal |
|
|
567
|
+
| `!` | `<>` | `{'!status': 'deleted'}` | Not equal |
|
|
568
|
+
| `!=` | `<>` | `{'!=status': 'deleted'}` | Not equal (alternative) |
|
|
569
|
+
| `<>` | `<>` | `{'<>status': 'deleted'}` | Not equal (SQL style) |
|
|
570
|
+
| `%` | `LIKE` | `{'%email': '@company.com'}` | Like pattern matching |
|
|
571
|
+
| `!%` | `NOT LIKE` | `{'!%name': 'test%'}` | Not like pattern matching |
|
|
572
|
+
| `><` | `BETWEEN` | `{'><age': [18, 65]}` | Between two values (inclusive) |
|
|
573
|
+
| `!><` | `NOT BETWEEN` | `{'!><score': [0, 50]}` | Not between two values |
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
#### Aggregations and Grouping
|
|
577
|
+
|
|
578
|
+
```python
|
|
579
|
+
@engine.transaction
|
|
580
|
+
def analytics(tx):
|
|
581
|
+
orders = tx.table('orders')
|
|
582
|
+
|
|
583
|
+
# Count records
|
|
584
|
+
total_orders = orders.count()
|
|
585
|
+
recent_orders = orders.count(where={'>=created_at': '2023-01-01'})
|
|
586
|
+
|
|
587
|
+
# Aggregations
|
|
588
|
+
stats = orders.select(
|
|
589
|
+
columns=['COUNT(*) as total', 'SUM(amount) as revenue', 'AVG(amount) as avg_order'],
|
|
590
|
+
where={'status': 'completed'},
|
|
591
|
+
groupby='customer_id'
|
|
592
|
+
).all()
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### Raw SQL
|
|
596
|
+
|
|
597
|
+
When you need full control, execute raw SQL. The `tx.execute()` method returns a **Result object** that provides flexible data transformation:
|
|
598
|
+
|
|
599
|
+
```python
|
|
600
|
+
@engine.transaction
|
|
601
|
+
def raw_queries(tx):
|
|
602
|
+
# Execute raw SQL - returns a Result object
|
|
603
|
+
result = tx.execute("""
|
|
604
|
+
SELECT u.name, u.email, COUNT(o.id) as order_count
|
|
605
|
+
FROM users u
|
|
606
|
+
LEFT JOIN orders o ON u.id = o.user_id
|
|
607
|
+
WHERE u.status = %s
|
|
608
|
+
GROUP BY u.id, u.name, u.email
|
|
609
|
+
HAVING COUNT(o.id) > %s
|
|
610
|
+
""", ['active', 5])
|
|
611
|
+
|
|
612
|
+
# Multiple ways to work with the Result object:
|
|
613
|
+
|
|
614
|
+
# Get all rows as list of dictionaries (default)
|
|
615
|
+
rows = result.all()
|
|
616
|
+
for row in rows:
|
|
617
|
+
print(f"User: {row['name']} ({row['email']}) - {row['order_count']} orders")
|
|
618
|
+
|
|
619
|
+
# Or iterate one row at a time
|
|
620
|
+
for row in result:
|
|
621
|
+
print(f"User: {row['name']}")
|
|
622
|
+
|
|
623
|
+
# Transform data format
|
|
624
|
+
result.as_tuple().all() # List of tuples
|
|
625
|
+
result.as_list().all() # List of lists
|
|
626
|
+
result.as_json().all() # List of JSON strings
|
|
627
|
+
result.as_named_tuple().all() # List of (name, value) pairs
|
|
628
|
+
|
|
629
|
+
# Get single values
|
|
630
|
+
total = tx.execute("SELECT COUNT(*) FROM users").scalar()
|
|
631
|
+
|
|
632
|
+
# Get simple list of single column values
|
|
633
|
+
names = tx.execute("SELECT name FROM users").as_simple_list().all()
|
|
634
|
+
|
|
635
|
+
# Get just the first row
|
|
636
|
+
first_user = tx.execute("SELECT * FROM users LIMIT 1").one()
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
#### Result Object Methods
|
|
640
|
+
|
|
641
|
+
The **Result object** returned by `tx.execute()` provides powerful data transformation capabilities:
|
|
642
|
+
|
|
643
|
+
| Method | Description | Returns |
|
|
644
|
+
|--------|-------------|---------|
|
|
645
|
+
| `.all()` | Get all rows at once | `List[Dict]` (default) or transformed format |
|
|
646
|
+
| `.one(default=None)` | Get first row only | `Dict` or `default` if no rows |
|
|
647
|
+
| `.scalar(default=None)` | Get first column of first row | Single value or `default` |
|
|
648
|
+
| `.batch(qty=1)` | Iterate in batches | Generator yielding lists of rows |
|
|
649
|
+
| **Data Format Transformations:** |
|
|
650
|
+
| `.as_dict()` | Rows as dictionaries (default) | `{'column': value, ...}` |
|
|
651
|
+
| `.as_tuple()` | Rows as tuples | `(value1, value2, ...)` |
|
|
652
|
+
| `.as_list()` | Rows as lists | `[value1, value2, ...]` |
|
|
653
|
+
| `.as_json()` | Rows as JSON strings | `'{"column": "value", ...}'` |
|
|
654
|
+
| `.as_named_tuple()` | Rows as name-value pairs | `[('column', value), ...]` |
|
|
655
|
+
| `.as_simple_list(pos=0)` | Extract single column | `value` (from position pos) |
|
|
656
|
+
| **Utility Methods:** |
|
|
657
|
+
| `.headers` | Get column names | `['col1', 'col2', ...]` |
|
|
658
|
+
| `.close()` | Close the cursor | `None` |
|
|
659
|
+
| `.enum()` | Add row numbers | `(index, row)` tuples |
|
|
660
|
+
|
|
661
|
+
```python
|
|
662
|
+
@engine.transaction
|
|
663
|
+
def result_examples(tx):
|
|
664
|
+
# Different output formats for the same query
|
|
665
|
+
result = tx.execute("SELECT name, email FROM users LIMIT 3")
|
|
666
|
+
|
|
667
|
+
# As dictionaries (default)
|
|
668
|
+
dicts = result.as_dict().all()
|
|
669
|
+
# [{'name': 'John', 'email': 'john@example.com'}, ...]
|
|
670
|
+
|
|
671
|
+
# As tuples
|
|
672
|
+
tuples = result.as_tuple().all()
|
|
673
|
+
# [('John', 'john@example.com'), ...]
|
|
674
|
+
|
|
675
|
+
# As JSON strings
|
|
676
|
+
json_rows = result.as_json().all()
|
|
677
|
+
# ['{"name": "John", "email": "john@example.com"}', ...]
|
|
678
|
+
|
|
679
|
+
# Just email addresses
|
|
680
|
+
emails = result.as_simple_list(1).all() # Position 1 = email column
|
|
681
|
+
# ['john@example.com', 'jane@example.com', ...]
|
|
682
|
+
|
|
683
|
+
# With row numbers
|
|
684
|
+
numbered = result.enum().all()
|
|
685
|
+
# [(0, {'name': 'John', 'email': 'john@example.com'}), ...]
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
## Automatic Schema Evolution
|
|
689
|
+
|
|
690
|
+
One of Velocity.DB's most powerful features is **automatic table and column creation**. The library uses decorators to catch database schema errors and automatically evolve your schema as your code changes.
|
|
691
|
+
|
|
692
|
+
### How Automatic Creation Works
|
|
693
|
+
|
|
694
|
+
Velocity.DB uses the `@create_missing` decorator on key table operations. When you try to:
|
|
695
|
+
|
|
696
|
+
- **Insert data** with new columns
|
|
697
|
+
- **Update rows** with new columns
|
|
698
|
+
- **Query tables** that don't exist
|
|
699
|
+
- **Reference columns** that don't exist
|
|
700
|
+
|
|
701
|
+
The library automatically:
|
|
702
|
+
|
|
703
|
+
1. **Catches the database error** (table missing, column missing)
|
|
704
|
+
2. **Analyzes the data** you're trying to work with
|
|
705
|
+
3. **Creates the missing table/columns** with appropriate types
|
|
706
|
+
4. **Retries the original operation** seamlessly
|
|
707
|
+
|
|
708
|
+
```python
|
|
709
|
+
@engine.transaction
|
|
710
|
+
def create_user_profile(tx):
|
|
711
|
+
# This table and columns don't exist yet - that's OK!
|
|
712
|
+
users = tx.table('users') # Table will be created automatically
|
|
713
|
+
|
|
714
|
+
# Insert data with new columns - they'll be created automatically
|
|
715
|
+
user = users.new()
|
|
716
|
+
user['name'] = 'John Doe' # VARCHAR column created automatically
|
|
717
|
+
user['age'] = 28 # INTEGER column created automatically
|
|
718
|
+
user['salary'] = 75000.50 # NUMERIC column created automatically
|
|
719
|
+
user['is_active'] = True # BOOLEAN column created automatically
|
|
720
|
+
user['bio'] = 'Software engineer' # TEXT column created automatically
|
|
721
|
+
|
|
722
|
+
# The table and all columns are now created and data is inserted
|
|
723
|
+
return user['sys_id']
|
|
724
|
+
|
|
725
|
+
# Call this function - table and columns created seamlessly
|
|
726
|
+
user_id = create_user_profile()
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
### Type Inference
|
|
730
|
+
|
|
731
|
+
Velocity.DB automatically infers SQL types from Python values:
|
|
732
|
+
|
|
733
|
+
| Python Type | SQL Type (PostgreSQL) | SQL Type (MySQL) | SQL Type (SQLite) |
|
|
734
|
+
|-------------|------------------------|-------------------|-------------------|
|
|
735
|
+
| `str` | `TEXT` | `TEXT` | `TEXT` |
|
|
736
|
+
| `int` | `BIGINT` | `BIGINT` | `INTEGER` |
|
|
737
|
+
| `float` | `NUMERIC(19,6)` | `DECIMAL(19,6)` | `REAL` |
|
|
738
|
+
| `bool` | `BOOLEAN` | `BOOLEAN` | `INTEGER` |
|
|
739
|
+
| `datetime` | `TIMESTAMP` | `DATETIME` | `TEXT` |
|
|
740
|
+
| `date` | `DATE` | `DATE` | `TEXT` |
|
|
741
|
+
|
|
742
|
+
### Progressive Schema Evolution
|
|
743
|
+
|
|
744
|
+
Your schema evolves naturally as your application grows:
|
|
745
|
+
|
|
746
|
+
```python
|
|
747
|
+
# Week 1: Start simple
|
|
748
|
+
@engine.transaction
|
|
749
|
+
def create_basic_user(tx):
|
|
750
|
+
users = tx.table('users')
|
|
751
|
+
user = users.new()
|
|
752
|
+
user['name'] = 'Alice'
|
|
753
|
+
user['email'] = 'alice@example.com'
|
|
754
|
+
return user['sys_id']
|
|
755
|
+
|
|
756
|
+
# Week 2: Add more fields
|
|
757
|
+
@engine.transaction
|
|
758
|
+
def create_detailed_user(tx):
|
|
759
|
+
users = tx.table('users')
|
|
760
|
+
user = users.new()
|
|
761
|
+
user['name'] = 'Bob'
|
|
762
|
+
user['email'] = 'bob@example.com'
|
|
763
|
+
user['phone'] = '+1-555-0123' # New column added automatically
|
|
764
|
+
user['department'] = 'Engineering' # Another new column added automatically
|
|
765
|
+
user['start_date'] = date.today() # Date column added automatically
|
|
766
|
+
return user['sys_id']
|
|
767
|
+
|
|
768
|
+
# Week 3: Even more fields
|
|
769
|
+
@engine.transaction
|
|
770
|
+
def create_full_user(tx):
|
|
771
|
+
users = tx.table('users')
|
|
772
|
+
user = users.new()
|
|
773
|
+
user['name'] = 'Carol'
|
|
774
|
+
user['email'] = 'carol@example.com'
|
|
775
|
+
user['phone'] = '+1-555-0124'
|
|
776
|
+
user['department'] = 'Marketing'
|
|
777
|
+
user['start_date'] = date.today()
|
|
778
|
+
user['salary'] = 85000.00 # Salary column added automatically
|
|
779
|
+
user['is_manager'] = True # Boolean column added automatically
|
|
780
|
+
user['notes'] = 'Excellent performer' # Notes column added automatically
|
|
781
|
+
return user['sys_id']
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
### Behind the Scenes
|
|
785
|
+
|
|
786
|
+
The `@create_missing` decorator works by:
|
|
787
|
+
|
|
788
|
+
```python
|
|
789
|
+
# This is what happens automatically:
|
|
790
|
+
def create_missing(func):
|
|
791
|
+
def wrapper(self, *args, **kwds):
|
|
792
|
+
try:
|
|
793
|
+
# Try the original operation
|
|
794
|
+
return func(self, *args, **kwds)
|
|
795
|
+
except DbTableMissingError:
|
|
796
|
+
# Table doesn't exist - create it from the data
|
|
797
|
+
data = extract_data_from_args(args, kwds)
|
|
798
|
+
self.create(data) # Create table with inferred columns
|
|
799
|
+
return func(self, *args, **kwds) # Retry operation
|
|
800
|
+
except DbColumnMissingError:
|
|
801
|
+
# Column doesn't exist - add it to the table
|
|
802
|
+
data = extract_data_from_args(args, kwds)
|
|
803
|
+
self.alter(data) # Add missing columns
|
|
804
|
+
return func(self, *args, **kwds) # Retry operation
|
|
805
|
+
return wrapper
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
### Which Operations Are Protected
|
|
809
|
+
|
|
810
|
+
These table operations automatically create missing schema elements:
|
|
811
|
+
|
|
812
|
+
- `table.insert(data)` - Creates table and columns
|
|
813
|
+
- `table.update(data, where)` - Creates missing columns in data
|
|
814
|
+
- `table.merge(data, pk)` - Creates table and columns (upsert)
|
|
815
|
+
- `table.alter_type(column, type)` - Creates column if missing
|
|
816
|
+
- `table.alter(columns)` - Adds missing columns
|
|
817
|
+
|
|
818
|
+
### Manual Schema Control
|
|
819
|
+
|
|
820
|
+
If you prefer explicit control, you can disable automatic creation:
|
|
821
|
+
|
|
822
|
+
```python
|
|
823
|
+
@engine.transaction
|
|
824
|
+
def explicit_schema_control(tx):
|
|
825
|
+
users = tx.table('users')
|
|
826
|
+
|
|
827
|
+
# Check if table exists before using it
|
|
828
|
+
if not users.exists():
|
|
829
|
+
users.create({
|
|
830
|
+
'name': str,
|
|
831
|
+
'email': str,
|
|
832
|
+
'age': int,
|
|
833
|
+
'is_active': bool
|
|
834
|
+
})
|
|
835
|
+
|
|
836
|
+
# Check if column exists before using it
|
|
837
|
+
if 'phone' not in users.column_names():
|
|
838
|
+
users.alter({'phone': str})
|
|
839
|
+
|
|
840
|
+
# Now safely use the table
|
|
841
|
+
user = users.new()
|
|
842
|
+
user['name'] = 'David'
|
|
843
|
+
user['email'] = 'david@example.com'
|
|
844
|
+
user['phone'] = '+1-555-0125'
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
### Development Benefits
|
|
848
|
+
|
|
849
|
+
**For Development:**
|
|
850
|
+
- **Rapid prototyping**: Focus on business logic, not database setup
|
|
851
|
+
- **Zero configuration**: No migration scripts or schema files needed
|
|
852
|
+
- **Natural evolution**: Schema grows with your application
|
|
853
|
+
|
|
854
|
+
**For Production:**
|
|
855
|
+
- **Controlled deployment**: Use `sql_only=True` to generate schema changes for review
|
|
856
|
+
- **Safe migrations**: Test automatic changes in staging environments
|
|
857
|
+
- **Backwards compatibility**: New columns are added, existing data preserved
|
|
858
|
+
|
|
859
|
+
```python
|
|
860
|
+
# Generate SQL for review without executing
|
|
861
|
+
@engine.transaction
|
|
862
|
+
def preview_schema_changes(tx):
|
|
863
|
+
users = tx.table('users')
|
|
864
|
+
|
|
865
|
+
# See what SQL would be generated
|
|
866
|
+
sql, vals = users.insert({
|
|
867
|
+
'name': 'Test User',
|
|
868
|
+
'new_field': 'New Value'
|
|
869
|
+
}, sql_only=True)
|
|
870
|
+
|
|
871
|
+
print("SQL that would be executed:")
|
|
872
|
+
print(sql)
|
|
873
|
+
# Shows: ALTER TABLE users ADD COLUMN new_field TEXT; INSERT INTO users...
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
**Key Benefits:**
|
|
877
|
+
- **Zero-friction development**: Write code, not schema migrations
|
|
878
|
+
- **Type-safe evolution**: Python types automatically map to appropriate SQL types
|
|
879
|
+
- **Production-ready**: Generate reviewable SQL for controlled deployments
|
|
880
|
+
- **Database-agnostic**: Works consistently across PostgreSQL, MySQL, SQLite, and SQL Server
|
|
881
|
+
|
|
882
|
+
## Error Handling
|
|
883
|
+
|
|
884
|
+
The "one transaction per function" design automatically handles rollbacks on exceptions:
|
|
885
|
+
|
|
886
|
+
```python
|
|
887
|
+
@engine.transaction
|
|
888
|
+
def safe_transfer(tx, from_id, to_id, amount):
|
|
889
|
+
try:
|
|
890
|
+
# Multiple operations that must succeed together
|
|
891
|
+
from_account = tx.table('accounts').find(from_id)
|
|
892
|
+
to_account = tx.table('accounts').find(to_id)
|
|
893
|
+
|
|
894
|
+
# Work with rows like dictionaries
|
|
895
|
+
if from_account['balance'] < amount:
|
|
896
|
+
raise ValueError("Insufficient funds")
|
|
897
|
+
|
|
898
|
+
from_account['balance'] -= amount # This change...
|
|
899
|
+
to_account['balance'] += amount # ...and this change are atomic
|
|
900
|
+
|
|
901
|
+
# If any operation fails, entire transaction rolls back automatically
|
|
902
|
+
|
|
903
|
+
except Exception as e:
|
|
904
|
+
# Transaction automatically rolled back - no manual intervention needed
|
|
905
|
+
logger.error(f"Transfer failed: {e}")
|
|
906
|
+
raise # Re-raise to let caller handle the business logic
|
|
907
|
+
|
|
908
|
+
@engine.transaction
|
|
909
|
+
def create_user_with_validation(tx, user_data):
|
|
910
|
+
# Each function is a complete business operation
|
|
911
|
+
users = tx.table('users')
|
|
912
|
+
|
|
913
|
+
# Check if user already exists
|
|
914
|
+
existing = users.find({'email': user_data['email']})
|
|
915
|
+
if existing:
|
|
916
|
+
raise ValueError("User already exists")
|
|
917
|
+
|
|
918
|
+
# Create new user using dictionary interface
|
|
919
|
+
user = users.new()
|
|
920
|
+
user['name'] = user_data['name']
|
|
921
|
+
user['email'] = user_data['email']
|
|
922
|
+
user['created_at'] = datetime.now()
|
|
923
|
+
|
|
924
|
+
# If we reach here, everything commits automatically
|
|
925
|
+
return user['sys_id']
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
**Key Benefits of Transaction-Per-Function:**
|
|
929
|
+
- **Automatic rollback**: Any exception undoes all changes in that function
|
|
930
|
+
- **Clear error boundaries**: Each function represents one business operation
|
|
931
|
+
- **No resource leaks**: Connections and transactions are always properly cleaned up
|
|
932
|
+
- **Predictable behavior**: Functions either complete fully or have no effect
|
|
933
|
+
|
|
934
|
+
## Development
|
|
935
|
+
|
|
936
|
+
### Setting up for Development
|
|
937
|
+
|
|
938
|
+
This is currently a private repository. If you have access to the repository:
|
|
939
|
+
|
|
940
|
+
```bash
|
|
941
|
+
git clone <repository-url>
|
|
942
|
+
cd velocity-python
|
|
943
|
+
pip install -e .[dev]
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
### Running Tests
|
|
947
|
+
|
|
948
|
+
```bash
|
|
949
|
+
pytest tests/
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
### Code Quality
|
|
953
|
+
|
|
954
|
+
```bash
|
|
955
|
+
# Format code
|
|
956
|
+
black src/
|
|
957
|
+
|
|
958
|
+
# Type checking
|
|
959
|
+
mypy src/
|
|
960
|
+
|
|
961
|
+
# Linting
|
|
962
|
+
flake8 src/
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
## License
|
|
966
|
+
|
|
967
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
968
|
+
|
|
969
|
+
## Contributing
|
|
970
|
+
|
|
971
|
+
This is currently a private repository and we are not accepting public contributions at this time. However, this may change in the future based on community interest and project needs.
|
|
972
|
+
|
|
973
|
+
If you are interested in contributing to Velocity.DB, please reach out to discuss potential collaboration opportunities.
|
|
974
|
+
|
|
975
|
+
## Changelog
|
|
976
|
+
|
|
977
|
+
See [CHANGELOG.md](CHANGELOG.md) for a list of changes and version history.
|