FASE 1: Parts Database - Added part_categories, part_groups, parts, vehicle_parts tables - 12 categories, 190 groups with Spanish translations - API endpoints for categories, groups, parts CRUD FASE 2: Cross-References & Aftermarket - Added manufacturers, aftermarket_parts, part_cross_references tables - 24 manufacturers, quality tier system (economy/standard/premium/oem) - Part number search across OEM and aftermarket FASE 3: Exploded Diagrams - Added diagrams, vehicle_diagrams, diagram_hotspots tables - SVG viewer with zoom controls and interactive hotspots - 3 sample diagrams (brake, oil filter, suspension) FASE 4: Search & VIN Decoder - SQLite FTS5 full-text search with auto-sync triggers - NHTSA VIN decoder API integration with 30-day cache - Unified search endpoint FASE 5: Optimization & UX - API pagination (page/per_page, max 100 items) - Dark mode with localStorage persistence - Keyboard shortcuts (/, Ctrl+K, Escape, Backspace, Ctrl+D) - Breadcrumb navigation - ARIA accessibility (labels, roles, focus management) - Skip link for keyboard users Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
313 lines
11 KiB
Python
Executable File
313 lines
11 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
FASE 4: Full-Text Search and VIN Decoder
|
|
This script creates FASE 4 tables (FTS5, triggers, vin_cache) and populates
|
|
the parts_fts table with existing parts data.
|
|
"""
|
|
|
|
import sqlite3
|
|
import os
|
|
import json
|
|
from typing import Optional
|
|
from datetime import datetime, timedelta
|
|
|
|
# Database path configuration
|
|
DB_PATH = os.path.join(os.path.dirname(__file__), '..', 'vehicle_database.db')
|
|
SCHEMA_PATH = os.path.join(os.path.dirname(__file__), '..', 'sql', 'schema.sql')
|
|
|
|
|
|
class Fase4Manager:
|
|
"""Manager for FASE 4 tables: parts_fts, vin_cache, and related triggers"""
|
|
|
|
def __init__(self, db_path: str = DB_PATH):
|
|
self.db_path = db_path
|
|
self.connection = None
|
|
|
|
def connect(self):
|
|
"""Connect to the SQLite database"""
|
|
self.connection = sqlite3.connect(self.db_path)
|
|
self.connection.row_factory = sqlite3.Row
|
|
print(f"Connected to database: {self.db_path}")
|
|
|
|
def disconnect(self):
|
|
"""Close the database connection"""
|
|
if self.connection:
|
|
self.connection.close()
|
|
print("Disconnected from database")
|
|
|
|
def create_fase4_tables(self):
|
|
"""Create FASE 4 tables from schema file"""
|
|
if not os.path.exists(SCHEMA_PATH):
|
|
raise FileNotFoundError(f"Schema file not found: {SCHEMA_PATH}")
|
|
|
|
with open(SCHEMA_PATH, 'r') as f:
|
|
schema = f.read()
|
|
|
|
if self.connection:
|
|
cursor = self.connection.cursor()
|
|
cursor.executescript(schema)
|
|
self.connection.commit()
|
|
print("FASE 4 tables created successfully")
|
|
|
|
def check_fts_table_exists(self) -> bool:
|
|
"""Check if the parts_fts FTS5 table exists"""
|
|
cursor = self.connection.cursor()
|
|
cursor.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='table' AND name='parts_fts'"
|
|
)
|
|
return cursor.fetchone() is not None
|
|
|
|
def check_fts_populated(self) -> bool:
|
|
"""Check if the FTS table has any data"""
|
|
cursor = self.connection.cursor()
|
|
try:
|
|
cursor.execute("SELECT COUNT(*) FROM parts_fts")
|
|
count = cursor.fetchone()[0]
|
|
return count > 0
|
|
except sqlite3.OperationalError:
|
|
return False
|
|
|
|
def populate_fts_from_parts(self):
|
|
"""Populate the parts_fts table with existing parts data"""
|
|
if not self.check_fts_table_exists():
|
|
print("FTS table does not exist, creating tables first...")
|
|
self.create_fase4_tables()
|
|
|
|
# Check if already populated
|
|
if self.check_fts_populated():
|
|
print("parts_fts table already has data, skipping population")
|
|
return
|
|
|
|
cursor = self.connection.cursor()
|
|
|
|
# Get count of parts
|
|
cursor.execute("SELECT COUNT(*) FROM parts")
|
|
parts_count = cursor.fetchone()[0]
|
|
|
|
if parts_count == 0:
|
|
print("No parts found in parts table, nothing to populate")
|
|
return
|
|
|
|
# Populate FTS table from parts
|
|
cursor.execute("""
|
|
INSERT INTO parts_fts(rowid, oem_part_number, name, name_es, description, description_es)
|
|
SELECT id, oem_part_number, name, name_es, description, description_es FROM parts
|
|
""")
|
|
self.connection.commit()
|
|
|
|
# Rebuild FTS index for proper search functionality
|
|
cursor.execute("INSERT INTO parts_fts(parts_fts) VALUES('rebuild')")
|
|
self.connection.commit()
|
|
|
|
# Verify population
|
|
cursor.execute("SELECT COUNT(*) FROM parts_fts")
|
|
fts_count = cursor.fetchone()[0]
|
|
print(f"Populated parts_fts with {fts_count} entries from {parts_count} parts")
|
|
|
|
def get_vin_by_vin(self, vin: str) -> Optional[int]:
|
|
"""Get VIN cache entry ID by VIN, returns None if not found"""
|
|
cursor = self.connection.cursor()
|
|
cursor.execute("SELECT id FROM vin_cache WHERE vin = ?", (vin,))
|
|
result = cursor.fetchone()
|
|
return result[0] if result else None
|
|
|
|
def insert_vin_cache(self, vin: str, decoded_data: dict, make: str, model: str,
|
|
year: int, engine_info: str = None, body_class: str = None,
|
|
drive_type: str = None, model_year_engine_id: int = None,
|
|
expires_days: int = 30) -> int:
|
|
"""Insert a VIN cache entry if it doesn't exist, return its ID"""
|
|
existing_id = self.get_vin_by_vin(vin)
|
|
if existing_id:
|
|
print(f" VIN '{vin}' already exists in cache (ID: {existing_id})")
|
|
return existing_id
|
|
|
|
cursor = self.connection.cursor()
|
|
expires_at = datetime.now() + timedelta(days=expires_days)
|
|
|
|
cursor.execute(
|
|
"""INSERT INTO vin_cache
|
|
(vin, decoded_data, make, model, year, engine_info, body_class,
|
|
drive_type, model_year_engine_id, expires_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
(vin, json.dumps(decoded_data), make, model, year, engine_info,
|
|
body_class, drive_type, model_year_engine_id, expires_at.isoformat())
|
|
)
|
|
self.connection.commit()
|
|
vin_id = cursor.lastrowid
|
|
print(f" Inserted VIN cache: {vin} -> {make} {model} {year} (ID: {vin_id})")
|
|
return vin_id
|
|
|
|
def populate_sample_vins(self):
|
|
"""Populate sample VIN cache entries for testing"""
|
|
print("\nPopulating sample VIN cache entries...")
|
|
|
|
sample_vins = [
|
|
{
|
|
'vin': '4T1BF1FK5CU123456',
|
|
'decoded_data': {
|
|
'Make': 'TOYOTA',
|
|
'Model': 'Camry',
|
|
'ModelYear': '2023',
|
|
'EngineModel': '2.5L I4',
|
|
'BodyClass': 'Sedan/Saloon',
|
|
'DriveType': 'FWD',
|
|
'PlantCountry': 'UNITED STATES (USA)',
|
|
'VehicleType': 'PASSENGER CAR'
|
|
},
|
|
'make': 'Toyota',
|
|
'model': 'Camry',
|
|
'year': 2023,
|
|
'engine_info': '2.5L I4 DOHC 16V',
|
|
'body_class': 'Sedan',
|
|
'drive_type': 'FWD'
|
|
},
|
|
{
|
|
'vin': '1HGBH41JXMN109186',
|
|
'decoded_data': {
|
|
'Make': 'HONDA',
|
|
'Model': 'Civic',
|
|
'ModelYear': '2023',
|
|
'EngineModel': '2.0L I4',
|
|
'BodyClass': 'Sedan/Saloon',
|
|
'DriveType': 'FWD',
|
|
'PlantCountry': 'UNITED STATES (USA)',
|
|
'VehicleType': 'PASSENGER CAR'
|
|
},
|
|
'make': 'Honda',
|
|
'model': 'Civic',
|
|
'year': 2023,
|
|
'engine_info': '2.0L I4 DOHC 16V',
|
|
'body_class': 'Sedan',
|
|
'drive_type': 'FWD'
|
|
},
|
|
{
|
|
'vin': '1FA6P8CF5L5123456',
|
|
'decoded_data': {
|
|
'Make': 'FORD',
|
|
'Model': 'Mustang',
|
|
'ModelYear': '2020',
|
|
'EngineModel': '5.0L V8',
|
|
'BodyClass': 'Coupe',
|
|
'DriveType': 'RWD',
|
|
'PlantCountry': 'UNITED STATES (USA)',
|
|
'VehicleType': 'PASSENGER CAR'
|
|
},
|
|
'make': 'Ford',
|
|
'model': 'Mustang',
|
|
'year': 2020,
|
|
'engine_info': '5.0L V8 Coyote',
|
|
'body_class': 'Coupe',
|
|
'drive_type': 'RWD'
|
|
}
|
|
]
|
|
|
|
for vin_data in sample_vins:
|
|
self.insert_vin_cache(
|
|
vin=vin_data['vin'],
|
|
decoded_data=vin_data['decoded_data'],
|
|
make=vin_data['make'],
|
|
model=vin_data['model'],
|
|
year=vin_data['year'],
|
|
engine_info=vin_data['engine_info'],
|
|
body_class=vin_data['body_class'],
|
|
drive_type=vin_data['drive_type']
|
|
)
|
|
|
|
def verify_installation(self):
|
|
"""Verify FASE 4 installation"""
|
|
cursor = self.connection.cursor()
|
|
|
|
print("\n" + "=" * 50)
|
|
print("FASE 4 Installation Verification")
|
|
print("=" * 50)
|
|
|
|
# Check FTS table
|
|
cursor.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='table' AND name='parts_fts'"
|
|
)
|
|
fts_exists = cursor.fetchone() is not None
|
|
print(f"parts_fts table: {'OK' if fts_exists else 'MISSING'}")
|
|
|
|
if fts_exists:
|
|
cursor.execute("SELECT COUNT(*) FROM parts_fts")
|
|
fts_count = cursor.fetchone()[0]
|
|
print(f" - FTS entries: {fts_count}")
|
|
|
|
# Check triggers
|
|
triggers = ['parts_fts_insert', 'parts_fts_delete', 'parts_fts_update']
|
|
for trigger_name in triggers:
|
|
cursor.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='trigger' AND name=?",
|
|
(trigger_name,)
|
|
)
|
|
trigger_exists = cursor.fetchone() is not None
|
|
print(f"Trigger {trigger_name}: {'OK' if trigger_exists else 'MISSING'}")
|
|
|
|
# Check vin_cache table
|
|
cursor.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='table' AND name='vin_cache'"
|
|
)
|
|
vin_exists = cursor.fetchone() is not None
|
|
print(f"vin_cache table: {'OK' if vin_exists else 'MISSING'}")
|
|
|
|
if vin_exists:
|
|
cursor.execute("SELECT COUNT(*) FROM vin_cache")
|
|
vin_count = cursor.fetchone()[0]
|
|
print(f" - VIN cache entries: {vin_count}")
|
|
|
|
cursor.execute("SELECT vin, make, model, year FROM vin_cache")
|
|
for row in cursor.fetchall():
|
|
print(f" - {row['vin']}: {row['make']} {row['model']} {row['year']}")
|
|
|
|
# Check indexes
|
|
indexes = ['idx_vin_cache_vin', 'idx_vin_cache_make_model']
|
|
for index_name in indexes:
|
|
cursor.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='index' AND name=?",
|
|
(index_name,)
|
|
)
|
|
index_exists = cursor.fetchone() is not None
|
|
print(f"Index {index_name}: {'OK' if index_exists else 'MISSING'}")
|
|
|
|
print("=" * 50)
|
|
|
|
|
|
def main():
|
|
"""Main function to populate FASE 4 tables"""
|
|
print("=" * 60)
|
|
print("FASE 4: Full-Text Search and VIN Decoder Population")
|
|
print("=" * 60)
|
|
|
|
manager = Fase4Manager()
|
|
|
|
try:
|
|
manager.connect()
|
|
|
|
# Step 1: Create FASE 4 tables (FTS5, triggers, vin_cache)
|
|
print("\n[1/4] Creating FASE 4 tables...")
|
|
manager.create_fase4_tables()
|
|
|
|
# Step 2: Populate FTS table with existing parts
|
|
print("\n[2/4] Populating Full-Text Search index...")
|
|
manager.populate_fts_from_parts()
|
|
|
|
# Step 3: Add sample VIN cache entries
|
|
print("\n[3/4] Adding sample VIN cache entries...")
|
|
manager.populate_sample_vins()
|
|
|
|
# Step 4: Verify installation
|
|
print("\n[4/4] Verifying FASE 4 installation...")
|
|
manager.verify_installation()
|
|
|
|
print("\nFASE 4 population completed successfully!")
|
|
|
|
except Exception as e:
|
|
print(f"\nError during FASE 4 population: {e}")
|
|
raise
|
|
finally:
|
|
manager.disconnect()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|