DynamoDB Tables
Table definitions are the heart of Da Vinci. They serve as the single source of truth for both your application code and infrastructure.
Table Objects
Table objects define your DynamoDB tables using Python classes that inherit from TableObject.
Basic Table Definition
from da_vinci.core.orm.table_object import (
TableObject,
TableObjectAttribute,
TableObjectAttributeType,
)
class UserTable(TableObject):
"""User table for storing user profiles"""
table_name = "users"
partition_key_attribute = "user_id"
attributes = [
TableObjectAttribute(
name="user_id",
attribute_type=TableObjectAttributeType.STRING,
description="Unique user identifier",
),
TableObjectAttribute(
name="email",
attribute_type=TableObjectAttributeType.STRING,
description="User email address",
),
TableObjectAttribute(
name="name",
attribute_type=TableObjectAttributeType.STRING,
description="User full name",
),
]
Attribute Types
Da Vinci supports various attribute types:
- STRING
Plain text strings
- NUMBER
Numeric values (integers or floats)
- BOOLEAN
True/False values
- DATETIME
Python datetime objects (stored as ISO strings)
- JSON
Native DynamoDB JSON (not safe for empty attributes)
- JSON_STRING
JSON serialized as string (safe for empty attributes)
- STRING_LIST
List of strings
- NUMBER_LIST
List of numbers
- JSON_LIST
List of JSON objects (native)
- JSON_STRING_LIST
List of JSON objects (as strings)
- COMPOSITE_STRING
Multiple values combined into a single string (useful for composite keys)
- STRING_SET
DynamoDB string set
- NUMBER_SET
DynamoDB number set
Example with Various Types
class ProductTable(TableObject):
table_name = "products"
partition_key_attribute = "product_id"
attributes = [
TableObjectAttribute(
name="product_id",
attribute_type=TableObjectAttributeType.STRING,
),
TableObjectAttribute(
name="price",
attribute_type=TableObjectAttributeType.NUMBER,
),
TableObjectAttribute(
name="in_stock",
attribute_type=TableObjectAttributeType.BOOLEAN,
),
TableObjectAttribute(
name="tags",
attribute_type=TableObjectAttributeType.STRING_LIST,
),
TableObjectAttribute(
name="metadata",
attribute_type=TableObjectAttributeType.JSON_STRING,
),
TableObjectAttribute(
name="created_at",
attribute_type=TableObjectAttributeType.DATETIME,
),
]
Composite Keys
Use composite strings for multi-part partition or sort keys:
class OrderItemTable(TableObject):
table_name = "order_items"
partition_key_attribute = "composite_key"
sort_key_attribute = "item_id"
attributes = [
TableObjectAttribute(
name="composite_key",
attribute_type=TableObjectAttributeType.COMPOSITE_STRING,
argument_names=["order_id", "user_id"],
),
TableObjectAttribute(
name="item_id",
attribute_type=TableObjectAttributeType.STRING,
),
TableObjectAttribute(
name="quantity",
attribute_type=TableObjectAttributeType.NUMBER,
),
]
# Usage
item = OrderItemTable(
order_id="order-123",
user_id="user-456",
item_id="item-789",
quantity=2
)
Global Secondary Indexes
Add GSIs for alternative query patterns:
class UserTable(TableObject):
table_name = "users"
partition_key_attribute = "user_id"
global_secondary_indexes = [
{
"index_name": "email_index",
"partition_key": "email",
},
{
"index_name": "status_created_index",
"partition_key": "status",
"sort_key": "created_at",
},
]
attributes = [
TableObjectAttribute(
name="user_id",
attribute_type=TableObjectAttributeType.STRING,
),
TableObjectAttribute(
name="email",
attribute_type=TableObjectAttributeType.STRING,
),
TableObjectAttribute(
name="status",
attribute_type=TableObjectAttributeType.STRING,
),
TableObjectAttribute(
name="created_at",
attribute_type=TableObjectAttributeType.DATETIME,
),
]
Local Secondary Indexes
Add LSIs for alternative sort key patterns on the same partition key:
class EventTable(TableObject):
table_name = "events"
partition_key_attribute = "user_id"
sort_key_attribute = "event_id"
local_secondary_indexes = [
{
"index_name": "user_timestamp_index",
"sort_key": "timestamp",
},
]
attributes = [
TableObjectAttribute(
name="user_id",
attribute_type=TableObjectAttributeType.STRING,
),
TableObjectAttribute(
name="event_id",
attribute_type=TableObjectAttributeType.STRING,
),
TableObjectAttribute(
name="timestamp",
attribute_type=TableObjectAttributeType.DATETIME,
),
]
Optional Attributes and Defaults
Make attributes optional or provide default values:
class ConfigTable(TableObject):
table_name = "config"
partition_key_attribute = "key"
attributes = [
TableObjectAttribute(
name="key",
attribute_type=TableObjectAttributeType.STRING,
),
TableObjectAttribute(
name="value",
attribute_type=TableObjectAttributeType.STRING,
),
TableObjectAttribute(
name="enabled",
attribute_type=TableObjectAttributeType.BOOLEAN,
default=True, # Default value
),
TableObjectAttribute(
name="description",
attribute_type=TableObjectAttributeType.STRING,
optional=True, # Explicitly optional
),
]
Custom Import/Export
Use custom functions to transform data during import/export:
def encrypt_data(value):
# Custom encryption logic
return encrypted_value
def decrypt_data(value):
# Custom decryption logic
return decrypted_value
class SecureTable(TableObject):
table_name = "secure_data"
partition_key_attribute = "id"
attributes = [
TableObjectAttribute(
name="id",
attribute_type=TableObjectAttributeType.STRING,
),
TableObjectAttribute(
name="sensitive_data",
attribute_type=TableObjectAttributeType.STRING,
custom_exporter=encrypt_data,
custom_importer=decrypt_data,
),
]
Table Client Operations
Use the TableClient for CRUD operations:
Get Item
from da_vinci.core.orm.client import TableClient
client = TableClient(UserTable)
user = client.get("user-123")
Put Item
user = UserTable(
user_id="user-123",
email="alice@example.com",
name="Alice"
)
client.put(user)
Delete Item
client.delete("user-123")
Scan Table
# Scan all items
for user in client.scan():
print(user.name)
# Scan with filter
from da_vinci.core.orm.client import TableScanDefinition
scan_def = TableScanDefinition(UserTable)
scan_def.add("email", "contains", "@example.com")
for user in client.scan(scan_definition=scan_def):
print(user.email)
Query Index
# Query GSI
users = client.query(
index_name="email_index",
partition_key_value="alice@example.com"
)
# Query with sort key condition
users = client.query(
index_name="status_created_index",
partition_key_value="active",
sort_key_condition="created_at > :date",
expression_values={":date": some_date}
)
Best Practices
- Centralize Definitions
Keep all table definitions in a single module (e.g.,
tables.py) that both runtime and CDK code can import.- Use Descriptive Names
Give tables and attributes clear, descriptive names that indicate their purpose.
- Add Descriptions
Use the
descriptionparameter for documentation and LLM context.- Choose Appropriate Types
Use
JSON_STRINGinstead ofJSONif you need to store empty values.- Index Wisely
Only create indexes you actually need. Each GSI has a cost.
- Consider Access Patterns
Design your partition and sort keys based on how you’ll query the data.
- Use Composite Keys When Needed
Composite keys allow multiple attributes to form a single key, enabling more flexible access patterns.