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:
12
README.md
12
README.md
@@ -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
|
||||
|
||||
**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
|
||||
- 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**
|
||||
```bash
|
||||
git clone https://git.consultoria-as.com/[usuario]/Autoparts-DB.git
|
||||
cd Autoparts-DB
|
||||
git clone https://git.consultoria-as.com/[usuario]/Nexus-Autoparts.git
|
||||
cd Nexus-Autoparts
|
||||
```
|
||||
|
||||
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
20
config.py
Normal 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"
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
"""
|
||||
Configuration settings for the AUTOPARTES console application.
|
||||
Configuration settings for the NEXUS AUTOPARTS console application.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Application metadata
|
||||
VERSION = "1.0.0"
|
||||
APP_NAME = "AUTOPARTES"
|
||||
APP_SUBTITLE = "Sistema de Catalogo de Autopartes"
|
||||
VERSION = "2.0.0"
|
||||
APP_NAME = "NEXUS AUTOPARTS"
|
||||
APP_SUBTITLE = "Tu conexión directa con las partes que necesitas"
|
||||
|
||||
# Database path (relative to the console/ directory, resolved to absolute)
|
||||
_CONSOLE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
DB_PATH = os.path.join(_CONSOLE_DIR, "..", "vehicle_database", "vehicle_database.db")
|
||||
DB_PATH = os.path.normpath(DB_PATH)
|
||||
# Database URL (PostgreSQL)
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
|
||||
from config import DB_URL
|
||||
|
||||
# NHTSA VIN Decoder API
|
||||
NHTSA_API_URL = "https://vpic.nhtsa.dot.gov/api/vehicles/DecodeVin"
|
||||
|
||||
@@ -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
|
||||
current screen, reads a keypress, dispatches it, and follows any
|
||||
|
||||
831
console/db.py
831
console/db.py
File diff suppressed because it is too large
Load Diff
@@ -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:
|
||||
python -m console # via package
|
||||
|
||||
@@ -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
|
||||
:class:`BaseRenderer` and implement all of its methods. Screens call
|
||||
|
||||
@@ -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
|
||||
by classic Pick/UNIX VT220 terminals. All drawing is done through Python's
|
||||
|
||||
@@ -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
|
||||
delete (F8/Del) operations for the part_cross_references table.
|
||||
|
||||
@@ -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)
|
||||
operations for the manufacturers table.
|
||||
|
||||
@@ -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
|
||||
export data to JSON files. Uses the renderer's show_input and
|
||||
|
||||
@@ -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
|
||||
delete (F8/Del) operations. Form editing is handled inline with
|
||||
|
||||
@@ -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)
|
||||
and displays matching results in a table. Selecting a result navigates
|
||||
|
||||
@@ -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
|
||||
the FTS5 full-text search engine (with LIKE fallback). Results are
|
||||
|
||||
@@ -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:
|
||||
Categories -> Groups -> Parts. An optional vehicle filter (mye_id)
|
||||
|
||||
@@ -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
|
||||
alternatives. The first column is always the OEM part; subsequent columns
|
||||
|
||||
@@ -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
|
||||
:meth:`Database.get_stats`.
|
||||
|
||||
@@ -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
|
||||
application sections. Number keys jump directly; arrow keys move the
|
||||
|
||||
@@ -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.)
|
||||
with a table of aftermarket alternatives. Number keys navigate to
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
via the NHTSA vPIC API (with local caching), and displays the decoded
|
||||
|
||||
@@ -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
|
||||
terminal, allowing end-to-end testing of the screen -> renderer pipeline
|
||||
|
||||
@@ -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
|
||||
quality-tier visual bars.
|
||||
|
||||
@@ -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
|
||||
Product Information Catalog (vPIC) DecodeVin endpoint to retrieve vehicle
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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 rel="stylesheet" href="/shared.css">
|
||||
<style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Admin Panel JavaScript
|
||||
* CRUD operations and CSV import/export for Autopartes DB
|
||||
* CRUD operations and CSV import/export for Nexus Autoparts
|
||||
*/
|
||||
|
||||
// State
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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 rel="stylesheet" href="/shared.css">
|
||||
<style>
|
||||
@@ -1095,7 +1095,7 @@
|
||||
<div class="footer-brand">
|
||||
<div class="logo">
|
||||
<div class="logo-icon">⚙️</div>
|
||||
<div class="logo-text">AUTOPARTS DB</div>
|
||||
<div class="logo-text">NEXUS AUTOPARTS</div>
|
||||
</div>
|
||||
<p>Sistema de catálogo de autopartes con navegación jerárquica, diagramas explosionados y decodificador de VIN.</p>
|
||||
<div class="social-links">
|
||||
@@ -1131,7 +1131,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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 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">
|
||||
|
||||
@@ -10,7 +10,7 @@ const enhancedSearch = {
|
||||
debounceMs: 300,
|
||||
maxResults: 8,
|
||||
maxRecent: 5,
|
||||
storageKey: 'autopartes_recent_searches'
|
||||
storageKey: 'nexus_recent_searches'
|
||||
},
|
||||
|
||||
// State
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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 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">
|
||||
|
||||
@@ -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>.
|
||||
* Auto-highlights the current page link based on window.location.pathname.
|
||||
@@ -86,7 +86,7 @@
|
||||
+ '-webkit-background-clip: text;'
|
||||
+ '-webkit-text-fill-color: transparent;'
|
||||
+ 'background-clip: text;'
|
||||
+ '">AUTOPARTS DB</span>'
|
||||
+ '">NEXUS AUTOPARTS</span>'
|
||||
+ '</a>'
|
||||
// Slot for extra page-specific content (search bars, stats, etc.)
|
||||
+ '<div id="shared-nav-extra" style="display: contents;"></div>'
|
||||
|
||||
4547
dashboard/server.py
4547
dashboard/server.py
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
/* ============================================================
|
||||
shared.css -- Common styles for all AutoParts DB pages
|
||||
shared.css -- Common styles for all Nexus Autoparts pages
|
||||
============================================================ */
|
||||
|
||||
/* --- Reset --- */
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Documentación de Base de Datos - Autoparts DB
|
||||
# Documentación de Base de Datos - Nexus Autoparts
|
||||
|
||||
## Resumen
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Guía de Instalación - Autoparts DB
|
||||
# Guía de Instalación - Nexus Autoparts
|
||||
|
||||
## Requisitos del Sistema
|
||||
|
||||
@@ -28,10 +28,10 @@ El proyecto es compatible con:
|
||||
|
||||
```bash
|
||||
# 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
|
||||
cd Autoparts-DB
|
||||
cd Nexus-Autoparts
|
||||
|
||||
# 3. Instalar dependencias
|
||||
pip install -r requirements.txt
|
||||
@@ -48,8 +48,8 @@ python3 server.py
|
||||
### Paso 1: Clonar el Repositorio
|
||||
|
||||
```bash
|
||||
git clone https://git.consultoria-as.com/[usuario]/Autoparts-DB.git
|
||||
cd Autoparts-DB
|
||||
git clone https://git.consultoria-as.com/[usuario]/Nexus-Autoparts.git
|
||||
cd Nexus-Autoparts
|
||||
```
|
||||
|
||||
### Paso 2: Crear Entorno Virtual (Recomendado)
|
||||
@@ -254,16 +254,16 @@ gunicorn -w 4 -b 0.0.0.0:8080 server:app
|
||||
|
||||
### Usando systemd
|
||||
|
||||
Crear archivo `/etc/systemd/system/autoparts-db.service`:
|
||||
Crear archivo `/etc/systemd/system/nexus-autoparts.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Autoparts DB Dashboard
|
||||
Description=Nexus Autoparts Dashboard
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=www-data
|
||||
WorkingDirectory=/path/to/Autoparts-DB/dashboard
|
||||
WorkingDirectory=/path/to/Nexus-Autoparts/dashboard
|
||||
ExecStart=/usr/bin/python3 server.py
|
||||
Restart=always
|
||||
|
||||
@@ -274,8 +274,8 @@ WantedBy=multi-user.target
|
||||
Habilitar e iniciar:
|
||||
|
||||
```bash
|
||||
sudo systemctl enable autoparts-db
|
||||
sudo systemctl start autoparts-db
|
||||
sudo systemctl enable nexus-autoparts
|
||||
sudo systemctl start nexus-autoparts
|
||||
```
|
||||
|
||||
### Usando Docker (Opcional)
|
||||
@@ -294,8 +294,8 @@ CMD ["python3", "dashboard/server.py"]
|
||||
```
|
||||
|
||||
```bash
|
||||
docker build -t autoparts-db .
|
||||
docker run -p 5000:5000 autoparts-db
|
||||
docker build -t nexus-autoparts .
|
||||
docker run -p 5000:5000 nexus-autoparts
|
||||
```
|
||||
|
||||
---
|
||||
@@ -319,7 +319,7 @@ pip install --upgrade -r requirements.txt
|
||||
deactivate
|
||||
|
||||
# Eliminar directorio del proyecto
|
||||
rm -rf Autoparts-DB
|
||||
rm -rf Nexus-Autoparts
|
||||
|
||||
# Eliminar entorno virtual (si está separado)
|
||||
rm -rf venv
|
||||
|
||||
599
docs/METABASE_GUIDE.md
Normal file
599
docs/METABASE_GUIDE.md
Normal 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
470
migrate_to_postgres.py
Normal 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
410
models.py
Normal 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();
|
||||
"""
|
||||
@@ -2,3 +2,6 @@ flask==2.3.3
|
||||
requests>=2.28.0
|
||||
beautifulsoup4>=4.11.0
|
||||
lxml>=4.9.0
|
||||
sqlalchemy>=2.0
|
||||
psycopg2-binary>=2.9
|
||||
flask-sqlalchemy>=3.1
|
||||
|
||||
Reference in New Issue
Block a user