Push Tracker
ria-ran--sim-drop/plan/01-database.md

91 lines
2.9 KiB
Markdown
Raw Normal View History

# Unit 01 — Database Layer
## Goal
SQLite persistence layer with CRUD operations and MAC-address-based rate limiting.
## Files
- `server/database.py`
- `server/models.py`
---
## `server/models.py`
```python
from dataclasses import dataclass
from datetime import datetime
from typing import Optional
@dataclass
class Subscriber:
id: Optional[int]
imsi: str
iccid: str
ki: str # hex string
opc: str # hex string
mac_address: Optional[str]
created_at: datetime
synced_to_core: bool
```
---
## `server/database.py`
### Schema
```sql
CREATE TABLE IF NOT EXISTS subscribers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
imsi TEXT UNIQUE NOT NULL,
iccid TEXT UNIQUE NOT NULL,
ki TEXT NOT NULL,
opc TEXT NOT NULL,
mac_address TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
synced_to_core BOOLEAN DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_imsi ON subscribers(imsi);
CREATE INDEX IF NOT EXISTS idx_mac_address ON subscribers(mac_address);
```
### Functions
| Function | Signature | Returns | Notes |
|----------|-----------|---------|-------|
| `init_db` | `(db_path: str) -> None` | — | Creates schema idempotently |
| `get_connection` | `(db_path: str) -> sqlite3.Connection` | connection | `row_factory = sqlite3.Row` |
| `add_subscriber` | `(conn, imsi, iccid, ki, opc, mac_address) -> int` | row `id` | Raises `ValueError` on duplicate IMSI/ICCID |
| `get_subscriber_by_imsi` | `(conn, imsi: str) -> Optional[Subscriber]` | Subscriber or None | |
| `get_subscriber_by_id` | `(conn, sub_id: int) -> Optional[Subscriber]` | Subscriber or None | |
| `list_unsynced` | `(conn) -> list[Subscriber]` | list | WHERE synced_to_core = 0 |
| `mark_synced` | `(conn, imsi: str) -> None` | — | UPDATE synced_to_core = 1 |
| `check_rate_limit` | `(conn, mac_address: str, window_hours: int) -> bool` | True = allowed | Counts rows for mac in last N hours |
| `export_all` | `(conn) -> list[dict]` | list of dicts | All rows as serializable dicts |
### Rate Limiting Logic
```python
def check_rate_limit(conn, mac_address, window_hours):
cutoff = datetime.utcnow() - timedelta(hours=window_hours)
cur = conn.execute(
"SELECT COUNT(*) FROM subscribers WHERE mac_address = ? AND created_at > ?",
(mac_address, cutoff.isoformat())
)
count = cur.fetchone()[0]
return count == 0 # True = allowed (no prior provisioning in window)
```
## Verification
```bash
python -c "
from server.database import init_db, get_connection, add_subscriber, check_rate_limit
import tempfile, os
db = tempfile.mktemp(suffix='.db')
init_db(db)
conn = get_connection(db)
sid = add_subscriber(conn, '302720000000001', '8910302720000000001', 'aa'*16, 'bb'*16, 'aa:bb:cc:dd:ee:ff')
print('inserted id:', sid)
print('rate limit (should be False now):', check_rate_limit(conn, 'aa:bb:cc:dd:ee:ff', 1))
conn.close(); os.unlink(db)
"
```