2023-10-21 21:12:58 +03:00
package db
2023-10-22 21:19:11 +03:00
import cats.data.NonEmptyList
2023-10-21 21:12:58 +03:00
import cats.effect._
2023-12-15 13:46:45 +02:00
import cats.implicits._
2023-10-21 21:12:58 +03:00
import doobie._
import doobie.implicits._
import java.time.format.DateTimeFormatter
2023-12-24 23:54:16 +02:00
import java.time. { LocalDate , LocalDateTime , OffsetDateTime }
2023-10-21 21:12:58 +03:00
import doobie.postgres.implicits._
2023-10-22 23:51:59 +03:00
import org.typelevel.log4cats.Logger
import org.typelevel.log4cats.slf4j.Slf4jLogger
2023-12-17 22:02:53 +02:00
import parse.Aggregate. { AggregateKey , AggregateValue , DoubleValue , TimeDoubleList , UserQuery }
2023-10-23 16:20:45 +03:00
import parse. { Parser , WeatherStationData }
2023-10-21 21:12:58 +03:00
2023-12-17 22:02:53 +02:00
import java.time.temporal.ChronoUnit
2024-01-01 20:56:50 +02:00
import scala.util.matching.Regex
2023-12-17 22:02:53 +02:00
2023-10-22 23:51:59 +03:00
object PostgresService {
def of ( transactor : Transactor [ IO ]) : IO [ PostgresService ] = {
Slf4jLogger . create [ IO ]. map ( logger => new PostgresService ( transactor , logger ))
}
}
2023-10-21 21:12:58 +03:00
2023-12-17 22:48:24 +02:00
class PostgresService ( transactor : Transactor [ IO ], log : Logger [ IO ]) {
2023-10-23 16:20:45 +03:00
def save ( fileName : String , content : String ) : IO [ String ] = {
2024-01-01 20:56:50 +02:00
val YearPattern : Regex = """(\d{4})\d{4}_\d{4}\.csv""" . r
def extractYear ( filename : String ) : Option [ String ] = {
YearPattern . findFirstMatchIn ( filename ). map ( _ . group ( 1 ))
}
extractYear ( fileName ) match {
case Some ( year ) =>
val strLines = content . split ( System . lineSeparator ()). toList
val weatherStationData = strLines . flatMap ( line => Parser . parseLine ( line , year ))
insertInWeatherTable ( weatherStationData )
. attempt
. flatMap {
case Left ( error ) => log . error ( s"Write db ' $fileName ' failed with error: ${ error . getMessage } " ) *> IO . raiseError ( error )
case Right ( rowCount ) => log . info ( s"write rows: $rowCount file: $fileName in year: $year " ). as ( fileName )
}
case None =>
IO . raiseError ( new RuntimeException ( s"Could not extract year from file name: $fileName " ))
2023-10-23 16:20:45 +03:00
}
}
2023-12-24 23:54:16 +02:00
def queryCountry ( from : LocalDateTime , to : LocalDateTime , fieldList : NonEmptyList [ String ]) : IO [ Map [ String , ( Option [ Double ] , Option [ Double ] , Option [ Double ] , Option [ Double ])]] = {
def queryForField ( field : String , from : LocalDateTime , to : LocalDateTime ) : ConnectionIO [( String , ( Option [ Double ] , Option [ Double ] , Option [ Double ] , Option [ Double ]))] =
{
val query =
fr "SELECT " ++
fr " ROUND(CAST(MIN(" ++ Fragment . const ( field ) ++ fr ") AS NUMERIC), 1), " ++
fr " ROUND(CAST(MAX(" ++ Fragment . const ( field ) ++ fr ") AS NUMERIC), 1), " ++
fr " ROUND(CAST(AVG(" ++ Fragment . const ( field ) ++ fr ") AS NUMERIC), 1), " ++
fr " ROUND(CAST(SUM(" ++ Fragment . const ( field ) ++ fr ") AS NUMERIC), 1)" ++
fr " FROM weather" ++
fr " WHERE dateTime BETWEEN $from AND $to"
query . query [( Option [ Double ] , Option [ Double ] , Option [ Double ] , Option [ Double ])]
. unique
. map { case ( min , max , avg , sum ) =>
field -> ( min , max , avg , sum )
}
}
fieldList . toList . traverse { field =>
queryForField ( field , from , to ). transact ( transactor )
}. map ( _ . toMap )
}
2023-12-15 13:46:45 +02:00
def query ( userQuery : UserQuery ) : IO [ Map [ String , Option [ AggregateValue ]]] = {
2023-12-17 22:02:53 +02:00
if ( userQuery . field == "phenomena" ) { // this handles strings
// TODO query list and distinct values from phenomena
IO ( Map ()) // empty result
} else if ( List (
2023-12-15 13:46:45 +02:00
AggregateKey . Max ,
AggregateKey . Min ,
AggregateKey . Avg ,
2023-12-17 22:02:53 +02:00
AggregateKey . Sum ,
AggregateKey . Distinct
2023-12-15 13:46:45 +02:00
). contains ( userQuery . key )) {
val query =
2023-12-17 22:02:53 +02:00
fr "SELECT city, ROUND(CAST(" ++ Fragment . const ( userQuery . key . toString . toUpperCase ) ++ fr "(" ++ Fragment . const ( userQuery . field ) ++ fr ") AS NUMERIC), 1) AS value" ++
fr " FROM weather" ++
fr " WHERE " ++ Fragments . in ( fr "city" , userQuery . cities ) ++
fr " AND dateTime BETWEEN ${userQuery.from} AND ${userQuery.to}" ++
fr " GROUP BY city"
2023-12-15 13:46:45 +02:00
query . query [( String , Option [ Double ])]
. to [ List ]
. transact ( transactor )
. map { resultList =>
resultList . map {
case ( city , maybeValue ) =>
city -> maybeValue . map ( DoubleValue )
}. toMap : Map [ String , Option [ AggregateValue ]]
}
2023-12-17 22:02:53 +02:00
} else if ( userQuery . key == AggregateKey . List ) {
val byField = userQuery . field match {
case "tempMax" => "MAX(tempMax)"
case "tempMin" => "MIN(tempMin)"
case "tempAvg" => "AVG(tempAvg)"
case "precipitation" => "SUM(precipitation)"
case "windAvg" => "AVG(windAvg)"
case "windMax" => "MAX(windMax)"
case "visibilityMin" => "MIN(visibilityMin)"
case "visibilityAvg" => "AVG(visibilityAvg)"
case "snowAvg" => "AVG(snowAvg)"
case "atmPressure" => "AVG(atmPressure)"
case "dewPoint" => "AVG(dewPoint)"
case "humidity" => "AVG(humidity)"
case "sunDuration" => "SUM(sunDuration)"
}
val selectField = if ( userQuery . granularity == ChronoUnit . HOURS ) userQuery . field else byField ;
val selectTime = userQuery . granularity match {
case ChronoUnit . HOURS => "dateTime"
case ChronoUnit . DAYS => "DATE(dateTime)"
case ChronoUnit . MONTHS => "TO_CHAR(DATE_TRUNC('month', dateTime), 'YYYY-MM')"
case ChronoUnit . YEARS => "TO_CHAR(DATE_TRUNC('year', dateTime), 'YYYY')"
case _ => ""
}
val groupBy = if ( userQuery . granularity == ChronoUnit . HOURS ) "" else "GROUP BY city, " + selectTime ;
val query =
fr "SELECT city, " ++ Fragment . const ( selectTime ) ++ fr " as time, ROUND(CAST(" ++ Fragment . const ( selectField ) ++ fr " AS NUMERIC), 1) as value" ++
fr " FROM weather" ++
fr " WHERE " ++ Fragments . in ( fr "city" , userQuery . cities ) ++
fr " AND dateTime BETWEEN ${userQuery.from} AND ${userQuery.to}" ++
fr " " ++ Fragment . const ( groupBy ) ++
fr " ORDER BY city, time;"
/*
SELECT city, dateTime, tempmax
FROM weather
WHERE city IN ('Rīga', 'Rēzekne', 'Kolka')
AND dateTime BETWEEN '2023-12-10' AND '2023-12-14'
ORDER BY city, dateTime;
SELECT city, DATE(dateTime) as day, MAX(tempmax) as max_temp
FROM weather
WHERE city IN ('Rīga', 'Rēzekne', 'Kolka')
AND dateTime BETWEEN '2023-12-10' AND '2023-12-14'
GROUP BY city, DATE(dateTime)
ORDER BY city, day;
SELECT city, TO_CHAR(DATE_TRUNC('month', dateTime), 'YYYY-MM') as month, MAX(tempmax) as max_temp
FROM weather
WHERE city IN ('Rīga', 'Rēzekne', 'Kolka')
AND dateTime BETWEEN '2023-01-01' AND '2023-12-31'
GROUP BY city, TO_CHAR(DATE_TRUNC('month', dateTime), 'YYYY-MM')
ORDER BY city, month;
SELECT city, TO_CHAR(DATE_TRUNC('year', dateTime), 'YYYY') as year, MAX(tempmax) as max_temp
FROM weather
WHERE city IN ('Rīga', 'Rēzekne', 'Kolka')
AND dateTime BETWEEN '2023-01-01' AND '2023-12-31'
GROUP BY city, TO_CHAR(DATE_TRUNC('year', dateTime), 'YYYY')
ORDER BY city, year;
*/
query . query [( String , String , Option [ Double ])]
. to [ List ]
. transact ( transactor )
. map { resultList =>
resultList
. groupBy ( _ . _1 )
. view . mapValues { list =>
TimeDoubleList ( list . map { case ( _ , date , maybeValue ) =>
( date , maybeValue )
}). some
}
. toMap : Map [ String , Option [ AggregateValue ]]
}
2023-12-15 13:46:45 +02:00
} else {
2023-12-17 22:02:53 +02:00
IO ( Map ()) // empty result
2023-12-15 13:46:45 +02:00
}
}
2023-10-23 16:20:45 +03:00
def getDatesByMonths ( monthList : List [ LocalDate ]) : IO [ List [ LocalDate ]] = {
val monthYearPairs = monthList . map { localDate =>
2023-12-18 16:19:27 +02:00
val month = localDate . atStartOfDay . getMonthValue
val year = localDate . atStartOfDay . getYear
2023-10-23 16:20:45 +03:00
( month , year )
}
val whereClauses = monthYearPairs . map {
case ( month , year ) => fr "(EXTRACT(MONTH FROM dateTime) = $month AND EXTRACT(YEAR FROM dateTime) = $year)"
}
val combinedWhereClause = whereClauses . intercalate ( fr " OR " )
(
fr """
SELECT DISTINCT DATE(dateTime)
FROM weather
WHERE """ ++ combinedWhereClause
)
. query [ LocalDate ]
. to [ List ]
. transact ( transactor )
}
2023-12-12 15:36:30 +02:00
def getDateTimeEntries ( dateTime : LocalDateTime ) : IO [ List [ String ]] = {
2023-12-12 16:15:36 +02:00
val columnNames = List ( "City; TempMax; TempMin; TempAvg; Precipitation; WindAvg; WindMax; VisibilityMin; VisibilityAvg; SnowAvg; AtmPressure; DewPoint; Humidity; SunDuration; Phenomena" )
val result = fr """
SELECT city || ';' || COALESCE(tempmax::text, '' ) || ';' || COALESCE(tempmin::text, '' ) || ';' || COALESCE(tempavg::text, '' ) || ';' || COALESCE(precipitation::text, '' ) || ';' || COALESCE(windavg::text, '' ) || ';' || COALESCE(windmax::text, '' ) || ';' || COALESCE(tempmax::text, '' ) || ';' || COALESCE(visibilitymin::text, '' ) || ';' || COALESCE(visibilityavg::text, '' ) || ';' || COALESCE(snowavg::text, '' ) || ';' || COALESCE(atmpressure::text, '' ) || ';' || COALESCE(dewpoint::text, '' ) || ';' || COALESCE(humidity::text, '' ) || ';' || COALESCE(sunduration::text, '' ) || ';' || array_to_string(phenomena, ', ')
2023-12-12 15:36:30 +02:00
FROM weather
WHERE dateTime = $dateTime
ORDER BY city
"""
. query [ String ]
. to [ List ]
. transact ( transactor )
2023-12-12 16:15:36 +02:00
result . map ( columnNames ++ _ )
2023-12-12 15:36:30 +02:00
}
def getDateFileNames ( date : LocalDate ) : IO [ List [ String ]] = {
2023-12-18 16:19:27 +02:00
val year = date . atStartOfDay . getYear
val month = date . atStartOfDay . getMonthValue
val day = date . atStartOfDay . getDayOfMonth
2023-12-12 15:36:30 +02:00
val whereClauses = fr "(EXTRACT(DAY FROM dateTime) = $day AND EXTRACT(MONTH FROM dateTime) = $month AND EXTRACT(YEAR FROM dateTime) = $year)"
val formatter = DateTimeFormatter . ofPattern ( "yyyyMMdd_HH00" )
(
fr """
SELECT DISTINCT dateTime
FROM weather
WHERE """ ++ whereClauses ++
fr "ORDER BY dateTime"
)
. query [ OffsetDateTime ]
. to [ List ]
. transact ( transactor )
2023-12-12 16:15:36 +02:00
. map { dateTimes =>
dateTimes . map ( dateTime =>
2023-12-18 16:19:27 +02:00
dateTime . format ( formatter )
2023-12-12 16:15:36 +02:00
)
2023-12-12 15:36:30 +02:00
}
}
2023-12-12 16:15:36 +02:00
2023-10-21 21:12:58 +03:00
def getResourceContent ( path : String ) : IO [ String ] = {
val streamResource = Resource . make ( IO ( getClass . getResourceAsStream ( path ))) { stream =>
IO ( stream . close ()). handleErrorWith ( _ => IO . unit )
}
streamResource . use { stream =>
IO ( scala . io . Source . fromInputStream ( stream ). mkString )
}
}
2023-10-24 11:59:23 +03:00
def createWeatherTable : IO [ Int ] = {
2023-10-21 21:12:58 +03:00
for {
createTableSql <- getResourceContent ( "/db/create_weather_table.sql" )
2023-10-22 23:51:59 +03:00
result <- Update0 ( createTableSql , None ). run . transact ( transactor )
2023-10-21 21:12:58 +03:00
} yield result
}
implicit val doubleOptionMeta : Meta [ Option [ Double ]] = Meta [ Double ]. imap ( Option ( _ ))( _ . getOrElse ( Double . NaN ))
2023-10-23 16:20:45 +03:00
def insertInWeatherTable ( data : List [ WeatherStationData ]) : IO [ Int ] = {
2023-12-11 00:12:52 +02:00
getResourceContent ( "/db/insert_weather_table.sql" ). flatMap { insertTableSql =>
val insertData = data . map ( line => {
val w = line . weather
2023-12-18 16:19:27 +02:00
( line . timestamp , line . city , w . tempMax , w . tempMin , w . tempAvg , w . precipitation , w . windAvg , w . windMax , w . visibilityMin , w . visibilityAvg , w . snowAvg , w . atmPressure , w . dewPoint , w . humidity , w . sunDuration , w . phenomena )
2023-12-11 00:12:52 +02:00
})
2023-12-18 16:19:27 +02:00
Update [( LocalDateTime , String , Option [ Double ] , Option [ Double ] , Option [ Double ] , Option [ Double ] , Option [ Double ] , Option [ Double ] , Option [ Double ] , Option [ Double ] , Option [ Double ] , Option [ Double ] , Option [ Double ] , Option [ Double ] , Option [ Double ] , List [ String ])]( insertTableSql )
2023-12-11 00:12:52 +02:00
. updateMany ( insertData )
2023-10-23 16:20:45 +03:00
. transact ( transactor )
2023-12-11 00:12:52 +02:00
}
2023-10-21 21:12:58 +03:00
}
}