Compare commits

...

13 Commits

13 changed files with 640 additions and 36 deletions

View File

@@ -1 +1 @@
DATABASE_URL=postgresql://postgres:password@localhost:5432/db PG_FROM_DB_URL=postgresql://postgres:password@localhost:5432/db

192
cmd/main.go Normal file
View File

@@ -0,0 +1,192 @@
package main
import (
"context"
"fmt"
"os"
"git.ksdemosapps.com/kylesoda/pgx-learning/internal/config"
"git.ksdemosapps.com/kylesoda/pgx-learning/internal/db"
"git.ksdemosapps.com/kylesoda/pgx-learning/internal/db/postgres"
"git.ksdemosapps.com/kylesoda/pgx-learning/internal/models"
"git.ksdemosapps.com/kylesoda/pgx-learning/internal/repository"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
func main() {
ctx := context.Background()
pool, err := db.Connect(ctx, config.Db.Url)
if err != nil {
logrus.Fatal(err)
}
defer db.Close(pool)
taskRespository := postgres.NewTaskRepository(pool)
app := &cli.App{
Name: "gotodo",
Usage: "A simple CLI program to manage your tasks",
Action: func(c *cli.Context) error {
completedState := false
tasks, err := taskRespository.GetAll(ctx, repository.GetAllTaskFilters{
Limit: 100,
Completed: &completedState,
})
if err != nil {
return err
}
fmt.Println("Pending Tasks:")
printTasks(tasks)
return nil
},
Commands: []*cli.Command{
{
Name: "add",
Aliases: []string{"a"},
Usage: "Add a new task",
Action: func(c *cli.Context) error {
text := c.Args().First()
if text == "" {
return fmt.Errorf("task text cannot be empty")
}
return taskRespository.Save(ctx, &models.Task{
Text: text,
})
},
},
{
Name: "list",
Aliases: []string{"ls"},
Usage: "List tasks with filters",
Flags: []cli.Flag{
&cli.BoolFlag{Name: "all", Aliases: []string{"a"}, Usage: "List all tasks"},
&cli.BoolFlag{Name: "completed", Aliases: []string{"c"}, Usage: "List only completed tasks"},
&cli.BoolFlag{Name: "pending", Aliases: []string{"p"}, Usage: "List only pending tasks"},
},
Action: func(c *cli.Context) error {
var tasks []models.Task
var err error
title := "Pending Tasks:"
if c.Bool("all") {
tasks, err = taskRespository.GetAll(ctx, repository.GetAllTaskFilters{
Limit: 100,
})
title = "All Tasks:"
} else if c.Bool("completed") {
completedState := true
tasks, err = taskRespository.GetAll(ctx, repository.GetAllTaskFilters{
Limit: 100,
Completed: &completedState,
})
title = "Completed Tasks:"
} else {
completedState := false
tasks, err = taskRespository.GetAll(ctx, repository.GetAllTaskFilters{
Limit: 100,
Completed: &completedState,
})
title = "Pending Tasks:"
}
if err != nil {
return err
}
fmt.Println(title)
printTasks(tasks)
return nil
},
},
{
Name: "done",
Aliases: []string{"do"},
Usage: "Mark a task as completed",
Action: func(c *cli.Context) error {
idStr := c.Args().First()
if idStr == "" {
return fmt.Errorf("task ID required")
}
var id int
if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil {
return fmt.Errorf("invalid task ID: %s", idStr)
}
completedState := true
_, err := taskRespository.Update(ctx, id, &models.UpdateTaskInput{
Completed: &completedState,
})
return err
},
},
{
Name: "undo",
Aliases: []string{"ud"},
Usage: "Mark a task as not completed",
Action: func(c *cli.Context) error {
idStr := c.Args().First()
if idStr == "" {
return fmt.Errorf("task ID required")
}
var id int
if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil {
return fmt.Errorf("invalid task ID: %s", idStr)
}
completedState := false
_, err := taskRespository.Update(ctx, id, &models.UpdateTaskInput{
Completed: &completedState,
})
return err
},
},
{
Name: "remove",
Aliases: []string{"rm"},
Usage: "Remove a task",
Action: func(c *cli.Context) error {
idStr := c.Args().First()
if idStr == "" {
return fmt.Errorf("task ID required")
}
var id int
if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil {
return fmt.Errorf("invalid task ID: %s", idStr)
}
return taskRespository.Delete(ctx, id)
},
},
},
}
err = app.Run(os.Args)
if err != nil {
logrus.Fatal(err)
}
}
func printTasks(tasks []models.Task) {
if len(tasks) == 0 {
fmt.Println("No tasks found")
return
}
for _, task := range tasks {
status := "[ ]"
if task.Completed {
status = "[✓]"
}
fmt.Printf("%d. %s %s (Created: %s)\n",
task.Id,
status,
task.Text,
task.CreatedAt.Format("2006-01-02 15:04:05"))
}
}

8
go.mod
View File

@@ -1,16 +1,22 @@
module git.ksdemosapps.com/kylesoda/postgres-migrator module git.ksdemosapps.com/kylesoda/pgx-learning
go 1.25.7 go 1.25.7
require ( require (
github.com/jackc/pgx/v5 v5.9.1 github.com/jackc/pgx/v5 v5.9.1
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/sirupsen/logrus v1.9.4
github.com/urfave/cli/v2 v2.27.7
) )
require ( require (
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
golang.org/x/sync v0.17.0 // indirect golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.29.0 // indirect golang.org/x/text v0.29.0 // indirect
) )

12
go.sum
View File

@@ -1,3 +1,5 @@
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -13,13 +15,23 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

34
internal/config/main.go Normal file
View File

@@ -0,0 +1,34 @@
package config
import (
"os"
"github.com/joho/godotenv"
"github.com/sirupsen/logrus"
)
type dbConfig struct {
Url string
}
func loadEnv() {
err := godotenv.Load()
if err != nil {
logrus.Warn("Warning: could not load .env file")
}
}
func getDbConfig() *dbConfig {
loadEnv()
dbUrl := os.Getenv("PG_FROM_DB_URL")
if dbUrl == "" {
logrus.Fatal("PG_FROM_DB_URL environment variable not set")
}
return &dbConfig{
Url: dbUrl,
}
}
var Db *dbConfig = getDbConfig()

28
internal/db/connection.go Normal file
View File

@@ -0,0 +1,28 @@
package db
import (
"context"
"fmt"
"github.com/jackc/pgx/v5/pgxpool"
)
func Connect(ctx context.Context, dbURL string) (*pgxpool.Pool, error) {
pool, err := pgxpool.New(ctx, dbURL)
if err != nil {
return nil, fmt.Errorf("unable to connect to database: %w", err)
}
if err := pool.Ping(ctx); err != nil {
pool.Close()
return nil, fmt.Errorf("unable to ping database: %w", err)
}
return pool, nil
}
func Close(pool *pgxpool.Pool) {
if pool != nil {
pool.Close()
}
}

View File

@@ -0,0 +1,150 @@
package postgres
import (
"context"
"errors"
"fmt"
"git.ksdemosapps.com/kylesoda/pgx-learning/internal/models"
"git.ksdemosapps.com/kylesoda/pgx-learning/internal/repository"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type pgxRepository struct {
db *pgxpool.Pool
}
func NewTaskRepository(pool *pgxpool.Pool) repository.TaskRepository {
return &pgxRepository{
db: pool,
}
}
func (r *pgxRepository) Save(ctx context.Context, task *models.Task) error {
sql := `
INSERT INTO public.tasks (text, completed)
VALUES ($1, $2)
RETURNING id, created_at, updated_at
`
return r.db.QueryRow(ctx, sql, task.Text, task.Completed).Scan(&task.Id, &task.CreatedAt, &task.UpdatedAt)
}
func (r *pgxRepository) GetById(ctx context.Context, id int) (*models.Task, error) {
sql := `
SELECT id, text, completed, created_at, updated_at
FROM public.tasks
WHERE id = $1
`
var task models.Task
err := r.db.QueryRow(ctx, sql, id).Scan(
&task.Id,
&task.Text,
&task.Completed,
&task.CreatedAt,
&task.UpdatedAt,
)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
return nil, fmt.Errorf("Unexpected error when retrieving task: %w", err)
}
return &task, nil
}
func (r *pgxRepository) GetAll(ctx context.Context, filters repository.GetAllTaskFilters) ([]models.Task, error) {
sql := `SELECT id, text, completed, created_at, updated_at FROM public.tasks WHERE 1 = 1`
args := []any{}
if filters.Completed != nil {
args = append(args, *filters.Completed)
sql += fmt.Sprintf(" AND completed = $%d", len(args))
}
limit := 1000
if filters.Limit > 0 {
limit = filters.Limit
}
args = append(args, limit)
sql += fmt.Sprintf(" LIMIT $%d", len(args))
if filters.Offset > 0 {
args = append(args, filters.Offset)
sql += fmt.Sprintf(" OFFSET $%d", len(args))
}
rows, err := r.db.Query(ctx, sql, args...)
if err != nil {
return nil, fmt.Errorf("Unexpected error when querying tasks: %w", err)
}
defer rows.Close()
tasks, err := pgx.CollectRows(rows, pgx.RowToStructByName[models.Task])
if err != nil {
return nil, fmt.Errorf("Unexpected error when collecting tasks: %w", err)
}
return tasks, nil
}
func (r *pgxRepository) Update(ctx context.Context, id int, input *models.UpdateTaskInput) (*models.Task, error) {
sql := "UPDATE public.tasks SET updated_at = CURRENT_TIMESTAMP"
args := []any{}
if input.Text != nil {
args = append(args, input.Text)
sql += fmt.Sprintf(", text = $%d", len(args))
}
if input.Completed != nil {
args = append(args, input.Completed)
sql += fmt.Sprintf(", completed = $%d", len(args))
}
if len(args) == 0 {
return r.GetById(ctx, id)
}
args = append(args, id)
sql += fmt.Sprintf(" WHERE id = $%d RETURNING id, text, completed, created_at, updated_at", len(args))
var task models.Task
err := r.db.QueryRow(ctx, sql, args...).Scan(
&task.Id,
&task.Text,
&task.Completed,
&task.CreatedAt,
&task.UpdatedAt,
)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, fmt.Errorf("task not found")
}
return nil, fmt.Errorf("updating task: %w", err)
}
return &task, nil
}
func (r *pgxRepository) Delete(ctx context.Context, id int) error {
sql := "DELETE FROM public.tasks WHERE id = $1"
commandTag, err := r.db.Exec(ctx, sql, id)
if err != nil {
return fmt.Errorf("Unexpected error when deleting task: %w", err)
}
if commandTag.RowsAffected() == 0 {
return fmt.Errorf("No task found with id %d", id)
}
return nil
}

20
internal/models/task.go Normal file
View File

@@ -0,0 +1,20 @@
package models
import (
"time"
// "github.com/jackc/pgx/v5/pgtype"
)
type Task struct {
Id int `db:"id" json:"id"`
Text string `db:"text" json:"text"`
Completed bool `db:"completed" json:"completed"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt *time.Time `db:"updated_at" json:"updated_at"`
// UpdatedAt pgtype.Timestamptz `json:"updated_at"` /* evaluar la posibilidad de utilizarlo */
}
type UpdateTaskInput struct {
Text *string `json:"text"`
Completed *bool `json:"completed"`
}

View File

@@ -0,0 +1,21 @@
package repository
import (
"context"
"git.ksdemosapps.com/kylesoda/pgx-learning/internal/models"
)
type GetAllTaskFilters struct {
Limit int
Offset int
Completed *bool
}
type TaskRepository interface {
Save(ctx context.Context, task *models.Task) error
GetById(ctx context.Context, id int) (*models.Task, error)
GetAll(ctx context.Context, filters GetAllTaskFilters) ([]models.Task, error)
Update(ctx context.Context, id int, input *models.UpdateTaskInput) (*models.Task, error)
Delete(ctx context.Context, id int) error
}

34
main.go
View File

@@ -1,34 +0,0 @@
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/joho/godotenv"
)
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal("Error al cargar el archivo .env")
}
dbpool, err := pgxpool.New(context.Background(), os.Getenv("DATABASE_URL"))
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to create connection pool: %v\n", err)
os.Exit(1)
}
defer dbpool.Close()
var greeting string
err = dbpool.QueryRow(context.Background(), "select 'Hello, world!'").Scan(&greeting)
if err != nil {
fmt.Fprintf(os.Stderr, "QueryRow failed: %v\n", err)
os.Exit(1)
}
fmt.Println(greeting)
}

40
scripts/reset-db/main.go Normal file
View File

@@ -0,0 +1,40 @@
package main
import (
"context"
"fmt"
"time"
"git.ksdemosapps.com/kylesoda/pgx-learning/internal/config"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/sirupsen/logrus"
)
func dropTables(db *pgxpool.Pool, ctx context.Context) error {
dropTableSQL := `DROP TABLE IF EXISTS tasks`
_, err := db.Exec(ctx, dropTableSQL)
if err != nil {
return fmt.Errorf("error droping tasks table: %w", err)
}
return nil
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
db, err := pgxpool.New(ctx, config.Db.Url)
if err != nil {
logrus.Fatalf("Unable to create connection pool: %v", err)
}
if err := dropTables(db, ctx); err != nil {
logrus.Fatalf("Unexpected error: %v", err)
} else {
logrus.Info("Database reset completed succesfully")
}
defer db.Close()
}

82
scripts/seed-db/main.go Normal file
View File

@@ -0,0 +1,82 @@
package main
import (
"context"
"fmt"
"math/rand"
"time"
"git.ksdemosapps.com/kylesoda/pgx-learning/internal/config"
"git.ksdemosapps.com/kylesoda/pgx-learning/internal/models"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/sirupsen/logrus"
)
func generateTasks(count int) []models.Task {
tasks := make([]models.Task, count)
for i := 1; i <= count; i++ {
tasks[i-1] = models.Task{
Text: fmt.Sprintf("random task Nº %v", i),
Completed: rand.Float64() > 0.5,
}
}
return tasks
}
func saveTasks(db *pgxpool.Pool, ctx context.Context, tasks []models.Task) error {
start := time.Now()
rowsCopied, err := db.CopyFrom(
ctx,
pgx.Identifier{"tasks"},
[]string{"text", "completed"},
pgx.CopyFromSlice(len(tasks), func(i int) ([]any, error) {
return []any{
(tasks)[i].Text,
(tasks)[i].Completed,
}, nil
}),
)
if err != nil {
return fmt.Errorf("Error al realizar el CopyFrom: %w", err)
}
duration := time.Since(start)
logrus.Infof("Se insertaron exitosamente %d registros en %v\n", rowsCopied, duration)
return nil
}
func main() {
logrus.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
TimestampFormat: time.StampMilli,
})
logrus.Info("Starting seed process")
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
db, err := pgxpool.New(ctx, config.Db.Url)
if err != nil {
logrus.Fatalf("Unable to create connection pool: %v", err)
} else {
logrus.Info("Successfully connected to the database")
}
defer db.Close()
count := 100000
logrus.Infof("Generando %d registros...\n", count)
tasks := generateTasks(count)
if err := saveTasks(db, ctx, tasks); err != nil {
logrus.Fatalf("Unexpected error: %v", err)
} else {
logrus.Info("Database seed completed succesfully")
}
}

53
scripts/setup-db/main.go Normal file
View File

@@ -0,0 +1,53 @@
package main
import (
"context"
"fmt"
"time"
"git.ksdemosapps.com/kylesoda/pgx-learning/internal/config"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/sirupsen/logrus"
)
func createTasksTable(db *pgxpool.Pool, ctx context.Context) error {
createTableSQL := `
CREATE TABLE IF NOT EXISTS tasks (
id BIGSERIAL PRIMARY KEY,
text TEXT NOT NULL,
completed BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ
);
`
_, err := db.Exec(ctx, createTableSQL)
if err != nil {
return fmt.Errorf("error creating tasks table: %w", err)
}
return nil
}
func main() {
logrus.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
TimestampFormat: time.StampMilli,
})
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
db, err := pgxpool.New(ctx, config.Db.Url)
if err != nil {
logrus.Fatalf("Unable to create connection pool: %v", err)
}
if err := createTasksTable(db, ctx); err != nil {
logrus.Fatalf("Unexpected error: %v", err)
} else {
logrus.Info("Database setup completed succesfully")
}
defer db.Close()
}