t-sql 4.2.1__tar.gz → 4.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {t_sql-4.2.1 → t_sql-4.3.0}/PKG-INFO +43 -1
- {t_sql-4.2.1 → t_sql-4.3.0}/README.md +42 -0
- {t_sql-4.2.1 → t_sql-4.3.0}/pyproject.toml +1 -1
- {t_sql-4.2.1 → t_sql-4.3.0}/tests/test_alembic_integration.py +106 -1
- {t_sql-4.2.1 → t_sql-4.3.0}/tests/test_sqlalchemy_integration.py +156 -0
- {t_sql-4.2.1 → t_sql-4.3.0}/tsql/query_builder.py +31 -2
- {t_sql-4.2.1 → t_sql-4.3.0}/.dockerignore +0 -0
- {t_sql-4.2.1 → t_sql-4.3.0}/.github/workflows/publish.yml +0 -0
- {t_sql-4.2.1 → t_sql-4.3.0}/.github/workflows/test.yml +0 -0
- {t_sql-4.2.1 → t_sql-4.3.0}/.gitignore +0 -0
- {t_sql-4.2.1 → t_sql-4.3.0}/Dockerfile +0 -0
- {t_sql-4.2.1 → t_sql-4.3.0}/LICENSE +0 -0
- {t_sql-4.2.1 → t_sql-4.3.0}/compose.yaml +0 -0
- {t_sql-4.2.1 → t_sql-4.3.0}/context7.json +0 -0
- {t_sql-4.2.1 → t_sql-4.3.0}/pytest.ini +0 -0
- {t_sql-4.2.1 → t_sql-4.3.0}/tests/test_asyncpg_integration.py +0 -0
- {t_sql-4.2.1 → t_sql-4.3.0}/tests/test_different_object_types.py +0 -0
- {t_sql-4.2.1 → t_sql-4.3.0}/tests/test_escaped.py +0 -0
- {t_sql-4.2.1 → t_sql-4.3.0}/tests/test_escaped_binary_hex.py +0 -0
- {t_sql-4.2.1 → t_sql-4.3.0}/tests/test_helper_functions.py +0 -0
- {t_sql-4.2.1 → t_sql-4.3.0}/tests/test_injection_edge_cases.py +0 -0
- {t_sql-4.2.1 → t_sql-4.3.0}/tests/test_injection_protection_validation.py +0 -0
- {t_sql-4.2.1 → t_sql-4.3.0}/tests/test_injections_for_escaped.py +0 -0
- {t_sql-4.2.1 → t_sql-4.3.0}/tests/test_mysql_integration.py +0 -0
- {t_sql-4.2.1 → t_sql-4.3.0}/tests/test_parameter_names.py +0 -0
- {t_sql-4.2.1 → t_sql-4.3.0}/tests/test_query_builder.py +0 -0
- {t_sql-4.2.1 → t_sql-4.3.0}/tests/test_sqlite_integration.py +0 -0
- {t_sql-4.2.1 → t_sql-4.3.0}/tests/test_styles.py +0 -0
- {t_sql-4.2.1 → t_sql-4.3.0}/tests/test_tsql.py +0 -0
- {t_sql-4.2.1 → t_sql-4.3.0}/tsql/__init__.py +0 -0
- {t_sql-4.2.1 → t_sql-4.3.0}/tsql/styles.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: t-sql
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.3.0
|
|
4
4
|
Summary: Safe SQL. SQL queries for python t-strings (PEP 750)
|
|
5
5
|
Project-URL: Homepage, https://github.com/nhumrich/t-sql
|
|
6
6
|
License-File: LICENSE
|
|
@@ -526,6 +526,48 @@ query = Users.select().where(Users.age > 18)
|
|
|
526
526
|
|
|
527
527
|
The `SAColumn` wrapper tells type checkers it returns a tsql `Column`, while at runtime it creates a SQLAlchemy `Column`. This gives you proper IDE completions for methods like `.is_null()`, `.like()`, etc.
|
|
528
528
|
|
|
529
|
+
### Table Constraints
|
|
530
|
+
|
|
531
|
+
For Alembic migrations, you can define table-level constraints using the `constraints` attribute:
|
|
532
|
+
|
|
533
|
+
```python
|
|
534
|
+
from sqlalchemy import MetaData, String, UniqueConstraint, CheckConstraint, Index
|
|
535
|
+
from tsql.query_builder import Table, SAColumn
|
|
536
|
+
|
|
537
|
+
metadata = MetaData()
|
|
538
|
+
|
|
539
|
+
class Clients(Table, table_name='clients', metadata=metadata):
|
|
540
|
+
id = SAColumn(String, primary_key=True)
|
|
541
|
+
tenant_id = SAColumn(String)
|
|
542
|
+
email = SAColumn(String, nullable=False)
|
|
543
|
+
|
|
544
|
+
# Define table-level constraints
|
|
545
|
+
constraints = [
|
|
546
|
+
UniqueConstraint('tenant_id', 'email', name='uq_clients_tenant_email'),
|
|
547
|
+
CheckConstraint('length(email) > 0', name='ck_clients_email_not_empty'),
|
|
548
|
+
Index('ix_clients_tenant', 'tenant_id')
|
|
549
|
+
]
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
The `constraints` attribute accepts both lists and tuples, and supports all SQLAlchemy constraint types:
|
|
553
|
+
- `UniqueConstraint` - Multi-column unique constraints
|
|
554
|
+
- `CheckConstraint` - Table-level check constraints
|
|
555
|
+
- `Index` - Multi-column indexes
|
|
556
|
+
- `ForeignKeyConstraint` - Table-level foreign keys
|
|
557
|
+
|
|
558
|
+
**Note:** Single-column constraints like unique indexes and foreign keys can still be defined directly on `SAColumn` (e.g., `SAColumn(String, unique=True, index=True)`).
|
|
559
|
+
|
|
560
|
+
### Table Comments
|
|
561
|
+
|
|
562
|
+
Add database-level documentation with the `comment` parameter:
|
|
563
|
+
|
|
564
|
+
```python
|
|
565
|
+
class Users(Table, metadata=metadata, comment='Application user accounts'):
|
|
566
|
+
id = SAColumn(Integer, primary_key=True)
|
|
567
|
+
email = SAColumn(String(255), nullable=False)
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
Table comments appear in database introspection tools and migration files, making your schema self-documenting.
|
|
529
571
|
|
|
530
572
|
## Schema Support
|
|
531
573
|
|
|
@@ -516,6 +516,48 @@ query = Users.select().where(Users.age > 18)
|
|
|
516
516
|
|
|
517
517
|
The `SAColumn` wrapper tells type checkers it returns a tsql `Column`, while at runtime it creates a SQLAlchemy `Column`. This gives you proper IDE completions for methods like `.is_null()`, `.like()`, etc.
|
|
518
518
|
|
|
519
|
+
### Table Constraints
|
|
520
|
+
|
|
521
|
+
For Alembic migrations, you can define table-level constraints using the `constraints` attribute:
|
|
522
|
+
|
|
523
|
+
```python
|
|
524
|
+
from sqlalchemy import MetaData, String, UniqueConstraint, CheckConstraint, Index
|
|
525
|
+
from tsql.query_builder import Table, SAColumn
|
|
526
|
+
|
|
527
|
+
metadata = MetaData()
|
|
528
|
+
|
|
529
|
+
class Clients(Table, table_name='clients', metadata=metadata):
|
|
530
|
+
id = SAColumn(String, primary_key=True)
|
|
531
|
+
tenant_id = SAColumn(String)
|
|
532
|
+
email = SAColumn(String, nullable=False)
|
|
533
|
+
|
|
534
|
+
# Define table-level constraints
|
|
535
|
+
constraints = [
|
|
536
|
+
UniqueConstraint('tenant_id', 'email', name='uq_clients_tenant_email'),
|
|
537
|
+
CheckConstraint('length(email) > 0', name='ck_clients_email_not_empty'),
|
|
538
|
+
Index('ix_clients_tenant', 'tenant_id')
|
|
539
|
+
]
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
The `constraints` attribute accepts both lists and tuples, and supports all SQLAlchemy constraint types:
|
|
543
|
+
- `UniqueConstraint` - Multi-column unique constraints
|
|
544
|
+
- `CheckConstraint` - Table-level check constraints
|
|
545
|
+
- `Index` - Multi-column indexes
|
|
546
|
+
- `ForeignKeyConstraint` - Table-level foreign keys
|
|
547
|
+
|
|
548
|
+
**Note:** Single-column constraints like unique indexes and foreign keys can still be defined directly on `SAColumn` (e.g., `SAColumn(String, unique=True, index=True)`).
|
|
549
|
+
|
|
550
|
+
### Table Comments
|
|
551
|
+
|
|
552
|
+
Add database-level documentation with the `comment` parameter:
|
|
553
|
+
|
|
554
|
+
```python
|
|
555
|
+
class Users(Table, metadata=metadata, comment='Application user accounts'):
|
|
556
|
+
id = SAColumn(Integer, primary_key=True)
|
|
557
|
+
email = SAColumn(String(255), nullable=False)
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
Table comments appear in database introspection tools and migration files, making your schema self-documenting.
|
|
519
561
|
|
|
520
562
|
## Schema Support
|
|
521
563
|
|
|
@@ -360,4 +360,109 @@ def test_alembic_with_schema_parameter(temp_alembic_env):
|
|
|
360
360
|
assert table.schema == 'public'
|
|
361
361
|
|
|
362
362
|
|
|
363
|
-
from sqlalchemy import text as sa_text
|
|
363
|
+
from sqlalchemy import text as sa_text
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def test_alembic_detects_unique_constraint(temp_alembic_env):
|
|
367
|
+
"""Test that Alembic autogenerate detects UniqueConstraint"""
|
|
368
|
+
from sqlalchemy import UniqueConstraint
|
|
369
|
+
|
|
370
|
+
temp_dir, alembic_ini = temp_alembic_env
|
|
371
|
+
metadata = MetaData()
|
|
372
|
+
engine = create_engine("sqlite:///:memory:")
|
|
373
|
+
|
|
374
|
+
class Clients(Table, table_name='clients', metadata=metadata):
|
|
375
|
+
id = Column(String, primary_key=True)
|
|
376
|
+
tenant_id = Column(String)
|
|
377
|
+
email = Column(String, nullable=False)
|
|
378
|
+
|
|
379
|
+
constraints = [
|
|
380
|
+
UniqueConstraint('tenant_id', 'email', name='uq_clients_tenant_email')
|
|
381
|
+
]
|
|
382
|
+
|
|
383
|
+
cfg = Config(str(alembic_ini))
|
|
384
|
+
cfg.attributes['target_metadata'] = metadata
|
|
385
|
+
cfg.attributes['connection'] = engine
|
|
386
|
+
|
|
387
|
+
with engine.begin() as connection:
|
|
388
|
+
mc = MigrationContext.configure(connection)
|
|
389
|
+
diff = compare_metadata(mc, metadata)
|
|
390
|
+
|
|
391
|
+
# Should detect the new table
|
|
392
|
+
add_table_ops = [op for op in diff if op[0] == 'add_table']
|
|
393
|
+
assert len(add_table_ops) == 1
|
|
394
|
+
|
|
395
|
+
table = add_table_ops[0][1]
|
|
396
|
+
assert table.name == 'clients'
|
|
397
|
+
|
|
398
|
+
# Verify the UniqueConstraint is in the table definition
|
|
399
|
+
unique_constraints = [c for c in table.constraints if isinstance(c, UniqueConstraint)]
|
|
400
|
+
assert len(unique_constraints) == 1
|
|
401
|
+
|
|
402
|
+
uc = unique_constraints[0]
|
|
403
|
+
assert uc.name == 'uq_clients_tenant_email'
|
|
404
|
+
assert set(c.name for c in uc.columns) == {'tenant_id', 'email'}
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def test_alembic_detects_check_constraint(temp_alembic_env):
|
|
408
|
+
"""Test that Alembic autogenerate detects CheckConstraint"""
|
|
409
|
+
from sqlalchemy import CheckConstraint
|
|
410
|
+
|
|
411
|
+
temp_dir, alembic_ini = temp_alembic_env
|
|
412
|
+
metadata = MetaData()
|
|
413
|
+
engine = create_engine("sqlite:///:memory:")
|
|
414
|
+
|
|
415
|
+
class Products(Table, table_name='products', metadata=metadata):
|
|
416
|
+
id = Column(Integer, primary_key=True)
|
|
417
|
+
name = Column(String, nullable=False)
|
|
418
|
+
price = Column(Integer)
|
|
419
|
+
|
|
420
|
+
constraints = [
|
|
421
|
+
CheckConstraint('price > 0', name='ck_products_positive_price')
|
|
422
|
+
]
|
|
423
|
+
|
|
424
|
+
cfg = Config(str(alembic_ini))
|
|
425
|
+
cfg.attributes['target_metadata'] = metadata
|
|
426
|
+
cfg.attributes['connection'] = engine
|
|
427
|
+
|
|
428
|
+
with engine.begin() as connection:
|
|
429
|
+
mc = MigrationContext.configure(connection)
|
|
430
|
+
diff = compare_metadata(mc, metadata)
|
|
431
|
+
|
|
432
|
+
add_table_ops = [op for op in diff if op[0] == 'add_table']
|
|
433
|
+
assert len(add_table_ops) == 1
|
|
434
|
+
|
|
435
|
+
table = add_table_ops[0][1]
|
|
436
|
+
|
|
437
|
+
# Verify the CheckConstraint is in the table definition
|
|
438
|
+
check_constraints = [c for c in table.constraints if isinstance(c, CheckConstraint)]
|
|
439
|
+
assert len(check_constraints) == 1
|
|
440
|
+
|
|
441
|
+
cc = check_constraints[0]
|
|
442
|
+
assert cc.name == 'ck_products_positive_price'
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def test_alembic_detects_table_comment(temp_alembic_env):
|
|
446
|
+
"""Test that Alembic autogenerate preserves table comments"""
|
|
447
|
+
temp_dir, alembic_ini = temp_alembic_env
|
|
448
|
+
metadata = MetaData()
|
|
449
|
+
engine = create_engine("sqlite:///:memory:")
|
|
450
|
+
|
|
451
|
+
class Settings(Table, table_name='settings', metadata=metadata, comment='Application configuration'):
|
|
452
|
+
id = Column(Integer, primary_key=True)
|
|
453
|
+
key = Column(String, nullable=False)
|
|
454
|
+
value = Column(String)
|
|
455
|
+
|
|
456
|
+
cfg = Config(str(alembic_ini))
|
|
457
|
+
cfg.attributes['target_metadata'] = metadata
|
|
458
|
+
cfg.attributes['connection'] = engine
|
|
459
|
+
|
|
460
|
+
with engine.begin() as connection:
|
|
461
|
+
mc = MigrationContext.configure(connection)
|
|
462
|
+
diff = compare_metadata(mc, metadata)
|
|
463
|
+
|
|
464
|
+
add_table_ops = [op for op in diff if op[0] == 'add_table']
|
|
465
|
+
assert len(add_table_ops) == 1
|
|
466
|
+
|
|
467
|
+
table = add_table_ops[0][1]
|
|
468
|
+
assert table.comment == 'Application configuration'
|
|
@@ -264,3 +264,159 @@ def test_sa_column_annotations_are_correct_type():
|
|
|
264
264
|
def gen_id(prefix):
|
|
265
265
|
"""Dummy function for test"""
|
|
266
266
|
return f"{prefix}_123"
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def test_table_with_unique_constraint():
|
|
270
|
+
"""Test that UniqueConstraint is properly added to SA table"""
|
|
271
|
+
from sqlalchemy import UniqueConstraint
|
|
272
|
+
|
|
273
|
+
metadata = MetaData()
|
|
274
|
+
|
|
275
|
+
class Clients(Table, table_name='clients', metadata=metadata):
|
|
276
|
+
id = Column(String, primary_key=True)
|
|
277
|
+
tenant_id = Column(String, ForeignKey('tenants.id'))
|
|
278
|
+
email = Column(String, nullable=False)
|
|
279
|
+
|
|
280
|
+
constraints = [
|
|
281
|
+
UniqueConstraint('tenant_id', 'email', name='uq_clients_tenant_email')
|
|
282
|
+
]
|
|
283
|
+
|
|
284
|
+
assert 'clients' in metadata.tables
|
|
285
|
+
sa_table = metadata.tables['clients']
|
|
286
|
+
|
|
287
|
+
# Find the unique constraint
|
|
288
|
+
unique_constraints = [c for c in sa_table.constraints if isinstance(c, UniqueConstraint)]
|
|
289
|
+
assert len(unique_constraints) == 1
|
|
290
|
+
|
|
291
|
+
uc = unique_constraints[0]
|
|
292
|
+
assert uc.name == 'uq_clients_tenant_email'
|
|
293
|
+
assert set(c.name for c in uc.columns) == {'tenant_id', 'email'}
|
|
294
|
+
|
|
295
|
+
# Query builder still works
|
|
296
|
+
query = Clients.select(Clients.id, Clients.email)
|
|
297
|
+
sql, params = query.render()
|
|
298
|
+
assert 'SELECT clients.id, clients.email FROM clients' in sql
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def test_table_with_check_constraint():
|
|
302
|
+
"""Test that CheckConstraint is properly added to SA table"""
|
|
303
|
+
from sqlalchemy import CheckConstraint
|
|
304
|
+
|
|
305
|
+
metadata = MetaData()
|
|
306
|
+
|
|
307
|
+
class Products(Table, table_name='products', metadata=metadata):
|
|
308
|
+
id = Column(Integer, primary_key=True)
|
|
309
|
+
name = Column(String, nullable=False)
|
|
310
|
+
price = Column(Integer)
|
|
311
|
+
|
|
312
|
+
constraints = [
|
|
313
|
+
CheckConstraint('price > 0', name='ck_products_positive_price')
|
|
314
|
+
]
|
|
315
|
+
|
|
316
|
+
sa_table = metadata.tables['products']
|
|
317
|
+
|
|
318
|
+
# Find the check constraint
|
|
319
|
+
check_constraints = [c for c in sa_table.constraints if isinstance(c, CheckConstraint)]
|
|
320
|
+
assert len(check_constraints) == 1
|
|
321
|
+
|
|
322
|
+
cc = check_constraints[0]
|
|
323
|
+
assert cc.name == 'ck_products_positive_price'
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def test_table_with_multiple_constraints():
|
|
327
|
+
"""Test that multiple constraints can be added together"""
|
|
328
|
+
from sqlalchemy import UniqueConstraint, CheckConstraint, Index
|
|
329
|
+
|
|
330
|
+
metadata = MetaData()
|
|
331
|
+
|
|
332
|
+
class Orders(Table, table_name='orders', metadata=metadata):
|
|
333
|
+
id = Column(String, primary_key=True)
|
|
334
|
+
user_id = Column(String, nullable=False)
|
|
335
|
+
order_number = Column(String, nullable=False)
|
|
336
|
+
amount = Column(Integer)
|
|
337
|
+
status = Column(String)
|
|
338
|
+
|
|
339
|
+
constraints = [
|
|
340
|
+
UniqueConstraint('order_number', name='uq_orders_order_number'),
|
|
341
|
+
CheckConstraint('amount >= 0', name='ck_orders_non_negative_amount'),
|
|
342
|
+
Index('ix_orders_user_status', 'user_id', 'status')
|
|
343
|
+
]
|
|
344
|
+
|
|
345
|
+
sa_table = metadata.tables['orders']
|
|
346
|
+
|
|
347
|
+
# Verify all constraints are present
|
|
348
|
+
unique_constraints = [c for c in sa_table.constraints if isinstance(c, UniqueConstraint)]
|
|
349
|
+
check_constraints = [c for c in sa_table.constraints if isinstance(c, CheckConstraint)]
|
|
350
|
+
|
|
351
|
+
assert len(unique_constraints) == 1
|
|
352
|
+
assert len(check_constraints) == 1
|
|
353
|
+
|
|
354
|
+
# Verify index
|
|
355
|
+
assert len(sa_table.indexes) == 1
|
|
356
|
+
idx = list(sa_table.indexes)[0]
|
|
357
|
+
assert idx.name == 'ix_orders_user_status'
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def test_table_with_constraints_as_tuple():
|
|
361
|
+
"""Test that constraints attribute works with tuple format"""
|
|
362
|
+
from sqlalchemy import UniqueConstraint
|
|
363
|
+
|
|
364
|
+
metadata = MetaData()
|
|
365
|
+
|
|
366
|
+
class Items(Table, table_name='items', metadata=metadata):
|
|
367
|
+
id = Column(Integer, primary_key=True)
|
|
368
|
+
category = Column(String)
|
|
369
|
+
code = Column(String)
|
|
370
|
+
|
|
371
|
+
constraints = (
|
|
372
|
+
UniqueConstraint('category', 'code', name='uq_items_category_code'),
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
sa_table = metadata.tables['items']
|
|
376
|
+
|
|
377
|
+
unique_constraints = [c for c in sa_table.constraints if isinstance(c, UniqueConstraint)]
|
|
378
|
+
assert len(unique_constraints) == 1
|
|
379
|
+
assert unique_constraints[0].name == 'uq_items_category_code'
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def test_table_with_comment():
|
|
383
|
+
"""Test that comment parameter is passed to SQLAlchemy table"""
|
|
384
|
+
metadata = MetaData()
|
|
385
|
+
|
|
386
|
+
class Settings(Table, table_name='settings', metadata=metadata, comment='Application settings and configuration'):
|
|
387
|
+
id = Column(Integer, primary_key=True)
|
|
388
|
+
key = Column(String, nullable=False)
|
|
389
|
+
value = Column(String)
|
|
390
|
+
|
|
391
|
+
sa_table = metadata.tables['settings']
|
|
392
|
+
assert sa_table.comment == 'Application settings and configuration'
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def test_table_with_constraints_and_comment():
|
|
396
|
+
"""Test that both constraints and comment work together"""
|
|
397
|
+
from sqlalchemy import UniqueConstraint
|
|
398
|
+
|
|
399
|
+
metadata = MetaData()
|
|
400
|
+
|
|
401
|
+
class ApiKeys(Table, table_name='api_keys', metadata=metadata, comment='API authentication keys'):
|
|
402
|
+
id = Column(String, primary_key=True)
|
|
403
|
+
user_id = Column(String, nullable=False)
|
|
404
|
+
key_hash = Column(String, nullable=False)
|
|
405
|
+
|
|
406
|
+
constraints = [
|
|
407
|
+
UniqueConstraint('key_hash', name='uq_api_keys_key_hash')
|
|
408
|
+
]
|
|
409
|
+
|
|
410
|
+
sa_table = metadata.tables['api_keys']
|
|
411
|
+
|
|
412
|
+
assert sa_table.comment == 'API authentication keys'
|
|
413
|
+
|
|
414
|
+
unique_constraints = [c for c in sa_table.constraints if isinstance(c, UniqueConstraint)]
|
|
415
|
+
assert len(unique_constraints) == 1
|
|
416
|
+
assert unique_constraints[0].name == 'uq_api_keys_key_hash'
|
|
417
|
+
|
|
418
|
+
# Query builder still works
|
|
419
|
+
query = ApiKeys.select().where(ApiKeys.user_id == 'user123')
|
|
420
|
+
sql, params = query.render()
|
|
421
|
+
assert 'WHERE api_keys.user_id = ?' in sql
|
|
422
|
+
assert params == ['user123']
|
|
@@ -210,11 +210,30 @@ class Table:
|
|
|
210
210
|
class Users(Table, metadata=metadata):
|
|
211
211
|
id: Column = SACol(Integer, primary_key=True)
|
|
212
212
|
name: Column = SACol(String(100))
|
|
213
|
+
|
|
214
|
+
Table-level constraints (for SQLAlchemy/Alembic migrations):
|
|
215
|
+
|
|
216
|
+
from sqlalchemy import UniqueConstraint, CheckConstraint
|
|
217
|
+
|
|
218
|
+
class Users(Table, metadata=metadata):
|
|
219
|
+
id = SAColumn(String, primary_key=True)
|
|
220
|
+
tenant_id = SAColumn(String)
|
|
221
|
+
email = SAColumn(String)
|
|
222
|
+
|
|
223
|
+
constraints = [
|
|
224
|
+
UniqueConstraint('tenant_id', 'email', name='uq_users_tenant_email'),
|
|
225
|
+
CheckConstraint('length(email) > 0', name='ck_users_email_not_empty')
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
Table comment (for database documentation in migrations):
|
|
229
|
+
|
|
230
|
+
class Users(Table, metadata=metadata, comment='Application user accounts'):
|
|
231
|
+
id = SAColumn(Integer, primary_key=True)
|
|
213
232
|
"""
|
|
214
233
|
table_name: ClassVar[str]
|
|
215
234
|
schema: ClassVar[Optional[str]] = None
|
|
216
235
|
|
|
217
|
-
def __init_subclass__(cls, table_name: Optional[str] = None, metadata: Optional[Any] = None, schema: Optional[str] = None, **kwargs):
|
|
236
|
+
def __init_subclass__(cls, table_name: Optional[str] = None, metadata: Optional[Any] = None, schema: Optional[str] = None, comment: Optional[str] = None, **kwargs):
|
|
218
237
|
super().__init_subclass__(**kwargs)
|
|
219
238
|
|
|
220
239
|
# Set table_name: use provided name, or default to lowercase class name
|
|
@@ -334,7 +353,17 @@ class Table:
|
|
|
334
353
|
|
|
335
354
|
# Create SQLAlchemy Table if metadata provided
|
|
336
355
|
if metadata is not None and HAS_SQLALCHEMY:
|
|
337
|
-
|
|
356
|
+
# Extract constraints from class attribute (supports both tuple and list)
|
|
357
|
+
table_constraints = getattr(cls, 'constraints', [])
|
|
358
|
+
if isinstance(table_constraints, tuple):
|
|
359
|
+
table_constraints = list(table_constraints)
|
|
360
|
+
|
|
361
|
+
# Build keyword args for SATable
|
|
362
|
+
table_kwargs = {'schema': schema}
|
|
363
|
+
if comment is not None:
|
|
364
|
+
table_kwargs['comment'] = comment
|
|
365
|
+
|
|
366
|
+
cls._sa_table = SATable(cls.table_name, metadata, *sa_columns, *table_constraints, **table_kwargs)
|
|
338
367
|
|
|
339
368
|
# Add the ALL column for wildcard column selection
|
|
340
369
|
cls.ALL = Column(cls.table_name, '*', schema=schema)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|