Use Postgres instead of SQLite

This commit is contained in:
Denis Donici 2025-03-18 00:37:28 +02:00
parent 42999604f6
commit b095ab6661
9 changed files with 364 additions and 315 deletions

7
.env.example Normal file
View File

@ -0,0 +1,7 @@
# DB_DATABASE = "./world.db"
ORIGIN=http://localhost:3000
PGHOST=localhost
PGPORT=5432
PGUSERNAME=user
PGPASSWORD=password
PGDATABASE=pg_db

Binary file not shown.

View File

@ -1,6 +1,20 @@
//import Database from 'better-sqlite3'; // import { Database } from 'bun:sqlite';
import { Database } from 'bun:sqlite'; // const db = new Database('./database/data.db');
const db = new Database('./database/data.db'); import { SQL } from 'bun';
const db = new SQL({
// Pool configuration
max: 20, // Maximum 20 concurrent connections
idleTimeout: 30, // Close idle connections after 30s
maxLifetime: 3600, // Max connection lifetime 1 hour
connectionTimeout: 10, // Connection timeout 10s
onconnect: (client) => {
console.log('Connected to database');
},
onclose: (client) => {
console.log('Connection closed');
}
});
export default db; export default db;

View File

@ -1,7 +1,8 @@
import db from "."; // import db from '.';
import type { CityBasic, CityTemperature, CityTemperaturSimple, CountryBasic } from '$lib/types';
// export const allCountries = db.query('SELECT id, name, native, emoji FROM countries'); import { sql } from 'bun';
export const allCountries = db.query(` // // export const allCountries = await sql('SELECT id, name, native, emoji FROM countries');
export const allCountries = (await sql`
SELECT id, name, native, emoji SELECT id, name, native, emoji
FROM countries FROM countries
WHERE id IN( WHERE id IN(
@ -9,119 +10,147 @@ export const allCountries = db.query(`
FROM cities FROM cities
WHERE capital IN('capital', 'admin') OR(population >= 100000 AND population IS NOT NULL) WHERE capital IN('capital', 'admin') OR(population >= 100000 AND population IS NOT NULL)
) )
`); `) as CountryBasic[];
export const allCities = db.query("SELECT id, city as name, country_id, lat as latitude, lng as longitude FROM cities WHERE capital IN ('capital', 'admin') OR (population >= 100000 AND population != '')"); export const allCities =
export const allCitiesWithNoTemperatureToday = db.query(` (await sql`SELECT id, city AS name, country_id, lat AS latitude, lng AS longitude
FROM cities
WHERE capital IN ('capital', 'admin')
OR (population >= 100000 AND population::text != '')`) as CityBasic[];
export const allCitiesWithNoTemperatureToday = async ({
date
}: {
date: string;
}): Promise<CityBasic[]> =>
await sql`
SELECT c.id, c.city as name, c.country_id, c.lat as latitude, c.lng as longitude SELECT c.id, c.city as name, c.country_id, c.lat as latitude, c.lng as longitude
FROM cities c FROM cities c
LEFT JOIN meteo_data md ON c.id = md.city_id AND md.date = $date LEFT JOIN meteo_data md ON c.id = md.city_id AND md.date = ${date}
WHERE (c.capital IN ('capital', 'admin') OR (c.population >= 100000 AND c.population != '')) WHERE (c.capital IN ('capital', 'admin') OR (c.population >= 100000 AND c.population::text != ''))
AND md.city_id IS NULL; AND md.city_id IS NULL;
`); `;
export const cityTemperatureToday = db.query(` export const cityTemperatureToday = async ({
id,
date
}: {
id: number;
date: string;
}): Promise<CityTemperature | null> =>
await sql`
SELECT c.id, c.city as name, md.min, md.max, md.date SELECT c.id, c.city as name, md.min, md.max, md.date
FROM cities as c FROM cities as c
JOIN meteo_data as md ON c.id = md.city_id JOIN meteo_data as md ON c.id = md.city_id
WHERE c.id = $id WHERE c.id = ${id}
AND md.date = $date AND md.date = ${date}
`); `;
export const insertCityTemperatureData = db.query(` export const insertCityTemperatureData = async ({
city_id,
min,
max,
date
}: CityTemperaturSimple): Promise<CityTemperaturSimple> =>
await sql`
INSERT INTO meteo_data (city_id, min, max, date) INSERT INTO meteo_data (city_id, min, max, date)
VALUES ($id, $min, $max, $date) VALUES (${city_id}, ${min}, ${max}, ${date})
`); RETURNING *
export const getAllCitiesInCountryByTemp = db.query(` `;
SELECT
c.id as city_id,
c.city as city_name,
md.max as max_temp,
md.min as min_temp,
md.date
FROM cities c
JOIN meteo_data md ON c.id = md.city_id
WHERE c.country_id = $country_id
AND md.date = $date
ORDER BY md.max DESC
`);
export const getHottestAndColdestCityInCountry = db.query(` // export const getAllCitiesInCountryByTemp = await sql`
// SELECT
// c.id as city_id,
// c.city as city_name,
// md.max as max_temp,
// md.min as min_temp,
// md.date
// FROM cities c
// JOIN meteo_data md ON c.id = md.city_id
// WHERE c.country_id = $country_id
// AND md.date = $date
// ORDER BY md.max DESC
// `;
export const getHottestAndColdestCityInCountry = async ({
country_id,
date
}: {
country_id: number;
date: string;
}) =>
await sql`
WITH city_temps AS ( WITH city_temps AS (
SELECT SELECT
c.id as city_id, c.id as city_id,
c.city as city_name, c.city as city_name,
c.country_id, c.country_id,
md.max as max_temp, ROUND(md.max::numeric, 2) as max_temp,
md.date, md.date,
ROW_NUMBER() OVER (PARTITION BY c.country_id ORDER BY md.max DESC) as hottest_rank, ROW_NUMBER() OVER (PARTITION BY c.country_id ORDER BY md.max DESC)::INTEGER as hottest_rank,
ROW_NUMBER() OVER (PARTITION BY c.country_id ORDER BY md.max ASC) as coldest_rank ROW_NUMBER() OVER (PARTITION BY c.country_id ORDER BY md.max ASC)::INTEGER as coldest_rank
FROM cities c FROM cities c
JOIN meteo_data md ON c.id = md.city_id JOIN meteo_data md ON c.id = md.city_id
WHERE md.date = $date WHERE md.date = ${date}
AND c.country_id = $country_id AND c.country_id = ${country_id}
) )
SELECT * FROM city_temps SELECT * FROM city_temps
WHERE hottest_rank = 1 OR coldest_rank = 1 WHERE hottest_rank = 1 OR coldest_rank = 1
`); `;
export const getHottestAndColdestCityInWorld = db.query(`
WITH city_temps AS (
SELECT
c.id as city_id,
c.city as city_name,
c.country_id,
co.name as country_name,
md.max as max_temp,
md.min as min_temp,
md.date,
ROW_NUMBER() OVER (ORDER BY md.max DESC) as hottest_rank,
ROW_NUMBER() OVER (ORDER BY md.min ASC) as coldest_rank
FROM cities c
JOIN meteo_data md ON c.id = md.city_id
JOIN countries co ON c.country_id = co.id
WHERE md.date = $date
)
SELECT * FROM city_temps
WHERE hottest_rank = 1 OR coldest_rank = 1
LIMIT 2
`);
export const getWorldTemperatureStats = db.query(`
WITH city_temps AS (
SELECT
c.id as city_id,
c.city as city_name,
c.country_id,
co.name as country_name,
md.max as max_temp,
md.min as min_temp,
md.date,
ROW_NUMBER() OVER (ORDER BY md.max DESC) as hottest_rank,
ROW_NUMBER() OVER (ORDER BY md.max ASC) as coldest_rank
FROM cities c
JOIN meteo_data md ON c.id = md.city_id
JOIN countries co ON c.country_id = co.id
WHERE md.date = $date
),
world_stats AS (
SELECT
ct1.city_id,
ct1.city_name,
ct1.country_id,
ct1.country_name,
ct1.max_temp,
ct1.date,
ct1.hottest_rank,
ct1.coldest_rank
FROM city_temps ct1
WHERE ct1.hottest_rank = 1 OR ct1.coldest_rank = 1
)
SELECT * FROM world_stats
ORDER BY max_temp DESC
LIMIT 2
`);
// export const getHottestAndColdestCityInWorld = await sql`
// WITH city_temps AS (
// SELECT
// c.id as city_id,
// c.city as city_name,
// c.country_id,
// co.name as country_name,
// md.max as max_temp,
// md.min as min_temp,
// md.date,
// ROW_NUMBER() OVER (ORDER BY md.max DESC) as hottest_rank,
// ROW_NUMBER() OVER (ORDER BY md.min ASC) as coldest_rank
// FROM cities c
// JOIN meteo_data md ON c.id = md.city_id
// JOIN countries co ON c.country_id = co.id
// WHERE md.date = $date
// )
// SELECT * FROM city_temps
// WHERE hottest_rank = 1 OR coldest_rank = 1
// LIMIT 2
// `;
// export const getWorldTemperatureStats = await sql`
// WITH city_temps AS (
// SELECT
// c.id as city_id,
// c.city as city_name,
// c.country_id,
// co.name as country_name,
// md.max as max_temp,
// md.min as min_temp,
// md.date,
// ROW_NUMBER() OVER (ORDER BY md.max DESC) as hottest_rank,
// ROW_NUMBER() OVER (ORDER BY md.max ASC) as coldest_rank
// FROM cities c
// JOIN meteo_data md ON c.id = md.city_id
// JOIN countries co ON c.country_id = co.id
// WHERE md.date = $date
// ),
// world_stats AS (
// SELECT
// ct1.city_id,
// ct1.city_name,
// ct1.country_id,
// ct1.country_name,
// ct1.max_temp,
// ct1.date,
// ct1.hottest_rank,
// ct1.coldest_rank
// FROM city_temps ct1
// WHERE ct1.hottest_rank = 1 OR ct1.coldest_rank = 1
// )
// SELECT * FROM world_stats
// ORDER BY max_temp DESC
// LIMIT 2
// `;

View File

@ -1,56 +1,53 @@
import { allCities, allCitiesWithNoTemperatureToday, cityTemperatureToday, insertCityTemperatureData } from "$lib/db/queries"; import { allCitiesWithNoTemperatureToday, insertCityTemperatureData } from '$lib/db/queries';
import type { CityBasic, CityTemperature } from "$lib/types"; import { getLocationTemperature } from './meteoService';
import { getLocationTemperature } from "./meteoService";
const COOLDOWN_TIME = 333; const COOLDOWN_TIME = 333;
export const fetchAllCitiesTemperatures = async (date: string) => { export const fetchAllCitiesTemperatures = async (date: string) => {
try { try {
const cities = await allCitiesWithNoTemperatureToday({ date });
console.log('Today date: ', date);
console.log('Total Cities to fetch: ', cities.length);
const cities = allCitiesWithNoTemperatureToday.all({ $date: date }) as CityBasic[]; for (const [index, city] of cities.entries()) {
try {
console.log(`Processing city ${index + 1}/${cities.length}`);
console.log('Today date: ', date); const { data } = await getLocationTemperature(
console.log('Total Cities to fetch: ', cities.length); city.id,
city.latitude.toString(),
city.longitude.toString()
);
for (const [index, city] of cities.entries()) { if (data) {
try { await insertCityTemperatureData({
console.log(`Processing city ${index + 1}/${cities.length}`); city_id: city.id, // Changed from data.id to city.id
const existingRecord = cityTemperatureToday.get({ $id: city.id, $date: date }) as CityTemperature | null; min: data.min,
if (!existingRecord) { max: data.max,
date: data.date
});
console.log(`Temperature data stored for ${city.name}`);
} else {
console.log(`No temperature data available for ${city.name}`);
}
const { data } = await getLocationTemperature( // Add cooldown delay to avoid API throttling
city.id, await new Promise((resolve) => setTimeout(resolve, COOLDOWN_TIME));
city.latitude.toString(), } catch (error) {
city.longitude.toString() console.error(`Failed to fetch temperature for city ${city.name}:`, error);
); // Consider adding more specific error handling here
if (data) { }
insertCityTemperatureData.run({ }
$id: data.id,
$min: data.min,
$max: data.max,
$date: data.date
});
console.log(`Temperature data stored for ${city.name}`);
}
// Add cooldown delay to avoid API throttling
await new Promise(resolve => setTimeout(resolve, COOLDOWN_TIME));
} else {
console.log(`Temperature data already exists for ${city.name}`);
}
console.log('Finished fetching temperatures for all cities: ', date);
} catch (error) { return {
console.error(`Failed to fetch temperature for city ${city.name}:`, error); ok: true
} };
} } catch (error) {
console.log('Finished fetching temperatures for all cities: ', date); console.error('Failed to fetch cities:', error);
return { return {
ok: true, ok: false,
} error: error?.message // Added error message for better debugging
} catch (error) { };
console.error('Failed to fetch cities:', error); }
return { };
ok: false
}
}
}

View File

@ -1,38 +1,37 @@
import type { MeteoData, MeteoResponseType } from "$lib/types"; import type { MeteoData, MeteoResponseType } from '$lib/types';
export const getLocationTemperature = async (id: number, latitude: string, longitude: string) => { export const getLocationTemperature = async (id: number, latitude: string, longitude: string) => {
const meteoEndpoint = "https://api.open-meteo.com/v1/forecast"; const meteoEndpoint = 'https://api.open-meteo.com/v1/forecast';
// https://api.open-meteo.com/v1/forecast?latitude=48.0356&longitude=27.8129&hourly=temperature_2m&daily=temperature_2m_max,temperature_2m_min&forecast_days=1&format=json&timeformat=unixtime // https://api.open-meteo.com/v1/forecast?latitude=48.0356&longitude=27.8129&hourly=temperature_2m&daily=temperature_2m_max,temperature_2m_min&forecast_days=1&format=json&timeformat=unixtime
const url = new URL(meteoEndpoint); const url = new URL(meteoEndpoint);
url.searchParams.set('latitude', latitude); url.searchParams.set('latitude', latitude);
url.searchParams.set('longitude', longitude); url.searchParams.set('longitude', longitude);
url.searchParams.set('daily', 'temperature_2m_max,temperature_2m_min'); url.searchParams.set('daily', 'temperature_2m_max,temperature_2m_min');
url.searchParams.set('format', 'json'); url.searchParams.set('format', 'json');
url.searchParams.set('forecast_days', '1'); url.searchParams.set('forecast_days', '1');
try {
try { const response = await fetch(url);
const response = await fetch(url); console.log('Fetching data from open meteo...');
if (response.ok) { if (response.ok) {
const data = await response.json() as MeteoResponseType; const data = (await response.json()) as MeteoResponseType;
console.log('Fetched DATA from open meteo >>>>'); console.log('Fetched DATA from open meteo >>>>');
return { return {
data: { data: {
id, id,
max: data.daily.temperature_2m_max[0], max: data.daily.temperature_2m_max[0],
min: data.daily.temperature_2m_min[0], min: data.daily.temperature_2m_min[0],
date: data.daily.time[0], date: data.daily.time[0]
} as MeteoData } as MeteoData
}; };
} else { } else {
return { data: null }; return { data: null };
} }
} catch (error) {
} catch (error) { console.log(error);
console.log(error); return {
return { data: null
data: null };
} }
} };
}

View File

@ -1,72 +1,79 @@
export type CountryBasic = { export type CountryBasic = {
id: number; id: number;
name: string; name: string;
native: string; native: string;
emoji: string; emoji: string;
}; };
export type CityBasic = { export type CityBasic = {
id: number; id: number;
name: string; name: string;
country_id: number; country_id: number;
latitude: number; latitude: number;
longitude: number; longitude: number;
}; };
export type CityTemperature = { export type CityTemperature = {
id: number; id: number;
name: string; name: string;
min: number; min: number;
max: number; max: number;
date: string; date: string;
} };
export type CityTemperaturSimple = {
city_id: number;
min: number;
max: number;
date: string;
};
export type HotColdCity = { export type HotColdCity = {
city_id: number; city_id: number;
city_name: string; city_name: string;
country_id: number; country_id: number;
country_name: string; country_name: string;
max_temp: number; max_temp: number;
date: string; date: string;
hottest_rank: number; hottest_rank: number;
coldest_rank: number; coldest_rank: number;
} };
export type ComboOption = { export type ComboOption = {
id: number; id: number;
name: string; name: string;
emoji?: string; emoji?: string;
country_id?: number; country_id?: number;
} };
export type MeteoResponseType = { export type MeteoResponseType = {
latitude: number; latitude: number;
longitude: number; longitude: number;
generationtime_ms: number; generationtime_ms: number;
utc_offset_seconds: number; utc_offset_seconds: number;
timezone: string; timezone: string;
timezone_abbreviation: string; timezone_abbreviation: string;
elevation: number; elevation: number;
daily_units: { daily_units: {
time: string; time: string;
temperature_2m_max: string; temperature_2m_max: string;
temperature_2m_min: string; temperature_2m_min: string;
}, };
daily: { daily: {
time: string[]; time: string[];
temperature_2m_max: number[]; temperature_2m_max: number[];
temperature_2m_min: number[]; temperature_2m_min: number[];
}, };
} };
export type MeteoData = { export type MeteoData = {
id: number; id: number;
max: number; max: number;
min: number; min: number;
date: string; date: string;
} };
export enum UnitsEnum { export enum UnitsEnum {
METRIC = 'METRIC', METRIC = 'METRIC',
IMPERIAL = 'IMPERIAL' IMPERIAL = 'IMPERIAL'
} }

View File

@ -1,4 +1,9 @@
import { allCities, allCountries, getHottestAndColdestCityInCountry } from '$lib/db/queries'; import {
allCities,
allCitiesWithNoTemperatureToday,
allCountries,
getHottestAndColdestCityInCountry
} from '$lib/db/queries';
import type { CityBasic, CityTemperature, CountryBasic, HotColdCity } from '$lib/types'; import type { CityBasic, CityTemperature, CountryBasic, HotColdCity } from '$lib/types';
import { type Actions } from '@sveltejs/kit'; import { type Actions } from '@sveltejs/kit';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
@ -6,74 +11,67 @@ import type { PageServerLoad } from './$types';
// import { fetchAllCitiesTemperatures } from '$lib/server/meteoDataHandler'; // import { fetchAllCitiesTemperatures } from '$lib/server/meteoDataHandler';
const errorObject = { const errorObject = {
ok: false, ok: false,
data: null, data: null,
error: 'Failed to fetch temperature' error: 'Failed to fetch temperature'
} };
export const load: PageServerLoad = async ({ url }) => { export const load: PageServerLoad = async ({ url }) => {
try { try {
const countries = allCountries.all() as CountryBasic[]; const countries = allCountries;
const cities = allCities.all() as CityBasic[]; const cities = allCities;
const country_id = url.searchParams.get('country'); const country_id = url.searchParams.get('country');
// console.log('country_id >>> ', country_id);
let temperatureData: HotColdCity[] | null = null; let temperatureData: HotColdCity[] | null = null;
if (country_id) { if (country_id) {
temperatureData = getHottestAndColdestCityInCountry.all({ temperatureData = (await getHottestAndColdestCityInCountry({
$country_id: Number(country_id), country_id: Number(country_id),
$date: new Date().toISOString().split('T')[0] date: new Date().toISOString().split('T')[0]
}) as HotColdCity[]; })) as HotColdCity[];
} }
// console.log('Loading >>>>', temperatureData); return {
countries,
return { cities,
countries, temperatureData,
cities, country_id: country_id ? parseInt(country_id) : null
temperatureData, };
country_id: country_id ? parseInt(country_id) : null } catch (error) {
}; console.log(error);
} catch (error) { return {
console.log(error); countries: [],
return { cities: [],
countries: [], temperatureData: null,
cities: [], country_id: null
temperatureData: null, };
country_id: null }
};
}
}; };
export const actions: Actions = { export const actions: Actions = {
fetchTemperature: async ({ request }) => { fetchTemperature: async ({ request }) => {
try { try {
const formData = await request.formData(); const formData = await request.formData();
const country_id = formData.get('country_id') as string; const country_id = formData.get('country_id') as string;
// console.log('FORM ACTION >>>', country_id) // console.log('FORM ACTION >>>', country_id)
const response = getHottestAndColdestCityInCountry.all({ const response = (await getHottestAndColdestCityInCountry({
$country_id: Number(country_id), country_id: Number(country_id),
$date: new Date().toISOString().split('T')[0] date: new Date().toISOString().split('T')[0]
}) as HotColdCity[]; })) as HotColdCity[];
if (response.length == 2) { if (response.length == 2) {
// console.log('RESPONSE >>>>', response); return {
ok: true,
return { data: response,
ok: true, error: null,
data: response, country_id: country_id ?? null
error: null, };
country_id: country_id ?? null } else {
}; return errorObject;
} else { }
return errorObject; } catch (error) {
} console.log(error);
return errorObject;
} catch (error) { }
console.log(error); }
return errorObject; };
}
}
}

View File

@ -20,15 +20,15 @@
let temperatureData: HotColdCity[] | null = $state(data.temperatureData); let temperatureData: HotColdCity[] | null = $state(data.temperatureData);
let tempDiff: number | null = $derived( let tempDiff: number | null = $derived(
temperatureData temperatureData
? Number(Math.abs(temperatureData[0].max_temp - temperatureData[1].max_temp).toFixed(2)) ? Number(Math.abs(temperatureData[0]?.max_temp - temperatureData[1]?.max_temp).toFixed(2))
: null : null
); );
let tempDiffImperial: number | null = $derived( let tempDiffImperial: number | null = $derived(
temperatureData temperatureData
? Number( ? Number(
Math.abs( Math.abs(
celsiusToFahrenheit(temperatureData[0].max_temp) - celsiusToFahrenheit(temperatureData[0]?.max_temp) -
celsiusToFahrenheit(temperatureData[1].max_temp) celsiusToFahrenheit(temperatureData[1]?.max_temp)
).toFixed(2) ).toFixed(2)
) )
: null : null
@ -58,8 +58,6 @@
temperatureData = form.data; temperatureData = form.data;
} }
}); });
$effect(() => console.log(tempDiff));
</script> </script>
<svelte:head> <svelte:head>
@ -109,7 +107,7 @@
</form> </form>
{#if selectedCountryId && temperatureData} {#if selectedCountryId && temperatureData}
<div class="my-8 flex flex-col justify-evenly gap-12 md:flex-row"> <div class="my-8 flex flex-col justify-evenly gap-12 md:flex-row">
{#each temperatureData as temperature} {#each temperatureData as temperature, index}
<dl class="flex flex-col gap-2"> <dl class="flex flex-col gap-2">
{#if temperature.coldest_rank === 1} {#if temperature.coldest_rank === 1}
<dt class="text-center text-lg text-blue-600">❄️ Coldest City</dt> <dt class="text-center text-lg text-blue-600">❄️ Coldest City</dt>