Skip to content

Table Management Example

Learn to create and manage Airtable schemas programmatically.


Overview

This example demonstrates:

  • Creating tables from Pydantic models
  • Customizing field types and options
  • Schema synchronization
  • Handling model evolution

Complete Code

"""
Table Management Example for Pydantic Airtable
"""
from pydantic_airtable import (
    airtable_model,
    configure_from_env,
    airtable_field,
    AirtableFieldType,
    AirtableManager,
    AirtableConfig
)
from pydantic import BaseModel
from typing import Optional, List
from datetime import datetime
from enum import Enum

configure_from_env()

# Enum definitions
class TaskStatus(str, Enum):
    TODO = "To Do"
    IN_PROGRESS = "In Progress"
    REVIEW = "Review"
    DONE = "Done"

class Priority(str, Enum):
    LOW = "Low"
    MEDIUM = "Medium"
    HIGH = "High"
    URGENT = "Urgent"

# Model definitions
@airtable_model(table_name="Tasks")
class Task(BaseModel):
    title: str
    description: Optional[str] = None  # LONG_TEXT (detected)
    status: TaskStatus = TaskStatus.TODO
    priority: Priority = Priority.MEDIUM
    assignee_email: Optional[str] = None  # EMAIL (detected)
    due_date: Optional[datetime] = None
    is_blocked: bool = False

    # Custom field with explicit options
    tags: List[str] = airtable_field(
        field_type=AirtableFieldType.MULTI_SELECT,
        choices=["Bug", "Feature", "Documentation", "Refactor"],
        default=[]
    )

    # Custom field name in Airtable
    estimated_hours: Optional[float] = airtable_field(
        field_name="Estimated Hours",
        default=None
    )

@airtable_model(table_name="Users")
class User(BaseModel):
    name: str
    email: str
    phone: Optional[str] = None
    department: Optional[str] = None
    is_active: bool = True

@airtable_model(table_name="Projects")
class Project(BaseModel):
    name: str
    description: Optional[str] = None
    status: str = airtable_field(
        field_type=AirtableFieldType.SELECT,
        choices=["Planning", "Active", "On Hold", "Completed"],
        default="Planning"
    )
    budget: Optional[float] = airtable_field(
        field_name="Budget ($)",
        field_type=AirtableFieldType.CURRENCY,
        default=None
    )
    start_date: Optional[datetime] = None
    end_date: Optional[datetime] = None

def create_tables():
    """Create all tables from models"""
    models = [Task, User, Project]

    for model in models:
        try:
            result = model.create_table()
            print(f"✅ Created {model.__name__}: {result['id']}")
        except Exception as e:
            if "already exists" in str(e).lower():
                print(f"ℹ️ {model.__name__} already exists")
            else:
                print(f"❌ Error creating {model.__name__}: {e}")

def sync_schema():
    """Sync model changes to existing tables"""
    models = [Task, User, Project]

    for model in models:
        result = model.sync_table(
            create_missing_fields=True,
            update_field_types=False
        )
        print(f"\n{model.__name__} sync:")
        print(f"  Created: {result.get('fields_created', [])}")
        print(f"  Skipped: {result.get('fields_skipped', [])}")

def demonstrate_crud():
    """Demonstrate CRUD with managed tables"""

    # Create a user
    user = User.create(
        name="Alice Johnson",
        email="alice@example.com",
        department="Engineering"
    )
    print(f"\n✅ Created user: {user.name}")

    # Create a project
    project = Project.create(
        name="Website Redesign",
        description="Complete redesign of company website",
        budget=50000.00
    )
    print(f"✅ Created project: {project.name}")

    # Create tasks
    tasks = Task.bulk_create([
        {
            "title": "Design mockups",
            "description": "Create initial design mockups",
            "priority": Priority.HIGH,
            "assignee_email": "alice@example.com",
            "tags": ["Feature"],
            "estimated_hours": 8.0
        },
        {
            "title": "Review designs",
            "description": "Team review of mockups",
            "priority": Priority.MEDIUM,
            "tags": ["Feature"],
            "estimated_hours": 2.0
        }
    ])
    print(f"✅ Created {len(tasks)} tasks")

    # Query and update
    high_priority = Task.find_by(priority=Priority.HIGH)
    print(f"\n🔍 High priority tasks: {len(high_priority)}")

    for task in high_priority:
        task.status = TaskStatus.IN_PROGRESS
        task.save()
        print(f"  Updated: {task.title}")

def main():
    print("=== Table Management Demo ===\n")

    print("1. Creating tables...")
    create_tables()

    print("\n2. Syncing schema...")
    sync_schema()

    print("\n3. CRUD operations...")
    demonstrate_crud()

    print("\n✅ Demo complete!")

if __name__ == "__main__":
    main()

Key Concepts

Table Creation from Models

# Define model
@airtable_model(table_name="Tasks")
class Task(BaseModel):
    title: str
    status: TaskStatus = TaskStatus.TODO

# Create table
result = Task.create_table()
# Returns: {'id': 'tblXXX', 'name': 'Tasks', 'fields': [...]}

Field Type Mapping

Model Definition Airtable Field
title: str singleLineText
description: str multilineText (detected)
status: TaskStatus singleSelect with enum values
priority: Priority singleSelect with enum values
due_date: datetime dateTime
is_blocked: bool checkbox
tags: List[str] multipleSelects

Custom Field Configuration

# Explicit type
tags: List[str] = airtable_field(
    field_type=AirtableFieldType.MULTI_SELECT,
    choices=["Bug", "Feature", "Documentation"]
)

# Custom Airtable name
budget: float = airtable_field(
    field_name="Budget ($)",
    field_type=AirtableFieldType.CURRENCY
)

Schema Synchronization

# Add new fields to existing table
result = Task.sync_table(
    create_missing_fields=True,  # Add missing fields
    update_field_types=False     # Don't change existing types
)

Running the Example

cd examples/table_management
pip install -r requirements.txt

# Set environment
export AIRTABLE_ACCESS_TOKEN="pat_your_token"
export AIRTABLE_BASE_ID="appYourBaseId"

python table_management.py

Expected Output

=== Table Management Demo ===

1. Creating tables...
✅ Created Task: tblXXXXXXXXXXXXXX
✅ Created User: tblYYYYYYYYYYYYYY
✅ Created Project: tblZZZZZZZZZZZZZZ

2. Syncing schema...
Task sync:
  Created: []
  Skipped: []
User sync:
  Created: []
  Skipped: []
Project sync:
  Created: []
  Skipped: []

3. CRUD operations...
✅ Created user: Alice Johnson
✅ Created project: Website Redesign
✅ Created 2 tasks

🔍 High priority tasks: 1
  Updated: Design mockups

✅ Demo complete!

Schema Evolution Pattern

Adding New Fields

# Original model
@airtable_model(table_name="Tasks")
class Task(BaseModel):
    title: str
    status: str

# After creating table, add new fields
@airtable_model(table_name="Tasks")
class Task(BaseModel):
    title: str
    status: str
    priority: Optional[str] = None  # New field
    due_date: Optional[datetime] = None  # New field

# Sync to add new fields
Task.sync_table(create_missing_fields=True)

Migration Script

def migrate_v1_to_v2():
    """
    Migration: Add priority and tags to Tasks
    """
    @airtable_model(table_name="Tasks")
    class TaskV2(BaseModel):
        title: str
        status: str
        # New in v2
        priority: str = airtable_field(
            field_type=AirtableFieldType.SELECT,
            choices=["Low", "Medium", "High"],
            default="Medium"
        )
        tags: List[str] = airtable_field(
            field_type=AirtableFieldType.MULTI_SELECT,
            choices=["Bug", "Feature"],
            default=[]
        )

    result = TaskV2.sync_table(create_missing_fields=True)

    print("Migration complete:")
    print(f"  Fields added: {result['fields_created']}")

    # Optionally, set default values for existing records
    tasks = TaskV2.all()
    for task in tasks:
        if not task.priority:
            task.priority = "Medium"
            task.save()

Using AirtableManager

For advanced operations, use the manager directly:

from pydantic_airtable import AirtableManager, AirtableConfig

config = AirtableConfig(
    access_token="pat_xxx",
    base_id="appXXX"
)

manager = AirtableManager(config)

# List bases
bases = manager.list_bases()
for base in bases:
    print(f"{base['name']}: {base['id']}")

# Get base schema
schema = manager.get_base_schema()
for table in schema['tables']:
    print(f"\nTable: {table['name']}")
    for field in table['fields']:
        print(f"  - {field['name']}: {field['type']}")

# Create custom table
manager.create_table("CustomTable", [
    {"name": "Name", "type": "singleLineText"},
    {"name": "Score", "type": "number", "options": {"precision": 2}}
])

Best Practices

1. Version Your Schemas

# Keep track of schema versions
SCHEMA_VERSION = "2.0.0"

def check_schema_version():
    """Verify schema is up to date"""
    # Store version in a Settings table
    pass

2. Test Migrations First

# Use a test base for migrations
@pytest.fixture
def test_base():
    return AirtableConfig(
        access_token=os.getenv("TEST_TOKEN"),
        base_id=os.getenv("TEST_BASE")
    )

def test_migration(test_base):
    # Test migration on test base first
    pass

3. Document Schema Changes

@airtable_model(table_name="Tasks")
class Task(BaseModel):
    """
    Task model - v2.0

    Changes from v1.0:
    - Added priority field (SELECT)
    - Added tags field (MULTI_SELECT)
    - Renamed 'desc' to 'description'
    """
    title: str
    description: Optional[str] = None
    priority: Priority = Priority.MEDIUM
    tags: List[str] = []

Next Steps