velocity-python 0.0.93__py3-none-any.whl → 0.0.95__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.

@@ -0,0 +1,977 @@
1
+ Metadata-Version: 2.4
2
+ Name: velocity-python
3
+ Version: 0.0.95
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.