From 1018da8036071a61abdff46ef2f4e25dc6840ffd Mon Sep 17 00:00:00 2001 From: Tiago Yamamoto Date: Wed, 24 Dec 2025 15:04:21 -0300 Subject: [PATCH] feat(location): add comprehensive geographic hierarchy (continents, countries, states, cities) - Add migration 021_location_hierarchy.sql with new table structure - Add location-loader.js seeder to import SQL dumps - Update all seeder files to use country_id instead of region_id - Rename companies.region_id to country_id --- backend/migrations/021_location_hierarchy.sql | 158 +++++++++++++++ seeder-api/src/index.js | 12 +- seeder-api/src/seeders/acme.js | 10 +- seeder-api/src/seeders/companies.js | 13 +- seeder-api/src/seeders/epic-companies.js | 12 +- seeder-api/src/seeders/fictional-companies.js | 24 +-- seeder-api/src/seeders/location-loader.js | 186 ++++++++++++++++++ 7 files changed, 379 insertions(+), 36 deletions(-) create mode 100644 backend/migrations/021_location_hierarchy.sql create mode 100644 seeder-api/src/seeders/location-loader.js diff --git a/backend/migrations/021_location_hierarchy.sql b/backend/migrations/021_location_hierarchy.sql new file mode 100644 index 0000000..1db03d5 --- /dev/null +++ b/backend/migrations/021_location_hierarchy.sql @@ -0,0 +1,158 @@ +-- Migration: 021_location_hierarchy.sql +-- Description: Restructure location tables to use comprehensive geographic hierarchy +-- Data Source: GeoDB Cities (https://github.com/dr5hn/countries-states-cities-database) + +-- ============================================================================ +-- PHASE 1: Backup existing tables +-- ============================================================================ + +-- Rename old tables to preserve data +ALTER TABLE IF EXISTS regions RENAME TO regions_old; +ALTER TABLE IF EXISTS cities RENAME TO cities_old; + +-- Drop old indexes (they will conflict) +DROP INDEX IF EXISTS idx_regions_country; +DROP INDEX IF EXISTS idx_cities_region; + +-- ============================================================================ +-- PHASE 2: Create new location hierarchy +-- ============================================================================ + +-- 2.1 Continents (formerly "regions" in GeoDB) +CREATE TABLE IF NOT EXISTS continents ( + id BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, + name VARCHAR(100) NOT NULL, + translations TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + flag SMALLINT DEFAULT 1 NOT NULL, + wiki_data_id VARCHAR(255) +); + +COMMENT ON TABLE continents IS 'Geographic continents (Africa, Americas, Asia, Europe, Oceania, Polar)'; + +-- 2.2 Subregions (e.g., Northern Africa, South America) +CREATE TABLE IF NOT EXISTS subregions ( + id BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, + name VARCHAR(100) NOT NULL, + continent_id BIGINT NOT NULL REFERENCES continents(id), + translations TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + flag SMALLINT DEFAULT 1 NOT NULL, + wiki_data_id VARCHAR(255) +); + +CREATE INDEX idx_subregions_continent ON subregions(continent_id); +COMMENT ON TABLE subregions IS 'Geographic subregions within continents'; + +-- 2.3 Countries +CREATE TABLE IF NOT EXISTS countries ( + id BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, + name VARCHAR(100) NOT NULL, + iso2 CHAR(2), + iso3 CHAR(3), + numeric_code CHAR(3), + phonecode VARCHAR(255), + capital VARCHAR(255), + currency VARCHAR(255), + currency_name VARCHAR(255), + currency_symbol VARCHAR(255), + tld VARCHAR(255), + native VARCHAR(255), + continent_id BIGINT REFERENCES continents(id), + subregion_id BIGINT REFERENCES subregions(id), + nationality VARCHAR(255), + latitude DECIMAL(10,8), + longitude DECIMAL(11,8), + emoji VARCHAR(10), + emoji_u VARCHAR(50), + timezones TEXT, + translations TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + flag SMALLINT DEFAULT 1 NOT NULL, + wiki_data_id VARCHAR(255) +); + +CREATE INDEX idx_countries_iso2 ON countries(iso2); +CREATE INDEX idx_countries_iso3 ON countries(iso3); +CREATE INDEX idx_countries_continent ON countries(continent_id); +CREATE INDEX idx_countries_subregion ON countries(subregion_id); +COMMENT ON TABLE countries IS 'All countries with ISO codes, currencies, and metadata'; + +-- 2.4 States/Provinces +CREATE TABLE IF NOT EXISTS states ( + id BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, + name VARCHAR(255) NOT NULL, + country_id BIGINT NOT NULL REFERENCES countries(id), + country_code CHAR(2) NOT NULL, + iso2 VARCHAR(10), + fips_code VARCHAR(255), + type VARCHAR(191), + latitude DECIMAL(10,8), + longitude DECIMAL(11,8), + timezone VARCHAR(255), + translations TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + flag SMALLINT DEFAULT 1 NOT NULL, + wiki_data_id VARCHAR(255) +); + +CREATE INDEX idx_states_country ON states(country_id); +CREATE INDEX idx_states_country_code ON states(country_code); +CREATE INDEX idx_states_iso2 ON states(iso2); +COMMENT ON TABLE states IS 'States, provinces, and administrative regions'; + +-- 2.5 Cities +CREATE TABLE IF NOT EXISTS cities ( + id BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, + name VARCHAR(255) NOT NULL, + state_id BIGINT NOT NULL REFERENCES states(id), + state_code VARCHAR(255), + country_id BIGINT NOT NULL REFERENCES countries(id), + country_code CHAR(2) NOT NULL, + latitude DECIMAL(10,8) NOT NULL, + longitude DECIMAL(11,8) NOT NULL, + population BIGINT, + timezone VARCHAR(255), + translations TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + flag SMALLINT DEFAULT 1 NOT NULL, + wiki_data_id VARCHAR(255) +); + +CREATE INDEX idx_cities_state ON cities(state_id); +CREATE INDEX idx_cities_country ON cities(country_id); +CREATE INDEX idx_cities_country_code ON cities(country_code); +CREATE INDEX idx_cities_population ON cities(population); +COMMENT ON TABLE cities IS 'Cities with coordinates and population data'; + +-- ============================================================================ +-- PHASE 3: Update companies table FKs +-- ============================================================================ + +-- Drop old FK constraint if exists (ignore errors) +ALTER TABLE companies DROP CONSTRAINT IF EXISTS companies_region_id_fkey; +ALTER TABLE companies DROP CONSTRAINT IF EXISTS companies_city_id_fkey; + +-- Update column types to BIGINT (to match new tables) +ALTER TABLE companies ALTER COLUMN region_id TYPE BIGINT USING region_id::BIGINT; +ALTER TABLE companies ALTER COLUMN city_id TYPE BIGINT USING city_id::BIGINT; + +-- Rename region_id to country_id for clarity +ALTER TABLE companies RENAME COLUMN region_id TO country_id; + +-- Add new FK constraints (without REFERENCES for now - data will be populated by seeder) +-- These will be validated after seeder populates the data + +-- Update indexes +DROP INDEX IF EXISTS idx_companies_region; +CREATE INDEX idx_companies_country ON companies(country_id); +CREATE INDEX idx_companies_city ON companies(city_id); + +-- Add comments +COMMENT ON COLUMN companies.country_id IS 'Reference to countries table'; +COMMENT ON COLUMN companies.city_id IS 'Reference to cities table'; diff --git a/seeder-api/src/index.js b/seeder-api/src/index.js index 2bd6208..9218ab4 100644 --- a/seeder-api/src/index.js +++ b/seeder-api/src/index.js @@ -1,6 +1,5 @@ import { pool, testConnection, closePool } from './db.js'; -import { seedRegions } from './seeders/regions.js'; -import { seedCities } from './seeders/cities.js'; +import { seedLocationData } from './seeders/location-loader.js'; import { seedUsers } from './seeders/users.js'; import { seedCompanies } from './seeders/companies.js'; import { seedJobs } from './seeders/jobs.js'; @@ -64,8 +63,10 @@ async function seedDatabase() { console.log(''); // Seed in order (respecting foreign key dependencies) - await seedRegions(); - await seedCities(); + // 1. Location data first (continents -> subregions -> countries -> states -> cities) + await seedLocationData(); + + // 2. Then companies (need countries) await seedCompanies(); await seedUsers(); await seedJobs(); @@ -78,8 +79,7 @@ async function seedDatabase() { console.log('\nβœ… Database seeding completed successfully!'); console.log('\nπŸ“Š Summary:'); - console.log(' - Regions seeded'); - console.log(' - Cities seeded'); + console.log(' - 🌍 Location data (continents, subregions, countries, states, cities)'); console.log(' - 1 SuperAdmin'); console.log(' - 43 Companies (30 + 13 fictΓ­cias)'); console.log(' - 1129+ Jobs total'); diff --git a/seeder-api/src/seeders/acme.js b/seeder-api/src/seeders/acme.js index 91b7819..be56e48 100644 --- a/seeder-api/src/seeders/acme.js +++ b/seeder-api/src/seeders/acme.js @@ -98,16 +98,16 @@ export async function seedAcmeCorp() { console.log('🏭 Seeding ACME Corporation e 69 vagas hilariantes...'); try { - // Get or create a default region - const regionsRes = await pool.query('SELECT id FROM regions LIMIT 1'); - const defaultRegionId = regionsRes.rows[0]?.id || null; + // Get USA's country ID from new countries table + const countryResult = await pool.query("SELECT id FROM countries WHERE iso2 = 'US'"); + const usaId = countryResult.rows[0]?.id || null; // 1. Create ACME Company const acmeSlug = 'ACME Corporation'; const acmeCNPJ = '99.999.999/0001-99'; await pool.query(` - INSERT INTO companies (name, slug, type, document, address, region_id, phone, email, website, description, verified, active, logo_url) + INSERT INTO companies (name, slug, type, document, address, country_id, phone, email, website, description, verified, active, logo_url) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, @@ -120,7 +120,7 @@ export async function seedAcmeCorp() { 'company', acmeCNPJ, 'Estrada do Deserto s/n, Monument Valley, Mojave, USA', - defaultRegionId, + usaId, '+1-800-ACME-TNT', 'careers@acme.corp', 'https://acme.corp', diff --git a/seeder-api/src/seeders/companies.js b/seeder-api/src/seeders/companies.js index b86c38f..d69cdda 100644 --- a/seeder-api/src/seeders/companies.js +++ b/seeder-api/src/seeders/companies.js @@ -54,11 +54,9 @@ function generateCNPJ(index) { export async function seedCompanies() { console.log('🏒 Seeding 30 companies...'); - // Get region IDs - const regions = await pool.query('SELECT id, code FROM regions'); - const regMap = {}; - regions.rows.forEach(r => regMap[r.code] = r.id); - const defaultRegionId = regMap['13'] || (regions.rows.length > 0 ? regions.rows[0].id : null); + // Get Brazil's country ID from new countries table + const countryResult = await pool.query("SELECT id FROM countries WHERE iso2 = 'BR'"); + const brazilId = countryResult.rows[0]?.id || null; try { for (let i = 0; i < companyData.length; i++) { @@ -67,7 +65,7 @@ export async function seedCompanies() { const slug = generateSlug(company.name); await pool.query(` - INSERT INTO companies (name, slug, type, document, address, region_id, phone, email, website, description, verified, active) + INSERT INTO companies (name, slug, type, document, address, country_id, phone, email, website, description, verified, active) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, @@ -80,7 +78,7 @@ export async function seedCompanies() { 'company', generateCNPJ(i), city, - defaultRegionId, + brazilId, `+55-11-${3000 + i}-${String(i).padStart(4, '0')}`, `careers@${slug}.com`, `https://${slug}.com`, @@ -88,6 +86,7 @@ export async function seedCompanies() { true, true ]); + } // Seed System Company for SuperAdmin diff --git a/seeder-api/src/seeders/epic-companies.js b/seeder-api/src/seeders/epic-companies.js index b0ef2fb..8bffe10 100644 --- a/seeder-api/src/seeders/epic-companies.js +++ b/seeder-api/src/seeders/epic-companies.js @@ -260,12 +260,12 @@ const sprawlMartJobs = [ async function createCompanyAndJobs(companyData, jobs) { try { - const regionsRes = await pool.query('SELECT id FROM regions LIMIT 1'); - const defaultRegionId = regionsRes.rows[0]?.id || null; + const countryResult = await pool.query("SELECT id FROM countries WHERE iso2 = 'US'"); + const usaId = countryResult.rows[0]?.id || null; // Create Company (companies uses SERIAL id) await pool.query(` - INSERT INTO companies (name, slug, type, document, address, region_id, phone, email, website, description, verified, active) + INSERT INTO companies (name, slug, type, document, address, country_id, phone, email, website, description, verified, active) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description `, [ @@ -274,7 +274,7 @@ async function createCompanyAndJobs(companyData, jobs) { 'company', companyData.cnpj, companyData.address, - defaultRegionId, + usaId, companyData.phone, companyData.email, companyData.website, @@ -294,14 +294,14 @@ async function createCompanyAndJobs(companyData, jobs) { for (let i = 0; i < jobs.length; i++) { const job = jobs[i]; - + await pool.query(` INSERT INTO jobs ( company_id, created_by, title, description, salary_min, salary_max, salary_type, employment_type, working_hours, location, requirements, benefits, visa_support, language_level, status, work_mode) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) `, [ - + companyId, seedUserId, job.title, diff --git a/seeder-api/src/seeders/fictional-companies.js b/seeder-api/src/seeders/fictional-companies.js index c4ce7f4..7a5cc1f 100644 --- a/seeder-api/src/seeders/fictional-companies.js +++ b/seeder-api/src/seeders/fictional-companies.js @@ -147,12 +147,12 @@ export async function seedStarkIndustries() { console.log('🦾 Seeding Stark Industries (Marvel)...'); try { - const regionsRes = await pool.query('SELECT id FROM regions LIMIT 1'); - const defaultRegionId = regionsRes.rows[0]?.id || null; + const countryResult = await pool.query("SELECT id FROM countries WHERE iso2 = 'US'"); + const usaId = countryResult.rows[0]?.id || null; // Create Company await pool.query(` - INSERT INTO companies (name, slug, type, document, address, region_id, phone, email, website, description, verified, active) + INSERT INTO companies (name, slug, type, document, address, country_id, phone, email, website, description, verified, active) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description `, [ @@ -161,7 +161,7 @@ export async function seedStarkIndustries() { 'company', '77.777.777/0001-77', 'Stark Tower, 200 Park Avenue, Manhattan, New York, NY 10166', - defaultRegionId, + usaId, '+1-212-STARK-01', 'careers@starkindustries.com', 'https://starkindustries.com', @@ -226,12 +226,12 @@ export async function seedLosPollosHermanos() { console.log('πŸ” Seeding Los Pollos Hermanos (Breaking Bad)...'); try { - const regionsRes = await pool.query('SELECT id FROM regions LIMIT 1'); - const defaultRegionId = regionsRes.rows[0]?.id || null; + const countryResult = await pool.query("SELECT id FROM countries WHERE iso2 = 'US'"); + const usaId = countryResult.rows[0]?.id || null; // Create Company await pool.query(` - INSERT INTO companies (name, slug, type, document, address, region_id, phone, email, website, description, verified, active) + INSERT INTO companies (name, slug, type, document, address, country_id, phone, email, website, description, verified, active) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description `, [ @@ -240,7 +240,7 @@ export async function seedLosPollosHermanos() { 'company', '66.666.666/0001-66', '308 Negra Arroyo Lane, Albuquerque, NM 87104', - defaultRegionId, + usaId, '+1-505-POLLOS', 'careers@lospollos.com', 'https://lospollos.com', @@ -311,12 +311,12 @@ export async function seedSpringfieldNuclear() { console.log('☒️ Seeding Springfield Nuclear Power Plant (Simpsons)...'); try { - const regionsRes = await pool.query('SELECT id FROM regions LIMIT 1'); - const defaultRegionId = regionsRes.rows[0]?.id || null; + const countryResult = await pool.query("SELECT id FROM countries WHERE iso2 = 'US'"); + const usaId = countryResult.rows[0]?.id || null; // Create Company await pool.query(` - INSERT INTO companies (name, slug, type, document, address, region_id, phone, email, website, description, verified, active) + INSERT INTO companies (name, slug, type, document, address, country_id, phone, email, website, description, verified, active) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description `, [ @@ -325,7 +325,7 @@ export async function seedSpringfieldNuclear() { 'company', '88.888.888/0001-88', '100 Industrial Way, Springfield, State Unknown', - defaultRegionId, + usaId, '+1-555-BURNS', 'careers@snpp.com', 'https://snpp.com', diff --git a/seeder-api/src/seeders/location-loader.js b/seeder-api/src/seeders/location-loader.js new file mode 100644 index 0000000..dae0793 --- /dev/null +++ b/seeder-api/src/seeders/location-loader.js @@ -0,0 +1,186 @@ +import { pool } from '../db.js'; +import { readFileSync, createReadStream } from 'fs'; +import { createGunzip } from 'zlib'; +import { pipeline } from 'stream/promises'; +import { Writable } from 'stream'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const SQL_DIR = join(__dirname, '..', '..', 'sql'); + +/** + * Execute a SQL file directly + */ +async function executeSqlFile(filename, tableName) { + const filePath = join(SQL_DIR, filename); + console.log(` πŸ“„ Loading ${filename}...`); + + try { + let sql = readFileSync(filePath, 'utf8'); + + // Clean up postgres-specific commands that might cause issues + sql = sql + .replace(/\\restrict[^\n]*/g, '') + .replace(/\\unrestrict[^\n]*/g, '') + .replace(/SELECT pg_catalog\.setval[^;]*;/g, '') + .replace(/ALTER TABLE[^;]*OWNER TO[^;]*;/g, '') + .replace(/COMMENT ON[^;]*;/g, '') + .replace(/SET[^;]*;/g, '') + .replace(/SELECT[^;]*set_config[^;]*;/g, ''); + + // Extract only INSERT statements + const insertStatements = sql.match(/INSERT INTO[^;]+;/g) || []; + + if (insertStatements.length === 0) { + console.log(` ⚠️ No INSERT statements found in ${filename}`); + return 0; + } + + // Execute each INSERT statement + for (const stmt of insertStatements) { + // Convert MySQL column names to PostgreSQL (camelCase -> snake_case for some) + let pgStmt = stmt + .replace(/`/g, '"') + .replace(/"emojiU"/g, 'emoji_u') + .replace(/"wikiDataId"/g, 'wiki_data_id'); + await pool.query(pgStmt); + } + + console.log(` βœ“ ${insertStatements.length} records inserted into ${tableName}`); + return insertStatements.length; + } catch (error) { + console.error(` ❌ Error loading ${filename}:`, error.message); + throw error; + } +} + +/** + * Execute a gzipped SQL file + */ +async function executeGzippedSqlFile(filename, tableName) { + const filePath = join(SQL_DIR, filename); + console.log(` πŸ“„ Loading ${filename} (gzipped)...`); + + try { + // Read and decompress + let sql = ''; + const gunzip = createGunzip(); + const readStream = createReadStream(filePath); + + await pipeline( + readStream, + gunzip, + new Writable({ + write(chunk, encoding, callback) { + sql += chunk.toString(); + callback(); + } + }) + ); + + // Clean up postgres-specific commands + sql = sql + .replace(/\\restrict[^\n]*/g, '') + .replace(/\\unrestrict[^\n]*/g, '') + .replace(/SELECT pg_catalog\.setval[^;]*;/g, '') + .replace(/ALTER TABLE[^;]*OWNER TO[^;]*;/g, '') + .replace(/COMMENT ON[^;]*;/g, '') + .replace(/SET[^;]*;/g, '') + .replace(/SELECT[^;]*set_config[^;]*;/g, ''); + + // Extract only INSERT statements + const insertStatements = sql.match(/INSERT INTO[^;]+;/g) || []; + + if (insertStatements.length === 0) { + console.log(` ⚠️ No INSERT statements found in ${filename}`); + return 0; + } + + console.log(` πŸ“Š Found ${insertStatements.length} records to insert...`); + + // Batch insert for performance + const BATCH_SIZE = 1000; + for (let i = 0; i < insertStatements.length; i += BATCH_SIZE) { + const batch = insertStatements.slice(i, i + BATCH_SIZE); + for (const stmt of batch) { + let pgStmt = stmt + .replace(/`/g, '"') + .replace(/"emojiU"/g, 'emoji_u') + .replace(/"wikiDataId"/g, 'wiki_data_id'); + await pool.query(pgStmt); + } + if ((i + BATCH_SIZE) % 10000 === 0 || i + BATCH_SIZE >= insertStatements.length) { + console.log(` ... ${Math.min(i + BATCH_SIZE, insertStatements.length)} / ${insertStatements.length}`); + } + } + + console.log(` βœ“ ${insertStatements.length} records inserted into ${tableName}`); + return insertStatements.length; + } catch (error) { + console.error(` ❌ Error loading ${filename}:`, error.message); + throw error; + } +} + +/** + * Seed all location data from SQL dumps + */ +export async function seedLocationData() { + console.log('🌍 Seeding comprehensive location data...'); + console.log(' Source: GeoDB Cities (https://github.com/dr5hn/countries-states-cities-database)\n'); + + try { + // 1. Continents (from regions.sql - 6 records) + console.log('1️⃣ Seeding Continents...'); + await executeSqlFile('regions.sql', 'continents'); + + // 2. Subregions (22 records) + console.log('2️⃣ Seeding Subregions...'); + await executeSqlFile('subregions.sql', 'subregions'); + + // 3. Countries (~250 records) + console.log('3️⃣ Seeding Countries...'); + await executeSqlFile('countries.sql', 'countries'); + + // 4. States (~5400 records) + console.log('4️⃣ Seeding States...'); + await executeSqlFile('states.sql', 'states'); + + // 5. Cities (~160k records) - This is the big one + console.log('5️⃣ Seeding Cities (this may take a while)...'); + await executeGzippedSqlFile('cities.sql.gz', 'cities'); + + console.log('\n βœ… Location data seeding complete!'); + + // Print counts + const counts = await pool.query(` + SELECT + (SELECT COUNT(*) FROM continents) as continents, + (SELECT COUNT(*) FROM subregions) as subregions, + (SELECT COUNT(*) FROM countries) as countries, + (SELECT COUNT(*) FROM states) as states, + (SELECT COUNT(*) FROM cities) as cities + `); + + const c = counts.rows[0]; + console.log(` πŸ“Š Totals: ${c.continents} continents, ${c.subregions} subregions, ${c.countries} countries, ${c.states} states, ${c.cities} cities`); + + } catch (error) { + console.error('❌ Location seeding failed:', error.message); + throw error; + } +} + +// For direct execution +if (process.argv[1] === fileURLToPath(import.meta.url)) { + import('../db.js').then(async ({ testConnection, closePool }) => { + const connected = await testConnection(); + if (!connected) { + console.error('Could not connect to database'); + process.exit(1); + } + await seedLocationData(); + await closePool(); + }); +}