feat: migrate to PostgreSQL + SQLAlchemy ORM, rebrand to Nexus Autoparts

- Migrate from SQLite to PostgreSQL with normalized schema
- Add 11 lookup tables (fuel_type, body_type, drivetrain, transmission,
  materials, position_part, manufacture_type, quality_tier, countries,
  reference_type, shapes)
- Rewrite dashboard/server.py (76 routes) using SQLAlchemy text() queries
- Rewrite console/db.py (27 methods) using SQLAlchemy ORM
- Add models.py with 27 SQLAlchemy model definitions
- Add config.py for centralized DB_URL configuration
- Add migrate_to_postgres.py migration script
- Add docs/METABASE_GUIDE.md with complete data entry guide
- Rebrand from "AUTOPARTS DB" to "NEXUS AUTOPARTS"
- Fill vehicle data gaps via NHTSA API + heuristics:
  engines (cylinders, power, torque), brands (country, founded_year),
  models (body_type, production years), MYE (drivetrain, transmission, trim)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 05:24:47 +00:00
parent 7ecf1295a5
commit 7b2a904498
41 changed files with 3519 additions and 3489 deletions

View File

@@ -1,10 +1,10 @@
# Autoparts DB # Nexus Autoparts
Sistema completo de gestión de base de datos de vehículos y autopartes con dashboard web, herramientas de web scraping y múltiples interfaces de consulta. Sistema completo de gestión de base de datos de vehículos y nexus-autoparts con dashboard web, herramientas de web scraping y múltiples interfaces de consulta.
## Descripción ## Descripción
**Autoparts DB** es una solución integral para la gestión de información de vehículos que incluye: **Nexus Autoparts** es una solución integral para la gestión de información de vehículos que incluye:
- Base de datos SQLite normalizada con información de marcas, modelos, motores y años - Base de datos SQLite normalizada con información de marcas, modelos, motores y años
- Dashboard web moderno y responsivo para consultar y explorar datos - Dashboard web moderno y responsivo para consultar y explorar datos
@@ -105,8 +105,8 @@ Funcionalidades: navegación por vehículo (marca→modelo→año→motor), bús
1. **Clonar el repositorio** 1. **Clonar el repositorio**
```bash ```bash
git clone https://git.consultoria-as.com/[usuario]/Autoparts-DB.git git clone https://git.consultoria-as.com/[usuario]/Nexus-Autoparts.git
cd Autoparts-DB cd Nexus-Autoparts
``` ```
2. **Instalar dependencias** 2. **Instalar dependencias**
@@ -330,4 +330,4 @@ Para más información, contactar al equipo de desarrollo.
--- ---
**Autoparts DB** - Sistema de Gestión de Base de Datos de Vehículos **Nexus Autoparts** - Sistema de Gestión de Base de Datos de Vehículos

20
config.py Normal file
View File

@@ -0,0 +1,20 @@
"""
Central configuration for Nexus Autoparts.
"""
import os
# Database
DB_URL = os.environ.get(
"DATABASE_URL",
"postgresql://nexus:nexus_autoparts_2026@localhost/nexus_autoparts"
)
# Legacy SQLite path (used only by migration script)
SQLITE_PATH = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"vehicle_database", "vehicle_database.db"
)
# Application identity
APP_NAME = "NEXUS AUTOPARTS"
APP_SLOGAN = "Tu conexión directa con las partes que necesitas"

View File

@@ -1,6 +1,6 @@
# AUTOPARTES Console - Sistema Pick/VT220 # NEXUS AUTOPARTS Console - Sistema Pick/VT220
Interfaz de consola para el catálogo de autopartes, inspirada en los sistemas Pick/D3 con estética de terminal VT220. Funciona 100% con teclado, verde sobre negro. Interfaz de consola para el catálogo de nexus-autoparts, inspirada en los sistemas Pick/D3 con estética de terminal VT220. Funciona 100% con teclado, verde sobre negro.
## Requisitos ## Requisitos

View File

@@ -1,18 +1,18 @@
""" """
Configuration settings for the AUTOPARTES console application. Configuration settings for the NEXUS AUTOPARTS console application.
""" """
import os import os
import sys
# Application metadata # Application metadata
VERSION = "1.0.0" VERSION = "2.0.0"
APP_NAME = "AUTOPARTES" APP_NAME = "NEXUS AUTOPARTS"
APP_SUBTITLE = "Sistema de Catalogo de Autopartes" APP_SUBTITLE = "Tu conexión directa con las partes que necesitas"
# Database path (relative to the console/ directory, resolved to absolute) # Database URL (PostgreSQL)
_CONSOLE_DIR = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
DB_PATH = os.path.join(_CONSOLE_DIR, "..", "vehicle_database", "vehicle_database.db") from config import DB_URL
DB_PATH = os.path.normpath(DB_PATH)
# NHTSA VIN Decoder API # NHTSA VIN Decoder API
NHTSA_API_URL = "https://vpic.nhtsa.dot.gov/api/vehicles/DecodeVin" NHTSA_API_URL = "https://vpic.nhtsa.dot.gov/api/vehicles/DecodeVin"

View File

@@ -1,5 +1,5 @@
""" """
Main application controller for the AUTOPARTES console application. Main application controller for the NEXUS AUTOPARTS console application.
The :class:`App` class owns the screen lifecycle loop: it renders the The :class:`App` class owns the screen lifecycle loop: it renders the
current screen, reads a keypress, dispatches it, and follows any current screen, reads a keypress, dispatches it, and follows any

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
""" """
Entry point for the AUTOPARTES Pick/VT220-style console application. Entry point for the NEXUS AUTOPARTS Pick/VT220-style console application.
Usage: Usage:
python -m console # via package python -m console # via package

View File

@@ -1,5 +1,5 @@
""" """
Abstract base renderer interface for the AUTOPARTES console application. Abstract base renderer interface for the NEXUS AUTOPARTS console application.
Every renderer (curses VT220, Textual/Rich, etc.) must subclass Every renderer (curses VT220, Textual/Rich, etc.) must subclass
:class:`BaseRenderer` and implement all of its methods. Screens call :class:`BaseRenderer` and implement all of its methods. Screens call

View File

@@ -1,5 +1,5 @@
""" """
Curses-based VT220 renderer for the AUTOPARTES console application. Curses-based VT220 renderer for the NEXUS AUTOPARTS console application.
Implements :class:`BaseRenderer` with a green-on-black aesthetic inspired Implements :class:`BaseRenderer` with a green-on-black aesthetic inspired
by classic Pick/UNIX VT220 terminals. All drawing is done through Python's by classic Pick/UNIX VT220 terminals. All drawing is done through Python's

View File

@@ -1,5 +1,5 @@
""" """
Admin CRUD screen for Cross-References in the AUTOPARTES console application. Admin CRUD screen for Cross-References in the NEXUS AUTOPARTS console application.
Provides a paginated list view with create (F3), edit (ENTER), and Provides a paginated list view with create (F3), edit (ENTER), and
delete (F8/Del) operations for the part_cross_references table. delete (F8/Del) operations for the part_cross_references table.

View File

@@ -1,5 +1,5 @@
""" """
Admin CRUD screen for Manufacturers in the AUTOPARTES console application. Admin CRUD screen for Manufacturers in the NEXUS AUTOPARTS console application.
Provides a list view with create (F3), edit (ENTER), and delete (F8/Del) Provides a list view with create (F3), edit (ENTER), and delete (F8/Del)
operations for the manufacturers table. operations for the manufacturers table.

View File

@@ -1,5 +1,5 @@
""" """
Import/Export screen for the AUTOPARTES console application. Import/Export screen for the NEXUS AUTOPARTS console application.
Provides a simple menu flow to import CSV files into the database or Provides a simple menu flow to import CSV files into the database or
export data to JSON files. Uses the renderer's show_input and export data to JSON files. Uses the renderer's show_input and

View File

@@ -1,5 +1,5 @@
""" """
Admin CRUD screen for Parts in the AUTOPARTES console application. Admin CRUD screen for Parts in the NEXUS AUTOPARTS console application.
Provides a paginated list view with create (F3), edit (ENTER), and Provides a paginated list view with create (F3), edit (ENTER), and
delete (F8/Del) operations. Form editing is handled inline with delete (F8/Del) operations. Form editing is handled inline with

View File

@@ -1,5 +1,5 @@
""" """
Part number search screen for the AUTOPARTES console application. Part number search screen for the NEXUS AUTOPARTS console application.
Prompts the user for a part number (OEM, aftermarket, or cross-reference) Prompts the user for a part number (OEM, aftermarket, or cross-reference)
and displays matching results in a table. Selecting a result navigates and displays matching results in a table. Selecting a result navigates

View File

@@ -1,5 +1,5 @@
""" """
Full-text search screen for the AUTOPARTES console application. Full-text search screen for the NEXUS AUTOPARTS console application.
Prompts the user for a search query and displays matching parts using Prompts the user for a search query and displays matching parts using
the FTS5 full-text search engine (with LIKE fallback). Results are the FTS5 full-text search engine (with LIKE fallback). Results are

View File

@@ -1,5 +1,5 @@
""" """
Catalog navigation screen for the AUTOPARTES console application. Catalog navigation screen for the NEXUS AUTOPARTS console application.
Provides a three-level drill-down through the parts hierarchy: Provides a three-level drill-down through the parts hierarchy:
Categories -> Groups -> Parts. An optional vehicle filter (mye_id) Categories -> Groups -> Parts. An optional vehicle filter (mye_id)

View File

@@ -1,5 +1,5 @@
""" """
Part comparator screen for the AUTOPARTES console application. Part comparator screen for the NEXUS AUTOPARTS console application.
Displays a side-by-side comparison of an OEM part against its aftermarket Displays a side-by-side comparison of an OEM part against its aftermarket
alternatives. The first column is always the OEM part; subsequent columns alternatives. The first column is always the OEM part; subsequent columns

View File

@@ -1,5 +1,5 @@
""" """
Statistics dashboard screen for the AUTOPARTES console application. Statistics dashboard screen for the NEXUS AUTOPARTS console application.
Displays database table counts and coverage metrics retrieved via Displays database table counts and coverage metrics retrieved via
:meth:`Database.get_stats`. :meth:`Database.get_stats`.

View File

@@ -1,5 +1,5 @@
""" """
Main menu screen for the AUTOPARTES console application. Main menu screen for the NEXUS AUTOPARTS console application.
Displays a numbered Pick-style menu with navigation options for all Displays a numbered Pick-style menu with navigation options for all
application sections. Number keys jump directly; arrow keys move the application sections. Number keys jump directly; arrow keys move the

View File

@@ -1,5 +1,5 @@
""" """
Part detail screen for the AUTOPARTES console application. Part detail screen for the NEXUS AUTOPARTS console application.
Shows full part information (OEM number, name, group, category, etc.) Shows full part information (OEM number, name, group, category, etc.)
with a table of aftermarket alternatives. Number keys navigate to with a table of aftermarket alternatives. Number keys navigate to

View File

@@ -1,5 +1,5 @@
""" """
Vehicle drill-down navigation screen for the AUTOPARTES console application. Vehicle drill-down navigation screen for the NEXUS AUTOPARTS console application.
Guides the user through a four-level hierarchy: Guides the user through a four-level hierarchy:

View File

@@ -1,5 +1,5 @@
""" """
VIN decoder screen for the AUTOPARTES console application. VIN decoder screen for the NEXUS AUTOPARTS console application.
Prompts for a 17-character Vehicle Identification Number, decodes it Prompts for a 17-character Vehicle Identification Number, decodes it
via the NHTSA vPIC API (with local caching), and displays the decoded via the NHTSA vPIC API (with local caching), and displays the decoded

View File

@@ -1,5 +1,5 @@
""" """
Integration tests for the AUTOPARTES console application. Integration tests for the NEXUS AUTOPARTS console application.
Uses a MockRenderer that records draw calls instead of painting to a real Uses a MockRenderer that records draw calls instead of painting to a real
terminal, allowing end-to-end testing of the screen -> renderer pipeline terminal, allowing end-to-end testing of the screen -> renderer pipeline

View File

@@ -1,5 +1,5 @@
""" """
Display formatting utilities for the AUTOPARTES console application. Display formatting utilities for the NEXUS AUTOPARTS console application.
Functions for currency, numbers, text truncation, table layout, and Functions for currency, numbers, text truncation, table layout, and
quality-tier visual bars. quality-tier visual bars.

View File

@@ -1,5 +1,5 @@
""" """
NHTSA VIN Decoder API client for the AUTOPARTES console application. NHTSA VIN Decoder API client for the NEXUS AUTOPARTS console application.
Wraps the National Highway Traffic Safety Administration (NHTSA) Vehicle Wraps the National Highway Traffic Safety Administration (NHTSA) Vehicle
Product Information Catalog (vPIC) DecodeVin endpoint to retrieve vehicle Product Information Catalog (vPIC) DecodeVin endpoint to retrieve vehicle

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Panel - Autopartes DB</title> <title>Admin Panel - Nexus Autoparts</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Orbitron:wght@700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Orbitron:wght@700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/shared.css"> <link rel="stylesheet" href="/shared.css">
<style> <style>

View File

@@ -1,6 +1,6 @@
/** /**
* Admin Panel JavaScript * Admin Panel JavaScript
* CRUD operations and CSV import/export for Autopartes DB * CRUD operations and CSV import/export for Nexus Autoparts
*/ */
// State // State

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AutoParts DB - Tienda de Autopartes</title> <title>Nexus Autoparts - Tu conexión directa con las partes que necesitas</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Orbitron:wght@400;500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Orbitron:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/shared.css"> <link rel="stylesheet" href="/shared.css">
<style> <style>
@@ -1095,7 +1095,7 @@
<div class="footer-brand"> <div class="footer-brand">
<div class="logo"> <div class="logo">
<div class="logo-icon">⚙️</div> <div class="logo-icon">⚙️</div>
<div class="logo-text">AUTOPARTS DB</div> <div class="logo-text">NEXUS AUTOPARTS</div>
</div> </div>
<p>Sistema de catálogo de autopartes con navegación jerárquica, diagramas explosionados y decodificador de VIN.</p> <p>Sistema de catálogo de autopartes con navegación jerárquica, diagramas explosionados y decodificador de VIN.</p>
<div class="social-links"> <div class="social-links">
@@ -1131,7 +1131,7 @@
</div> </div>
</div> </div>
<div class="footer-bottom"> <div class="footer-bottom">
<p>© 2024 AutoParts DB. Sistema de Catálogo de Autopartes.</p> <p>© 2026 Nexus Autoparts. Tu conexión directa con las partes que necesitas.</p>
<p>Desarrollado con Flask + SQLite</p> <p>Desarrollado con Flask + SQLite</p>
</div> </div>
</footer> </footer>

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Diagramas de Suspensión - AutoParts DB</title> <title>Diagramas - Nexus Autoparts</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🔧</text></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🔧</text></svg>">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Orbitron:wght@400;500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Orbitron:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">

View File

@@ -10,7 +10,7 @@ const enhancedSearch = {
debounceMs: 300, debounceMs: 300,
maxResults: 8, maxResults: 8,
maxRecent: 5, maxRecent: 5,
storageKey: 'autopartes_recent_searches' storageKey: 'nexus_recent_searches'
}, },
// State // State

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Catálogo de Autopartes - AutoParts DB</title> <title>Catálogo - Nexus Autoparts</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🔧</text></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🔧</text></svg>">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Orbitron:wght@400;500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Orbitron:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">

View File

@@ -1,5 +1,5 @@
/** /**
* nav.js -- Shared navigation component for AutoParts DB * nav.js -- Shared navigation component for NEXUS AUTOPARTS
* *
* Injects a consistent header/nav bar into <div id="shared-nav"></div>. * Injects a consistent header/nav bar into <div id="shared-nav"></div>.
* Auto-highlights the current page link based on window.location.pathname. * Auto-highlights the current page link based on window.location.pathname.
@@ -86,7 +86,7 @@
+ '-webkit-background-clip: text;' + '-webkit-background-clip: text;'
+ '-webkit-text-fill-color: transparent;' + '-webkit-text-fill-color: transparent;'
+ 'background-clip: text;' + 'background-clip: text;'
+ '">AUTOPARTS DB</span>' + '">NEXUS AUTOPARTS</span>'
+ '</a>' + '</a>'
// Slot for extra page-specific content (search bars, stats, etc.) // Slot for extra page-specific content (search bars, stats, etc.)
+ '<div id="shared-nav-extra" style="display: contents;"></div>' + '<div id="shared-nav-extra" style="display: contents;"></div>'

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
/* ============================================================ /* ============================================================
shared.css -- Common styles for all AutoParts DB pages shared.css -- Common styles for all Nexus Autoparts pages
============================================================ */ ============================================================ */
/* --- Reset --- */ /* --- Reset --- */

View File

@@ -1,6 +1,6 @@
# API Reference - Autoparts DB # API Reference - Nexus Autoparts
Documentación completa de la API REST del sistema Autoparts DB. Documentación completa de la API REST del sistema Nexus Autoparts.
## Base URL ## Base URL

View File

@@ -1,4 +1,4 @@
# Documentación de Base de Datos - Autoparts DB # Documentación de Base de Datos - Nexus Autoparts
## Resumen ## Resumen

View File

@@ -1,4 +1,4 @@
# Guía de Instalación - Autoparts DB # Guía de Instalación - Nexus Autoparts
## Requisitos del Sistema ## Requisitos del Sistema
@@ -28,10 +28,10 @@ El proyecto es compatible con:
```bash ```bash
# 1. Clonar el repositorio # 1. Clonar el repositorio
git clone https://git.consultoria-as.com/[usuario]/Autoparts-DB.git git clone https://git.consultoria-as.com/[usuario]/Nexus-Autoparts.git
# 2. Entrar al directorio # 2. Entrar al directorio
cd Autoparts-DB cd Nexus-Autoparts
# 3. Instalar dependencias # 3. Instalar dependencias
pip install -r requirements.txt pip install -r requirements.txt
@@ -48,8 +48,8 @@ python3 server.py
### Paso 1: Clonar el Repositorio ### Paso 1: Clonar el Repositorio
```bash ```bash
git clone https://git.consultoria-as.com/[usuario]/Autoparts-DB.git git clone https://git.consultoria-as.com/[usuario]/Nexus-Autoparts.git
cd Autoparts-DB cd Nexus-Autoparts
``` ```
### Paso 2: Crear Entorno Virtual (Recomendado) ### Paso 2: Crear Entorno Virtual (Recomendado)
@@ -254,16 +254,16 @@ gunicorn -w 4 -b 0.0.0.0:8080 server:app
### Usando systemd ### Usando systemd
Crear archivo `/etc/systemd/system/autoparts-db.service`: Crear archivo `/etc/systemd/system/nexus-autoparts.service`:
```ini ```ini
[Unit] [Unit]
Description=Autoparts DB Dashboard Description=Nexus Autoparts Dashboard
After=network.target After=network.target
[Service] [Service]
User=www-data User=www-data
WorkingDirectory=/path/to/Autoparts-DB/dashboard WorkingDirectory=/path/to/Nexus-Autoparts/dashboard
ExecStart=/usr/bin/python3 server.py ExecStart=/usr/bin/python3 server.py
Restart=always Restart=always
@@ -274,8 +274,8 @@ WantedBy=multi-user.target
Habilitar e iniciar: Habilitar e iniciar:
```bash ```bash
sudo systemctl enable autoparts-db sudo systemctl enable nexus-autoparts
sudo systemctl start autoparts-db sudo systemctl start nexus-autoparts
``` ```
### Usando Docker (Opcional) ### Usando Docker (Opcional)
@@ -294,8 +294,8 @@ CMD ["python3", "dashboard/server.py"]
``` ```
```bash ```bash
docker build -t autoparts-db . docker build -t nexus-autoparts .
docker run -p 5000:5000 autoparts-db docker run -p 5000:5000 nexus-autoparts
``` ```
--- ---
@@ -319,7 +319,7 @@ pip install --upgrade -r requirements.txt
deactivate deactivate
# Eliminar directorio del proyecto # Eliminar directorio del proyecto
rm -rf Autoparts-DB rm -rf Nexus-Autoparts
# Eliminar entorno virtual (si está separado) # Eliminar entorno virtual (si está separado)
rm -rf venv rm -rf venv

599
docs/METABASE_GUIDE.md Normal file
View File

@@ -0,0 +1,599 @@
# Guía Metabase — Nexus Autoparts
## 1. Conexión a la Base de Datos
### Datos de conexión PostgreSQL
| Campo | Valor |
|-------|-------|
| **Host** | `localhost` (o la IP del servidor: `192.168.10.198`) |
| **Puerto** | `5432` |
| **Base de datos** | `nexus_autoparts` |
| **Usuario** | `nexus` |
| **Contraseña** | `nexus_autoparts_2026` |
| **SSL** | No requerido (conexión local) |
### Pasos en Metabase
1. Ir a **Admin****Databases****Add database**
2. Seleccionar **PostgreSQL**
3. Llenar los campos con los datos anteriores
4. Click **Save**
5. Metabase sincronizará las tablas automáticamente (~30 segundos)
> **Tip:** Si Metabase está en otro servidor, usar la IP `192.168.10.198` en lugar de `localhost`.
---
## 2. Estructura de la Base de Datos
### Diagrama de relaciones simplificado
```
brands ──→ models ──→ model_year_engine (MYE) ←── years
↑ ↑ ↑
engines │ vehicle_parts ──→ parts
│ ↑
vehicle_diagrams part_cross_references
↓ aftermarket_parts
diagrams diagram_hotspots
```
### Tablas principales (con datos)
| Tabla | Registros | Descripción |
|-------|-----------|-------------|
| `brands` | 102 | Marcas de vehículos |
| `models` | 4,031 | Modelos por marca |
| `years` | 80 | Años (1946-2026) |
| `engines` | 13,430 | Motores con specs |
| `model_year_engine` | 47,858 | Combinación marca-modelo-año-motor |
### Tablas de piezas (vacías — para llenar)
| Tabla | Descripción |
|-------|-------------|
| `parts` | Piezas OEM (número de parte, nombre, grupo) |
| `vehicle_parts` | Relación pieza ↔ vehículo (fitments) |
| `aftermarket_parts` | Piezas aftermarket por fabricante |
| `part_cross_references` | Intercambios y referencias cruzadas |
| `manufacturers` | 47 fabricantes ya cargados |
### Tablas lookup (catálogos de referencia)
| Tabla | Valores actuales |
|-------|-----------------|
| `part_categories` | 12: Body, Brake, Cooling, Drivetrain, Electrical, Engine, Exhaust, Fuel, HVAC, Steering, Suspension, Transmission |
| `part_groups` | 63 grupos dentro de las categorías |
| `quality_tier` | economy, standard, oem, premium |
| `reference_type` | competitor, interchange, oem_alternate, supersession |
| `position_part` | front, rear |
| `manufacture_type` | aftermarket, oem |
| `materials` | (vacía — agregar según se necesite) |
---
## 3. Alta de Piezas OEM
### Orden obligatorio de carga
```
1. materials (si aplica)
2. parts ← PRIMERO las piezas
3. vehicle_parts ← DESPUÉS los fitments
4. aftermarket_parts
5. part_cross_references
```
> **IMPORTANTE:** Respetar este orden. `vehicle_parts` y `aftermarket_parts` requieren que la pieza ya exista en `parts`.
---
### 3.1 Agregar una pieza OEM (`parts`)
En Metabase: click **+ New** → **SQL query** → ejecutar:
```sql
INSERT INTO parts (oem_part_number, name_part, name_es, group_id, description, description_es, weight_kg, id_material)
VALUES (
'04465-06090', -- Número OEM (obligatorio, único por grupo)
'Front Brake Pad Set', -- Nombre en inglés (obligatorio)
'Juego de Balatas Delanteras', -- Nombre en español (opcional)
16, -- group_id: 16 = Brake Pads (ver tabla abajo)
'Ceramic brake pad set for front axle', -- Descripción EN (opcional)
'Juego de balatas cerámicas para eje delantero', -- Descripción ES (opcional)
1.2, -- Peso en kg (opcional)
NULL -- id_material (opcional, ver tabla materials)
);
```
### Carga masiva de piezas
```sql
INSERT INTO parts (oem_part_number, name_part, name_es, group_id, description) VALUES
('04465-06090', 'Front Brake Pad Set', 'Juego Balatas Delanteras', 16, 'Ceramic front pads'),
('04465-33471', 'Front Brake Pad Set', 'Juego Balatas Delanteras', 16, 'Semi-metallic front pads'),
('43512-06150', 'Front Brake Rotor', 'Disco de Freno Delantero', 17, 'Vented front rotor 296mm'),
('19101-28491', 'Radiator', 'Radiador', 31, 'Aluminum core radiator'),
('16400-28531', 'Cooling Fan Assembly', 'Ensamble Ventilador', 35, 'Electric cooling fan')
ON CONFLICT DO NOTHING;
```
### Referencia de `group_id` (grupos de piezas)
| group_id | Grupo | Categoría |
|----------|-------|-----------|
| **Frenos y Ruedas** | | |
| 16 | Brake Pads | Brake & Wheel Hub |
| 17 | Brake Rotors | Brake & Wheel Hub |
| 20 | Brake Calipers | Brake & Wheel Hub |
| 27 | Wheel Bearings | Brake & Wheel Hub |
| 28 | Wheel Hubs | Brake & Wheel Hub |
| **Motor** | | |
| 70 | Oil Filters | Engine |
| 71 | Air Filters | Engine |
| 72 | Spark Plugs | Engine |
| 73 | Belts | Engine |
| 76 | Timing Components | Engine |
| 91 | Engine Mounts | Engine |
| **Enfriamiento** | | |
| 31 | Radiators | Cooling System |
| 33 | Water Pumps | Cooling System |
| 34 | Thermostats | Cooling System |
| 35 | Cooling Fans | Cooling System |
| **Eléctrico** | | |
| 55 | Alternators | Electrical & Lighting |
| 56 | Starters | Electrical & Lighting |
| 57 | Ignition Coils | Electrical & Lighting |
| 65 | Sensors | Electrical & Lighting |
| **Combustible** | | |
| 110 | Fuel Pumps | Fuel & Air |
| 111 | Fuel Filters | Fuel & Air |
| 112 | Fuel Injectors | Fuel & Air |
| **Escape** | | |
| 99 | Catalytic Converters | Exhaust |
| 100 | Mufflers | Exhaust |
| 108 | Headers | Exhaust |
| **Dirección** | | |
| 141 | Power Steering Pumps | Steering |
| 144 | Steering Racks | Steering |
| 145 | Steering Gearboxes | Steering |
| 146 | Tie Rods | Steering |
| 147 | Tie Rod Ends | Steering |
| 148 | Inner Tie Rods | Steering |
| 151 | Pitman Arms | Steering |
| 152 | Idler Arms | Steering |
| 153 | Center Links | Steering |
| 154 | Drag Links | Steering |
| 155 | Steering Knuckles | Steering |
| 191 | Steering Dampers | Steering |
| **Suspensión** | | |
| 156 | Shocks | Suspension |
| 157 | Struts | Suspension |
| 158 | Strut Mounts | Suspension |
| 159 | Coil Springs | Suspension |
| 160 | Leaf Springs | Suspension |
| 161 | Control Arms | Suspension |
| 164 | Ball Joints | Suspension |
| 165 | Bushings | Suspension |
| 167 | Sway Bar Links | Suspension |
| 168 | Sway Bar Bushings | Suspension |
| 169 | Torsion Bars | Suspension |
| 170 | Trailing Arms | Suspension |
| **Transmisión** | | |
| 175 | Transmission Filters | Transmission |
| 185 | Transmission Mounts | Transmission |
| **A/C y Calefacción** | | |
| 127 | AC Compressors | Heat & Air Conditioning |
| 135 | Blower Motors | Heat & Air Conditioning |
| 138 | Cabin Air Filters | Heat & Air Conditioning |
| **Tren Motriz** | | |
| 44 | CV Axles | Drivetrain |
| 45 | CV Joints | Drivetrain |
| 50 | Axle Shafts | Drivetrain |
| **Carrocería** | | |
| 15 | Moldings & Trim | Body & Lamp Assembly |
---
## 4. Alta de Fitments (Pieza ↔ Vehículo)
Un fitment vincula una pieza con un vehículo específico (combinación modelo-año-motor).
### 4.1 Encontrar el `id_mye` del vehículo
```sql
-- Buscar el id_mye para un vehículo específico
SELECT mye.id_mye, b.name_brand, m.name_model, y.year_car, e.name_engine
FROM model_year_engine mye
JOIN models m ON mye.model_id = m.id_model
JOIN brands b ON m.brand_id = b.id_brand
JOIN years y ON mye.year_id = y.id_year
JOIN engines e ON mye.engine_id = e.id_engine
WHERE b.name_brand ILIKE 'Toyota'
AND m.name_model ILIKE 'Camry'
AND y.year_car = 2020
ORDER BY e.name_engine;
```
### 4.2 Encontrar el `id_part` de la pieza
```sql
-- Buscar pieza por número OEM
SELECT id_part, oem_part_number, name_part FROM parts
WHERE oem_part_number = '04465-06090';
```
### 4.3 Crear el fitment (`vehicle_parts`)
```sql
INSERT INTO vehicle_parts (model_year_engine_id, part_id, quantity_required, id_position_part, fitment_notes)
VALUES (
12345, -- id_mye del vehículo (del paso 4.1)
67890, -- id_part de la pieza (del paso 4.2)
1, -- Cantidad requerida (1 juego)
1, -- Posición: 1 = front, 2 = rear (ver position_part)
'Fits all trim levels' -- Notas de compatibilidad (opcional)
);
```
### 4.4 Fitment masivo (misma pieza en varios vehículos)
```sql
-- Ejemplo: Balata 04465-06090 compatible con todos los Camry 2018-2023
INSERT INTO vehicle_parts (model_year_engine_id, part_id, quantity_required, id_position_part)
SELECT mye.id_mye,
(SELECT id_part FROM parts WHERE oem_part_number = '04465-06090'),
1, -- cantidad
1 -- posición: front
FROM model_year_engine mye
JOIN models m ON mye.model_id = m.id_model
JOIN brands b ON m.brand_id = b.id_brand
JOIN years y ON mye.year_id = y.id_year
WHERE b.name_brand = 'TOYOTA'
AND m.name_model = 'Camry'
AND y.year_car BETWEEN 2018 AND 2023
ON CONFLICT DO NOTHING;
```
### Valores de `position_part`
| id_position_part | Nombre |
|-----------------|--------|
| 1 | front |
| 2 | rear |
> Para agregar más posiciones (left, right, upper, lower):
> ```sql
> INSERT INTO position_part (name_position_part) VALUES ('left'), ('right'), ('upper'), ('lower');
> ```
---
## 5. Alta de Piezas Aftermarket
Vincula una pieza aftermarket con su equivalente OEM y el fabricante.
### 5.1 Crear pieza aftermarket (`aftermarket_parts`)
```sql
INSERT INTO aftermarket_parts (
oem_part_id, manufacturer_id, part_number,
name_aftermarket_parts, name_es,
id_quality_tier, price_usd, warranty_months
) VALUES (
67890, -- id_part de la pieza OEM equivalente (de tabla parts)
3, -- id_manufacture del fabricante (ver manufacturers)
'CXD1293', -- Número de parte aftermarket
'Ceramic Brake Pad Set', -- Nombre EN
'Juego Balatas Cerámicas', -- Nombre ES
3, -- Calidad: 1=economy, 2=oem, 3=premium, 4=standard
45.99, -- Precio USD
24 -- Garantía en meses
);
```
### 5.2 Carga masiva aftermarket
```sql
INSERT INTO aftermarket_parts (oem_part_id, manufacturer_id, part_number, name_aftermarket_parts, id_quality_tier, price_usd, warranty_months) VALUES
(67890, 3, 'CXD1293', 'Premium Ceramic Brake Pad', 3, 45.99, 24),
(67890, 5, 'MKD1293', 'Economy Semi-Metallic Brake Pad', 1, 22.50, 12),
(67890, 8, 'PBR1293', 'OEM Replacement Brake Pad', 2, 38.00, 18)
ON CONFLICT DO NOTHING;
```
### Referencia de `quality_tier`
| id_quality_tier | Nombre | Descripción |
|----------------|--------|-------------|
| 1 | economy | Económica, calidad básica |
| 2 | oem | Calidad equivalente al original |
| 3 | premium | Calidad superior al original |
| 4 | standard | Calidad estándar del mercado |
### Consultar fabricantes existentes
```sql
SELECT m.id_manufacture, m.name_manufacture, mt.name_type_manu AS tipo,
qt.name_quality AS calidad, c.name_country AS pais
FROM manufacturers m
LEFT JOIN manufacture_type mt ON m.id_type_manu = mt.id_type_manu
LEFT JOIN quality_tier qt ON m.id_quality_tier = qt.id_quality_tier
LEFT JOIN countries c ON m.id_country = c.id_country
ORDER BY m.name_manufacture;
```
### Agregar un nuevo fabricante
```sql
INSERT INTO manufacturers (name_manufacture, id_type_manu, id_quality_tier, id_country, website)
VALUES (
'BREMBO',
1, -- 1=aftermarket, 2=oem
3, -- 1=economy, 2=oem, 3=premium, 4=standard
NULL, -- id_country (buscar en tabla countries o agregar)
'https://www.brembo.com'
);
```
---
## 6. Alta de Referencias Cruzadas / Intercambios
Las cross-references vinculan una pieza OEM con números de parte alternativos.
### 6.1 Crear referencia cruzada (`part_cross_references`)
```sql
INSERT INTO part_cross_references (part_id, cross_reference_number, id_ref_type, source_ref, notes)
VALUES (
67890, -- id_part de la pieza OEM
'D1293', -- Número de referencia cruzada
2, -- Tipo: 2 = interchange (ver tabla abajo)
'Wagner Catalog', -- Fuente de la referencia (opcional)
'Direct replacement' -- Notas (opcional)
);
```
### 6.2 Carga masiva de cross-references
```sql
INSERT INTO part_cross_references (part_id, cross_reference_number, id_ref_type, source_ref) VALUES
(67890, 'D1293', 2, 'Wagner'), -- interchange
(67890, '04465-06100', 3, 'Toyota'), -- oem_alternate (número OEM alterno)
(67890, 'BC1293', 1, 'Akebono'), -- competitor
(67890, '04465-06080', 4, 'Toyota') -- supersession (reemplaza a esta)
ON CONFLICT DO NOTHING;
```
### Valores de `reference_type`
| id_ref_type | Nombre | Uso |
|------------|--------|-----|
| 1 | competitor | Número equivalente de competidor |
| 2 | interchange | Intercambio directo compatible |
| 3 | oem_alternate | Número OEM alterno del mismo fabricante |
| 4 | supersession | El número OEM que esta pieza reemplaza |
---
## 7. Alta de Materiales
Si necesitas especificar el material de una pieza:
```sql
-- Agregar materiales
INSERT INTO materials (name_material) VALUES
('Steel'),
('Aluminum'),
('Ceramic'),
('Rubber'),
('Cast Iron'),
('Stainless Steel'),
('Copper'),
('Plastic')
ON CONFLICT (name_material) DO NOTHING;
-- Luego, al crear una pieza, usar el id_material correspondiente:
-- SELECT id_material FROM materials WHERE name_material = 'Ceramic';
```
---
## 8. Queries Útiles para Metabase (Dashboards)
### Vista completa de una pieza con todos sus datos
```sql
SELECT
p.oem_part_number AS "Número OEM",
p.name_part AS "Nombre EN",
p.name_es AS "Nombre ES",
pg.name_part_group AS "Grupo",
pc.name_part_category AS "Categoría",
p.weight_kg AS "Peso (kg)",
mat.name_material AS "Material",
COUNT(DISTINCT vp.model_year_engine_id) AS "Vehículos compatibles",
COUNT(DISTINCT ap.id_aftermarket_parts) AS "Opciones aftermarket",
COUNT(DISTINCT pcr.id_part_cross_ref) AS "Cross-references"
FROM parts p
JOIN part_groups pg ON p.group_id = pg.id_part_group
JOIN part_categories pc ON pg.category_id = pc.id_part_category
LEFT JOIN materials mat ON p.id_material = mat.id_material
LEFT JOIN vehicle_parts vp ON vp.part_id = p.id_part
LEFT JOIN aftermarket_parts ap ON ap.oem_part_id = p.id_part
LEFT JOIN part_cross_references pcr ON pcr.part_id = p.id_part
GROUP BY p.id_part, p.oem_part_number, p.name_part, p.name_es,
pg.name_part_group, pc.name_part_category, p.weight_kg, mat.name_material
ORDER BY pc.name_part_category, pg.name_part_group, p.name_part;
```
### Catálogo de piezas por vehículo
```sql
SELECT
b.name_brand AS "Marca",
m.name_model AS "Modelo",
y.year_car AS "Año",
e.name_engine AS "Motor",
pc.name_part_category AS "Categoría",
pg.name_part_group AS "Grupo",
p.oem_part_number AS "# OEM",
p.name_part AS "Pieza",
vp.quantity_required AS "Cantidad",
pp.name_position_part AS "Posición"
FROM vehicle_parts vp
JOIN model_year_engine mye ON vp.model_year_engine_id = mye.id_mye
JOIN models m ON mye.model_id = m.id_model
JOIN brands b ON m.brand_id = b.id_brand
JOIN years y ON mye.year_id = y.id_year
JOIN engines e ON mye.engine_id = e.id_engine
JOIN parts p ON vp.part_id = p.id_part
JOIN part_groups pg ON p.group_id = pg.id_part_group
JOIN part_categories pc ON pg.category_id = pc.id_part_category
LEFT JOIN position_part pp ON vp.id_position_part = pp.id_position_part
WHERE b.name_brand ILIKE '{{marca}}'
AND m.name_model ILIKE '{{modelo}}'
ORDER BY pc.display_order, pg.display_order, p.name_part;
```
> En Metabase, `{{marca}}` y `{{modelo}}` se convierten en filtros interactivos.
### Piezas aftermarket con precios
```sql
SELECT
p.oem_part_number AS "OEM #",
p.name_part AS "Pieza OEM",
mfr.name_manufacture AS "Fabricante",
ap.part_number AS "# Aftermarket",
ap.name_aftermarket_parts AS "Nombre",
qt.name_quality AS "Calidad",
ap.price_usd AS "Precio USD",
ap.warranty_months AS "Garantía (meses)"
FROM aftermarket_parts ap
JOIN parts p ON ap.oem_part_id = p.id_part
JOIN manufacturers mfr ON ap.manufacturer_id = mfr.id_manufacture
LEFT JOIN quality_tier qt ON ap.id_quality_tier = qt.id_quality_tier
ORDER BY p.oem_part_number, qt.name_quality DESC, ap.price_usd;
```
### Cross-references de una pieza
```sql
SELECT
p.oem_part_number AS "OEM #",
p.name_part AS "Pieza",
pcr.cross_reference_number AS "# Referencia",
rt.name_ref_type AS "Tipo",
pcr.source_ref AS "Fuente",
pcr.notes AS "Notas"
FROM part_cross_references pcr
JOIN parts p ON pcr.part_id = p.id_part
LEFT JOIN reference_type rt ON pcr.id_ref_type = rt.id_ref_type
WHERE p.oem_part_number = '{{numero_oem}}'
ORDER BY rt.name_ref_type, pcr.cross_reference_number;
```
### Estadísticas generales
```sql
SELECT
(SELECT COUNT(*) FROM parts) AS "Total piezas",
(SELECT COUNT(*) FROM vehicle_parts) AS "Total fitments",
(SELECT COUNT(*) FROM aftermarket_parts) AS "Total aftermarket",
(SELECT COUNT(*) FROM part_cross_references) AS "Total cross-refs",
(SELECT COUNT(*) FROM manufacturers) AS "Total fabricantes",
(SELECT COUNT(DISTINCT model_year_engine_id) FROM vehicle_parts) AS "Vehículos con piezas";
```
---
## 9. Flujo Completo: Ejemplo Paso a Paso
### Dar de alta "Balata delantera Toyota Camry 2020"
**Paso 1:** Agregar la pieza OEM
```sql
INSERT INTO parts (oem_part_number, name_part, name_es, group_id)
VALUES ('04465-06090', 'Front Brake Pad Set', 'Juego Balatas Delanteras', 16)
RETURNING id_part;
-- Resultado: id_part = 1 (anotar este ID)
```
**Paso 2:** Buscar los vehículos compatibles
```sql
SELECT mye.id_mye, y.year_car, e.name_engine
FROM model_year_engine mye
JOIN models m ON mye.model_id = m.id_model
JOIN brands b ON m.brand_id = b.id_brand
JOIN years y ON mye.year_id = y.id_year
JOIN engines e ON mye.engine_id = e.id_engine
WHERE b.name_brand = 'TOYOTA' AND m.name_model = 'Camry'
AND y.year_car BETWEEN 2018 AND 2023;
-- Resultado: lista de id_mye para cada configuración
```
**Paso 3:** Crear los fitments
```sql
INSERT INTO vehicle_parts (model_year_engine_id, part_id, quantity_required, id_position_part)
SELECT mye.id_mye, 1, 1, 1 -- id_part=1, qty=1, position=front
FROM model_year_engine mye
JOIN models m ON mye.model_id = m.id_model
JOIN brands b ON m.brand_id = b.id_brand
JOIN years y ON mye.year_id = y.id_year
WHERE b.name_brand = 'TOYOTA' AND m.name_model = 'Camry'
AND y.year_car BETWEEN 2018 AND 2023
ON CONFLICT DO NOTHING;
```
**Paso 4:** Agregar opciones aftermarket
```sql
INSERT INTO aftermarket_parts (oem_part_id, manufacturer_id, part_number, name_aftermarket_parts, id_quality_tier, price_usd, warranty_months) VALUES
(1, 3, 'CXD1293', 'Premium Ceramic Pad', 3, 45.99, 24),
(1, 5, 'MKD1293', 'Economy Brake Pad', 1, 22.50, 12);
```
**Paso 5:** Agregar cross-references
```sql
INSERT INTO part_cross_references (part_id, cross_reference_number, id_ref_type, source_ref) VALUES
(1, 'D1293', 2, 'Wagner'),
(1, 'BC1293', 1, 'Akebono'),
(1, '04465-06100', 3, 'Toyota OEM Alternate');
```
---
## 10. Notas Importantes
### Restricciones únicas (evitan duplicados)
- `parts`: No tiene constraint único en `oem_part_number` (puede haber mismo OEM en diferentes grupos)
- `vehicle_parts`: Único por `(model_year_engine_id, part_id, id_position_part)`
- `vehicle_diagrams`: Único por `(diagram_id, model_year_engine_id)`
- `model_year_engine`: Único por `(model_id, year_id, engine_id, trim_level)`
### Búsqueda full-text
La tabla `parts` tiene un trigger que auto-genera `search_vector` al insertar/actualizar. Esto permite búsquedas rápidas en español:
```sql
SELECT * FROM parts
WHERE search_vector @@ plainto_tsquery('spanish', 'balata delantera');
```
### Para agregar nuevas categorías o grupos
```sql
-- Nueva categoría
INSERT INTO part_categories (name_part_category, name_es, display_order)
VALUES ('Interior', 'Interior', 13);
-- Nuevo grupo dentro de una categoría
INSERT INTO part_groups (category_id, name_part_group, name_es, display_order)
VALUES (1, 'Headlights', 'Faros Delanteros', 10);
-- (category_id 1 = Body & Lamp Assembly)
```
### Para agregar un país (para fabricantes)
```sql
INSERT INTO countries (name_country) VALUES ('Italy')
ON CONFLICT (name_country) DO NOTHING;
```

470
migrate_to_postgres.py Normal file
View File

@@ -0,0 +1,470 @@
#!/usr/bin/env python3
"""
Migrate data from SQLite (vehicle_database.db) to PostgreSQL (nexus_autoparts).
Usage:
python3 migrate_to_postgres.py
"""
import sqlite3
import sys
import time
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
from config import DB_URL, SQLITE_PATH
from models import (
Base, SEARCH_VECTOR_TRIGGER_SQL,
FuelType, BodyType, Drivetrain, Transmission, Material,
PositionPart, ManufactureType, QualityTier, Country,
ReferenceType, Shape,
Brand, Year, Engine, Model, ModelYearEngine,
PartCategory, PartGroup, Part, VehiclePart,
Manufacturer, AftermarketPart, PartCrossReference,
Diagram, VehicleDiagram, DiagramHotspot, VinCache,
)
BATCH_SIZE = 5000
def log(msg):
print(f" {msg}")
def connect_sqlite():
conn = sqlite3.connect(SQLITE_PATH)
conn.row_factory = sqlite3.Row
return conn
def populate_lookup(pg, sqlite_conn, LookupClass, pk_col, name_col,
sql_query):
"""Extract distinct values from SQLite and insert into a lookup table.
Returns a dict mapping text value → new PK id.
"""
rows = sqlite_conn.execute(sql_query).fetchall()
values = sorted(set(r[0] for r in rows if r[0]))
mapping = {}
for i, val in enumerate(values, start=1):
obj = LookupClass(**{pk_col: i, name_col: val})
pg.add(obj)
mapping[val] = i
pg.flush()
log(f" {LookupClass.__tablename__}: {len(mapping)} values")
return mapping
def migrate_table(pg, sqlite_conn, query, build_obj_fn, label, batch=BATCH_SIZE):
"""Generic batch-migrate helper."""
rows = sqlite_conn.execute(query).fetchall()
total = len(rows)
count = 0
for i in range(0, total, batch):
chunk = rows[i:i + batch]
for row in chunk:
obj = build_obj_fn(row)
if obj is not None:
pg.add(obj)
pg.flush()
count += len(chunk)
if count % 50000 == 0 or count == total:
log(f" {label}: {count}/{total}")
return total
def main():
print("=" * 60)
print(" NEXUS AUTOPARTS — SQLite → PostgreSQL Migration")
print("=" * 60)
t0 = time.time()
# Connect
print("\n[1] Connecting...")
sqlite_conn = connect_sqlite()
engine = create_engine(DB_URL, echo=False)
Session = sessionmaker(bind=engine)
# Drop & recreate all tables
print("\n[2] Creating schema...")
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
log("All tables created")
pg = Session()
# ── Lookup tables ──────────────────────────────────────
print("\n[3] Populating lookup tables...")
fuel_map = populate_lookup(
pg, sqlite_conn, FuelType, "id_fuel", "name_fuel",
"SELECT DISTINCT fuel_type FROM engines WHERE fuel_type IS NOT NULL")
body_map = populate_lookup(
pg, sqlite_conn, BodyType, "id_body", "name_body",
"SELECT DISTINCT body_type FROM models WHERE body_type IS NOT NULL")
drive_map = populate_lookup(
pg, sqlite_conn, Drivetrain, "id_drivetrain", "name_drivetrain",
"SELECT DISTINCT drivetrain FROM model_year_engine WHERE drivetrain IS NOT NULL")
trans_map = populate_lookup(
pg, sqlite_conn, Transmission, "id_transmission", "name_transmission",
"SELECT DISTINCT transmission FROM model_year_engine WHERE transmission IS NOT NULL")
mat_map = populate_lookup(
pg, sqlite_conn, Material, "id_material", "name_material",
"SELECT DISTINCT material FROM parts WHERE material IS NOT NULL")
pos_map = populate_lookup(
pg, sqlite_conn, PositionPart, "id_position_part", "name_position_part",
"SELECT DISTINCT position FROM vehicle_parts WHERE position IS NOT NULL")
mtype_map = populate_lookup(
pg, sqlite_conn, ManufactureType, "id_type_manu", "name_type_manu",
"SELECT DISTINCT type FROM manufacturers WHERE type IS NOT NULL")
qtier_map = populate_lookup(
pg, sqlite_conn, QualityTier, "id_quality_tier", "name_quality",
"""SELECT DISTINCT quality_tier FROM (
SELECT quality_tier FROM manufacturers WHERE quality_tier IS NOT NULL
UNION
SELECT quality_tier FROM aftermarket_parts WHERE quality_tier IS NOT NULL
)""")
country_map = populate_lookup(
pg, sqlite_conn, Country, "id_country", "name_country",
"SELECT DISTINCT country FROM manufacturers WHERE country IS NOT NULL")
reftype_map = populate_lookup(
pg, sqlite_conn, ReferenceType, "id_ref_type", "name_ref_type",
"SELECT DISTINCT reference_type FROM part_cross_references WHERE reference_type IS NOT NULL")
shape_map = populate_lookup(
pg, sqlite_conn, Shape, "id_shape", "name_shape",
"SELECT DISTINCT shape FROM diagram_hotspots WHERE shape IS NOT NULL")
pg.commit()
# ── Core tables ────────────────────────────────────────
print("\n[4] Migrating core tables...")
# brands
n = migrate_table(pg, sqlite_conn,
"SELECT * FROM brands ORDER BY id",
lambda r: Brand(
id_brand=r["id"], name_brand=r["name"],
country=r["country"], founded_year=r["founded_year"],
created_at=r["created_at"]),
"brands")
pg.commit()
log(f"brands: {n} rows")
# years
n = migrate_table(pg, sqlite_conn,
"SELECT * FROM years ORDER BY id",
lambda r: Year(
id_year=r["id"], year_car=r["year"],
created_at=r["created_at"]),
"years")
pg.commit()
log(f"years: {n} rows")
# engines
n = migrate_table(pg, sqlite_conn,
"SELECT * FROM engines ORDER BY id",
lambda r: Engine(
id_engine=r["id"], name_engine=r["name"],
displacement_cc=r["displacement_cc"], cylinders=r["cylinders"],
id_fuel=fuel_map.get(r["fuel_type"]),
power_hp=r["power_hp"], torque_nm=r["torque_nm"],
engine_code=r["engine_code"], created_at=r["created_at"]),
"engines")
pg.commit()
log(f"engines: {n} rows")
# models
n = migrate_table(pg, sqlite_conn,
"SELECT * FROM models ORDER BY id",
lambda r: Model(
id_model=r["id"], brand_id=r["brand_id"],
name_model=r["name"],
id_body=body_map.get(r["body_type"]),
generation=r["generation"],
production_start_year=r["production_start_year"],
production_end_year=r["production_end_year"],
created_at=r["created_at"]),
"models")
pg.commit()
log(f"models: {n} rows")
# model_year_engine
n = migrate_table(pg, sqlite_conn,
"SELECT * FROM model_year_engine ORDER BY id",
lambda r: ModelYearEngine(
id_mye=r["id"], model_id=r["model_id"],
year_id=r["year_id"], engine_id=r["engine_id"],
trim_level=r["trim_level"],
id_drivetrain=drive_map.get(r["drivetrain"]),
id_transmission=trans_map.get(r["transmission"]),
created_at=r["created_at"]),
"model_year_engine")
pg.commit()
log(f"model_year_engine: {n} rows")
# part_categories
n = migrate_table(pg, sqlite_conn,
"SELECT * FROM part_categories ORDER BY id",
lambda r: PartCategory(
id_part_category=r["id"],
name_part_category=r["name"], name_es=r["name_es"],
parent_id=r["parent_id"], slug=r["slug"],
icon_name=r["icon_name"], display_order=r["display_order"],
created_at=r["created_at"]),
"part_categories")
pg.commit()
log(f"part_categories: {n} rows")
# part_groups
n = migrate_table(pg, sqlite_conn,
"SELECT * FROM part_groups ORDER BY id",
lambda r: PartGroup(
id_part_group=r["id"], category_id=r["category_id"],
name_part_group=r["name"], name_es=r["name_es"],
slug=r["slug"], display_order=r["display_order"],
created_at=r["created_at"]),
"part_groups")
pg.commit()
log(f"part_groups: {n} rows")
# parts (without search_vector — trigger will fill it)
n = migrate_table(pg, sqlite_conn,
"SELECT * FROM parts ORDER BY id",
lambda r: Part(
id_part=r["id"], oem_part_number=r["oem_part_number"],
name_part=r["name"], name_es=r["name_es"],
group_id=r["group_id"],
description=r["description"], description_es=r["description_es"],
weight_kg=r["weight_kg"],
id_material=mat_map.get(r["material"]),
created_at=r["created_at"]),
"parts")
pg.commit()
log(f"parts: {n} rows")
# vehicle_parts
n = migrate_table(pg, sqlite_conn,
"SELECT * FROM vehicle_parts ORDER BY id",
lambda r: VehiclePart(
id_vehicle_part=r["id"],
model_year_engine_id=r["model_year_engine_id"],
part_id=r["part_id"],
quantity_required=r["quantity_required"],
id_position_part=pos_map.get(r["position"]),
fitment_notes=r["fitment_notes"],
created_at=r["created_at"]),
"vehicle_parts", batch=10000)
pg.commit()
log(f"vehicle_parts: {n} rows")
# manufacturers
n = migrate_table(pg, sqlite_conn,
"SELECT * FROM manufacturers ORDER BY id",
lambda r: Manufacturer(
id_manufacture=r["id"], name_manufacture=r["name"],
id_type_manu=mtype_map.get(r["type"]),
id_quality_tier=qtier_map.get(r["quality_tier"]),
id_country=country_map.get(r["country"]),
logo_url=r["logo_url"], website=r["website"],
created_at=r["created_at"]),
"manufacturers")
pg.commit()
log(f"manufacturers: {n} rows")
# aftermarket_parts (skip orphans with missing oem_part_id)
valid_part_ids = set(r[0] for r in sqlite_conn.execute("SELECT id FROM parts").fetchall())
n = migrate_table(pg, sqlite_conn,
"SELECT * FROM aftermarket_parts ORDER BY id",
lambda r: AftermarketPart(
id_aftermarket_parts=r["id"],
oem_part_id=r["oem_part_id"],
manufacturer_id=r["manufacturer_id"],
part_number=r["part_number"],
name_aftermarket_parts=r["name"], name_es=r["name_es"],
id_quality_tier=qtier_map.get(r["quality_tier"]),
price_usd=r["price_usd"],
warranty_months=r["warranty_months"],
created_at=r["created_at"]) if r["oem_part_id"] in valid_part_ids else None,
"aftermarket_parts")
pg.commit()
log(f"aftermarket_parts: {n} rows")
# part_cross_references
n = migrate_table(pg, sqlite_conn,
"SELECT * FROM part_cross_references ORDER BY id",
lambda r: PartCrossReference(
id_part_cross_ref=r["id"], part_id=r["part_id"],
cross_reference_number=r["cross_reference_number"],
id_ref_type=reftype_map.get(r["reference_type"]),
source_ref=r["source"], notes=r["notes"],
created_at=r["created_at"]),
"part_cross_references")
pg.commit()
log(f"part_cross_references: {n} rows")
# diagrams
n = migrate_table(pg, sqlite_conn,
"SELECT * FROM diagrams ORDER BY id",
lambda r: Diagram(
id_diagram=r["id"], name_diagram=r["name"],
name_es=r["name_es"], group_id=r["group_id"],
image_path=r["image_path"],
thumbnail_path=r["thumbnail_path"],
display_order=r["display_order"],
source_diagram=r["source"],
created_at=r["created_at"]),
"diagrams")
pg.commit()
log(f"diagrams: {n} rows")
# vehicle_diagrams (skip orphans with missing diagram_id)
valid_diagram_ids = set(r[0] for r in sqlite_conn.execute("SELECT id FROM diagrams").fetchall())
n = migrate_table(pg, sqlite_conn,
"SELECT * FROM vehicle_diagrams ORDER BY id",
lambda r: VehicleDiagram(
id_vehicle_dgr=r["id"], diagram_id=r["diagram_id"],
model_year_engine_id=r["model_year_engine_id"],
notes=r["notes"], created_at=r["created_at"])
if r["diagram_id"] in valid_diagram_ids else None,
"vehicle_diagrams")
pg.commit()
log(f"vehicle_diagrams: {n} rows")
# diagram_hotspots
n = migrate_table(pg, sqlite_conn,
"SELECT * FROM diagram_hotspots ORDER BY id",
lambda r: DiagramHotspot(
id_dgr_hotspot=r["id"], diagram_id=r["diagram_id"],
part_id=r["part_id"], callout_number=r["callout_number"],
id_shape=shape_map.get(r["shape"]),
coords=r["coords"], created_at=r["created_at"]),
"diagram_hotspots")
pg.commit()
log(f"diagram_hotspots: {n} rows")
# vin_cache
import json
n = migrate_table(pg, sqlite_conn,
"SELECT * FROM vin_cache ORDER BY id",
lambda r: VinCache(
id=r["id"], vin=r["vin"],
decoded_data=json.loads(r["decoded_data"]) if r["decoded_data"] else {},
make=r["make"], model=r["model"], year=r["year"],
engine_info=r["engine_info"], body_class=r["body_class"],
drive_type=r["drive_type"],
model_year_engine_id=r["model_year_engine_id"],
created_at=r["created_at"], expires_at=r["expires_at"]),
"vin_cache")
pg.commit()
log(f"vin_cache: {n} rows")
# ── Reset sequences ───────────────────────────────────
print("\n[5] Resetting sequences...")
seq_tables = [
("brands", "id_brand"),
("years", "id_year"),
("engines", "id_engine"),
("models", "id_model"),
("model_year_engine", "id_mye"),
("part_categories", "id_part_category"),
("part_groups", "id_part_group"),
("parts", "id_part"),
("vehicle_parts", "id_vehicle_part"),
("manufacturers", "id_manufacture"),
("aftermarket_parts", "id_aftermarket_parts"),
("part_cross_references", "id_part_cross_ref"),
("diagrams", "id_diagram"),
("vehicle_diagrams", "id_vehicle_dgr"),
("diagram_hotspots", "id_dgr_hotspot"),
("vin_cache", "id"),
("fuel_type", "id_fuel"),
("body_type", "id_body"),
("drivetrain", "id_drivetrain"),
("transmission", "id_transmission"),
("materials", "id_material"),
("position_part", "id_position_part"),
("manufacture_type", "id_type_manu"),
("quality_tier", "id_quality_tier"),
("countries", "id_country"),
("reference_type", "id_ref_type"),
("shapes", "id_shape"),
]
with engine.connect() as conn:
for table, pk in seq_tables:
conn.execute(text(
f"SELECT setval(pg_get_serial_sequence('{table}', '{pk}'), "
f"COALESCE((SELECT MAX({pk}) FROM {table}), 0) + 1, false)"
))
conn.commit()
log("All sequences reset")
# ── Full-text search trigger ──────────────────────────
print("\n[6] Creating search trigger & updating vectors...")
with engine.connect() as conn:
conn.execute(text(SEARCH_VECTOR_TRIGGER_SQL))
conn.commit()
# Backfill search_vector for existing rows
conn.execute(text("""
UPDATE parts SET search_vector = to_tsvector('spanish',
coalesce(oem_part_number, '') || ' ' ||
coalesce(name_part, '') || ' ' ||
coalesce(name_es, '') || ' ' ||
coalesce(description, ''))
"""))
conn.commit()
log("Search vectors populated")
# ── Verify counts ─────────────────────────────────────
print("\n[7] Verifying row counts...")
sqlite_tables = [
"brands", "models", "years", "engines", "model_year_engine",
"part_categories", "part_groups", "parts", "vehicle_parts",
"manufacturers", "aftermarket_parts", "part_cross_references",
"diagrams", "vehicle_diagrams", "diagram_hotspots", "vin_cache"
]
pg_tables = [
("brands", "id_brand"), ("models", "id_model"),
("years", "id_year"), ("engines", "id_engine"),
("model_year_engine", "id_mye"),
("part_categories", "id_part_category"),
("part_groups", "id_part_group"), ("parts", "id_part"),
("vehicle_parts", "id_vehicle_part"),
("manufacturers", "id_manufacture"),
("aftermarket_parts", "id_aftermarket_parts"),
("part_cross_references", "id_part_cross_ref"),
("diagrams", "id_diagram"),
("vehicle_diagrams", "id_vehicle_dgr"),
("diagram_hotspots", "id_dgr_hotspot"),
("vin_cache", "id"),
]
ok = True
with engine.connect() as conn:
for st, (pt, pk) in zip(sqlite_tables, pg_tables):
s_count = sqlite_conn.execute(f"SELECT COUNT(*) FROM {st}").fetchone()[0]
p_count = conn.execute(text(f"SELECT COUNT(*) FROM {pt}")).scalar()
status = "OK" if s_count == p_count else "MISMATCH"
if status == "MISMATCH":
ok = False
log(f" {st}: SQLite={s_count} PG={p_count} [{status}]")
sqlite_conn.close()
elapsed = time.time() - t0
print("\n" + "=" * 60)
if ok:
print(f" Migration completed successfully in {elapsed:.1f}s")
else:
print(f" Migration completed with MISMATCHES in {elapsed:.1f}s")
print("=" * 60)
if __name__ == "__main__":
main()

410
models.py Normal file
View File

@@ -0,0 +1,410 @@
"""
SQLAlchemy ORM models for Nexus Autoparts.
"""
from sqlalchemy import (
Column, Integer, String, Float, Boolean, Text, DateTime, ForeignKey,
UniqueConstraint, Index, func, text
)
from sqlalchemy.dialects.postgresql import JSONB, TSVECTOR
from sqlalchemy.orm import relationship, DeclarativeBase
from datetime import datetime
class Base(DeclarativeBase):
pass
# ─────────────────────────────────────────────
# Lookup tables
# ─────────────────────────────────────────────
class FuelType(Base):
__tablename__ = "fuel_type"
id_fuel = Column(Integer, primary_key=True)
name_fuel = Column(String(50), nullable=False, unique=True)
class BodyType(Base):
__tablename__ = "body_type"
id_body = Column(Integer, primary_key=True)
name_body = Column(String(50), nullable=False, unique=True)
class Drivetrain(Base):
__tablename__ = "drivetrain"
id_drivetrain = Column(Integer, primary_key=True)
name_drivetrain = Column(String(50), nullable=False, unique=True)
class Transmission(Base):
__tablename__ = "transmission"
id_transmission = Column(Integer, primary_key=True)
name_transmission = Column(String(50), nullable=False, unique=True)
class Material(Base):
__tablename__ = "materials"
id_material = Column(Integer, primary_key=True)
name_material = Column(String(100), nullable=False, unique=True)
class PositionPart(Base):
__tablename__ = "position_part"
id_position_part = Column(Integer, primary_key=True)
name_position_part = Column(String(50), nullable=False, unique=True)
class ManufactureType(Base):
__tablename__ = "manufacture_type"
id_type_manu = Column(Integer, primary_key=True)
name_type_manu = Column(String(50), nullable=False, unique=True)
class QualityTier(Base):
__tablename__ = "quality_tier"
id_quality_tier = Column(Integer, primary_key=True)
name_quality = Column(String(50), nullable=False, unique=True)
class Country(Base):
__tablename__ = "countries"
id_country = Column(Integer, primary_key=True)
name_country = Column(String(100), nullable=False, unique=True)
class ReferenceType(Base):
__tablename__ = "reference_type"
id_ref_type = Column(Integer, primary_key=True)
name_ref_type = Column(String(50), nullable=False, unique=True)
class Shape(Base):
__tablename__ = "shapes"
id_shape = Column(Integer, primary_key=True)
name_shape = Column(String(50), nullable=False, unique=True)
# ─────────────────────────────────────────────
# Core tables
# ─────────────────────────────────────────────
class Brand(Base):
__tablename__ = "brands"
id_brand = Column(Integer, primary_key=True)
name_brand = Column(String(200), nullable=False, unique=True)
country = Column(String(100))
founded_year = Column(Integer)
created_at = Column(DateTime, default=datetime.utcnow)
models = relationship("Model", back_populates="brand")
class Year(Base):
__tablename__ = "years"
id_year = Column(Integer, primary_key=True)
year_car = Column(Integer, nullable=False, unique=True)
created_at = Column(DateTime, default=datetime.utcnow)
class Engine(Base):
__tablename__ = "engines"
id_engine = Column(Integer, primary_key=True)
name_engine = Column(String(300), nullable=False)
displacement_cc = Column(Float)
cylinders = Column(Integer)
id_fuel = Column(Integer, ForeignKey("fuel_type.id_fuel"))
power_hp = Column(Integer)
torque_nm = Column(Integer)
engine_code = Column(String(100))
created_at = Column(DateTime, default=datetime.utcnow)
fuel_type = relationship("FuelType")
class Model(Base):
__tablename__ = "models"
id_model = Column(Integer, primary_key=True)
brand_id = Column(Integer, ForeignKey("brands.id_brand"), nullable=False)
name_model = Column(String(300), nullable=False)
id_body = Column(Integer, ForeignKey("body_type.id_body"))
generation = Column(String(100))
production_start_year = Column(Integer)
production_end_year = Column(Integer)
created_at = Column(DateTime, default=datetime.utcnow)
brand = relationship("Brand", back_populates="models")
body_type = relationship("BodyType")
__table_args__ = (
Index("idx_models_brand", "brand_id"),
)
class ModelYearEngine(Base):
__tablename__ = "model_year_engine"
id_mye = Column(Integer, primary_key=True)
model_id = Column(Integer, ForeignKey("models.id_model"), nullable=False)
year_id = Column(Integer, ForeignKey("years.id_year"), nullable=False)
engine_id = Column(Integer, ForeignKey("engines.id_engine"), nullable=False)
trim_level = Column(String(100))
id_drivetrain = Column(Integer, ForeignKey("drivetrain.id_drivetrain"))
id_transmission = Column(Integer, ForeignKey("transmission.id_transmission"))
created_at = Column(DateTime, default=datetime.utcnow)
model = relationship("Model")
year = relationship("Year")
engine = relationship("Engine")
drivetrain = relationship("Drivetrain")
transmission = relationship("Transmission")
__table_args__ = (
UniqueConstraint("model_id", "year_id", "engine_id", "trim_level",
name="uq_mye_combo"),
Index("idx_mye_model", "model_id"),
Index("idx_mye_year", "year_id"),
Index("idx_mye_engine", "engine_id"),
)
class PartCategory(Base):
__tablename__ = "part_categories"
id_part_category = Column(Integer, primary_key=True)
name_part_category = Column(String(200), nullable=False)
name_es = Column(String(200))
parent_id = Column(Integer, ForeignKey("part_categories.id_part_category"))
slug = Column(String(200), unique=True)
icon_name = Column(String(100))
display_order = Column(Integer, default=0)
created_at = Column(DateTime, default=datetime.utcnow)
parent = relationship("PartCategory", remote_side="PartCategory.id_part_category")
__table_args__ = (
Index("idx_part_categories_parent", "parent_id"),
Index("idx_part_categories_slug", "slug"),
)
class PartGroup(Base):
__tablename__ = "part_groups"
id_part_group = Column(Integer, primary_key=True)
category_id = Column(Integer, ForeignKey("part_categories.id_part_category"), nullable=False)
name_part_group = Column(String(200), nullable=False)
name_es = Column(String(200))
slug = Column(String(200))
display_order = Column(Integer, default=0)
created_at = Column(DateTime, default=datetime.utcnow)
category = relationship("PartCategory")
__table_args__ = (
Index("idx_part_groups_category", "category_id"),
)
class Part(Base):
__tablename__ = "parts"
id_part = Column(Integer, primary_key=True)
oem_part_number = Column(String(100), nullable=False)
name_part = Column(String(300), nullable=False)
name_es = Column(String(300))
group_id = Column(Integer, ForeignKey("part_groups.id_part_group"))
description = Column(Text)
description_es = Column(Text)
weight_kg = Column(Float)
id_material = Column(Integer, ForeignKey("materials.id_material"))
created_at = Column(DateTime, default=datetime.utcnow)
search_vector = Column(TSVECTOR)
group = relationship("PartGroup")
material = relationship("Material")
__table_args__ = (
Index("idx_parts_oem", "oem_part_number"),
Index("idx_parts_group", "group_id"),
Index("idx_parts_search", "search_vector", postgresql_using="gin"),
)
class VehiclePart(Base):
__tablename__ = "vehicle_parts"
id_vehicle_part = Column(Integer, primary_key=True)
model_year_engine_id = Column(Integer, ForeignKey("model_year_engine.id_mye"), nullable=False)
part_id = Column(Integer, ForeignKey("parts.id_part"), nullable=False)
quantity_required = Column(Integer, default=1)
id_position_part = Column(Integer, ForeignKey("position_part.id_position_part"))
fitment_notes = Column(Text)
created_at = Column(DateTime, default=datetime.utcnow)
model_year_engine = relationship("ModelYearEngine")
part = relationship("Part")
position = relationship("PositionPart")
__table_args__ = (
UniqueConstraint("model_year_engine_id", "part_id", "id_position_part",
name="uq_vehicle_part"),
Index("idx_vehicle_parts_mye", "model_year_engine_id"),
Index("idx_vehicle_parts_part", "part_id"),
)
class Manufacturer(Base):
__tablename__ = "manufacturers"
id_manufacture = Column(Integer, primary_key=True)
name_manufacture = Column(String(200), nullable=False, unique=True)
id_type_manu = Column(Integer, ForeignKey("manufacture_type.id_type_manu"))
id_quality_tier = Column(Integer, ForeignKey("quality_tier.id_quality_tier"))
id_country = Column(Integer, ForeignKey("countries.id_country"))
logo_url = Column(String(500))
website = Column(String(500))
created_at = Column(DateTime, default=datetime.utcnow)
manufacture_type = relationship("ManufactureType")
quality_tier = relationship("QualityTier")
country = relationship("Country")
class AftermarketPart(Base):
__tablename__ = "aftermarket_parts"
id_aftermarket_parts = Column(Integer, primary_key=True)
oem_part_id = Column(Integer, ForeignKey("parts.id_part"), nullable=False)
manufacturer_id = Column(Integer, ForeignKey("manufacturers.id_manufacture"), nullable=False)
part_number = Column(String(100), nullable=False)
name_aftermarket_parts = Column(String(300))
name_es = Column(String(300))
id_quality_tier = Column(Integer, ForeignKey("quality_tier.id_quality_tier"))
price_usd = Column(Float)
warranty_months = Column(Integer)
created_at = Column(DateTime, default=datetime.utcnow)
oem_part = relationship("Part")
manufacturer = relationship("Manufacturer")
quality_tier = relationship("QualityTier")
__table_args__ = (
Index("idx_aftermarket_oem", "oem_part_id"),
Index("idx_aftermarket_manufacturer", "manufacturer_id"),
Index("idx_aftermarket_part_number", "part_number"),
)
class PartCrossReference(Base):
__tablename__ = "part_cross_references"
id_part_cross_ref = Column(Integer, primary_key=True)
part_id = Column(Integer, ForeignKey("parts.id_part"), nullable=False)
cross_reference_number = Column(String(100), nullable=False)
id_ref_type = Column(Integer, ForeignKey("reference_type.id_ref_type"))
source_ref = Column(String(200))
notes = Column(Text)
created_at = Column(DateTime, default=datetime.utcnow)
part = relationship("Part")
reference_type = relationship("ReferenceType")
__table_args__ = (
Index("idx_cross_ref_part", "part_id"),
Index("idx_cross_ref_number", "cross_reference_number"),
)
class Diagram(Base):
__tablename__ = "diagrams"
id_diagram = Column(Integer, primary_key=True)
name_diagram = Column(String(300), nullable=False)
name_es = Column(String(300))
group_id = Column(Integer, ForeignKey("part_groups.id_part_group"), nullable=False)
image_path = Column(String(500), nullable=False)
thumbnail_path = Column(String(500))
display_order = Column(Integer, default=0)
source_diagram = Column(String(200))
created_at = Column(DateTime, default=datetime.utcnow)
group = relationship("PartGroup")
__table_args__ = (
Index("idx_diagrams_group", "group_id"),
)
class VehicleDiagram(Base):
__tablename__ = "vehicle_diagrams"
id_vehicle_dgr = Column(Integer, primary_key=True)
diagram_id = Column(Integer, ForeignKey("diagrams.id_diagram"), nullable=False)
model_year_engine_id = Column(Integer, ForeignKey("model_year_engine.id_mye"), nullable=False)
notes = Column(Text)
created_at = Column(DateTime, default=datetime.utcnow)
diagram = relationship("Diagram")
model_year_engine = relationship("ModelYearEngine")
__table_args__ = (
UniqueConstraint("diagram_id", "model_year_engine_id", name="uq_vehicle_diagram"),
Index("idx_vehicle_diagrams_diagram", "diagram_id"),
Index("idx_vehicle_diagrams_mye", "model_year_engine_id"),
)
class DiagramHotspot(Base):
__tablename__ = "diagram_hotspots"
id_dgr_hotspot = Column(Integer, primary_key=True)
diagram_id = Column(Integer, ForeignKey("diagrams.id_diagram"), nullable=False)
part_id = Column(Integer, ForeignKey("parts.id_part"))
callout_number = Column(Integer)
id_shape = Column(Integer, ForeignKey("shapes.id_shape"))
coords = Column(Text, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
diagram = relationship("Diagram")
part = relationship("Part")
shape = relationship("Shape")
__table_args__ = (
Index("idx_hotspots_diagram", "diagram_id"),
Index("idx_hotspots_part", "part_id"),
)
class VinCache(Base):
__tablename__ = "vin_cache"
id = Column(Integer, primary_key=True)
vin = Column(String(17), nullable=False, unique=True)
decoded_data = Column(JSONB, nullable=False)
make = Column(String(100))
model = Column(String(100))
year = Column(Integer)
engine_info = Column(String(200))
body_class = Column(String(100))
drive_type = Column(String(100))
model_year_engine_id = Column(Integer, ForeignKey("model_year_engine.id_mye"))
created_at = Column(DateTime, default=datetime.utcnow)
expires_at = Column(DateTime)
__table_args__ = (
Index("idx_vin_cache_vin", "vin"),
Index("idx_vin_cache_make_model", "make", "model", "year"),
)
# ─────────────────────────────────────────────
# Full-text search trigger SQL (run after table creation)
# ─────────────────────────────────────────────
SEARCH_VECTOR_TRIGGER_SQL = """
CREATE OR REPLACE FUNCTION parts_search_vector_update() RETURNS trigger AS $$
BEGIN
NEW.search_vector := to_tsvector('spanish',
coalesce(NEW.oem_part_number, '') || ' ' ||
coalesce(NEW.name_part, '') || ' ' ||
coalesce(NEW.name_es, '') || ' ' ||
coalesce(NEW.description, '')
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_parts_search_vector ON parts;
CREATE TRIGGER trg_parts_search_vector
BEFORE INSERT OR UPDATE ON parts
FOR EACH ROW
EXECUTE FUNCTION parts_search_vector_update();
"""

View File

@@ -2,3 +2,6 @@ flask==2.3.3
requests>=2.28.0 requests>=2.28.0
beautifulsoup4>=4.11.0 beautifulsoup4>=4.11.0
lxml>=4.9.0 lxml>=4.9.0
sqlalchemy>=2.0
psycopg2-binary>=2.9
flask-sqlalchemy>=3.1