mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-08-06 17:02:48 +02:00
@@ -0,0 +1,193 @@
|
||||
# High-Resolution Timestamp Migration
|
||||
|
||||
This document describes the implementation of GitHub issue #55: storing high-resolution timestamps as integers in the database for improved performance and query efficiency.
|
||||
|
||||
## Overview
|
||||
|
||||
The meshview database now stores timestamps in two formats:
|
||||
1. **TEXT format** (`import_time`): Human-readable ISO8601 format with microseconds (e.g., `2025-03-12 04:15:56.058038`)
|
||||
2. **INTEGER format** (`import_time_us`): Microseconds since Unix epoch (1970-01-01 00:00:00 UTC)
|
||||
|
||||
The dual format approach provides:
|
||||
- **Backward compatibility**: Existing `import_time` TEXT columns remain unchanged
|
||||
- **Performance**: Fast integer comparisons and math operations
|
||||
- **Precision**: Microsecond resolution for accurate timing
|
||||
- **Efficiency**: Compact storage and fast indexed lookups
|
||||
|
||||
## Database Changes
|
||||
|
||||
### New Columns Added
|
||||
|
||||
Three tables have new `import_time_us` columns:
|
||||
|
||||
1. **packet.import_time_us** (INTEGER)
|
||||
- Stores when the packet was imported into the database
|
||||
- Indexed for fast queries
|
||||
|
||||
2. **packet_seen.import_time_us** (INTEGER)
|
||||
- Stores when the packet_seen record was imported
|
||||
- Indexed for performance
|
||||
|
||||
3. **traceroute.import_time_us** (INTEGER)
|
||||
- Stores when the traceroute was imported
|
||||
- Indexed for fast lookups
|
||||
|
||||
### New Indexes
|
||||
|
||||
The following indexes were created for optimal query performance:
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_packet_import_time_us ON packet(import_time_us DESC);
|
||||
CREATE INDEX idx_packet_from_node_time_us ON packet(from_node_id, import_time_us DESC);
|
||||
CREATE INDEX idx_packet_seen_import_time_us ON packet_seen(import_time_us);
|
||||
CREATE INDEX idx_traceroute_import_time_us ON traceroute(import_time_us);
|
||||
```
|
||||
|
||||
## Migration Process
|
||||
|
||||
### For Existing Databases
|
||||
|
||||
Run the migration script to add the new columns and populate them from existing data:
|
||||
|
||||
```bash
|
||||
python migrate_add_timestamp_us.py [database_path]
|
||||
```
|
||||
|
||||
If no path is provided, it defaults to `packets.db` in the current directory.
|
||||
|
||||
The migration script:
|
||||
1. Checks if migration is needed (idempotent)
|
||||
2. Adds `import_time_us` columns to the three tables
|
||||
3. Populates the new columns from existing `import_time` values
|
||||
4. Creates indexes for optimal performance
|
||||
5. Verifies the migration completed successfully
|
||||
|
||||
### For New Databases
|
||||
|
||||
New databases created with the updated schema will automatically include the `import_time_us` columns. The MQTT store module populates both columns when inserting new records.
|
||||
|
||||
## Code Changes
|
||||
|
||||
### Models (meshview/models.py)
|
||||
|
||||
The ORM models now include the new `import_time_us` fields:
|
||||
|
||||
```python
|
||||
class Packet(Base):
|
||||
import_time: Mapped[datetime] = mapped_column(nullable=True)
|
||||
import_time_us: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
||||
```
|
||||
|
||||
### MQTT Store (meshview/mqtt_store.py)
|
||||
|
||||
The data ingestion logic now populates both timestamp columns using UTC time:
|
||||
|
||||
```python
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
now_us = int(now.timestamp() * 1_000_000)
|
||||
|
||||
# Both columns are populated
|
||||
import_time=now,
|
||||
import_time_us=now_us,
|
||||
```
|
||||
|
||||
**Important**: All new timestamps use UTC (Coordinated Universal Time) for consistency across time zones.
|
||||
|
||||
## Using the New Timestamps
|
||||
|
||||
### Example Queries
|
||||
|
||||
**Query packets from the last 7 days:**
|
||||
|
||||
```sql
|
||||
-- Old way (slower)
|
||||
SELECT * FROM packet
|
||||
WHERE import_time >= datetime('now', '-7 days');
|
||||
|
||||
-- New way (faster)
|
||||
SELECT * FROM packet
|
||||
WHERE import_time_us >= (strftime('%s', 'now', '-7 days') * 1000000);
|
||||
```
|
||||
|
||||
**Query packets in a specific time range:**
|
||||
|
||||
```sql
|
||||
SELECT * FROM packet
|
||||
WHERE import_time_us BETWEEN 1759254380000000 AND 1759254390000000;
|
||||
```
|
||||
|
||||
**Calculate time differences (in microseconds):**
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
id,
|
||||
(import_time_us - LAG(import_time_us) OVER (ORDER BY import_time_us)) / 1000000.0 as seconds_since_last
|
||||
FROM packet
|
||||
LIMIT 10;
|
||||
```
|
||||
|
||||
### Converting Timestamps
|
||||
|
||||
**From datetime to microseconds (UTC):**
|
||||
```python
|
||||
import datetime
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
now_us = int(now.timestamp() * 1_000_000)
|
||||
```
|
||||
|
||||
**From microseconds to datetime:**
|
||||
```python
|
||||
import datetime
|
||||
timestamp_us = 1759254380813451
|
||||
dt = datetime.datetime.fromtimestamp(timestamp_us / 1_000_000)
|
||||
```
|
||||
|
||||
**In SQL queries:**
|
||||
```sql
|
||||
-- Datetime to microseconds
|
||||
SELECT CAST((strftime('%s', import_time) || substr(import_time, 21, 6)) AS INTEGER);
|
||||
|
||||
-- Microseconds to datetime (approximate)
|
||||
SELECT datetime(import_time_us / 1000000, 'unixepoch');
|
||||
```
|
||||
|
||||
## Performance Benefits
|
||||
|
||||
The integer timestamp columns provide significant performance improvements:
|
||||
|
||||
1. **Faster comparisons**: Integer comparisons are much faster than string/datetime comparisons
|
||||
2. **Smaller index size**: Integer indexes are more compact than datetime indexes
|
||||
3. **Range queries**: BETWEEN operations on integers are highly optimized
|
||||
4. **Math operations**: Easy to calculate time differences, averages, etc.
|
||||
5. **Sorting**: Integer sorting is faster than datetime sorting
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
The original `import_time` TEXT columns remain unchanged:
|
||||
- Existing code continues to work
|
||||
- Human-readable timestamps still available
|
||||
- Gradual migration to new columns possible
|
||||
- No breaking changes for existing queries
|
||||
|
||||
## Future Work
|
||||
|
||||
Future improvements could include:
|
||||
- Migrating queries to use `import_time_us` columns
|
||||
- Deprecating the TEXT `import_time` columns (after transition period)
|
||||
- Adding helper functions for timestamp conversion
|
||||
- Creating views that expose both formats
|
||||
|
||||
## Testing
|
||||
|
||||
The migration was tested on a production database with:
|
||||
- 132,466 packet records
|
||||
- 1,385,659 packet_seen records
|
||||
- 28,414 traceroute records
|
||||
|
||||
All records were successfully migrated with microsecond precision preserved.
|
||||
|
||||
## References
|
||||
|
||||
- GitHub Issue: #55 - Storing High-Resolution Timestamps in SQLite
|
||||
- SQLite datetime functions: https://www.sqlite.org/lang_datefunc.html
|
||||
- Python datetime module: https://docs.python.org/3/library/datetime.html
|
||||
@@ -6,7 +6,7 @@ Create Date: 2025-10-26 20:59:04.347066
|
||||
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
@@ -14,9 +14,9 @@ from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '1717fa5c6545'
|
||||
down_revision: str | None = 'c88468b7ab0b'
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
down_revision: Union[str, None] = 'c88468b7ab0b'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
@@ -6,7 +6,7 @@ Create Date: 2025-10-26 20:56:50.285200
|
||||
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
@@ -14,9 +14,9 @@ from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'c88468b7ab0b'
|
||||
down_revision: str | None = None
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
"""add_microsecond_timestamp_columns
|
||||
|
||||
Adds import_time_us INTEGER columns to packet, packet_seen, and traceroute tables.
|
||||
This implements the changes described in GitHub issue #55:
|
||||
- Adds import_time_us INTEGER columns to track microsecond-precision timestamps
|
||||
- Populates new columns from existing import_time datetime values
|
||||
- Creates indexes on the new columns for performance
|
||||
|
||||
Revision ID: fb5781f0c470
|
||||
Revises: 1717fa5c6545
|
||||
Create Date: 2025-11-03 12:59:03.202458
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'fb5781f0c470'
|
||||
down_revision: Union[str, None] = '1717fa5c6545'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Add import_time_us columns and populate them from existing data."""
|
||||
|
||||
# Add import_time_us column to packet table
|
||||
with op.batch_alter_table('packet', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('import_time_us', sa.Integer(), nullable=True))
|
||||
|
||||
# Add import_time_us column to packet_seen table
|
||||
with op.batch_alter_table('packet_seen', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('import_time_us', sa.Integer(), nullable=True))
|
||||
|
||||
# Add import_time_us column to traceroute table
|
||||
with op.batch_alter_table('traceroute', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('import_time_us', sa.Integer(), nullable=True))
|
||||
|
||||
# Populate packet.import_time_us from existing import_time data
|
||||
# Note: import_time is stored as local time text, but we convert to UTC timestamp
|
||||
# strftime('%s', ...) interprets the datetime as UTC
|
||||
op.execute("""
|
||||
UPDATE packet
|
||||
SET import_time_us =
|
||||
CAST((strftime('%s', import_time) || substr(import_time, 21, 6)) AS INTEGER)
|
||||
WHERE import_time IS NOT NULL
|
||||
""")
|
||||
|
||||
# Populate packet_seen.import_time_us
|
||||
op.execute("""
|
||||
UPDATE packet_seen
|
||||
SET import_time_us =
|
||||
CAST((strftime('%s', import_time) || substr(import_time, 21, 6)) AS INTEGER)
|
||||
WHERE import_time IS NOT NULL
|
||||
""")
|
||||
|
||||
# Populate traceroute.import_time_us
|
||||
op.execute("""
|
||||
UPDATE traceroute
|
||||
SET import_time_us =
|
||||
CAST((strftime('%s', import_time) || substr(import_time, 21, 6)) AS INTEGER)
|
||||
WHERE import_time IS NOT NULL
|
||||
""")
|
||||
|
||||
# Create indexes on the new columns
|
||||
op.create_index('idx_packet_import_time_us', 'packet', [sa.text('import_time_us DESC')])
|
||||
op.create_index('idx_packet_from_node_time_us', 'packet', ['from_node_id', sa.text('import_time_us DESC')])
|
||||
op.create_index('idx_packet_seen_import_time_us', 'packet_seen', ['import_time_us'])
|
||||
op.create_index('idx_traceroute_import_time_us', 'traceroute', ['import_time_us'])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Remove import_time_us columns and their indexes."""
|
||||
|
||||
# Drop indexes
|
||||
op.drop_index('idx_traceroute_import_time_us', table_name='traceroute')
|
||||
op.drop_index('idx_packet_seen_import_time_us', table_name='packet_seen')
|
||||
op.drop_index('idx_packet_from_node_time_us', table_name='packet')
|
||||
op.drop_index('idx_packet_import_time_us', table_name='packet')
|
||||
|
||||
# Drop columns
|
||||
with op.batch_alter_table('traceroute', schema=None) as batch_op:
|
||||
batch_op.drop_column('import_time_us')
|
||||
|
||||
with op.batch_alter_table('packet_seen', schema=None) as batch_op:
|
||||
batch_op.drop_column('import_time_us')
|
||||
|
||||
with op.batch_alter_table('packet', schema=None) as batch_op:
|
||||
batch_op.drop_column('import_time_us')
|
||||
+10
-1
@@ -50,14 +50,17 @@ class Packet(Base):
|
||||
)
|
||||
payload: Mapped[bytes] = mapped_column(nullable=True)
|
||||
import_time: Mapped[datetime] = mapped_column(nullable=True)
|
||||
import_time_us: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
||||
channel: Mapped[str] = mapped_column(nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_packet_from_node_id", "from_node_id"),
|
||||
Index("idx_packet_to_node_id", "to_node_id"),
|
||||
Index("idx_packet_import_time", desc("import_time")),
|
||||
Index("idx_packet_import_time_us", desc("import_time_us")),
|
||||
# Composite index for /top endpoint performance - filters by from_node_id AND import_time
|
||||
Index("idx_packet_from_node_time", "from_node_id", desc("import_time")),
|
||||
Index("idx_packet_from_node_time_us", "from_node_id", desc("import_time_us")),
|
||||
)
|
||||
|
||||
|
||||
@@ -78,11 +81,13 @@ class PacketSeen(Base):
|
||||
rx_rssi: Mapped[int] = mapped_column(nullable=True)
|
||||
topic: Mapped[str] = mapped_column(nullable=True)
|
||||
import_time: Mapped[datetime] = mapped_column(nullable=True)
|
||||
import_time_us: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
Index("idx_packet_seen_node_id", "node_id"),
|
||||
# Index for /top endpoint performance - JOIN on packet_id
|
||||
Index("idx_packet_seen_packet_id", "packet_id"),
|
||||
Index("idx_packet_seen_import_time_us", "import_time_us"),
|
||||
)
|
||||
|
||||
|
||||
@@ -98,5 +103,9 @@ class Traceroute(Base):
|
||||
done: Mapped[bool] = mapped_column(nullable=True)
|
||||
route: Mapped[bytes] = mapped_column(nullable=True)
|
||||
import_time: Mapped[datetime] = mapped_column(nullable=True)
|
||||
import_time_us: Mapped[int] = mapped_column(BigInteger, nullable=True)
|
||||
|
||||
__table_args__ = (Index("idx_traceroute_import_time", "import_time"),)
|
||||
__table_args__ = (
|
||||
Index("idx_traceroute_import_time", "import_time"),
|
||||
Index("idx_traceroute_import_time_us", "import_time_us"),
|
||||
)
|
||||
|
||||
+12
-3
@@ -80,6 +80,8 @@ async def process_envelope(topic, env):
|
||||
if not packet:
|
||||
# FIXME: Not Used
|
||||
# new_packet = True
|
||||
now = datetime.datetime.now(datetime.UTC)
|
||||
now_us = int(now.timestamp() * 1_000_000)
|
||||
stmt = (
|
||||
sqlite_insert(Packet)
|
||||
.values(
|
||||
@@ -88,7 +90,8 @@ async def process_envelope(topic, env):
|
||||
from_node_id=getattr(env.packet, "from"),
|
||||
to_node_id=env.packet.to,
|
||||
payload=env.packet.SerializeToString(),
|
||||
import_time=datetime.datetime.now(),
|
||||
import_time=now,
|
||||
import_time_us=now_us,
|
||||
channel=env.channel_id,
|
||||
)
|
||||
.on_conflict_do_nothing(index_elements=["id"])
|
||||
@@ -112,6 +115,8 @@ async def process_envelope(topic, env):
|
||||
)
|
||||
)
|
||||
if not result.scalar_one_or_none():
|
||||
now = datetime.datetime.now(datetime.UTC)
|
||||
now_us = int(now.timestamp() * 1_000_000)
|
||||
seen = PacketSeen(
|
||||
packet_id=env.packet.id,
|
||||
node_id=int(env.gateway_id[1:], 16),
|
||||
@@ -122,7 +127,8 @@ async def process_envelope(topic, env):
|
||||
hop_limit=env.packet.hop_limit,
|
||||
hop_start=env.packet.hop_start,
|
||||
topic=topic,
|
||||
import_time=datetime.datetime.now(),
|
||||
import_time=now,
|
||||
import_time_us=now_us,
|
||||
)
|
||||
session.add(seen)
|
||||
|
||||
@@ -203,13 +209,16 @@ async def process_envelope(topic, env):
|
||||
if result.scalar_one_or_none():
|
||||
packet_id = env.packet.decoded.request_id
|
||||
if packet_id is not None:
|
||||
now = datetime.datetime.now(datetime.UTC)
|
||||
now_us = int(now.timestamp() * 1_000_000)
|
||||
session.add(
|
||||
Traceroute(
|
||||
packet_id=packet_id,
|
||||
route=env.packet.decoded.payload,
|
||||
done=not env.packet.decoded.want_response,
|
||||
gateway_node_id=int(env.gateway_id[1:], 16),
|
||||
import_time=datetime.datetime.now(),
|
||||
import_time=now,
|
||||
import_time_us=now_us,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration script to add microsecond timestamp columns to existing database.
|
||||
|
||||
This script implements the changes described in GitHub issue #55:
|
||||
- Adds import_time_us INTEGER columns to packet, packet_seen, and traceroute tables
|
||||
- Populates the new columns from existing import_time datetime values
|
||||
- Creates indexes on the new columns for performance
|
||||
|
||||
Usage:
|
||||
python migrate_add_timestamp_us.py [database_path]
|
||||
|
||||
If database_path is not provided, it will use 'packets.db' in the current directory.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||
|
||||
|
||||
async def migrate_database(db_path: str):
|
||||
"""Run the migration to add microsecond timestamp columns."""
|
||||
|
||||
print(f"Starting migration for database: {db_path}")
|
||||
|
||||
# Create async engine
|
||||
engine = create_async_engine(f"sqlite+aiosqlite:///{db_path}", echo=False)
|
||||
async_session = async_sessionmaker(engine, expire_on_commit=False)
|
||||
|
||||
async with async_session() as session:
|
||||
# Check if columns already exist
|
||||
print("\nChecking if migration is needed...")
|
||||
result = await session.execute(text("PRAGMA table_info(packet)"))
|
||||
columns = [row[1] for row in result.fetchall()]
|
||||
|
||||
if 'import_time_us' in columns:
|
||||
print("Migration already applied - import_time_us column already exists")
|
||||
return
|
||||
|
||||
print("\n=== Step 1: Adding new columns ===")
|
||||
|
||||
# Add import_time_us column to packet table
|
||||
print("Adding import_time_us to packet table...")
|
||||
await session.execute(text("ALTER TABLE packet ADD COLUMN import_time_us INTEGER"))
|
||||
await session.commit()
|
||||
|
||||
# Add import_time_us column to packet_seen table
|
||||
print("Adding import_time_us to packet_seen table...")
|
||||
await session.execute(text("ALTER TABLE packet_seen ADD COLUMN import_time_us INTEGER"))
|
||||
await session.commit()
|
||||
|
||||
# Add import_time_us column to traceroute table
|
||||
print("Adding import_time_us to traceroute table...")
|
||||
await session.execute(text("ALTER TABLE traceroute ADD COLUMN import_time_us INTEGER"))
|
||||
await session.commit()
|
||||
|
||||
print("\n=== Step 2: Populating new columns from existing data ===")
|
||||
|
||||
# Populate packet.import_time_us
|
||||
print("Populating packet.import_time_us...")
|
||||
# Note: import_time is stored as local time text, but we convert to UTC timestamp
|
||||
# strftime('%s', ...) interprets the datetime as UTC
|
||||
await session.execute(
|
||||
text("""
|
||||
UPDATE packet
|
||||
SET import_time_us =
|
||||
CAST((strftime('%s', import_time) || substr(import_time, 21, 6)) AS INTEGER)
|
||||
WHERE import_time IS NOT NULL
|
||||
""")
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
# Get count for verification
|
||||
result = await session.execute(
|
||||
text("SELECT COUNT(*) FROM packet WHERE import_time_us IS NOT NULL")
|
||||
)
|
||||
count = result.scalar()
|
||||
print(f" Updated {count} packet records")
|
||||
|
||||
# Populate packet_seen.import_time_us
|
||||
print("Populating packet_seen.import_time_us...")
|
||||
await session.execute(
|
||||
text("""
|
||||
UPDATE packet_seen
|
||||
SET import_time_us =
|
||||
CAST((strftime('%s', import_time) || substr(import_time, 21, 6)) AS INTEGER)
|
||||
WHERE import_time IS NOT NULL
|
||||
""")
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
result = await session.execute(
|
||||
text("SELECT COUNT(*) FROM packet_seen WHERE import_time_us IS NOT NULL")
|
||||
)
|
||||
count = result.scalar()
|
||||
print(f" Updated {count} packet_seen records")
|
||||
|
||||
# Populate traceroute.import_time_us
|
||||
print("Populating traceroute.import_time_us...")
|
||||
await session.execute(
|
||||
text("""
|
||||
UPDATE traceroute
|
||||
SET import_time_us =
|
||||
CAST((strftime('%s', import_time) || substr(import_time, 21, 6)) AS INTEGER)
|
||||
WHERE import_time IS NOT NULL
|
||||
""")
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
result = await session.execute(
|
||||
text("SELECT COUNT(*) FROM traceroute WHERE import_time_us IS NOT NULL")
|
||||
)
|
||||
count = result.scalar()
|
||||
print(f" Updated {count} traceroute records")
|
||||
|
||||
print("\n=== Step 3: Creating indexes ===")
|
||||
|
||||
# Create indexes on the new columns
|
||||
print("Creating index on packet.import_time_us...")
|
||||
await session.execute(
|
||||
text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_packet_import_time_us ON packet(import_time_us DESC)"
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
print("Creating composite index on packet(from_node_id, import_time_us)...")
|
||||
await session.execute(
|
||||
text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_packet_from_node_time_us ON packet(from_node_id, import_time_us DESC)"
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
print("Creating index on packet_seen.import_time_us...")
|
||||
await session.execute(
|
||||
text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_packet_seen_import_time_us ON packet_seen(import_time_us)"
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
print("Creating index on traceroute.import_time_us...")
|
||||
await session.execute(
|
||||
text(
|
||||
"CREATE INDEX IF NOT EXISTS idx_traceroute_import_time_us ON traceroute(import_time_us)"
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
print("\n=== Migration completed successfully! ===")
|
||||
print("\nVerification:")
|
||||
|
||||
# Verify the migration
|
||||
result = await session.execute(
|
||||
text("""
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
COUNT(import_time_us) as with_us_timestamp
|
||||
FROM packet
|
||||
""")
|
||||
)
|
||||
row = result.fetchone()
|
||||
print(f" Packet table: {row[1]}/{row[0]} records have microsecond timestamps")
|
||||
|
||||
result = await session.execute(
|
||||
text("""
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
COUNT(import_time_us) as with_us_timestamp
|
||||
FROM packet_seen
|
||||
""")
|
||||
)
|
||||
row = result.fetchone()
|
||||
print(f" PacketSeen table: {row[1]}/{row[0]} records have microsecond timestamps")
|
||||
|
||||
result = await session.execute(
|
||||
text("""
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
COUNT(import_time_us) as with_us_timestamp
|
||||
FROM traceroute
|
||||
""")
|
||||
)
|
||||
row = result.fetchone()
|
||||
print(f" Traceroute table: {row[1]}/{row[0]} records have microsecond timestamps")
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
def main():
|
||||
# Get database path from command line or use default
|
||||
if len(sys.argv) > 1:
|
||||
db_path = sys.argv[1]
|
||||
else:
|
||||
db_path = "packets.db"
|
||||
|
||||
# Check if database exists
|
||||
if not Path(db_path).exists():
|
||||
print(f"Error: Database file not found: {db_path}")
|
||||
sys.exit(1)
|
||||
|
||||
# Run the migration
|
||||
asyncio.run(migrate_database(db_path))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user