fix: Fix imports and database bugs. Removed legacy ETL code.
This commit is contained in:
@@ -23,6 +23,7 @@ const MIGRATION_ORDER = [
|
||||
'features/maintenance', // Depends on vehicles
|
||||
'features/stations', // Independent
|
||||
'features/admin', // Admin role management and oversight; depends on update_updated_at_column()
|
||||
'features/backup', // Admin backup feature; depends on update_updated_at_column()
|
||||
'features/notifications', // Depends on maintenance and documents
|
||||
'features/user-profile', // User profile management; independent
|
||||
];
|
||||
|
||||
386
backend/src/features/admin/scripts/bulk-import-catalog.ts
Normal file
386
backend/src/features/admin/scripts/bulk-import-catalog.ts
Normal file
@@ -0,0 +1,386 @@
|
||||
/**
|
||||
* Bulk Vehicle Catalog CSV Import
|
||||
*
|
||||
* Processes large CSV files (250k+ rows) using batch processing to avoid
|
||||
* memory and timeout issues that occur in the web import.
|
||||
*
|
||||
* Usage (from inside container):
|
||||
* ts-node src/features/admin/scripts/bulk-import-catalog.ts
|
||||
*
|
||||
* CSV Format:
|
||||
* Required columns: year, make, model, trim
|
||||
* Optional columns: engine_name, transmission_type
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as readline from 'readline';
|
||||
import { pool } from '../../../core/config/database';
|
||||
|
||||
const BATCH_SIZE = 5000;
|
||||
const CSV_PATH = '/tmp/catalog-import.csv';
|
||||
|
||||
interface ImportRow {
|
||||
year: number;
|
||||
make: string;
|
||||
model: string;
|
||||
trim: string;
|
||||
engineName: string | null;
|
||||
transmissionType: string | null;
|
||||
}
|
||||
|
||||
interface ImportStats {
|
||||
totalRows: number;
|
||||
batchesProcessed: number;
|
||||
errors: number;
|
||||
startTime: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a CSV line handling quoted fields
|
||||
*/
|
||||
function parseCSVLine(line: string): string[] {
|
||||
const result: string[] = [];
|
||||
let current = '';
|
||||
let inQuotes = false;
|
||||
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const char = line[i];
|
||||
|
||||
if (char === '"') {
|
||||
inQuotes = !inQuotes;
|
||||
} else if (char === ',' && !inQuotes) {
|
||||
result.push(current.trim());
|
||||
current = '';
|
||||
} else {
|
||||
current += char;
|
||||
}
|
||||
}
|
||||
|
||||
result.push(current.trim());
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk get or create engines
|
||||
* Returns map of engine_name -> engine_id
|
||||
*/
|
||||
async function getOrCreateEngines(
|
||||
client: any,
|
||||
engineNames: string[]
|
||||
): Promise<Map<string, number>> {
|
||||
if (engineNames.length === 0) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
// Build VALUES clause for bulk insert
|
||||
const values: any[] = [];
|
||||
const placeholders = engineNames
|
||||
.map((name, idx) => {
|
||||
values.push(name, 'Gas');
|
||||
return `($${idx * 2 + 1}, $${idx * 2 + 2})`;
|
||||
})
|
||||
.join(', ');
|
||||
|
||||
const query = `
|
||||
INSERT INTO engines (name, fuel_type)
|
||||
VALUES ${placeholders}
|
||||
ON CONFLICT (LOWER(name)) DO UPDATE
|
||||
SET name = EXCLUDED.name
|
||||
RETURNING id, name
|
||||
`;
|
||||
|
||||
const result = await client.query(query, values);
|
||||
const map = new Map<string, number>();
|
||||
|
||||
for (const row of result.rows) {
|
||||
map.set(row.name, row.id);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk get or create transmissions
|
||||
* Returns map of transmission_type -> transmission_id
|
||||
*/
|
||||
async function getOrCreateTransmissions(
|
||||
client: any,
|
||||
transmissionTypes: string[]
|
||||
): Promise<Map<string, number>> {
|
||||
if (transmissionTypes.length === 0) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
// Build VALUES clause for bulk insert
|
||||
const values: any[] = [];
|
||||
const placeholders = transmissionTypes
|
||||
.map((type, idx) => {
|
||||
values.push(type);
|
||||
return `($${idx + 1})`;
|
||||
})
|
||||
.join(', ');
|
||||
|
||||
const query = `
|
||||
INSERT INTO transmissions (type)
|
||||
VALUES ${placeholders}
|
||||
ON CONFLICT (LOWER(type)) DO UPDATE
|
||||
SET type = EXCLUDED.type
|
||||
RETURNING id, type
|
||||
`;
|
||||
|
||||
const result = await client.query(query, values);
|
||||
const map = new Map<string, number>();
|
||||
|
||||
for (const row of result.rows) {
|
||||
map.set(row.type, row.id);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a batch of rows
|
||||
*/
|
||||
async function processBatch(
|
||||
client: any,
|
||||
batch: ImportRow[],
|
||||
stats: ImportStats
|
||||
): Promise<void> {
|
||||
if (batch.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract unique engines and transmissions
|
||||
const uniqueEngines = new Set<string>();
|
||||
const uniqueTransmissions = new Set<string>();
|
||||
|
||||
for (const row of batch) {
|
||||
if (row.engineName) {
|
||||
uniqueEngines.add(row.engineName);
|
||||
}
|
||||
if (row.transmissionType) {
|
||||
uniqueTransmissions.add(row.transmissionType);
|
||||
}
|
||||
}
|
||||
|
||||
// Get/create engines and transmissions
|
||||
const engineMap = await getOrCreateEngines(client, Array.from(uniqueEngines));
|
||||
const transmissionMap = await getOrCreateTransmissions(
|
||||
client,
|
||||
Array.from(uniqueTransmissions)
|
||||
);
|
||||
|
||||
// Build vehicle_options batch upsert
|
||||
const values: any[] = [];
|
||||
const placeholders = batch
|
||||
.map((row, idx) => {
|
||||
const engineId = row.engineName ? engineMap.get(row.engineName) || null : null;
|
||||
const transmissionId = row.transmissionType
|
||||
? transmissionMap.get(row.transmissionType) || null
|
||||
: null;
|
||||
|
||||
values.push(
|
||||
row.year,
|
||||
row.make,
|
||||
row.model,
|
||||
row.trim,
|
||||
engineId,
|
||||
transmissionId
|
||||
);
|
||||
|
||||
const base = idx * 6;
|
||||
return `($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}, $${base + 5}, $${base + 6})`;
|
||||
})
|
||||
.join(', ');
|
||||
|
||||
const upsertQuery = `
|
||||
INSERT INTO vehicle_options (year, make, model, trim, engine_id, transmission_id)
|
||||
VALUES ${placeholders}
|
||||
ON CONFLICT (year, make, model, trim, engine_id, transmission_id)
|
||||
DO UPDATE SET
|
||||
updated_at = NOW()
|
||||
`;
|
||||
|
||||
await client.query(upsertQuery, values);
|
||||
stats.totalRows += batch.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main import function
|
||||
*/
|
||||
async function importCatalog(): Promise<void> {
|
||||
const stats: ImportStats = {
|
||||
totalRows: 0,
|
||||
batchesProcessed: 0,
|
||||
errors: 0,
|
||||
startTime: new Date(),
|
||||
};
|
||||
|
||||
console.log('='.repeat(60));
|
||||
console.log('Vehicle Catalog Bulk Import');
|
||||
console.log('='.repeat(60));
|
||||
console.log(`CSV File: ${CSV_PATH}`);
|
||||
console.log(`Batch Size: ${BATCH_SIZE}`);
|
||||
console.log('');
|
||||
|
||||
// Validate file exists
|
||||
if (!fs.existsSync(CSV_PATH)) {
|
||||
console.error(`Error: CSV file not found at ${CSV_PATH}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const fileStream = fs.createReadStream(CSV_PATH);
|
||||
const rl = readline.createInterface({
|
||||
input: fileStream,
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
|
||||
let headers: string[] = [];
|
||||
let headerIndices: Record<string, number> = {};
|
||||
let batch: ImportRow[] = [];
|
||||
let isFirstLine = true;
|
||||
|
||||
for await (const line of rl) {
|
||||
// Parse header row
|
||||
if (isFirstLine) {
|
||||
headers = parseCSVLine(line);
|
||||
const headerLower = headers.map((h) => h.toLowerCase().trim());
|
||||
|
||||
// Validate required headers
|
||||
const required = ['year', 'make', 'model', 'trim'];
|
||||
for (const req of required) {
|
||||
if (!headerLower.includes(req)) {
|
||||
console.error(`Error: Missing required header: ${req}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Build header index map
|
||||
headerIndices = {
|
||||
year: headerLower.indexOf('year'),
|
||||
make: headerLower.indexOf('make'),
|
||||
model: headerLower.indexOf('model'),
|
||||
trim: headerLower.indexOf('trim'),
|
||||
engineName: headerLower.indexOf('engine_name'),
|
||||
transmissionType: headerLower.indexOf('transmission_type'),
|
||||
};
|
||||
|
||||
isFirstLine = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse data row
|
||||
try {
|
||||
const fields = parseCSVLine(line);
|
||||
|
||||
const row: ImportRow = {
|
||||
year: parseInt(fields[headerIndices.year]),
|
||||
make: fields[headerIndices.make]?.trim() || '',
|
||||
model: fields[headerIndices.model]?.trim() || '',
|
||||
trim: fields[headerIndices.trim]?.trim() || '',
|
||||
engineName:
|
||||
headerIndices.engineName >= 0
|
||||
? fields[headerIndices.engineName]?.trim() || null
|
||||
: null,
|
||||
transmissionType:
|
||||
headerIndices.transmissionType >= 0
|
||||
? fields[headerIndices.transmissionType]?.trim() || null
|
||||
: null,
|
||||
};
|
||||
|
||||
batch.push(row);
|
||||
|
||||
// Process batch when full
|
||||
if (batch.length >= BATCH_SIZE) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
await processBatch(client, batch, stats);
|
||||
await client.query('COMMIT');
|
||||
stats.batchesProcessed++;
|
||||
|
||||
const elapsed = (Date.now() - stats.startTime.getTime()) / 1000;
|
||||
console.log(
|
||||
`Batch ${stats.batchesProcessed}: ${stats.totalRows.toLocaleString()} rows processed (${elapsed.toFixed(1)}s)`
|
||||
);
|
||||
} catch (error: any) {
|
||||
await client.query('ROLLBACK');
|
||||
console.error(`Error processing batch ${stats.batchesProcessed + 1}:`, error.message);
|
||||
stats.errors += batch.length;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
|
||||
batch = [];
|
||||
}
|
||||
} catch (error: any) {
|
||||
stats.errors++;
|
||||
console.error(`Error parsing row: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Process remaining rows
|
||||
if (batch.length > 0) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
await processBatch(client, batch, stats);
|
||||
await client.query('COMMIT');
|
||||
stats.batchesProcessed++;
|
||||
|
||||
const elapsed = (Date.now() - stats.startTime.getTime()) / 1000;
|
||||
console.log(
|
||||
`Batch ${stats.batchesProcessed}: ${stats.totalRows.toLocaleString()} rows processed (${elapsed.toFixed(1)}s)`
|
||||
);
|
||||
} catch (error: any) {
|
||||
await client.query('ROLLBACK');
|
||||
console.error(`Error processing final batch:`, error.message);
|
||||
stats.errors += batch.length;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
// Print summary
|
||||
const totalElapsed = (Date.now() - stats.startTime.getTime()) / 1000;
|
||||
console.log('');
|
||||
console.log('='.repeat(60));
|
||||
console.log('Import Summary');
|
||||
console.log('='.repeat(60));
|
||||
console.log(`Total rows processed: ${stats.totalRows.toLocaleString()}`);
|
||||
console.log(`Batches processed: ${stats.batchesProcessed}`);
|
||||
console.log(`Errors: ${stats.errors}`);
|
||||
console.log(`Elapsed time: ${Math.floor(totalElapsed / 60)}m ${(totalElapsed % 60).toFixed(0)}s`);
|
||||
console.log('');
|
||||
|
||||
// Verify counts
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
const voResult = await client.query('SELECT COUNT(*) FROM vehicle_options');
|
||||
const engResult = await client.query('SELECT COUNT(*) FROM engines');
|
||||
const transResult = await client.query('SELECT COUNT(*) FROM transmissions');
|
||||
|
||||
console.log('Database Verification:');
|
||||
console.log(` vehicle_options: ${parseInt(voResult.rows[0].count).toLocaleString()}`);
|
||||
console.log(` engines: ${parseInt(engResult.rows[0].count).toLocaleString()}`);
|
||||
console.log(` transmissions: ${parseInt(transResult.rows[0].count).toLocaleString()}`);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log('Import completed successfully!');
|
||||
console.log('='.repeat(60));
|
||||
}
|
||||
|
||||
// Run import
|
||||
importCatalog()
|
||||
.then(() => {
|
||||
pool.end();
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Fatal error:', error);
|
||||
pool.end();
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,139 +1,293 @@
|
||||
-- Create dedicated schema for normalized vehicle lookup data
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_namespace WHERE nspname = 'vehicles'
|
||||
) THEN
|
||||
EXECUTE 'CREATE SCHEMA vehicles';
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
-- Migration: Create Automotive Vehicle Selection Database
|
||||
-- Optimized for dropdown cascade queries
|
||||
-- Date: 2025-11-10
|
||||
|
||||
-- Create manufacturers table
|
||||
CREATE TABLE IF NOT EXISTS vehicles.make (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(150) NOT NULL UNIQUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
-- Drop existing tables if they exist
|
||||
DROP TABLE IF EXISTS vehicle_options CASCADE;
|
||||
DROP TABLE IF EXISTS engines CASCADE;
|
||||
DROP TABLE IF EXISTS transmissions CASCADE;
|
||||
DROP INDEX IF EXISTS idx_vehicle_year;
|
||||
DROP INDEX IF EXISTS idx_vehicle_make;
|
||||
DROP INDEX IF EXISTS idx_vehicle_model;
|
||||
DROP INDEX IF EXISTS idx_vehicle_trim;
|
||||
DROP INDEX IF EXISTS idx_vehicle_composite;
|
||||
|
||||
-- Create engines table with detailed specifications
|
||||
CREATE TABLE engines (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
displacement VARCHAR(50),
|
||||
configuration VARCHAR(50),
|
||||
horsepower VARCHAR(100),
|
||||
torque VARCHAR(100),
|
||||
fuel_type VARCHAR(100),
|
||||
fuel_system VARCHAR(255),
|
||||
aspiration VARCHAR(100),
|
||||
specs_json JSONB,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
-- Prevent duplicate engine display names (case-insensitive)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_engines_name_lower ON engines (LOWER(name));
|
||||
|
||||
CREATE INDEX idx_engines_displacement ON engines(displacement);
|
||||
CREATE INDEX idx_engines_config ON engines(configuration);
|
||||
|
||||
-- Create transmissions table
|
||||
CREATE TABLE transmissions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
type VARCHAR(100) NOT NULL,
|
||||
speeds VARCHAR(50),
|
||||
drive_type VARCHAR(100),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
-- Prevent duplicate transmission display names (case-insensitive)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_transmissions_type_lower ON transmissions (LOWER(type));
|
||||
|
||||
CREATE INDEX idx_transmissions_type ON transmissions(type);
|
||||
|
||||
-- Create denormalized vehicle_options table optimized for dropdown queries
|
||||
CREATE TABLE vehicle_options (
|
||||
id SERIAL PRIMARY KEY,
|
||||
year INTEGER NOT NULL,
|
||||
make VARCHAR(100) NOT NULL,
|
||||
model VARCHAR(255) NOT NULL,
|
||||
trim VARCHAR(255) NOT NULL,
|
||||
engine_id INTEGER REFERENCES engines(id) ON DELETE SET NULL,
|
||||
transmission_id INTEGER REFERENCES transmissions(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
-- Prevent duplicate vehicle option rows
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_vehicle_options_full ON vehicle_options (
|
||||
year, make, model, trim, engine_id, transmission_id
|
||||
);
|
||||
|
||||
CREATE OR REPLACE FUNCTION vehicles.touch_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
-- Indexes for cascading dropdown performance
|
||||
CREATE INDEX idx_vehicle_year ON vehicle_options(year);
|
||||
CREATE INDEX idx_vehicle_make ON vehicle_options(make);
|
||||
CREATE INDEX idx_vehicle_model ON vehicle_options(model);
|
||||
CREATE INDEX idx_vehicle_trim ON vehicle_options(trim);
|
||||
CREATE INDEX idx_vehicle_year_make ON vehicle_options(year, make);
|
||||
CREATE INDEX idx_vehicle_year_make_model ON vehicle_options(year, make, model);
|
||||
CREATE INDEX idx_vehicle_year_make_model_trim ON vehicle_options(year, make, model, trim);
|
||||
CREATE INDEX idx_vehicle_year_make_model_trim_engine ON vehicle_options(year, make, model, trim, engine_id);
|
||||
CREATE INDEX idx_vehicle_year_make_model_trim_trans ON vehicle_options(year, make, model, trim, transmission_id);
|
||||
|
||||
-- Full-text search index for admin catalog search
|
||||
CREATE INDEX idx_vehicle_options_fts ON vehicle_options
|
||||
USING gin(to_tsvector('english', year::text || ' ' || make || ' ' || model || ' ' || trim));
|
||||
|
||||
-- Index on engines.name for join performance during search
|
||||
CREATE INDEX idx_engines_name ON engines(name);
|
||||
|
||||
-- Views for dropdown queries
|
||||
|
||||
-- View: Get all available years
|
||||
CREATE OR REPLACE VIEW available_years AS
|
||||
SELECT DISTINCT year
|
||||
FROM vehicle_options
|
||||
ORDER BY year DESC;
|
||||
|
||||
-- View: Get makes by year
|
||||
CREATE OR REPLACE VIEW makes_by_year AS
|
||||
SELECT DISTINCT year, make
|
||||
FROM vehicle_options
|
||||
ORDER BY year DESC, make ASC;
|
||||
|
||||
-- View: Get models by year and make
|
||||
CREATE OR REPLACE VIEW models_by_year_make AS
|
||||
SELECT DISTINCT year, make, model
|
||||
FROM vehicle_options
|
||||
ORDER BY year DESC, make ASC, model ASC;
|
||||
|
||||
-- View: Get trims by year, make, and model
|
||||
CREATE OR REPLACE VIEW trims_by_year_make_model AS
|
||||
SELECT DISTINCT year, make, model, trim
|
||||
FROM vehicle_options
|
||||
ORDER BY year DESC, make ASC, model ASC, trim ASC;
|
||||
|
||||
-- View: Get complete vehicle configurations with engine and transmission details
|
||||
CREATE OR REPLACE VIEW complete_vehicle_configs AS
|
||||
SELECT
|
||||
vo.id,
|
||||
vo.year,
|
||||
vo.make,
|
||||
vo.model,
|
||||
vo.trim,
|
||||
e.name AS engine_name,
|
||||
e.displacement,
|
||||
e.configuration,
|
||||
e.horsepower,
|
||||
e.torque,
|
||||
e.fuel_type,
|
||||
t.type AS transmission_type,
|
||||
t.speeds AS transmission_speeds,
|
||||
t.drive_type
|
||||
FROM vehicle_options vo
|
||||
LEFT JOIN engines e ON vo.engine_id = e.id
|
||||
LEFT JOIN transmissions t ON vo.transmission_id = t.id
|
||||
ORDER BY vo.year DESC, vo.make ASC, vo.model ASC, vo.trim ASC;
|
||||
|
||||
-- Function to get makes for a specific year
|
||||
CREATE OR REPLACE FUNCTION get_makes_for_year(p_year INTEGER)
|
||||
RETURNS TABLE(make VARCHAR) AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
RETURN QUERY
|
||||
SELECT DISTINCT vehicle_options.make
|
||||
FROM vehicle_options
|
||||
WHERE vehicle_options.year = p_year
|
||||
ORDER BY vehicle_options.make ASC;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS touch_make_updated_at ON vehicles.make;
|
||||
CREATE TRIGGER touch_make_updated_at
|
||||
BEFORE UPDATE ON vehicles.make
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION vehicles.touch_updated_at();
|
||||
-- Function to get models for a specific year and make
|
||||
CREATE OR REPLACE FUNCTION get_models_for_year_make(p_year INTEGER, p_make VARCHAR)
|
||||
RETURNS TABLE(model VARCHAR) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT DISTINCT vehicle_options.model
|
||||
FROM vehicle_options
|
||||
WHERE vehicle_options.year = p_year
|
||||
AND vehicle_options.make = p_make
|
||||
ORDER BY vehicle_options.model ASC;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create models table
|
||||
CREATE TABLE IF NOT EXISTS vehicles.model (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
make_id BIGINT NOT NULL REFERENCES vehicles.make(id) ON DELETE CASCADE,
|
||||
name VARCHAR(150) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT vehicles_model_unique UNIQUE(make_id, name)
|
||||
);
|
||||
-- Function to get trims for a specific year, make, and model
|
||||
CREATE OR REPLACE FUNCTION get_trims_for_year_make_model(p_year INTEGER, p_make VARCHAR, p_model VARCHAR)
|
||||
RETURNS TABLE(trim_name VARCHAR) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT DISTINCT vehicle_options.trim
|
||||
FROM vehicle_options
|
||||
WHERE vehicle_options.year = p_year
|
||||
AND vehicle_options.make = p_make
|
||||
AND vehicle_options.model = p_model
|
||||
ORDER BY vehicle_options.trim ASC;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS touch_model_updated_at ON vehicles.model;
|
||||
CREATE TRIGGER touch_model_updated_at
|
||||
BEFORE UPDATE ON vehicles.model
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION vehicles.touch_updated_at();
|
||||
-- Function to get engine and transmission options for a specific vehicle
|
||||
CREATE OR REPLACE FUNCTION get_options_for_vehicle(p_year INTEGER, p_make VARCHAR, p_model VARCHAR, p_trim VARCHAR)
|
||||
RETURNS TABLE(
|
||||
engine_name VARCHAR,
|
||||
engine_displacement VARCHAR,
|
||||
engine_horsepower VARCHAR,
|
||||
transmission_type VARCHAR,
|
||||
transmission_speeds VARCHAR,
|
||||
drive_type VARCHAR
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
e.name,
|
||||
e.displacement,
|
||||
e.horsepower,
|
||||
t.type,
|
||||
t.speeds,
|
||||
t.drive_type
|
||||
FROM vehicle_options vo
|
||||
LEFT JOIN engines e ON vo.engine_id = e.id
|
||||
LEFT JOIN transmissions t ON vo.transmission_id = t.id
|
||||
WHERE vo.year = p_year
|
||||
AND vo.make = p_make
|
||||
AND vo.model = p_model
|
||||
AND vo.trim = p_trim;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create model_year table
|
||||
CREATE TABLE IF NOT EXISTS vehicles.model_year (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
model_id BIGINT NOT NULL REFERENCES vehicles.model(id) ON DELETE CASCADE,
|
||||
year INTEGER NOT NULL CHECK (year BETWEEN 1900 AND 2100),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT vehicles_model_year_unique UNIQUE(model_id, year)
|
||||
);
|
||||
-- Helper functions for trim-level options and pair-safe filtering
|
||||
CREATE OR REPLACE FUNCTION get_transmissions_for_vehicle(p_year INTEGER, p_make VARCHAR, p_model VARCHAR, p_trim VARCHAR)
|
||||
RETURNS TABLE(
|
||||
transmission_id INTEGER,
|
||||
transmission_type VARCHAR
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT DISTINCT
|
||||
t.id,
|
||||
t.type
|
||||
FROM vehicle_options vo
|
||||
JOIN transmissions t ON vo.transmission_id = t.id
|
||||
WHERE vo.year = p_year
|
||||
AND vo.make = p_make
|
||||
AND vo.model = p_model
|
||||
AND vo.trim = p_trim
|
||||
ORDER BY t.type ASC;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_model_year_year ON vehicles.model_year(year DESC);
|
||||
CREATE OR REPLACE FUNCTION get_engines_for_vehicle(p_year INTEGER, p_make VARCHAR, p_model VARCHAR, p_trim VARCHAR)
|
||||
RETURNS TABLE(
|
||||
engine_id INTEGER,
|
||||
engine_name VARCHAR
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT DISTINCT
|
||||
e.id,
|
||||
e.name
|
||||
FROM vehicle_options vo
|
||||
JOIN engines e ON vo.engine_id = e.id
|
||||
WHERE vo.year = p_year
|
||||
AND vo.make = p_make
|
||||
AND vo.model = p_model
|
||||
AND vo.trim = p_trim
|
||||
ORDER BY e.name ASC;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS touch_model_year_updated_at ON vehicles.model_year;
|
||||
CREATE TRIGGER touch_model_year_updated_at
|
||||
BEFORE UPDATE ON vehicles.model_year
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION vehicles.touch_updated_at();
|
||||
CREATE OR REPLACE FUNCTION get_transmissions_for_vehicle_engine(p_year INTEGER, p_make VARCHAR, p_model VARCHAR, p_trim VARCHAR, p_engine_name VARCHAR)
|
||||
RETURNS TABLE(
|
||||
transmission_id INTEGER,
|
||||
transmission_type VARCHAR
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT DISTINCT
|
||||
t.id,
|
||||
t.type
|
||||
FROM vehicle_options vo
|
||||
JOIN engines e ON vo.engine_id = e.id
|
||||
JOIN transmissions t ON vo.transmission_id = t.id
|
||||
WHERE vo.year = p_year
|
||||
AND vo.make = p_make
|
||||
AND vo.model = p_model
|
||||
AND vo.trim = p_trim
|
||||
AND e.name = p_engine_name
|
||||
ORDER BY t.type ASC;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create trims table
|
||||
CREATE TABLE IF NOT EXISTS vehicles.trim (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
model_year_id BIGINT NOT NULL REFERENCES vehicles.model_year(id) ON DELETE CASCADE,
|
||||
name VARCHAR(150) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT vehicles_trim_unique UNIQUE(model_year_id, name)
|
||||
);
|
||||
CREATE OR REPLACE FUNCTION get_engines_for_vehicle_trans(p_year INTEGER, p_make VARCHAR, p_model VARCHAR, p_trim VARCHAR, p_trans_type VARCHAR)
|
||||
RETURNS TABLE(
|
||||
engine_id INTEGER,
|
||||
engine_name VARCHAR
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT DISTINCT
|
||||
e.id,
|
||||
e.name
|
||||
FROM vehicle_options vo
|
||||
JOIN engines e ON vo.engine_id = e.id
|
||||
JOIN transmissions t ON vo.transmission_id = t.id
|
||||
WHERE vo.year = p_year
|
||||
AND vo.make = p_make
|
||||
AND vo.model = p_model
|
||||
AND vo.trim = p_trim
|
||||
AND t.type = p_trans_type
|
||||
ORDER BY e.name ASC;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_trim_model_year ON vehicles.trim(model_year_id);
|
||||
|
||||
DROP TRIGGER IF EXISTS touch_trim_updated_at ON vehicles.trim;
|
||||
CREATE TRIGGER touch_trim_updated_at
|
||||
BEFORE UPDATE ON vehicles.trim
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION vehicles.touch_updated_at();
|
||||
|
||||
-- Create engines table
|
||||
CREATE TABLE IF NOT EXISTS vehicles.engine (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(200) NOT NULL UNIQUE,
|
||||
code VARCHAR(50),
|
||||
displacement_l NUMERIC(5,2),
|
||||
cylinders SMALLINT,
|
||||
fuel_type VARCHAR(50),
|
||||
aspiration VARCHAR(50),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
DROP TRIGGER IF EXISTS touch_engine_updated_at ON vehicles.engine;
|
||||
CREATE TRIGGER touch_engine_updated_at
|
||||
BEFORE UPDATE ON vehicles.engine
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION vehicles.touch_updated_at();
|
||||
|
||||
-- Create trim-engine bridge table
|
||||
CREATE TABLE IF NOT EXISTS vehicles.trim_engine (
|
||||
trim_id BIGINT NOT NULL REFERENCES vehicles.trim(id) ON DELETE CASCADE,
|
||||
engine_id BIGINT NOT NULL REFERENCES vehicles.engine(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (trim_id, engine_id)
|
||||
);
|
||||
|
||||
-- Create transmissions table (static manual/automatic for now)
|
||||
CREATE TABLE IF NOT EXISTS vehicles.transmission (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(50) NOT NULL UNIQUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
DROP TRIGGER IF EXISTS touch_transmission_updated_at ON vehicles.transmission;
|
||||
CREATE TRIGGER touch_transmission_updated_at
|
||||
BEFORE UPDATE ON vehicles.transmission
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION vehicles.touch_updated_at();
|
||||
|
||||
-- Optional bridge for future proofing (not yet populated)
|
||||
CREATE TABLE IF NOT EXISTS vehicles.trim_transmission (
|
||||
trim_id BIGINT NOT NULL REFERENCES vehicles.trim(id) ON DELETE CASCADE,
|
||||
transmission_id BIGINT NOT NULL REFERENCES vehicles.transmission(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (trim_id, transmission_id)
|
||||
);
|
||||
|
||||
-- Helpful indexes for cascading dropdown lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_model_make ON vehicles.model(make_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_trim_name ON vehicles.trim(LOWER(name));
|
||||
CREATE INDEX IF NOT EXISTS idx_engine_name ON vehicles.engine(LOWER(name));
|
||||
CREATE INDEX IF NOT EXISTS idx_trim_engine_engine ON vehicles.trim_engine(engine_id);
|
||||
COMMENT ON TABLE vehicle_options IS 'Denormalized table optimized for cascading dropdown queries';
|
||||
COMMENT ON TABLE engines IS 'Engine specifications with detailed technical data';
|
||||
COMMENT ON TABLE transmissions IS 'Transmission specifications';
|
||||
COMMENT ON VIEW available_years IS 'Returns all distinct years available in the database';
|
||||
COMMENT ON VIEW makes_by_year IS 'Returns makes grouped by year for dropdown population';
|
||||
COMMENT ON VIEW models_by_year_make IS 'Returns models grouped by year and make';
|
||||
COMMENT ON VIEW trims_by_year_make_model IS 'Returns trims grouped by year, make, and model';
|
||||
COMMENT ON VIEW complete_vehicle_configs IS 'Complete vehicle configurations with all details';
|
||||
Reference in New Issue
Block a user