|
| 1 | +--- |
| 2 | +sidebar_position: 3 |
| 3 | +--- |
| 4 | + |
| 5 | +# Interact with Lakebase Tables |
| 6 | + |
| 7 | +This recipe demonstrates how to build a complete orders management API using Lakebase PostgreSQL database. These endpoints provide CRUD operations and various query patterns for handling orders data synchronized from Databricks Unity Catalog. |
| 8 | + |
| 9 | +:::info Prerequisites |
| 10 | +- Lakebase resources must be created using the [Create Lakebase Resources](./lakebase_resources_create.mdx) endpoint |
| 11 | +- The synced table pipeline must be completed and orders data synchronized from `samples.tpch.orders` |
| 12 | +- Database connection must be configured in your application environment |
| 13 | +::: |
| 14 | + |
| 15 | +:::info |
| 16 | +In this example, we demonstrate multiple HTTP methods (GET, POST) which are the standard choices for data operations in REST APIs as defined in RFC 7231: |
| 17 | +- `GET` requests are idempotent and cacheable, ideal for data retrieval |
| 18 | +- `POST` requests for updates and modifications |
| 19 | + |
| 20 | +For detailed specifications, refer to [RFC 7231](https://datatracker.ietf.org/doc/html/rfc7231) which defines HTTP method semantics. |
| 21 | +::: |
| 22 | + |
| 23 | +## Code snippet |
| 24 | + |
| 25 | +```python title="routes/v1/orders.py" |
| 26 | +import logging |
| 27 | + |
| 28 | +from config.database import get_async_db |
| 29 | +from models.orders import ( |
| 30 | + CursorPaginationInfo, |
| 31 | + Order, |
| 32 | + OrderCount, |
| 33 | + OrderListCursorResponse, |
| 34 | + OrderListResponse, |
| 35 | + OrderRead, |
| 36 | + OrderSample, |
| 37 | + OrderStatusUpdate, |
| 38 | + OrderStatusUpdateResponse, |
| 39 | + PaginationInfo, |
| 40 | +) |
| 41 | +from sqlalchemy import func, select |
| 42 | +from sqlalchemy.ext.asyncio import AsyncSession |
| 43 | + |
| 44 | +from fastapi import APIRouter, Depends, HTTPException, Query |
| 45 | + |
| 46 | +logger = logging.getLogger(__name__) |
| 47 | +router = APIRouter(tags=["orders"]) |
| 48 | + |
| 49 | +# 1. GET ORDERS COUNT |
| 50 | +@router.get("/count", response_model=OrderCount, summary="Get total order count") |
| 51 | +async def get_order_count(db: AsyncSession = Depends(get_async_db)): |
| 52 | + try: |
| 53 | + stmt = select(func.count(Order.o_orderkey)) |
| 54 | + result = await db.execute(stmt) |
| 55 | + count = result.scalar() |
| 56 | + return OrderCount(total_orders=count) |
| 57 | + except Exception as e: |
| 58 | + logger.error(f"Error getting order count: {e}") |
| 59 | + raise HTTPException(status_code=500, detail="Failed to retrieve order count") |
| 60 | + |
| 61 | +# 2. GET SAMPLE ORDERS |
| 62 | +@router.get("/sample", response_model=OrderSample, summary="Get 5 random order keys") |
| 63 | +async def get_sample_orders(db: AsyncSession = Depends(get_async_db)): |
| 64 | + try: |
| 65 | + stmt = select(Order.o_orderkey).limit(5) |
| 66 | + result = await db.execute(stmt) |
| 67 | + order_keys = result.scalars().all() |
| 68 | + return OrderSample(sample_order_keys=order_keys) |
| 69 | + except Exception as e: |
| 70 | + logger.error(f"Error getting sample orders: {e}") |
| 71 | + raise HTTPException(status_code=500, detail="Failed to retrieve sample orders") |
| 72 | + |
| 73 | +# 3. PAGE-BASED PAGINATION |
| 74 | +@router.get("/pages", response_model=OrderListResponse, summary="Get orders with page-based pagination") |
| 75 | +async def get_orders_by_page( |
| 76 | + page: int = Query(1, ge=1, description="Page number (1-based)"), |
| 77 | + page_size: int = Query(100, ge=1, le=1000, description="Number of records per page (max 1000)"), |
| 78 | + include_count: bool = Query(True, description="Include total count for pagination info"), |
| 79 | + db: AsyncSession = Depends(get_async_db), |
| 80 | +): |
| 81 | + try: |
| 82 | + if include_count: |
| 83 | + count_stmt = select(func.count(Order.o_orderkey)) |
| 84 | + count_result = await db.execute(count_stmt) |
| 85 | + total_count = count_result.scalar() |
| 86 | + total_pages = (total_count + page_size - 1) // page_size |
| 87 | + else: |
| 88 | + total_count = -1 |
| 89 | + total_pages = -1 |
| 90 | + |
| 91 | + offset = (page - 1) * page_size |
| 92 | + stmt = ( |
| 93 | + select(Order) |
| 94 | + .order_by(Order.o_orderkey) |
| 95 | + .offset(offset) |
| 96 | + .limit(page_size + 1) |
| 97 | + ) |
| 98 | + |
| 99 | + result = await db.execute(stmt) |
| 100 | + all_orders = result.scalars().all() |
| 101 | + |
| 102 | + has_next = len(all_orders) > page_size |
| 103 | + orders = all_orders[:page_size] |
| 104 | + has_previous = page > 1 |
| 105 | + |
| 106 | + pagination_info = PaginationInfo( |
| 107 | + page=page, |
| 108 | + page_size=page_size, |
| 109 | + total_pages=total_pages, |
| 110 | + total_count=total_count, |
| 111 | + has_next=has_next, |
| 112 | + has_previous=has_previous, |
| 113 | + ) |
| 114 | + |
| 115 | + return OrderListResponse(orders=orders, pagination=pagination_info) |
| 116 | + |
| 117 | + except Exception as e: |
| 118 | + logger.error(f"Error getting page-based orders: {e}") |
| 119 | + raise HTTPException(status_code=500, detail="Failed to retrieve orders") |
| 120 | + |
| 121 | +# 4. CURSOR-BASED PAGINATION |
| 122 | +@router.get("/stream", response_model=OrderListCursorResponse, summary="Get orders with cursor-based pagination") |
| 123 | +async def get_orders_by_cursor( |
| 124 | + cursor: int = Query(0, ge=0, description="Start after this order key (0 for beginning)"), |
| 125 | + page_size: int = Query(100, ge=1, le=1000, description="Number of records to fetch (max 1000)"), |
| 126 | + db: AsyncSession = Depends(get_async_db), |
| 127 | +): |
| 128 | + try: |
| 129 | + stmt = ( |
| 130 | + select(Order) |
| 131 | + .where(Order.o_orderkey > cursor) |
| 132 | + .order_by(Order.o_orderkey) |
| 133 | + .limit(page_size + 1) |
| 134 | + ) |
| 135 | + |
| 136 | + result = await db.execute(stmt) |
| 137 | + all_orders = result.scalars().all() |
| 138 | + |
| 139 | + has_next = len(all_orders) > page_size |
| 140 | + orders = all_orders[:page_size] |
| 141 | + has_previous = cursor > 0 |
| 142 | + |
| 143 | + next_cursor = orders[-1].o_orderkey if orders and has_next else None |
| 144 | + previous_cursor = max(0, cursor - page_size) if has_previous else None |
| 145 | + |
| 146 | + pagination_info = CursorPaginationInfo( |
| 147 | + page_size=page_size, |
| 148 | + has_next=has_next, |
| 149 | + has_previous=has_previous, |
| 150 | + next_cursor=next_cursor, |
| 151 | + previous_cursor=previous_cursor, |
| 152 | + ) |
| 153 | + |
| 154 | + return OrderListCursorResponse(orders=orders, pagination=pagination_info) |
| 155 | + |
| 156 | + except Exception as e: |
| 157 | + logger.error(f"Error getting cursor-based orders: {e}") |
| 158 | + raise HTTPException(status_code=500, detail="Failed to retrieve orders") |
| 159 | + |
| 160 | +# 5. GET SPECIFIC ORDER |
| 161 | +@router.get("/{order_key}", response_model=OrderRead, summary="Get an order by its key") |
| 162 | +async def read_order(order_key: int, db: AsyncSession = Depends(get_async_db)): |
| 163 | + try: |
| 164 | + if order_key <= 0: |
| 165 | + raise HTTPException(status_code=400, detail="Invalid order key provided") |
| 166 | + |
| 167 | + stmt = select(Order).where(Order.o_orderkey == order_key) |
| 168 | + result = await db.execute(stmt) |
| 169 | + order = result.scalars().first() |
| 170 | + |
| 171 | + if not order: |
| 172 | + raise HTTPException(status_code=404, detail=f"Order with key '{order_key}' not found") |
| 173 | + |
| 174 | + return order |
| 175 | + |
| 176 | + except HTTPException: |
| 177 | + raise |
| 178 | + except Exception as e: |
| 179 | + logger.error(f"Unexpected error fetching order {order_key}: {e}") |
| 180 | + raise HTTPException(status_code=500, detail="Internal server error occurred") |
| 181 | + |
| 182 | +# 6. UPDATE ORDER STATUS |
| 183 | +@router.post("/{order_key}/status", response_model=OrderStatusUpdateResponse, summary="Update order status") |
| 184 | +async def update_order_status( |
| 185 | + order_key: int, |
| 186 | + status_data: OrderStatusUpdate, |
| 187 | + db: AsyncSession = Depends(get_async_db), |
| 188 | +): |
| 189 | + try: |
| 190 | + if order_key <= 0: |
| 191 | + raise HTTPException(status_code=400, detail="Invalid order key provided") |
| 192 | + |
| 193 | + check_stmt = select(Order).where(Order.o_orderkey == order_key) |
| 194 | + check_result = await db.execute(check_stmt) |
| 195 | + existing_order = check_result.scalars().first() |
| 196 | + |
| 197 | + if not existing_order: |
| 198 | + raise HTTPException(status_code=404, detail=f"Order with key '{order_key}' not found") |
| 199 | + |
| 200 | + existing_order.o_orderstatus = status_data.o_orderstatus |
| 201 | + await db.commit() |
| 202 | + await db.refresh(existing_order) |
| 203 | + |
| 204 | + return OrderStatusUpdateResponse( |
| 205 | + o_orderkey=order_key, |
| 206 | + o_orderstatus=status_data.o_orderstatus, |
| 207 | + message="Order status updated successfully", |
| 208 | + ) |
| 209 | + |
| 210 | + except HTTPException: |
| 211 | + raise |
| 212 | + except Exception as e: |
| 213 | + logger.error(f"Error updating status for order {order_key}: {e}") |
| 214 | + raise HTTPException(status_code=500, detail="Failed to update order status") |
| 215 | +``` |
| 216 | + |
| 217 | +:::warning |
| 218 | + |
| 219 | +The above example is shortened for brevity and not suitable for production use. |
| 220 | +You can find a more advanced sample in the databricks-apps-cookbook GitHub repository. |
| 221 | + |
| 222 | +::: |
| 223 | + |
| 224 | +## Example Usage |
| 225 | + |
| 226 | +The orders API provides six main endpoints for different use cases: |
| 227 | + |
| 228 | +| Endpoint | Method | Purpose | Best For | |
| 229 | +|----------|--------|---------|----------| |
| 230 | +| `/orders/count` | GET | Get total orders count | Dashboards, monitoring | |
| 231 | +| `/orders/sample` | GET | Get 5 sample order keys | Testing, development | |
| 232 | +| `/orders/pages` | GET | Page-based pagination | Traditional UIs with page numbers | |
| 233 | +| `/orders/stream` | GET | Cursor-based pagination | Large datasets, infinite scroll | |
| 234 | +| `/orders/{order_key}` | GET | Get specific order | Order details, lookups | |
| 235 | +| `/orders/{order_key}/status` | POST | Update order status | Order processing workflows | |
| 236 | + |
| 237 | +### Get Orders Count |
| 238 | + |
| 239 | +```bash |
| 240 | +curl -X GET "http://localhost:8000/api/v1/orders/count" |
| 241 | +``` |
| 242 | + |
| 243 | +```json |
| 244 | +{ |
| 245 | + "total_orders": 1500000 |
| 246 | +} |
| 247 | +``` |
| 248 | + |
| 249 | +### Get Sample Orders |
| 250 | + |
| 251 | +```bash |
| 252 | +curl -X GET "http://localhost:8000/api/v1/orders/sample" |
| 253 | +``` |
| 254 | + |
| 255 | +```json |
| 256 | +{ |
| 257 | + "sample_order_keys": [1, 32, 33, 34, 35] |
| 258 | +} |
| 259 | +``` |
| 260 | + |
| 261 | +### Page-Based Pagination |
| 262 | + |
| 263 | +```bash |
| 264 | +curl -X GET "http://localhost:8000/api/v1/orders/pages?page=1&page_size=2" |
| 265 | +``` |
| 266 | + |
| 267 | +```json |
| 268 | +{ |
| 269 | + "orders": [ |
| 270 | + { |
| 271 | + "o_orderkey": 1, |
| 272 | + "o_custkey": 370, |
| 273 | + "o_orderstatus": "O", |
| 274 | + "o_totalprice": 172799.49, |
| 275 | + "o_orderdate": "1996-01-02", |
| 276 | + "o_orderpriority": "5-LOW", |
| 277 | + "o_clerk": "Clerk#000000951", |
| 278 | + "o_shippriority": 0, |
| 279 | + "o_comment": "nstructions sleep furiously among" |
| 280 | + }, |
| 281 | + { |
| 282 | + "o_orderkey": 2, |
| 283 | + "o_custkey": 781, |
| 284 | + "o_orderstatus": "O", |
| 285 | + "o_totalprice": 46929.18, |
| 286 | + "o_orderdate": "1996-12-01", |
| 287 | + "o_orderpriority": "1-URGENT", |
| 288 | + "o_clerk": "Clerk#000000880", |
| 289 | + "o_shippriority": 0, |
| 290 | + "o_comment": "foxes. pending accounts at the pending" |
| 291 | + } |
| 292 | + ], |
| 293 | + "pagination": { |
| 294 | + "page": 1, |
| 295 | + "page_size": 2, |
| 296 | + "total_pages": 750000, |
| 297 | + "total_count": 1500000, |
| 298 | + "has_next": true, |
| 299 | + "has_previous": false |
| 300 | + } |
| 301 | +} |
| 302 | +``` |
| 303 | + |
| 304 | +### Update Order Status |
| 305 | + |
| 306 | +```bash |
| 307 | +curl -X POST "http://localhost:8000/api/v1/orders/1/status" \ |
| 308 | + -H "Content-Type: application/json" \ |
| 309 | + -d '{"o_orderstatus": "F"}' |
| 310 | +``` |
| 311 | + |
| 312 | +```json |
| 313 | +{ |
| 314 | + "o_orderkey": 1, |
| 315 | + "o_orderstatus": "F", |
| 316 | + "message": "Order status updated successfully" |
| 317 | +} |
| 318 | +``` |
| 319 | + |
| 320 | +## Resources |
| 321 | + |
| 322 | +- [Lakebase PostgreSQL](https://docs.databricks.com/en/database/index.html) |
| 323 | +- [SQLAlchemy Async](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html) |
| 324 | +- [FastAPI Dependencies](https://fastapi.tiangolo.com/tutorial/dependencies/) |
| 325 | + |
| 326 | +## Permissions |
| 327 | + |
| 328 | +Your [app service principal](https://docs.databricks.com/aws/en/dev-tools/databricks-apps/#how-does-databricks-apps-manage-authorization) needs the following permissions: |
| 329 | + |
| 330 | +- Database instance connection access via OAuth tokens |
| 331 | +- `SELECT` permissions on the `orders_synced` table in your Lakebase database |
| 332 | +- `UPDATE` permissions on the `orders_synced` table for status updates |
| 333 | +- Database user role with appropriate table access |
| 334 | + |
| 335 | +See [Lakebase permissions](https://docs.databricks.com/en/database/permissions.html) for more information. |
| 336 | + |
| 337 | +## Dependencies |
| 338 | + |
| 339 | +- [Databricks SDK for Python](https://pypi.org/project/databricks-sdk/) - `databricks-sdk` |
| 340 | +- [FastAPI](https://pypi.org/project/fastapi/) - `fastapi` |
| 341 | +- [SQLAlchemy](https://pypi.org/project/sqlalchemy/) - `sqlalchemy` |
| 342 | +- [SQLModel](https://pypi.org/project/sqlmodel/) - `sqlmodel` |
| 343 | +- [asyncpg](https://pypi.org/project/asyncpg/) - `asyncpg` |
| 344 | +- [uvicorn](https://pypi.org/project/uvicorn/) - `uvicorn` |
| 345 | + |
| 346 | +```python title="requirements.txt" |
| 347 | +databricks-sdk |
| 348 | +fastapi |
| 349 | +sqlalchemy |
| 350 | +sqlmodel |
| 351 | +asyncpg |
| 352 | +uvicorn |
| 353 | +``` |
0 commit comments