diff --git a/.gitignore b/.gitignore index 02edf9a..e6ffd52 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,22 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Go workspace file +go.work + +# Misc .envrc .sqlfluff +data/ +seed.zip +tmp/ diff --git a/Taskfile.yaml b/Taskfile.yaml index c940e9b..32e1b55 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -10,12 +10,10 @@ tasks: - cmd: echo "Codegen complete" silent: true - db:dump: - dir: "{{.TASKFILE_DIR}}/sql/schema" + db:down: + dir: "{{.TASKFILE_DIR}}/docker" cmds: - - cmd: rm schema.sql - silent: true - - cmd: pg_dump -s -n f1db -d f1db -U $DATABASE_USER > schema.sql + - cmd: docker compose -f compose-dev.yaml down db:migrate: dir: "{{.TASKFILE_DIR}}/sql/schema" @@ -29,49 +27,43 @@ tasks: - cmd: goose create {{.CLI_ARGS}} sql silent: true - db:push: - cmds: - - cmd: echo $CR_PAT | docker login ghcr.io -u jsec --password-stdin - silent: true - - cmd: docker push ghcr.io/jsec/f1db:latest - - db:rollback: + db:migrate:down: dir: "{{.TASKFILE_DIR}}/sql/schema" cmds: - cmd: goose down silent: true - db:seed: - dir: "{{.TASKFILE_DIR}}/docker" + db:push: cmds: - - task: mysql:build - - cmd: docker compose -f compose-seed.yaml up --force-recreate --wait + - cmd: echo $CR_PAT | docker login ghcr.io -u jsec --password-stdin silent: true - - cmd: docker run --rm -it --net=host --name pgloader dimitri/pgloader:latest pgloader mysql://seed:seed@host.docker.internal:3306/f1db postgresql://$DATABASE_USER:$DATABASE_PASSWORD@host.docker.internal:5432/f1db - - db:seed:build: - dir: "{{.TASKFILE_DIR}}/docker" - cmds: - - cmd: docker build -f Dockerfile.mysql -t f1db/seed:latest --no-cache . + - cmd: docker push ghcr.io/jsec/f1db:latest - db:seed:down: + db:up: dir: "{{.TASKFILE_DIR}}/docker" cmds: - - cmd: docker compose -f compose-seed.yaml down -v - silent: true + - cmd: docker compose -f compose-dev.yaml up --wait db:update: cmds: - task: db:migrate - task: db:codegen - dev: + image:up: dir: "{{.TASKFILE_DIR}}/docker" cmds: - - cmd: docker compose -f compose-dev.yaml up --wait + - cmd: docker compose -f compose-seed.yaml up --wait + silent: true + - task: db:migrate - dev:down: + image:down: dir: "{{.TASKFILE_DIR}}/docker" cmds: - - cmd: docker compose -f compose-dev.yaml down + - cmd: docker compose -f compose-seed.yaml down + silent: true + image:new: + cmds: + - task: image:up + - cmd: go run ./cmd/imager + silent: true diff --git a/cmd/imager/main.go b/cmd/imager/main.go new file mode 100644 index 0000000..ba32029 --- /dev/null +++ b/cmd/imager/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "context" + "log" + + "github.com/jsec/f1-data-hub/internal/imager" +) + +func main() { + if err := imager.Run(context.Background()); err != nil { + log.Fatal(err) + } +} diff --git a/main.go b/cmd/server/main.go similarity index 100% rename from main.go rename to cmd/server/main.go diff --git a/docker/Dockerfile.postgres b/docker/Dockerfile similarity index 100% rename from docker/Dockerfile.postgres rename to docker/Dockerfile diff --git a/docker/Dockerfile.mysql b/docker/Dockerfile.mysql deleted file mode 100644 index e1dd028..0000000 --- a/docker/Dockerfile.mysql +++ /dev/null @@ -1,3 +0,0 @@ -FROM mysql:8.0 -COPY ./scripts/fetch-mysql-dump.sh . -RUN ["sh", "./fetch-mysql-dump.sh"] diff --git a/docker/compose-seed.yaml b/docker/compose-seed.yaml index 1c44cbf..96de66e 100644 --- a/docker/compose-seed.yaml +++ b/docker/compose-seed.yaml @@ -2,13 +2,13 @@ services: postgres: build: context: ./ - dockerfile: ./Dockerfile.postgres restart: always shm_size: 128mb + container_name: f1db-seed environment: POSTGRES_USER: ${DATABASE_USER} POSTGRES_PASSWORD: ${DATABASE_PASSWORD} - POSTGRES_DB: f1db + POSTGRES_DB: ${DATABASE_NAME} ports: - 5432:5432 healthcheck: @@ -16,20 +16,3 @@ services: interval: 10s timeout: 5s retries: 5 - - mysql: - image: f1db/seed - command: "--default-authentication-plugin=mysql_native_password" - restart: always - environment: - MYSQL_USER: seed - MYSQL_PASSWORD: seed - MYSQL_DATABASE: f1db - MYSQL_ROOT_PASSWORD: root - ports: - - 3306:3306 - healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] - interval: 10s - timeout: 5s - retries: 10 diff --git a/go.mod b/go.mod index 95faa3d..1827eb1 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,11 @@ module github.com/jsec/f1-data-hub go 1.23.1 require ( + github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 + github.com/jackc/pgx-shopspring-decimal v0.0.0-20220624020537-1d36b5a1853e github.com/jackc/pgx/v5 v5.7.1 github.com/labstack/echo v3.3.10+incompatible + github.com/shopspring/decimal v1.4.0 ) require ( diff --git a/go.sum b/go.sum index 192ff96..cc0334d 100644 --- a/go.sum +++ b/go.sum @@ -3,10 +3,14 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ= +github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx-shopspring-decimal v0.0.0-20220624020537-1d36b5a1853e h1:i3gQ/Zo7sk4LUVbsAjTNeC4gIjoPNIZVzs4EXstssV4= +github.com/jackc/pgx-shopspring-decimal v0.0.0-20220624020537-1d36b5a1853e/go.mod h1:zUHglCZ4mpDUPgIwqEKoba6+tcUQzRdb1+DPTuYe9pI= github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= @@ -22,6 +26,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= diff --git a/internal/database/circuit.sql.go b/internal/database/circuit.sql.go new file mode 100644 index 0000000..d979194 --- /dev/null +++ b/internal/database/circuit.sql.go @@ -0,0 +1,22 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 +// source: circuit.sql + +package database + +import ( + "github.com/shopspring/decimal" +) + +type SaveCircuitsParams struct { + ID int32 + Ref string + Name string + Location *string + Country *string + Lat *decimal.Decimal + Lng *decimal.Decimal + Alt *int32 + Url string +} diff --git a/internal/database/connection.go b/internal/database/connection.go new file mode 100644 index 0000000..f6ec8ac --- /dev/null +++ b/internal/database/connection.go @@ -0,0 +1,32 @@ +package database + +import ( + "context" + "fmt" + "os" + + pgxdecimal "github.com/jackc/pgx-shopspring-decimal" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" +) + +func Connect(ctx context.Context) (*pgxpool.Pool, error) { + connString := os.Getenv("DATABASE_URL") + + config, err := pgxpool.ParseConfig(connString) + if err != nil { + return nil, fmt.Errorf("Error parsing connection string: %w", err) + } + + config.AfterConnect = func(ctx context.Context, c *pgx.Conn) error { + pgxdecimal.Register(c.TypeMap()) + return nil + } + + pool, err := pgxpool.NewWithConfig(ctx, config) + if err != nil { + return nil, fmt.Errorf("Error creating connection: %w", err) + } + + return pool, nil +} diff --git a/internal/database/constructor.sql.go b/internal/database/constructor.sql.go new file mode 100644 index 0000000..19dda94 --- /dev/null +++ b/internal/database/constructor.sql.go @@ -0,0 +1,36 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 +// source: constructor.sql + +package database + +import ( + "github.com/shopspring/decimal" +) + +type SaveConstructorResultsParams struct { + ID int32 + RaceID int32 + ConstructorID int32 + Points *decimal.Decimal + Status *string +} + +type SaveConstructorStandingsParams struct { + ID int32 + RaceID int32 + ConstructorID int32 + Points decimal.Decimal + Position *int32 + PosText *string + Wins int32 +} + +type SaveConstructorsParams struct { + ID int32 + Ref string + Name string + Nationality *string + Url string +} diff --git a/internal/database/db.go b/internal/database/database.go similarity index 82% rename from internal/database/db.go rename to internal/database/database.go index 1d02744..093174f 100644 --- a/internal/database/db.go +++ b/internal/database/database.go @@ -15,6 +15,7 @@ type DBTX interface { Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) Query(context.Context, string, ...interface{}) (pgx.Rows, error) QueryRow(context.Context, string, ...interface{}) pgx.Row + CopyFrom(ctx context.Context, tableName pgx.Identifier, columnNames []string, rowSrc pgx.CopyFromSource) (int64, error) } func New(db DBTX) *Queries { diff --git a/internal/database/driver.sql.go b/internal/database/driver.sql.go new file mode 100644 index 0000000..c6326f7 --- /dev/null +++ b/internal/database/driver.sql.go @@ -0,0 +1,34 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 +// source: driver.sql + +package database + +import ( + "time" + + "github.com/shopspring/decimal" +) + +type SaveDriverStandingsParams struct { + ID int32 + RaceID int32 + DriverID int32 + Points decimal.Decimal + Position *int32 + PosText *string + Wins *int32 +} + +type SaveDriversParams struct { + ID int32 + Ref string + Number *int32 + Code *string + FirstName string + LastName string + DateOfBirth *time.Time + Nationality *string + Url string +} diff --git a/internal/database/entities.sql.go b/internal/database/entities.sql.go new file mode 100644 index 0000000..3ad00b0 --- /dev/null +++ b/internal/database/entities.sql.go @@ -0,0 +1,173 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 + +package database + +import ( + "time" + + "github.com/jackc/pgx/v5/pgtype" +) + +type Circuit struct { + ID int32 + Ref string + Name string + Location *string + Country *string + Lat pgtype.Numeric + Lng pgtype.Numeric + Alt *int32 + Url string +} + +type Constructor struct { + ID int32 + Ref string + Name string + Nationality *string + Url string +} + +type ConstructorResult struct { + ID int32 + RaceID int32 + ConstructorID int32 + Points pgtype.Numeric + Status *string +} + +type ConstructorStanding struct { + ID int32 + RaceID int32 + ConstructorID int32 + Points pgtype.Numeric + Position *int32 + PosText *string + Wins int32 +} + +type Driver struct { + ID int32 + Ref string + Number *int32 + Code *string + FirstName string + LastName string + DateOfBirth *time.Time + Nationality *string + Url string +} + +type DriverStanding struct { + ID int32 + RaceID int32 + DriverID int32 + Points pgtype.Numeric + Position *int32 + PosText *string + Wins *int32 +} + +type LapTime struct { + RaceID int32 + DriverID int32 + Lap int32 + Position *int32 + Time *string + Milliseconds *int32 +} + +type PitStop struct { + RaceID int32 + DriverID int32 + Stop int32 + Lap int32 + Time time.Time + Duration *string + Milliseconds *int32 +} + +type Qualifying struct { + ID int32 + RaceID int32 + DriverID int32 + ConstructorID int32 + Number int32 + Position *int32 + Q1 *string + Q2 *string + Q3 *string +} + +type Race struct { + ID int32 + Year int32 + Round int32 + CircuitID int32 + Name string + Date time.Time + Time *time.Time + Url *string + Fp1Date *time.Time + Fp1Time *time.Time + Fp2Date *time.Time + Fp2Time *time.Time + Fp3Date *time.Time + Fp3Time *time.Time + QualiDate *time.Time + QualiTime *time.Time + SprintDate *time.Time + SprintTime *time.Time +} + +type Result struct { + ID int32 + RaceID int32 + DriverID int32 + ConstructorID int32 + Number *int32 + Grid int32 + Position *int32 + PosText string + PosOrder int32 + Points pgtype.Numeric + Laps int32 + Time *string + Milliseconds *int32 + FastestLap *int32 + Rank *int32 + FastestLapTime *string + FastestLapSpeed *string + StatusID *int32 +} + +type Season struct { + Year int32 + Url string +} + +type SprintResult struct { + ID int32 + RaceID int32 + DriverID int32 + ConstructorID int32 + Number int32 + Grid int32 + Position *int32 + PosText string + PosOrder int32 + Points pgtype.Numeric + Laps int32 + Time *string + Milliseconds *int32 + FastestLap *int32 + FastestLapTime *string + StatusID *int32 +} + +type Status struct { + ID int32 + Status string +} diff --git a/internal/database/iterators.go b/internal/database/iterators.go new file mode 100644 index 0000000..d3181ed --- /dev/null +++ b/internal/database/iterators.go @@ -0,0 +1,564 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 +// source: iterators.go + +package database + +import ( + "context" +) + +// iteratorForSaveCircuits implements pgx.CopyFromSource. +type iteratorForSaveCircuits struct { + rows []SaveCircuitsParams + skippedFirstNextCall bool +} + +func (r *iteratorForSaveCircuits) Next() bool { + if len(r.rows) == 0 { + return false + } + if !r.skippedFirstNextCall { + r.skippedFirstNextCall = true + return true + } + r.rows = r.rows[1:] + return len(r.rows) > 0 +} + +func (r iteratorForSaveCircuits) Values() ([]interface{}, error) { + return []interface{}{ + r.rows[0].ID, + r.rows[0].Ref, + r.rows[0].Name, + r.rows[0].Location, + r.rows[0].Country, + r.rows[0].Lat, + r.rows[0].Lng, + r.rows[0].Alt, + r.rows[0].Url, + }, nil +} + +func (r iteratorForSaveCircuits) Err() error { + return nil +} + +func (q *Queries) SaveCircuits(ctx context.Context, arg []SaveCircuitsParams) (int64, error) { + return q.db.CopyFrom(ctx, []string{"circuits"}, []string{"id", "ref", "name", "location", "country", "lat", "lng", "alt", "url"}, &iteratorForSaveCircuits{rows: arg}) +} + +// iteratorForSaveConstructorResults implements pgx.CopyFromSource. +type iteratorForSaveConstructorResults struct { + rows []SaveConstructorResultsParams + skippedFirstNextCall bool +} + +func (r *iteratorForSaveConstructorResults) Next() bool { + if len(r.rows) == 0 { + return false + } + if !r.skippedFirstNextCall { + r.skippedFirstNextCall = true + return true + } + r.rows = r.rows[1:] + return len(r.rows) > 0 +} + +func (r iteratorForSaveConstructorResults) Values() ([]interface{}, error) { + return []interface{}{ + r.rows[0].ID, + r.rows[0].RaceID, + r.rows[0].ConstructorID, + r.rows[0].Points, + r.rows[0].Status, + }, nil +} + +func (r iteratorForSaveConstructorResults) Err() error { + return nil +} + +func (q *Queries) SaveConstructorResults(ctx context.Context, arg []SaveConstructorResultsParams) (int64, error) { + return q.db.CopyFrom(ctx, []string{"constructor_results"}, []string{"id", "race_id", "constructor_id", "points", "status"}, &iteratorForSaveConstructorResults{rows: arg}) +} + +// iteratorForSaveConstructorStandings implements pgx.CopyFromSource. +type iteratorForSaveConstructorStandings struct { + rows []SaveConstructorStandingsParams + skippedFirstNextCall bool +} + +func (r *iteratorForSaveConstructorStandings) Next() bool { + if len(r.rows) == 0 { + return false + } + if !r.skippedFirstNextCall { + r.skippedFirstNextCall = true + return true + } + r.rows = r.rows[1:] + return len(r.rows) > 0 +} + +func (r iteratorForSaveConstructorStandings) Values() ([]interface{}, error) { + return []interface{}{ + r.rows[0].ID, + r.rows[0].RaceID, + r.rows[0].ConstructorID, + r.rows[0].Points, + r.rows[0].Position, + r.rows[0].PosText, + r.rows[0].Wins, + }, nil +} + +func (r iteratorForSaveConstructorStandings) Err() error { + return nil +} + +func (q *Queries) SaveConstructorStandings(ctx context.Context, arg []SaveConstructorStandingsParams) (int64, error) { + return q.db.CopyFrom(ctx, []string{"constructor_standings"}, []string{"id", "race_id", "constructor_id", "points", "position", "pos_text", "wins"}, &iteratorForSaveConstructorStandings{rows: arg}) +} + +// iteratorForSaveConstructors implements pgx.CopyFromSource. +type iteratorForSaveConstructors struct { + rows []SaveConstructorsParams + skippedFirstNextCall bool +} + +func (r *iteratorForSaveConstructors) Next() bool { + if len(r.rows) == 0 { + return false + } + if !r.skippedFirstNextCall { + r.skippedFirstNextCall = true + return true + } + r.rows = r.rows[1:] + return len(r.rows) > 0 +} + +func (r iteratorForSaveConstructors) Values() ([]interface{}, error) { + return []interface{}{ + r.rows[0].ID, + r.rows[0].Ref, + r.rows[0].Name, + r.rows[0].Nationality, + r.rows[0].Url, + }, nil +} + +func (r iteratorForSaveConstructors) Err() error { + return nil +} + +func (q *Queries) SaveConstructors(ctx context.Context, arg []SaveConstructorsParams) (int64, error) { + return q.db.CopyFrom(ctx, []string{"constructors"}, []string{"id", "ref", "name", "nationality", "url"}, &iteratorForSaveConstructors{rows: arg}) +} + +// iteratorForSaveDriverStandings implements pgx.CopyFromSource. +type iteratorForSaveDriverStandings struct { + rows []SaveDriverStandingsParams + skippedFirstNextCall bool +} + +func (r *iteratorForSaveDriverStandings) Next() bool { + if len(r.rows) == 0 { + return false + } + if !r.skippedFirstNextCall { + r.skippedFirstNextCall = true + return true + } + r.rows = r.rows[1:] + return len(r.rows) > 0 +} + +func (r iteratorForSaveDriverStandings) Values() ([]interface{}, error) { + return []interface{}{ + r.rows[0].ID, + r.rows[0].RaceID, + r.rows[0].DriverID, + r.rows[0].Points, + r.rows[0].Position, + r.rows[0].PosText, + r.rows[0].Wins, + }, nil +} + +func (r iteratorForSaveDriverStandings) Err() error { + return nil +} + +func (q *Queries) SaveDriverStandings(ctx context.Context, arg []SaveDriverStandingsParams) (int64, error) { + return q.db.CopyFrom(ctx, []string{"driver_standings"}, []string{"id", "race_id", "driver_id", "points", "position", "pos_text", "wins"}, &iteratorForSaveDriverStandings{rows: arg}) +} + +// iteratorForSaveDrivers implements pgx.CopyFromSource. +type iteratorForSaveDrivers struct { + rows []SaveDriversParams + skippedFirstNextCall bool +} + +func (r *iteratorForSaveDrivers) Next() bool { + if len(r.rows) == 0 { + return false + } + if !r.skippedFirstNextCall { + r.skippedFirstNextCall = true + return true + } + r.rows = r.rows[1:] + return len(r.rows) > 0 +} + +func (r iteratorForSaveDrivers) Values() ([]interface{}, error) { + return []interface{}{ + r.rows[0].ID, + r.rows[0].Ref, + r.rows[0].Number, + r.rows[0].Code, + r.rows[0].FirstName, + r.rows[0].LastName, + r.rows[0].DateOfBirth, + r.rows[0].Nationality, + r.rows[0].Url, + }, nil +} + +func (r iteratorForSaveDrivers) Err() error { + return nil +} + +func (q *Queries) SaveDrivers(ctx context.Context, arg []SaveDriversParams) (int64, error) { + return q.db.CopyFrom(ctx, []string{"drivers"}, []string{"id", "ref", "number", "code", "first_name", "last_name", "date_of_birth", "nationality", "url"}, &iteratorForSaveDrivers{rows: arg}) +} + +// iteratorForSaveLapTimes implements pgx.CopyFromSource. +type iteratorForSaveLapTimes struct { + rows []SaveLapTimesParams + skippedFirstNextCall bool +} + +func (r *iteratorForSaveLapTimes) Next() bool { + if len(r.rows) == 0 { + return false + } + if !r.skippedFirstNextCall { + r.skippedFirstNextCall = true + return true + } + r.rows = r.rows[1:] + return len(r.rows) > 0 +} + +func (r iteratorForSaveLapTimes) Values() ([]interface{}, error) { + return []interface{}{ + r.rows[0].RaceID, + r.rows[0].DriverID, + r.rows[0].Lap, + r.rows[0].Position, + r.rows[0].Time, + r.rows[0].Milliseconds, + }, nil +} + +func (r iteratorForSaveLapTimes) Err() error { + return nil +} + +func (q *Queries) SaveLapTimes(ctx context.Context, arg []SaveLapTimesParams) (int64, error) { + return q.db.CopyFrom(ctx, []string{"lap_times"}, []string{"race_id", "driver_id", "lap", "position", "time", "milliseconds"}, &iteratorForSaveLapTimes{rows: arg}) +} + +// iteratorForSavePitStops implements pgx.CopyFromSource. +type iteratorForSavePitStops struct { + rows []SavePitStopsParams + skippedFirstNextCall bool +} + +func (r *iteratorForSavePitStops) Next() bool { + if len(r.rows) == 0 { + return false + } + if !r.skippedFirstNextCall { + r.skippedFirstNextCall = true + return true + } + r.rows = r.rows[1:] + return len(r.rows) > 0 +} + +func (r iteratorForSavePitStops) Values() ([]interface{}, error) { + return []interface{}{ + r.rows[0].RaceID, + r.rows[0].DriverID, + r.rows[0].Stop, + r.rows[0].Lap, + r.rows[0].Time, + r.rows[0].Duration, + r.rows[0].Milliseconds, + }, nil +} + +func (r iteratorForSavePitStops) Err() error { + return nil +} + +func (q *Queries) SavePitStops(ctx context.Context, arg []SavePitStopsParams) (int64, error) { + return q.db.CopyFrom(ctx, []string{"pit_stops"}, []string{"race_id", "driver_id", "stop", "lap", "time", "duration", "milliseconds"}, &iteratorForSavePitStops{rows: arg}) +} + +// iteratorForSaveQualifyingResults implements pgx.CopyFromSource. +type iteratorForSaveQualifyingResults struct { + rows []SaveQualifyingResultsParams + skippedFirstNextCall bool +} + +func (r *iteratorForSaveQualifyingResults) Next() bool { + if len(r.rows) == 0 { + return false + } + if !r.skippedFirstNextCall { + r.skippedFirstNextCall = true + return true + } + r.rows = r.rows[1:] + return len(r.rows) > 0 +} + +func (r iteratorForSaveQualifyingResults) Values() ([]interface{}, error) { + return []interface{}{ + r.rows[0].ID, + r.rows[0].RaceID, + r.rows[0].DriverID, + r.rows[0].ConstructorID, + r.rows[0].Number, + r.rows[0].Position, + r.rows[0].Q1, + r.rows[0].Q2, + r.rows[0].Q3, + }, nil +} + +func (r iteratorForSaveQualifyingResults) Err() error { + return nil +} + +func (q *Queries) SaveQualifyingResults(ctx context.Context, arg []SaveQualifyingResultsParams) (int64, error) { + return q.db.CopyFrom(ctx, []string{"qualifying"}, []string{"id", "race_id", "driver_id", "constructor_id", "number", "position", "q1", "q2", "q3"}, &iteratorForSaveQualifyingResults{rows: arg}) +} + +// iteratorForSaveRaces implements pgx.CopyFromSource. +type iteratorForSaveRaces struct { + rows []SaveRacesParams + skippedFirstNextCall bool +} + +func (r *iteratorForSaveRaces) Next() bool { + if len(r.rows) == 0 { + return false + } + if !r.skippedFirstNextCall { + r.skippedFirstNextCall = true + return true + } + r.rows = r.rows[1:] + return len(r.rows) > 0 +} + +func (r iteratorForSaveRaces) Values() ([]interface{}, error) { + return []interface{}{ + r.rows[0].ID, + r.rows[0].Year, + r.rows[0].Round, + r.rows[0].CircuitID, + r.rows[0].Name, + r.rows[0].Date, + r.rows[0].Time, + r.rows[0].Url, + r.rows[0].Fp1Date, + r.rows[0].Fp1Time, + r.rows[0].Fp2Date, + r.rows[0].Fp2Time, + r.rows[0].Fp3Date, + r.rows[0].Fp3Time, + r.rows[0].QualiDate, + r.rows[0].QualiTime, + r.rows[0].SprintDate, + r.rows[0].SprintTime, + }, nil +} + +func (r iteratorForSaveRaces) Err() error { + return nil +} + +func (q *Queries) SaveRaces(ctx context.Context, arg []SaveRacesParams) (int64, error) { + return q.db.CopyFrom(ctx, []string{"races"}, []string{"id", "year", "round", "circuit_id", "name", "date", "time", "url", "fp1_date", "fp1_time", "fp2_date", "fp2_time", "fp3_date", "fp3_time", "quali_date", "quali_time", "sprint_date", "sprint_time"}, &iteratorForSaveRaces{rows: arg}) +} + +// iteratorForSaveResults implements pgx.CopyFromSource. +type iteratorForSaveResults struct { + rows []SaveResultsParams + skippedFirstNextCall bool +} + +func (r *iteratorForSaveResults) Next() bool { + if len(r.rows) == 0 { + return false + } + if !r.skippedFirstNextCall { + r.skippedFirstNextCall = true + return true + } + r.rows = r.rows[1:] + return len(r.rows) > 0 +} + +func (r iteratorForSaveResults) Values() ([]interface{}, error) { + return []interface{}{ + r.rows[0].ID, + r.rows[0].RaceID, + r.rows[0].DriverID, + r.rows[0].ConstructorID, + r.rows[0].Number, + r.rows[0].Grid, + r.rows[0].Position, + r.rows[0].PosText, + r.rows[0].PosOrder, + r.rows[0].Points, + r.rows[0].Laps, + r.rows[0].Time, + r.rows[0].Milliseconds, + r.rows[0].FastestLap, + r.rows[0].Rank, + r.rows[0].FastestLapTime, + r.rows[0].FastestLapSpeed, + r.rows[0].StatusID, + }, nil +} + +func (r iteratorForSaveResults) Err() error { + return nil +} + +func (q *Queries) SaveResults(ctx context.Context, arg []SaveResultsParams) (int64, error) { + return q.db.CopyFrom(ctx, []string{"results"}, []string{"id", "race_id", "driver_id", "constructor_id", "number", "grid", "position", "pos_text", "pos_order", "points", "laps", "time", "milliseconds", "fastest_lap", "rank", "fastest_lap_time", "fastest_lap_speed", "status_id"}, &iteratorForSaveResults{rows: arg}) +} + +// iteratorForSaveSeasons implements pgx.CopyFromSource. +type iteratorForSaveSeasons struct { + rows []SaveSeasonsParams + skippedFirstNextCall bool +} + +func (r *iteratorForSaveSeasons) Next() bool { + if len(r.rows) == 0 { + return false + } + if !r.skippedFirstNextCall { + r.skippedFirstNextCall = true + return true + } + r.rows = r.rows[1:] + return len(r.rows) > 0 +} + +func (r iteratorForSaveSeasons) Values() ([]interface{}, error) { + return []interface{}{ + r.rows[0].Year, + r.rows[0].Url, + }, nil +} + +func (r iteratorForSaveSeasons) Err() error { + return nil +} + +func (q *Queries) SaveSeasons(ctx context.Context, arg []SaveSeasonsParams) (int64, error) { + return q.db.CopyFrom(ctx, []string{"seasons"}, []string{"year", "url"}, &iteratorForSaveSeasons{rows: arg}) +} + +// iteratorForSaveSprintResults implements pgx.CopyFromSource. +type iteratorForSaveSprintResults struct { + rows []SaveSprintResultsParams + skippedFirstNextCall bool +} + +func (r *iteratorForSaveSprintResults) Next() bool { + if len(r.rows) == 0 { + return false + } + if !r.skippedFirstNextCall { + r.skippedFirstNextCall = true + return true + } + r.rows = r.rows[1:] + return len(r.rows) > 0 +} + +func (r iteratorForSaveSprintResults) Values() ([]interface{}, error) { + return []interface{}{ + r.rows[0].ID, + r.rows[0].RaceID, + r.rows[0].DriverID, + r.rows[0].ConstructorID, + r.rows[0].Number, + r.rows[0].Grid, + r.rows[0].Position, + r.rows[0].PosText, + r.rows[0].PosOrder, + r.rows[0].Points, + r.rows[0].Laps, + r.rows[0].Time, + r.rows[0].Milliseconds, + r.rows[0].FastestLap, + r.rows[0].FastestLapTime, + r.rows[0].StatusID, + }, nil +} + +func (r iteratorForSaveSprintResults) Err() error { + return nil +} + +func (q *Queries) SaveSprintResults(ctx context.Context, arg []SaveSprintResultsParams) (int64, error) { + return q.db.CopyFrom(ctx, []string{"sprint_results"}, []string{"id", "race_id", "driver_id", "constructor_id", "number", "grid", "position", "pos_text", "pos_order", "points", "laps", "time", "milliseconds", "fastest_lap", "fastest_lap_time", "status_id"}, &iteratorForSaveSprintResults{rows: arg}) +} + +// iteratorForSaveStatuses implements pgx.CopyFromSource. +type iteratorForSaveStatuses struct { + rows []SaveStatusesParams + skippedFirstNextCall bool +} + +func (r *iteratorForSaveStatuses) Next() bool { + if len(r.rows) == 0 { + return false + } + if !r.skippedFirstNextCall { + r.skippedFirstNextCall = true + return true + } + r.rows = r.rows[1:] + return len(r.rows) > 0 +} + +func (r iteratorForSaveStatuses) Values() ([]interface{}, error) { + return []interface{}{ + r.rows[0].ID, + r.rows[0].Status, + }, nil +} + +func (r iteratorForSaveStatuses) Err() error { + return nil +} + +func (q *Queries) SaveStatuses(ctx context.Context, arg []SaveStatusesParams) (int64, error) { + return q.db.CopyFrom(ctx, []string{"status"}, []string{"id", "status"}, &iteratorForSaveStatuses{rows: arg}) +} diff --git a/internal/database/laptime.sql.go b/internal/database/laptime.sql.go new file mode 100644 index 0000000..3981b71 --- /dev/null +++ b/internal/database/laptime.sql.go @@ -0,0 +1,15 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 +// source: laptime.sql + +package database + +type SaveLapTimesParams struct { + RaceID int32 + DriverID int32 + Lap int32 + Position *int32 + Time *string + Milliseconds *int32 +} diff --git a/internal/database/models.go b/internal/database/models.go deleted file mode 100644 index bc82a3b..0000000 --- a/internal/database/models.go +++ /dev/null @@ -1,171 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.26.0 - -package database - -import ( - "github.com/jackc/pgx/v5/pgtype" -) - -type Circuit struct { - ID int32 - Ref string - Name string - Location pgtype.Text - Country pgtype.Text - Lat pgtype.Float8 - Lng pgtype.Float8 - Alt pgtype.Int4 - Url string -} - -type Constructor struct { - ID int32 - Ref string - Name string - Nationality pgtype.Text - Url string -} - -type ConstructorResult struct { - ID int32 - RaceID int32 - ConstructorID int32 - Points pgtype.Float8 - Status pgtype.Text -} - -type ConstructorStanding struct { - ID int32 - RaceID int32 - ConstructorID int32 - Points float64 - Position pgtype.Int4 - PositionText pgtype.Text - Wins int32 -} - -type Driver struct { - ID int32 - Ref string - Number pgtype.Int4 - Code pgtype.Text - FirstName string - LastName string - DateOfBirth pgtype.Date - Nationality pgtype.Text - Url string -} - -type DriverStanding struct { - ID int32 - RaceID int32 - DriverID int32 - Points float64 - Position pgtype.Int4 - PositionText pgtype.Text - Wins int32 -} - -type LapTime struct { - RaceID int32 - DriverID int32 - Lap int32 - Position pgtype.Int4 - Time pgtype.Text - Milliseconds pgtype.Int4 -} - -type PitStop struct { - RaceID int32 - DriverID int32 - Stop int32 - Lap int32 - Time pgtype.Time - Duration pgtype.Text - Milliseconds pgtype.Int4 -} - -type Qualifying struct { - ID int32 - RaceID int32 - DriverID int32 - ConstructorID int32 - Number int32 - Position pgtype.Int4 - Q1 pgtype.Text - Q2 pgtype.Text - Q3 pgtype.Text -} - -type Race struct { - ID int32 - Year int32 - Round int32 - CircuitID int32 - Name string - Date pgtype.Date - Time pgtype.Time - Url pgtype.Text - Fp1Date pgtype.Date - Fp1Time pgtype.Time - Fp2Date pgtype.Date - Fp2Time pgtype.Time - Fp3Date pgtype.Date - Fp3Time pgtype.Time - QualiDate pgtype.Date - QualiTime pgtype.Time - SprintDate pgtype.Date - SprintTime pgtype.Time -} - -type Result struct { - ID int32 - RaceID int32 - DriverID int32 - ConstructorID int32 - Number pgtype.Int4 - Grid int32 - Position pgtype.Int4 - PositionText string - PositionOrder int32 - Points float64 - Laps int32 - Time pgtype.Text - Milliseconds pgtype.Int4 - FastestLap pgtype.Int4 - Rank pgtype.Int4 - FastestLapTime pgtype.Text - FastestLapSpeed pgtype.Text - StatusID int32 -} - -type Season struct { - Year int32 - Url string -} - -type SprintResult struct { - ID int32 - RaceID int32 - DriverID int32 - ConstructorID int32 - Number int32 - Grid int32 - Position pgtype.Int4 - PositionText string - PositionOrder int32 - Points float64 - Laps int32 - Time pgtype.Text - Milliseconds pgtype.Int4 - FastestLap pgtype.Int4 - FastestLapTime pgtype.Text - StatusID int32 -} - -type Status struct { - ID int32 - Status string -} diff --git a/internal/database/pitstop.sql.go b/internal/database/pitstop.sql.go new file mode 100644 index 0000000..eb3b22c --- /dev/null +++ b/internal/database/pitstop.sql.go @@ -0,0 +1,20 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 +// source: pitstop.sql + +package database + +import ( + "time" +) + +type SavePitStopsParams struct { + RaceID int32 + DriverID int32 + Stop int32 + Lap int32 + Time time.Time + Duration *string + Milliseconds *int32 +} diff --git a/internal/database/qualifying.sql.go b/internal/database/qualifying.sql.go new file mode 100644 index 0000000..7e601cd --- /dev/null +++ b/internal/database/qualifying.sql.go @@ -0,0 +1,18 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 +// source: qualifying.sql + +package database + +type SaveQualifyingResultsParams struct { + ID int32 + RaceID int32 + DriverID int32 + ConstructorID int32 + Number int32 + Position *int32 + Q1 *string + Q2 *string + Q3 *string +} diff --git a/internal/database/race.sql.go b/internal/database/race.sql.go new file mode 100644 index 0000000..e3cc083 --- /dev/null +++ b/internal/database/race.sql.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 +// source: race.sql + +package database + +import ( + "time" +) + +type SaveRacesParams struct { + ID int32 + Year int32 + Round int32 + CircuitID int32 + Name string + Date time.Time + Time *time.Time + Url *string + Fp1Date *time.Time + Fp1Time *time.Time + Fp2Date *time.Time + Fp2Time *time.Time + Fp3Date *time.Time + Fp3Time *time.Time + QualiDate *time.Time + QualiTime *time.Time + SprintDate *time.Time + SprintTime *time.Time +} diff --git a/internal/database/result.sql.go b/internal/database/result.sql.go new file mode 100644 index 0000000..4d55c66 --- /dev/null +++ b/internal/database/result.sql.go @@ -0,0 +1,50 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 +// source: result.sql + +package database + +import ( + "github.com/shopspring/decimal" +) + +type SaveResultsParams struct { + ID int32 + RaceID int32 + DriverID int32 + ConstructorID int32 + Number *int32 + Grid int32 + Position *int32 + PosText string + PosOrder int32 + Points decimal.Decimal + Laps int32 + Time *string + Milliseconds *int32 + FastestLap *int32 + Rank *int32 + FastestLapTime *string + FastestLapSpeed *string + StatusID *int32 +} + +type SaveSprintResultsParams struct { + ID int32 + RaceID int32 + DriverID int32 + ConstructorID int32 + Number int32 + Grid int32 + Position *int32 + PosText string + PosOrder int32 + Points decimal.Decimal + Laps int32 + Time *string + Milliseconds *int32 + FastestLap *int32 + FastestLapTime *string + StatusID *int32 +} diff --git a/internal/database/season.sql.go b/internal/database/season.sql.go index dde7d83..68f41b6 100644 --- a/internal/database/season.sql.go +++ b/internal/database/season.sql.go @@ -7,6 +7,8 @@ package database import ( "context" + + "github.com/shopspring/decimal" ) const getDriverStandingsByYear = `-- name: GetDriverStandingsByYear :many @@ -27,7 +29,7 @@ type GetDriverStandingsByYearRow struct { DriverID int32 FirstName string LastName string - Points int64 + Points decimal.Decimal } func (q *Queries) GetDriverStandingsByYear(ctx context.Context, year int32) ([]GetDriverStandingsByYearRow, error) { @@ -54,3 +56,8 @@ func (q *Queries) GetDriverStandingsByYear(ctx context.Context, year int32) ([]G } return items, nil } + +type SaveSeasonsParams struct { + Year int32 + Url string +} diff --git a/internal/database/status.sql.go b/internal/database/status.sql.go new file mode 100644 index 0000000..a78fd1f --- /dev/null +++ b/internal/database/status.sql.go @@ -0,0 +1,11 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 +// source: status.sql + +package database + +type SaveStatusesParams struct { + ID int32 + Status string +} diff --git a/internal/imager/circuit.go b/internal/imager/circuit.go new file mode 100644 index 0000000..92d4401 --- /dev/null +++ b/internal/imager/circuit.go @@ -0,0 +1,62 @@ +package imager + +import ( + "context" + "fmt" + "os" + + "github.com/gocarina/gocsv" + "github.com/jackc/pgx/v5" + "github.com/jsec/f1-data-hub/internal/database" + "github.com/shopspring/decimal" +) + +type circuit struct { + ID int `csv:"circuitId"` + Ref string `csv:"circuitRef"` + Name string `csv:"name"` + Location string `csv:"location"` + Country string `csv:"country"` + Latitude decimal.Decimal `csv:"lat"` + Longitude decimal.Decimal `csv:"lng"` + Altitude optionalNumber `csv:"alt"` + URL string `csv:"url"` +} + +func (i Imager) loadCircuits(ctx context.Context, tx pgx.Tx) error { + file, err := os.OpenFile("data/circuits.csv", os.O_RDONLY, os.ModePerm) + if err != nil { + return fmt.Errorf("Error opening circuits CSV file: %w", err) + } + defer file.Close() + + var circuits []*circuit + + if err = gocsv.UnmarshalFile(file, &circuits); err != nil { + return fmt.Errorf("Error marshaling circuits CSV file: %w", err) + } + + records := []database.SaveCircuitsParams{} + + for _, circuit := range circuits { + records = append(records, database.SaveCircuitsParams{ + ID: int32(circuit.ID), + Ref: circuit.Ref, + Name: circuit.Name, + Location: &circuit.Location, + Country: &circuit.Country, + Lat: &circuit.Latitude, + Lng: &circuit.Longitude, + Alt: circuit.Altitude.Value, + Url: circuit.URL, + }) + } + + _, err = i.db.WithTx(tx).SaveCircuits(ctx, records) + if err != nil { + return fmt.Errorf("Error saving circuits: %w", err) + } + + fmt.Println("[Circuits] seeding complete") + return nil +} diff --git a/internal/imager/constructor.go b/internal/imager/constructor.go new file mode 100644 index 0000000..e800469 --- /dev/null +++ b/internal/imager/constructor.go @@ -0,0 +1,141 @@ +package imager + +import ( + "context" + "fmt" + "os" + + "github.com/gocarina/gocsv" + "github.com/jackc/pgx/v5" + "github.com/jsec/f1-data-hub/internal/database" + "github.com/shopspring/decimal" +) + +type constructor struct { + ID int `csv:"constructorId"` + Ref string `csv:"constructorRef"` + Name string `csv:"name"` + Nationality string `csv:"nationality"` + URL string `csv:"url"` +} + +func (i Imager) loadConstructors(ctx context.Context, tx pgx.Tx) error { + file, err := os.OpenFile("data/constructors.csv", os.O_RDONLY, os.ModePerm) + if err != nil { + return fmt.Errorf("Error opening constructors CSV file: %w", err) + } + defer file.Close() + + var constructors []*constructor + + if err = gocsv.UnmarshalFile(file, &constructors); err != nil { + return fmt.Errorf("Error marshaling constructors CSV file: %w", err) + } + + records := []database.SaveConstructorsParams{} + + for _, constructor := range constructors { + records = append(records, database.SaveConstructorsParams{ + ID: int32(constructor.ID), + Ref: constructor.Ref, + Name: constructor.Name, + Nationality: &constructor.Nationality, + Url: constructor.URL, + }) + } + + _, err = i.db.WithTx(tx).SaveConstructors(ctx, records) + if err != nil { + return fmt.Errorf("Error saving constructors: %w", err) + } + + return nil +} + +type constructorResult struct { + ID int32 `csv:"constructorResultsId"` + RaceID int32 `csv:"raceId"` + ConstructorID int32 `csv:"constructorId"` + Points decimal.Decimal `csv:"points"` + Status optionalString `csv:"status"` +} + +func (i Imager) loadConstructorResults(ctx context.Context, tx pgx.Tx) error { + file, err := os.OpenFile("data/constructor_results.csv", os.O_RDONLY, os.ModePerm) + if err != nil { + return fmt.Errorf("Error opening constructor results CSV file: %w", err) + } + defer file.Close() + + var results []*constructorResult + + if err = gocsv.UnmarshalFile(file, &results); err != nil { + return fmt.Errorf("Error marshaling constructor results CSV file: %w", err) + } + + records := []database.SaveConstructorResultsParams{} + + for _, result := range results { + records = append(records, database.SaveConstructorResultsParams{ + ID: result.ID, + RaceID: result.RaceID, + ConstructorID: result.ConstructorID, + Points: &result.Points, + Status: result.Status.Value, + }) + } + + _, err = i.db.WithTx(tx).SaveConstructorResults(ctx, records) + if err != nil { + return fmt.Errorf("Error saving constructor results: %w", err) + } + + fmt.Println("[Constructor Results] seeding complete") + return nil +} + +type constructorStanding struct { + ID int32 `csv:"constructorStandingsId"` + RaceID int32 `csv:"raceId"` + ConstructorID int32 `csv:"constructorId"` + Points decimal.Decimal `csv:"points"` + Position int32 `csv:"position"` + PositionText string `csv:"positionText"` + Wins int32 `csv:"wins"` +} + +func (i Imager) loadConstructorStandings(ctx context.Context, tx pgx.Tx) error { + file, err := os.OpenFile("data/constructor_standings.csv", os.O_RDONLY, os.ModePerm) + if err != nil { + return fmt.Errorf("Error opening constructor standings CSV file: %w", err) + } + defer file.Close() + + var standings []*constructorStanding + + if err = gocsv.UnmarshalFile(file, &standings); err != nil { + return fmt.Errorf("Error marshaling constructor standings CSV file: %w", err) + } + + records := []database.SaveConstructorStandingsParams{} + + for _, standing := range standings { + records = append(records, database.SaveConstructorStandingsParams{ + ID: standing.ID, + RaceID: standing.RaceID, + ConstructorID: standing.ConstructorID, + Points: standing.Points, + Position: &standing.Position, + PosText: &standing.PositionText, + Wins: standing.Wins, + }) + } + + _, err = i.db.WithTx(tx).SaveConstructorStandings(ctx, records) + if err != nil { + return fmt.Errorf("Error saving constructor standings: %w", err) + } + + fmt.Println("[Constructor Standings] seeding complete") + return nil +} diff --git a/internal/imager/converters.go b/internal/imager/converters.go new file mode 100644 index 0000000..942aa3c --- /dev/null +++ b/internal/imager/converters.go @@ -0,0 +1,87 @@ +package imager + +import ( + "strconv" + "strings" + "time" +) + +type dateOnly struct { + Value time.Time +} + +func (d *dateOnly) UnmarshalCSV(csv string) (err error) { + layout := "2006-01-02" + d.Value, err = time.Parse(layout, csv) + return err +} + +type timeOnly struct { + Value time.Time +} + +func (t *timeOnly) UnmarshalCSV(csv string) (err error) { + t.Value, err = time.Parse(time.TimeOnly, csv) + return err +} + +type optionalNumber struct { + Value *int32 +} + +func (o *optionalNumber) UnmarshalCSV(csv string) (err error) { + raw, err := strconv.Atoi(csv) + if err != nil { + o.Value = nil + return nil + } + + value := int32(raw) + o.Value = &value + return nil +} + +type optionalString struct { + Value *string +} + +func (o *optionalString) UnmarshalCSV(csv string) (err error) { + if strings.Contains(csv, "\\N") { + o.Value = nil + } else { + o.Value = &csv + } + + return nil +} + +type optionalTimeOnly struct { + Value *time.Time +} + +func (o *optionalTimeOnly) UnmarshalCSV(csv string) (err error) { + value, err := time.Parse(time.TimeOnly, csv) + if err != nil { + o.Value = nil + } else { + o.Value = &value + } + + return nil +} + +type optionalDateOnly struct { + Value *time.Time +} + +func (o *optionalDateOnly) UnmarshalCSV(csv string) (err error) { + layout := "2006-01-02" + value, err := time.Parse(layout, csv) + if err != nil { + o.Value = nil + } else { + o.Value = &value + } + + return nil +} diff --git a/internal/imager/download.go b/internal/imager/download.go new file mode 100644 index 0000000..98749e7 --- /dev/null +++ b/internal/imager/download.go @@ -0,0 +1,66 @@ +package imager + +import ( + "archive/zip" + "fmt" + "io" + "net/http" + "os" +) + +const zipName = "seed.zip" + +func FetchSeedData() error { + out, err := os.Create(zipName) + if err != nil { + return fmt.Errorf("Error creating seed file: %w", err) + } + defer out.Close() + + resp, err := http.Get("http://ergast.com/downloads/f1db_csv.zip") + if err != nil { + return fmt.Errorf("Error fetching seed data: %w", err) + } + defer resp.Body.Close() + + _, err = io.Copy(out, resp.Body) + if err != nil { + return fmt.Errorf("Error saving seed data: %w", err) + } + + return UnzipSeedData() +} + +func UnzipSeedData() error { + r, err := zip.OpenReader(zipName) + if err != nil { + return fmt.Errorf("Error opening zipfile: %w", err) + } + defer r.Close() + + err = os.MkdirAll("data", 0755) + if err != nil { + return fmt.Errorf("Error creating data dir: %w", err) + } + + for _, f := range r.File { + rc, err := f.Open() + if err != nil { + return fmt.Errorf("Error extracting file %s: %w", f.Name, err) + } + defer rc.Close() + + filePath := fmt.Sprintf("data/%s", f.Name) + file, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("Error creating uncompressed file %s: %w", f.Name, err) + } + + _, err = io.Copy(file, rc) + if err != nil { + return fmt.Errorf("Error decompressing file %s: %w", f.Name, err) + } + } + + return nil +} diff --git a/internal/imager/driver.go b/internal/imager/driver.go new file mode 100644 index 0000000..3dd4ece --- /dev/null +++ b/internal/imager/driver.go @@ -0,0 +1,108 @@ +package imager + +import ( + "context" + "fmt" + "os" + + "github.com/gocarina/gocsv" + "github.com/jackc/pgx/v5" + "github.com/jsec/f1-data-hub/internal/database" + "github.com/shopspring/decimal" +) + +type driver struct { + ID int32 `csv:"driverId"` + Ref string `csv:"driverRef"` + Number optionalNumber `csv:"number"` + Code optionalString `csv:"code"` + FirstName string `csv:"forename"` + LastName string `csv:"surname"` + DOB dateOnly `csv:"dob"` + Nationality string `csv:"nationality"` + URL string `csv:"url"` +} + +func (i Imager) loadDrivers(ctx context.Context, tx pgx.Tx) error { + file, err := os.OpenFile("data/drivers.csv", os.O_RDONLY, os.ModePerm) + if err != nil { + return fmt.Errorf("Error opening driver CSV file: %w", err) + } + defer file.Close() + + var drivers []*driver + + if err = gocsv.UnmarshalFile(file, &drivers); err != nil { + return fmt.Errorf("Error marshaling driver CSV file: %w", err) + } + + records := []database.SaveDriversParams{} + + for _, driver := range drivers { + records = append(records, database.SaveDriversParams{ + ID: driver.ID, + Ref: driver.Ref, + Number: driver.Number.Value, + Code: driver.Code.Value, + FirstName: driver.FirstName, + LastName: driver.LastName, + DateOfBirth: &driver.DOB.Value, + Nationality: &driver.Nationality, + Url: driver.URL, + }) + } + + _, err = i.db.WithTx(tx).SaveDrivers(ctx, records) + if err != nil { + return fmt.Errorf("Error saving drivers: %w", err) + } + + fmt.Println("[Drivers] seeding complete") + return nil +} + +type driverStanding struct { + ID int32 `csv:"driverStandingsId"` + RaceID int32 `csv:"raceId"` + DriverID int32 `csv:"driverId"` + Points decimal.Decimal `csv:"points"` + Position int32 `csv:"position"` + PositionText string `csv:"positionText"` + Wins int32 `csv:"wins"` +} + +func (i Imager) loadDriverStandings(ctx context.Context, tx pgx.Tx) error { + file, err := os.OpenFile("data/driver_standings.csv", os.O_RDONLY, os.ModePerm) + if err != nil { + return fmt.Errorf("Error opening driver standings CSV file: %w", err) + } + defer file.Close() + + var standings []*driverStanding + + if err = gocsv.UnmarshalFile(file, &standings); err != nil { + return fmt.Errorf("Error marshaling driver standings CSV file: %w", err) + } + + records := []database.SaveDriverStandingsParams{} + + for _, standing := range standings { + records = append(records, database.SaveDriverStandingsParams{ + ID: standing.ID, + RaceID: standing.RaceID, + DriverID: standing.DriverID, + Points: standing.Points, + Position: &standing.Position, + PosText: &standing.PositionText, + Wins: &standing.Wins, + }) + } + + _, err = i.db.WithTx(tx).SaveDriverStandings(ctx, records) + if err != nil { + return fmt.Errorf("Error saving driver standings: %w", err) + } + + fmt.Println("[Driver Standings] seeding complete") + return nil +} diff --git a/internal/imager/imager.go b/internal/imager/imager.go new file mode 100644 index 0000000..dd375a7 --- /dev/null +++ b/internal/imager/imager.go @@ -0,0 +1,60 @@ +package imager + +import ( + "context" + "fmt" + "log" + + "github.com/jackc/pgx/v5" + "github.com/jsec/f1-data-hub/internal/database" +) + +type Imager struct { + db *database.Queries +} + +func Run(ctx context.Context) error { + pool, err := database.Connect(ctx) + if err != nil { + log.Fatal(err) + } + + tx, err := pool.Begin(ctx) + if err != nil { + return fmt.Errorf("Error acquiring transaction: %w", err) + } + + imager := Imager{ + db: database.New(pool), + } + + return imager.Seed(ctx, tx) +} + +func (i Imager) Seed(ctx context.Context, tx pgx.Tx) error { + loaders := []func(ctx context.Context, tx pgx.Tx) error{ + i.loadSeasons, + i.loadStatuses, + i.loadDrivers, + i.loadConstructors, + i.loadCircuits, + i.loadRaces, + i.loadDriverStandings, + i.loadConstructorStandings, + i.loadLapTimes, + i.loadPitStops, + i.loadQualifying, + i.loadResults, + i.loadConstructorResults, + i.loadSprintResults, + } + + for _, loader := range loaders { + err := loader(ctx, tx) + if err != nil { + return err + } + } + + return nil +} diff --git a/internal/imager/laptime.go b/internal/imager/laptime.go new file mode 100644 index 0000000..f57fa01 --- /dev/null +++ b/internal/imager/laptime.go @@ -0,0 +1,55 @@ +package imager + +import ( + "context" + "fmt" + "os" + + "github.com/gocarina/gocsv" + "github.com/jackc/pgx/v5" + "github.com/jsec/f1-data-hub/internal/database" +) + +type lapTime struct { + RaceID int32 `csv:"raceId"` + DriverID int32 `csv:"driverId"` + Lap int32 `csv:"lap"` + Position int32 `csv:"position"` + Time string `csv:"time"` + Milliseconds int32 `csv:"milliseconds"` +} + +func (i Imager) loadLapTimes(ctx context.Context, tx pgx.Tx) error { + file, err := os.OpenFile("data/lap_times.csv", os.O_RDONLY, os.ModePerm) + if err != nil { + return fmt.Errorf("Error opening lap times CSV file: %w", err) + } + defer file.Close() + + var lapTimes []*lapTime + + if err = gocsv.UnmarshalFile(file, &lapTimes); err != nil { + return fmt.Errorf("Error marshaling lap times CSV file: %w", err) + } + + records := []database.SaveLapTimesParams{} + + for _, lapTime := range lapTimes { + records = append(records, database.SaveLapTimesParams{ + RaceID: lapTime.RaceID, + DriverID: lapTime.DriverID, + Lap: lapTime.Lap, + Position: &lapTime.Position, + Time: &lapTime.Time, + Milliseconds: &lapTime.Milliseconds, + }) + } + + _, err = i.db.WithTx(tx).SaveLapTimes(ctx, records) + if err != nil { + return fmt.Errorf("Error saving lap times: %w", err) + } + + fmt.Println("[Lap Times] seeding complete") + return nil +} diff --git a/internal/imager/pitstop.go b/internal/imager/pitstop.go new file mode 100644 index 0000000..451dcda --- /dev/null +++ b/internal/imager/pitstop.go @@ -0,0 +1,57 @@ +package imager + +import ( + "context" + "fmt" + "os" + + "github.com/gocarina/gocsv" + "github.com/jackc/pgx/v5" + "github.com/jsec/f1-data-hub/internal/database" +) + +type pitStop struct { + RaceID int32 `csv:"raceId"` + DriverID int32 `csv:"driverId"` + Stop int32 `csv:"stop"` + Lap int32 `csv:"lap"` + Time timeOnly `csv:"time"` + Duration string `csv:"duration"` + Milliseconds int32 `csv:"milliseconds"` +} + +func (i Imager) loadPitStops(ctx context.Context, tx pgx.Tx) error { + file, err := os.OpenFile("data/pit_stops.csv", os.O_RDONLY, os.ModePerm) + if err != nil { + return fmt.Errorf("Error opening pit stops CSV file: %w", err) + } + defer file.Close() + + var pitStops []*pitStop + + if err = gocsv.UnmarshalFile(file, &pitStops); err != nil { + return fmt.Errorf("Error marshaling pit stops CSV file: %w", err) + } + + records := []database.SavePitStopsParams{} + + for _, pitStop := range pitStops { + records = append(records, database.SavePitStopsParams{ + RaceID: pitStop.RaceID, + DriverID: pitStop.DriverID, + Stop: pitStop.Stop, + Lap: pitStop.Lap, + Time: pitStop.Time.Value, + Duration: &pitStop.Duration, + Milliseconds: &pitStop.Milliseconds, + }) + } + + _, err = i.db.WithTx(tx).SavePitStops(ctx, records) + if err != nil { + return fmt.Errorf("Error saving pit stops: %w", err) + } + + fmt.Println("[Pit Stops] seeding complete") + return nil +} diff --git a/internal/imager/qualifying.go b/internal/imager/qualifying.go new file mode 100644 index 0000000..2a43dc5 --- /dev/null +++ b/internal/imager/qualifying.go @@ -0,0 +1,61 @@ +package imager + +import ( + "context" + "fmt" + "os" + + "github.com/gocarina/gocsv" + "github.com/jackc/pgx/v5" + "github.com/jsec/f1-data-hub/internal/database" +) + +type qualifying struct { + ID int32 `csv:"qualifyId"` + RaceID int32 `csv:"raceId"` + DriverID int32 `csv:"driverId"` + ConstructorID int32 `csv:"constructorId"` + Number int32 `csv:"number"` + Position int32 `csv:"position"` + Q1 optionalString `csv:"q1"` + Q2 optionalString `csv:"q2"` + Q3 optionalString `csv:"q3"` +} + +func (i Imager) loadQualifying(ctx context.Context, tx pgx.Tx) error { + file, err := os.OpenFile("data/qualifying.csv", os.O_RDONLY, os.ModePerm) + if err != nil { + return fmt.Errorf("Error opening qualifying CSV file: %w", err) + } + defer file.Close() + + var results []*qualifying + + if err = gocsv.UnmarshalFile(file, &results); err != nil { + return fmt.Errorf("Error marshaling qualifying CSV file: %w", err) + } + + records := []database.SaveQualifyingResultsParams{} + + for _, result := range results { + records = append(records, database.SaveQualifyingResultsParams{ + ID: result.ID, + RaceID: result.RaceID, + DriverID: result.DriverID, + ConstructorID: result.ConstructorID, + Number: result.Number, + Position: &result.Position, + Q1: result.Q1.Value, + Q2: result.Q2.Value, + Q3: result.Q3.Value, + }) + } + + _, err = i.db.WithTx(tx).SaveQualifyingResults(ctx, records) + if err != nil { + return fmt.Errorf("Error saving qualifying results: %w", err) + } + + fmt.Println("[Qualifying] seeding complete") + return nil +} diff --git a/internal/imager/race.go b/internal/imager/race.go new file mode 100644 index 0000000..d18dd7c --- /dev/null +++ b/internal/imager/race.go @@ -0,0 +1,79 @@ +package imager + +import ( + "context" + "fmt" + "os" + + "github.com/gocarina/gocsv" + "github.com/jackc/pgx/v5" + "github.com/jsec/f1-data-hub/internal/database" +) + +type race struct { + ID int32 `csv:"raceId"` + Year int32 `csv:"year"` + Round int32 `csv:"round"` + CircuitID int32 `csv:"circuitId"` + Name string `csv:"name"` + Date dateOnly `csv:"date"` + Time optionalTimeOnly `csv:"time"` + URL string `csv:"url"` + Fp1Date optionalDateOnly `csv:"fp1_date"` + Fp1Time optionalTimeOnly `csv:"fp1_time"` + Fp2Date optionalDateOnly `csv:"fp2_date"` + Fp2Time optionalTimeOnly `csv:"fp2_time"` + Fp3Date optionalDateOnly `csv:"fp3_date"` + Fp3Time optionalTimeOnly `csv:"fp3_time"` + QualiDate optionalDateOnly `csv:"quali_date"` + QualiTime optionalTimeOnly `csv:"quali_time"` + SprintDate optionalDateOnly `csv:"sprint_date"` + SprintTime optionalTimeOnly `csv:"sprint_time"` +} + +func (i Imager) loadRaces(ctx context.Context, tx pgx.Tx) error { + file, err := os.OpenFile("data/races.csv", os.O_RDONLY, os.ModePerm) + if err != nil { + return fmt.Errorf("Error opening races CSV file: %w", err) + } + defer file.Close() + + var races []*race + + if err = gocsv.UnmarshalFile(file, &races); err != nil { + return fmt.Errorf("Error marshaling races CSV file: %w", err) + } + + records := []database.SaveRacesParams{} + + for _, race := range races { + records = append(records, database.SaveRacesParams{ + ID: race.ID, + Year: race.Year, + Round: race.Round, + CircuitID: race.CircuitID, + Name: race.Name, + Date: race.Date.Value, + Time: race.Time.Value, + Url: &race.URL, + Fp1Date: race.Fp1Date.Value, + Fp1Time: race.Fp1Time.Value, + Fp2Date: race.Fp2Date.Value, + Fp2Time: race.Fp2Time.Value, + Fp3Date: race.Fp3Date.Value, + Fp3Time: race.Fp3Time.Value, + QualiDate: race.QualiDate.Value, + QualiTime: race.QualiTime.Value, + SprintDate: race.SprintDate.Value, + SprintTime: race.SprintTime.Value, + }) + } + + _, err = i.db.WithTx(tx).SaveRaces(ctx, records) + if err != nil { + return fmt.Errorf("Error saving races: %w", err) + } + + fmt.Println("[Races] seeding complete") + return nil +} diff --git a/internal/imager/result.go b/internal/imager/result.go new file mode 100644 index 0000000..9b3827c --- /dev/null +++ b/internal/imager/result.go @@ -0,0 +1,146 @@ +package imager + +import ( + "context" + "fmt" + "os" + + "github.com/gocarina/gocsv" + "github.com/jackc/pgx/v5" + "github.com/jsec/f1-data-hub/internal/database" + "github.com/shopspring/decimal" +) + +type result struct { + ID int32 `csv:"resultId"` + RaceID int32 `csv:"raceId"` + DriverID int32 `csv:"driverId"` + ConstructorID int32 `csv:"constructorId"` + Number optionalNumber `csv:"number"` + Grid int32 `csv:"grid"` + Position optionalNumber `csv:"position"` + PositionText string `csv:"positionText"` + PositionOrder int32 `csv:"positionOrder"` + Points decimal.Decimal `csv:"points"` + Laps int32 `csv:"laps"` + Time optionalString `csv:"time"` + Milliseconds optionalNumber `csv:"milliseconds"` + FastestLap optionalNumber `csv:"fastestLap"` + Rank optionalNumber `csv:"rank"` + FastestLapTime optionalString `csv:"fastestLapTime"` + FastestLapSpeed optionalString `csv:"fastestLapSpeed"` + StatusID int32 `csv:"statusId"` +} + +func (i Imager) loadResults(ctx context.Context, tx pgx.Tx) error { + file, err := os.OpenFile("data/results.csv", os.O_RDONLY, os.ModePerm) + if err != nil { + return fmt.Errorf("Error opening results CSV file: %w", err) + } + defer file.Close() + + var results []*result + + if err = gocsv.UnmarshalFile(file, &results); err != nil { + return fmt.Errorf("Error marshaling results CSV file: %w", err) + } + + records := []database.SaveResultsParams{} + + for _, result := range results { + records = append(records, database.SaveResultsParams{ + ID: result.ID, + RaceID: result.RaceID, + DriverID: result.DriverID, + ConstructorID: result.ConstructorID, + Number: result.Number.Value, + Grid: result.Grid, + Position: result.Position.Value, + PosText: result.PositionText, + PosOrder: result.PositionOrder, + Points: result.Points, + Laps: result.Laps, + Time: result.Time.Value, + Milliseconds: result.Milliseconds.Value, + FastestLap: result.FastestLap.Value, + Rank: result.Rank.Value, + FastestLapTime: result.FastestLapTime.Value, + FastestLapSpeed: result.FastestLapSpeed.Value, + StatusID: &result.StatusID, + }) + } + + _, err = i.db.WithTx(tx).SaveResults(ctx, records) + if err != nil { + return fmt.Errorf("Error saving results: %w", err) + } + + fmt.Println("[Results] seeding complete") + return nil +} + +type sprintResult struct { + ID int32 `csv:"resultId"` + RaceID int32 `csv:"raceId"` + DriverID int32 `csv:"driverId"` + ConstructorID int32 `csv:"constructorId"` + Number int32 `csv:"number"` + Grid int32 `csv:"grid"` + Position optionalNumber `csv:"position"` + PositionText string `csv:"positionText"` + PositionOrder int32 `csv:"positionOrder"` + Points decimal.Decimal `csv:"points"` + Laps int32 `csv:"laps"` + Time optionalString `csv:"time"` + Milliseconds optionalNumber `csv:"milliseconds"` + FastestLap optionalNumber `csv:"fastestLap"` + Rank optionalNumber `csv:"rank"` + FastestLapTime optionalString `csv:"fastestLapTime"` + FastestLapSpeed optionalString `csv:"fastestLapSpeed"` + StatusID int32 `csv:"statusId"` +} + +func (i Imager) loadSprintResults(ctx context.Context, tx pgx.Tx) error { + file, err := os.OpenFile("data/sprint_results.csv", os.O_RDONLY, os.ModePerm) + if err != nil { + return fmt.Errorf("Error opening sprint results CSV file: %w", err) + } + defer file.Close() + + var results []*sprintResult + + if err = gocsv.UnmarshalFile(file, &results); err != nil { + return fmt.Errorf("Error marshaling sprint results CSV file: %w", err) + } + + records := []database.SaveSprintResultsParams{} + + for _, result := range results { + records = append(records, database.SaveSprintResultsParams{ + ID: result.ID, + RaceID: result.RaceID, + DriverID: result.DriverID, + ConstructorID: result.ConstructorID, + Number: result.Number, + Grid: result.Grid, + Position: result.Position.Value, + PosText: result.PositionText, + PosOrder: result.PositionOrder, + Points: result.Points, + Laps: result.Laps, + Time: result.Time.Value, + Milliseconds: result.Milliseconds.Value, + FastestLap: result.FastestLap.Value, + FastestLapTime: result.FastestLapTime.Value, + StatusID: &result.StatusID, + }) + } + + _, err = i.db.WithTx(tx).SaveSprintResults(ctx, records) + if err != nil { + return fmt.Errorf("Error saving sprint results: %w", err) + } + + fmt.Println("[Sprint Results] seeding complete") + return nil +} diff --git a/internal/imager/season.go b/internal/imager/season.go new file mode 100644 index 0000000..ed75f12 --- /dev/null +++ b/internal/imager/season.go @@ -0,0 +1,47 @@ +package imager + +import ( + "context" + "fmt" + "os" + + "github.com/gocarina/gocsv" + "github.com/jackc/pgx/v5" + "github.com/jsec/f1-data-hub/internal/database" +) + +type season struct { + Year int `csv:"year"` + Url string `csv:"url"` +} + +func (i Imager) loadSeasons(ctx context.Context, tx pgx.Tx) error { + file, err := os.OpenFile("data/seasons.csv", os.O_RDONLY, os.ModePerm) + if err != nil { + return fmt.Errorf("Error opening seasons CSV file: %w", err) + } + defer file.Close() + + var seasons []*season + + if err = gocsv.UnmarshalFile(file, &seasons); err != nil { + return fmt.Errorf("Error marshaling seasons CSV file: %w", err) + } + + records := []database.SaveSeasonsParams{} + + for _, season := range seasons { + records = append(records, database.SaveSeasonsParams{ + Year: int32(season.Year), + Url: season.Url, + }) + } + + _, err = i.db.WithTx(tx).SaveSeasons(ctx, records) + if err != nil { + return fmt.Errorf("Error saving seasons: %w", err) + } + + fmt.Println("[Seasons] seeding complete") + return nil +} diff --git a/internal/imager/status.go b/internal/imager/status.go new file mode 100644 index 0000000..9fc3aa1 --- /dev/null +++ b/internal/imager/status.go @@ -0,0 +1,47 @@ +package imager + +import ( + "context" + "fmt" + "os" + + "github.com/gocarina/gocsv" + "github.com/jackc/pgx/v5" + "github.com/jsec/f1-data-hub/internal/database" +) + +type status struct { + ID int `csv:"statusId"` + Status string `csv:"status"` +} + +func (i Imager) loadStatuses(ctx context.Context, tx pgx.Tx) error { + file, err := os.OpenFile("data/status.csv", os.O_RDONLY, os.ModePerm) + if err != nil { + return fmt.Errorf("Error opening status CSV file: %w", err) + } + defer file.Close() + + var statuses []*status + + if err = gocsv.UnmarshalFile(file, &statuses); err != nil { + return fmt.Errorf("Error marshaling status CSV file: %w", err) + } + + records := []database.SaveStatusesParams{} + + for _, status := range statuses { + records = append(records, database.SaveStatusesParams{ + ID: int32(status.ID), + Status: status.Status, + }) + } + + _, err = i.db.WithTx(tx).SaveStatuses(ctx, records) + if err != nil { + return fmt.Errorf("Error saving statuses: %w", err) + } + + fmt.Println("[Statuses] seeding complete") + return nil +} diff --git a/internal/repository/repository.go b/internal/repository/repository.go new file mode 100644 index 0000000..bd31456 --- /dev/null +++ b/internal/repository/repository.go @@ -0,0 +1,7 @@ +package repository + +import "github.com/jsec/f1-data-hub/internal/database" + +type Repository struct { + db *database.Queries +} diff --git a/internal/repository/seasons.go b/internal/repository/seasons.go new file mode 100644 index 0000000..70687d7 --- /dev/null +++ b/internal/repository/seasons.go @@ -0,0 +1,40 @@ +package repository + +import ( + "context" + "fmt" + + "github.com/jsec/f1-data-hub/internal/database" + "github.com/shopspring/decimal" +) + +func NewSeasonRepository(db *database.Queries) Repository { + return Repository{db: db} +} + +type DriverStanding struct { + ID int32 `json:"id"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + Points decimal.Decimal `json:"points"` +} + +func (r *Repository) GetDriverStandingsByYear(ctx context.Context, year int32) ([]DriverStanding, error) { + result, err := r.db.GetDriverStandingsByYear(ctx, year) + if err != nil { + return nil, fmt.Errorf("Error retrieving driver standings: %w", err) + } + + var standings []DriverStanding + + for _, row := range result { + standings = append(standings, DriverStanding{ + ID: row.DriverID, + FirstName: row.FirstName, + LastName: row.LastName, + Points: row.Points, + }) + } + + return standings, nil +} diff --git a/sql/queries/.gitkeep b/sql/queries/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/sql/queries/circuit.sql b/sql/queries/circuit.sql new file mode 100644 index 0000000..21e827c --- /dev/null +++ b/sql/queries/circuit.sql @@ -0,0 +1,13 @@ +-- name: SaveCircuits :copyfrom +INSERT INTO circuits ( + id, + ref, + name, + location, + country, + lat, + lng, + alt, + url +) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9); diff --git a/sql/queries/constructor.sql b/sql/queries/constructor.sql new file mode 100644 index 0000000..2953760 --- /dev/null +++ b/sql/queries/constructor.sql @@ -0,0 +1,31 @@ +-- name: SaveConstructors :copyfrom +INSERT INTO constructors ( + id, + ref, + name, + nationality, + url +) +VALUES ($1, $2, $3, $4, $5); + +-- name: SaveConstructorStandings :copyfrom +INSERT INTO constructor_standings ( + id, + race_id, + constructor_id, + points, + position, + pos_text, + wins +) +VALUES ($1, $2, $3, $4, $5, $6, $7); + +-- name: SaveConstructorResults :copyfrom +INSERT INTO constructor_results ( + id, + race_id, + constructor_id, + points, + status +) +VALUES ($1, $2, $3, $4, $5); diff --git a/sql/queries/driver.sql b/sql/queries/driver.sql new file mode 100644 index 0000000..f77409c --- /dev/null +++ b/sql/queries/driver.sql @@ -0,0 +1,24 @@ +-- name: SaveDrivers :copyfrom +INSERT INTO drivers ( + id, + ref, + number, + code, + first_name, + last_name, + date_of_birth, + nationality, + url +) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9); + +-- name: SaveDriverStandings :copyfrom +INSERT INTO driver_standings ( + id, + race_id, + driver_id, + points, + position, + pos_text, + wins +) +VALUES ($1, $2, $3, $4, $5, $6, $7); diff --git a/sql/queries/laptime.sql b/sql/queries/laptime.sql new file mode 100644 index 0000000..1c2f50c --- /dev/null +++ b/sql/queries/laptime.sql @@ -0,0 +1,10 @@ +-- name: SaveLapTimes :copyfrom +INSERT INTO lap_times ( + race_id, + driver_id, + lap, + position, + time, + milliseconds +) +VALUES ($1, $2, $3, $4, $5, $6); diff --git a/sql/queries/pitstop.sql b/sql/queries/pitstop.sql new file mode 100644 index 0000000..803a0d6 --- /dev/null +++ b/sql/queries/pitstop.sql @@ -0,0 +1,11 @@ +-- name: SavePitStops :copyfrom +INSERT INTO pit_stops ( + race_id, + driver_id, + stop, + lap, + time, + duration, + milliseconds +) +VALUES ($1, $2, $3, $4, $5, $6, $7); diff --git a/sql/queries/qualifying.sql b/sql/queries/qualifying.sql new file mode 100644 index 0000000..d5a2302 --- /dev/null +++ b/sql/queries/qualifying.sql @@ -0,0 +1,23 @@ +-- name: SaveQualifyingResults :copyfrom +INSERT INTO qualifying ( + id, + race_id, + driver_id, + constructor_id, + number, + position, + q1, + q2, + q3 +) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9 +); diff --git a/sql/queries/race.sql b/sql/queries/race.sql new file mode 100644 index 0000000..5126a37 --- /dev/null +++ b/sql/queries/race.sql @@ -0,0 +1,41 @@ +-- name: SaveRaces :copyfrom +INSERT INTO races ( + id, + year, + round, + circuit_id, + name, + date, + time, + url, + fp1_date, + fp1_time, + fp2_date, + fp2_time, + fp3_date, + fp3_time, + quali_date, + quali_time, + sprint_date, + sprint_time +) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13, + $14, + $15, + $16, + $17, + $18 +); diff --git a/sql/queries/result.sql b/sql/queries/result.sql new file mode 100644 index 0000000..229deed --- /dev/null +++ b/sql/queries/result.sql @@ -0,0 +1,79 @@ +-- name: SaveResults :copyfrom +INSERT INTO results ( + id, + race_id, + driver_id, + constructor_id, + number, + grid, + position, + pos_text, + pos_order, + points, + laps, + time, + milliseconds, + fastest_lap, + rank, + fastest_lap_time, + fastest_lap_speed, + status_id +) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13, + $14, + $15, + $16, + $17, + $18 +); + +-- name: SaveSprintResults :copyfrom +INSERT INTO sprint_results ( + id, + race_id, + driver_id, + constructor_id, + number, + grid, + position, + pos_text, + pos_order, + points, + laps, + time, + milliseconds, + fastest_lap, + fastest_lap_time, + status_id +) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13, + $14, + $15, + $16 +); diff --git a/sql/queries/season.sql b/sql/queries/season.sql index 03f18be..70e1aea 100644 --- a/sql/queries/season.sql +++ b/sql/queries/season.sql @@ -1,3 +1,7 @@ +-- name: SaveSeasons :copyfrom +INSERT INTO seasons (year, url) +VALUES ($1, $2); + -- name: GetDriverStandingsByYear :many SELECT d.id AS driver_id, diff --git a/sql/queries/status.sql b/sql/queries/status.sql new file mode 100644 index 0000000..138d049 --- /dev/null +++ b/sql/queries/status.sql @@ -0,0 +1,3 @@ +-- name: SaveStatuses :copyfrom +INSERT INTO status (id, status) +VALUES ($1, $2); diff --git a/sql/schema/20241013030711_update_initial_schema.sql b/sql/schema/20241013030711_update_initial_schema.sql deleted file mode 100644 index b64b149..0000000 --- a/sql/schema/20241013030711_update_initial_schema.sql +++ /dev/null @@ -1,76 +0,0 @@ --- +goose Up - --- +goose StatementBegin -ALTER TABLE constructorresults RENAME TO constructor_results; -ALTER TABLE constructorstandings RENAME TO constructor_standings; -ALTER TABLE driverstandings RENAME TO driver_standings; -ALTER TABLE laptimes RENAME TO lap_times; -ALTER TABLE pitstops RENAME TO pit_stops; -ALTER TABLE sprintresults RENAME TO sprint_results; - -ALTER TABLE circuits RENAME COLUMN circuitid TO id; -ALTER TABLE circuits RENAME COLUMN circuitref TO ref; - -ALTER TABLE constructor_results RENAME COLUMN constructorresultsid TO id; -ALTER TABLE constructor_results RENAME COLUMN raceid TO race_id; -ALTER TABLE constructor_results RENAME COLUMN constructorid TO constructor_id; - -ALTER TABLE constructors RENAME COLUMN constructorid TO id; -ALTER TABLE constructors RENAME COLUMN constructorref TO ref; - -ALTER TABLE constructor_standings RENAME COLUMN constructorstandingsid TO id; -ALTER TABLE constructor_standings RENAME COLUMN raceid TO race_id; -ALTER TABLE constructor_standings RENAME COLUMN constructorid TO constructor_id; -ALTER TABLE constructor_standings RENAME COLUMN positiontext TO position_text; - -ALTER TABLE drivers RENAME COLUMN driverid TO id; -ALTER TABLE drivers RENAME COLUMN driverref TO ref; -ALTER TABLE drivers RENAME COLUMN forename TO first_name; -ALTER TABLE drivers RENAME COLUMN surname TO last_name; -ALTER TABLE drivers RENAME COLUMN dob TO date_of_birth; - -ALTER TABLE driver_standings RENAME COLUMN driverstandingsid TO id; -ALTER TABLE driver_standings RENAME COLUMN raceid TO race_id; -ALTER TABLE driver_standings RENAME COLUMN driverid TO driver_id; -ALTER TABLE driver_standings RENAME COLUMN positiontext TO position_text; - -ALTER TABLE lap_times RENAME COLUMN raceid TO race_id; -ALTER TABLE lap_times RENAME COLUMN driverid TO driver_id; - -ALTER TABLE pit_stops RENAME COLUMN raceid TO race_id; -ALTER TABLE pit_stops RENAME COLUMN driverid TO driver_id; - -ALTER TABLE qualifying RENAME COLUMN qualifyid TO id; -ALTER TABLE qualifying RENAME COLUMN raceid TO race_id; -ALTER TABLE qualifying RENAME COLUMN driverid TO driver_id; -ALTER TABLE qualifying RENAME COLUMN constructorid TO constructor_id; - -ALTER TABLE races RENAME COLUMN raceid TO id; -ALTER TABLE races RENAME COLUMN circuitid TO circuit_id; - -ALTER TABLE results RENAME COLUMN resultid TO id; -ALTER TABLE results RENAME COLUMN raceid TO race_id; -ALTER TABLE results RENAME COLUMN driverid TO driver_id; -ALTER TABLE results RENAME COLUMN constructorid TO constructor_id; -ALTER TABLE results RENAME COLUMN positiontext TO position_text; -ALTER TABLE results RENAME COLUMN positionorder TO position_order; -ALTER TABLE results RENAME COLUMN fastestlap TO fastest_lap; -ALTER TABLE results RENAME COLUMN fastestlaptime TO fastest_lap_time; -ALTER TABLE results RENAME COLUMN fastestlapspeed TO fastest_lap_speed; -ALTER TABLE results RENAME COLUMN statusid TO status_id; - -ALTER TABLE sprint_results RENAME COLUMN sprintresultid TO id; -ALTER TABLE sprint_results RENAME COLUMN raceid TO race_id; -ALTER TABLE sprint_results RENAME COLUMN driverid TO driver_id; -ALTER TABLE sprint_results RENAME COLUMN constructorid TO constructor_id; -ALTER TABLE sprint_results RENAME COLUMN positiontext TO position_text; -ALTER TABLE sprint_results RENAME COLUMN positionorder TO position_order; -ALTER TABLE sprint_results RENAME COLUMN fastestlap TO fastest_lap; -ALTER TABLE sprint_results RENAME COLUMN fastestlaptime TO fastest_lap_time; -ALTER TABLE sprint_results RENAME COLUMN statusid TO status_id; - -ALTER TABLE status RENAME COLUMN statusid TO id; --- +goose StatementEnd - --- +goose Down -SELECT 'noop'; diff --git a/sql/schema/20241103231739_create_schema.sql b/sql/schema/20241103231739_create_schema.sql new file mode 100644 index 0000000..98d4f8c --- /dev/null +++ b/sql/schema/20241103231739_create_schema.sql @@ -0,0 +1,168 @@ +-- +goose Up + +CREATE TABLE circuits ( + id integer PRIMARY KEY, + ref text NOT NULL, + name text NOT NULL, + location text DEFAULT NULL, + country text DEFAULT NULL, + lat numeric DEFAULT NULL, + lng numeric DEFAULT NULL, + alt integer DEFAULT NULL, + url text NOT NULL UNIQUE +); + +CREATE TABLE status ( + id integer NOT NULL PRIMARY KEY, + status text NOT NULL DEFAULT '' +); + +CREATE TABLE seasons ( + year integer NOT NULL, + url text NOT NULL UNIQUE +); + +CREATE TABLE constructors ( + id integer PRIMARY KEY, + ref text NOT NULL, + name text NOT NULL UNIQUE, + nationality text DEFAULT NULL, + url text NOT NULL +); + +CREATE TABLE drivers ( + id integer PRIMARY KEY, + ref text NOT NULL DEFAULT '', + number integer DEFAULT NULL, + code text DEFAULT NULL, + first_name text NOT NULL DEFAULT '', + last_name text NOT NULL DEFAULT '', + date_of_birth date DEFAULT NULL, + nationality text DEFAULT NULL, + url text NOT NULL UNIQUE +); + +CREATE TABLE races ( + id integer NOT NULL PRIMARY KEY, + year integer NOT NULL, + round integer NOT NULL DEFAULT 0, + circuit_id integer NOT NULL REFERENCES circuits (id), + name text NOT NULL DEFAULT '', + date date NOT NULL, + time time DEFAULT NULL, + url text DEFAULT NULL UNIQUE, + fp1_date date DEFAULT NULL, + fp1_time time DEFAULT NULL, + fp2_date date DEFAULT NULL, + fp2_time time DEFAULT NULL, + fp3_date date DEFAULT NULL, + fp3_time time DEFAULT NULL, + quali_date date DEFAULT NULL, + quali_time time DEFAULT NULL, + sprint_date date DEFAULT NULL, + sprint_time time DEFAULT NULL +); + +CREATE TABLE constructor_results ( + id integer NOT NULL PRIMARY KEY, + race_id integer NOT NULL REFERENCES races (id), + constructor_id integer NOT NULL REFERENCES constructors (id), + points numeric, + status text +); + +CREATE TABLE constructor_standings ( + id integer PRIMARY KEY, + race_id integer NOT NULL REFERENCES races (id), + constructor_id integer NOT NULL REFERENCES constructors (id), + points numeric NOT NULL, + position integer DEFAULT NULL, + pos_text text DEFAULT NULL, + wins integer NOT NULL DEFAULT 0 +); + +CREATE TABLE driver_standings ( + id integer PRIMARY KEY, + race_id integer NOT NULL REFERENCES races (id), + driver_id integer NOT NULL REFERENCES drivers (id), + points numeric NOT NULL DEFAULT 0, + position integer DEFAULT NULL, + pos_text text DEFAULT NULL, + wins integer DEFAULT 0 +); + +CREATE TABLE lap_times ( + race_id integer NOT NULL REFERENCES races (id), + driver_id integer NOT NULL REFERENCES drivers (id), + lap integer NOT NULL, + position integer DEFAULT NULL, + time text DEFAULT NULL, + milliseconds integer DEFAULT NULL, + PRIMARY KEY (race_id, driver_id, lap) +); + +CREATE TABLE pit_stops ( + race_id integer NOT NULL REFERENCES races (id), + driver_id integer NOT NULL REFERENCES drivers (id), + stop integer NOT NULL, + lap integer NOT NULL, + time time NOT NULL, + duration text DEFAULT NULL, + milliseconds integer DEFAULT NULL, + PRIMARY KEY (race_id, driver_id, stop) +); + +CREATE TABLE qualifying ( + id integer NOT NULL PRIMARY KEY, + race_id integer NOT NULL REFERENCES races (id), + driver_id integer NOT NULL REFERENCES drivers (id), + constructor_id integer NOT NULL REFERENCES constructors (id), + number integer NOT NULL DEFAULT 0, + position integer DEFAULT NULL, + q1 text DEFAULT NULL, + q2 text DEFAULT NULL, + q3 text DEFAULT NULL +); + +CREATE TABLE results ( + id integer NOT NULL PRIMARY KEY, + race_id integer NOT NULL REFERENCES races (id), + driver_id integer NOT NULL REFERENCES drivers (id), + constructor_id integer NOT NULL REFERENCES constructors (id), + number integer DEFAULT NULL, + grid integer NOT NULL DEFAULT 0, + position integer DEFAULT NULL, + pos_text text NOT NULL DEFAULT '', + pos_order integer NOT NULL DEFAULT 0, + points numeric NOT NULL DEFAULT 0, + laps integer NOT NULL DEFAULT 0, + time text DEFAULT NULL, + milliseconds integer DEFAULT NULL, + fastest_lap integer DEFAULT NULL, + rank integer DEFAULT 0, + fastest_lap_time text DEFAULT NULL, + fastest_lap_speed text DEFAULT NULL, + status_id integer REFERENCES status (id) +); + +CREATE TABLE sprint_results ( + id integer NOT NULL PRIMARY KEY, + race_id integer NOT NULL REFERENCES races (id), + driver_id integer NOT NULL REFERENCES drivers (id), + constructor_id integer NOT NULL REFERENCES constructors (id), + number integer NOT NULL DEFAULT 0, + grid integer NOT NULL DEFAULT 0, + position integer DEFAULT NULL, + pos_text text NOT NULL DEFAULT '', + pos_order integer NOT NULL DEFAULT 0, + points numeric NOT NULL DEFAULT 0, + laps integer NOT NULL DEFAULT 0, + time text DEFAULT NULL, + milliseconds integer DEFAULT NULL, + fastest_lap integer DEFAULT NULL, + fastest_lap_time text DEFAULT NULL, + status_id integer REFERENCES status (id) +); + +-- +goose Down +DROP DATABASE diff --git a/sqlc.yaml b/sqlc.yaml index d19ca58..f62a7a6 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -1,9 +1,40 @@ version: 2 sql: - - schema: sql/schema.sql + - schema: sql/schema queries: sql/queries engine: postgresql + database: + uri: ${DATABASE_URL} gen: go: out: internal/database sql_package: pgx/v5 + output_db_file_name: database.go + output_models_file_name: entities.sql.go + output_copyfrom_file_name: iterators.go + emit_pointers_for_null_types: true + overrides: + - db_type: "date" + go_type: "time.Time" + - db_type: "date" + nullable: true + go_type: + import: "time" + type: "Time" + pointer: true + - db_type: "pg_catalog.time" + go_type: "time.Time" + - db_type: "pg_catalog.time" + nullable: true + go_type: + import: "time" + type: "Time" + pointer: true + - db_type: "numeric" + go_type: "github.com/shopspring/decimal.Decimal" + - db_type: "numeric" + nullable: true + go_type: + import: "github.com/shopspring/decimal" + type: "Decimal" + pointer: true