diff --git a/internal/database/click_log.sql.go b/internal/database/click_log.sql.go new file mode 100644 index 0000000..56fc7c9 --- /dev/null +++ b/internal/database/click_log.sql.go @@ -0,0 +1,82 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: click_log.sql + +package database + +import ( + "context" + "database/sql" +) + +const getClicks = `-- name: GetClicks :many +SELECT click_id, url_id, clicked_at, referrer, user_agent, ip_address FROM click_logs +` + +func (q *Queries) GetClicks(ctx context.Context) ([]ClickLog, error) { + rows, err := q.db.QueryContext(ctx, getClicks) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ClickLog + for rows.Next() { + var i ClickLog + if err := rows.Scan( + &i.ClickID, + &i.UrlID, + &i.ClickedAt, + &i.Referrer, + &i.UserAgent, + &i.IpAddress, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const trackClick = `-- name: TrackClick :one +INSERT INTO click_logs (url_id, referrer, user_agent, ip_address) +VALUES ( + $1, + $2, + $3, + $4 +) +RETURNING click_id, url_id, clicked_at, referrer, user_agent, ip_address +` + +type TrackClickParams struct { + UrlID int32 + Referrer sql.NullString + UserAgent sql.NullString + IpAddress sql.NullString +} + +func (q *Queries) TrackClick(ctx context.Context, arg TrackClickParams) (ClickLog, error) { + row := q.db.QueryRowContext(ctx, trackClick, + arg.UrlID, + arg.Referrer, + arg.UserAgent, + arg.IpAddress, + ) + var i ClickLog + err := row.Scan( + &i.ClickID, + &i.UrlID, + &i.ClickedAt, + &i.Referrer, + &i.UserAgent, + &i.IpAddress, + ) + return i, err +} diff --git a/internal/database/db.go b/internal/database/db.go new file mode 100644 index 0000000..9f7a5a6 --- /dev/null +++ b/internal/database/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package database + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/database/models.go b/internal/database/models.go new file mode 100644 index 0000000..7ba9ba9 --- /dev/null +++ b/internal/database/models.go @@ -0,0 +1,28 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package database + +import ( + "database/sql" + "time" +) + +type ClickLog struct { + ClickID int64 + UrlID int32 + ClickedAt time.Time + Referrer sql.NullString + UserAgent sql.NullString + IpAddress sql.NullString +} + +type Url struct { + UrlID int32 + LongUrl string + ShortCode string + CreatedAt time.Time + ExpiresAt sql.NullTime + IsActive bool +} diff --git a/internal/database/urls.sql.go b/internal/database/urls.sql.go new file mode 100644 index 0000000..d1ca403 --- /dev/null +++ b/internal/database/urls.sql.go @@ -0,0 +1,143 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: urls.sql + +package database + +import ( + "context" + "database/sql" + "time" +) + +const createURL = `-- name: CreateURL :one +INSERT INTO urls (created_at, expires_at, long_url, short_code, is_active) +VALUES ( + $1, + $2, + $3, + $4, + $5 +) +RETURNING url_id, long_url, short_code, created_at, expires_at, is_active +` + +type CreateURLParams struct { + CreatedAt time.Time + ExpiresAt sql.NullTime + LongUrl string + ShortCode string + IsActive bool +} + +func (q *Queries) CreateURL(ctx context.Context, arg CreateURLParams) (Url, error) { + row := q.db.QueryRowContext(ctx, createURL, + arg.CreatedAt, + arg.ExpiresAt, + arg.LongUrl, + arg.ShortCode, + arg.IsActive, + ) + var i Url + err := row.Scan( + &i.UrlID, + &i.LongUrl, + &i.ShortCode, + &i.CreatedAt, + &i.ExpiresAt, + &i.IsActive, + ) + return i, err +} + +const deleteURL = `-- name: DeleteURL :one +DELETE FROM urls WHERE url_id = $1 RETURNING url_id, long_url, short_code, created_at, expires_at, is_active +` + +func (q *Queries) DeleteURL(ctx context.Context, urlID int32) (Url, error) { + row := q.db.QueryRowContext(ctx, deleteURL, urlID) + var i Url + err := row.Scan( + &i.UrlID, + &i.LongUrl, + &i.ShortCode, + &i.CreatedAt, + &i.ExpiresAt, + &i.IsActive, + ) + return i, err +} + +const getURLByShortCode = `-- name: GetURLByShortCode :one +SELECT url_id, long_url FROM urls WHERE is_active = true AND short_code = $1 +` + +type GetURLByShortCodeRow struct { + UrlID int32 + LongUrl string +} + +func (q *Queries) GetURLByShortCode(ctx context.Context, shortCode string) (GetURLByShortCodeRow, error) { + row := q.db.QueryRowContext(ctx, getURLByShortCode, shortCode) + var i GetURLByShortCodeRow + err := row.Scan(&i.UrlID, &i.LongUrl) + return i, err +} + +const getURLs = `-- name: GetURLs :many +SELECT url_id, long_url, short_code, created_at, expires_at, is_active FROM urls +` + +func (q *Queries) GetURLs(ctx context.Context) ([]Url, error) { + rows, err := q.db.QueryContext(ctx, getURLs) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Url + for rows.Next() { + var i Url + if err := rows.Scan( + &i.UrlID, + &i.LongUrl, + &i.ShortCode, + &i.CreatedAt, + &i.ExpiresAt, + &i.IsActive, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const setActive = `-- name: SetActive :one +UPDATE urls SET is_active = $1 WHERE url_id = $2 RETURNING url_id, long_url, short_code, created_at, expires_at, is_active +` + +type SetActiveParams struct { + IsActive bool + UrlID int32 +} + +func (q *Queries) SetActive(ctx context.Context, arg SetActiveParams) (Url, error) { + row := q.db.QueryRowContext(ctx, setActive, arg.IsActive, arg.UrlID) + var i Url + err := row.Scan( + &i.UrlID, + &i.LongUrl, + &i.ShortCode, + &i.CreatedAt, + &i.ExpiresAt, + &i.IsActive, + ) + return i, err +} diff --git a/sql/queries/click_log.sql b/sql/queries/click_log.sql new file mode 100644 index 0000000..3981632 --- /dev/null +++ b/sql/queries/click_log.sql @@ -0,0 +1,12 @@ +-- name: TrackClick :one +INSERT INTO click_logs (url_id, referrer, user_agent, ip_address) +VALUES ( + $1, + $2, + $3, + $4 +) +RETURNING *; + +-- name: GetClicks :many +SELECT * FROM click_logs; \ No newline at end of file diff --git a/sql/queries/urls.sql b/sql/queries/urls.sql new file mode 100644 index 0000000..e93f714 --- /dev/null +++ b/sql/queries/urls.sql @@ -0,0 +1,22 @@ +-- name: CreateURL :one +INSERT INTO urls (created_at, expires_at, long_url, short_code, is_active) +VALUES ( + $1, + $2, + $3, + $4, + $5 +) +RETURNING *; + +-- name: GetURLs :many +SELECT * FROM urls; + +-- name: GetURLByShortCode :one +SELECT url_id, long_url FROM urls WHERE is_active = true AND short_code = $1; + +-- name: SetActive :one +UPDATE urls SET is_active = $1 WHERE url_id = $2 RETURNING *; + +-- name: DeleteURL :one +DELETE FROM urls WHERE url_id = $1 RETURNING *; \ No newline at end of file diff --git a/sql/schema/001_create_url_table.sql b/sql/schema/001_create_url_table.sql new file mode 100644 index 0000000..ed01f29 --- /dev/null +++ b/sql/schema/001_create_url_table.sql @@ -0,0 +1,16 @@ +-- +goose Up +CREATE TABLE urls ( + url_id SERIAL PRIMARY KEY, + long_url TEXT NOT NULL, + short_code VARCHAR(10) UNIQUE NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + expires_at TIMESTAMP WITH TIME ZONE, + is_active BOOLEAN DEFAULT TRUE NOT NULL +); + +-- Crucial index for lightning-fast lookups when redirecting +CREATE INDEX idx_urls_short_code ON urls(short_code); + +-- +goose Down +DROP INDEX idx_urls_short_code; +DROP TABLE urls; \ No newline at end of file diff --git a/sql/schema/002_create_click_logs_table.sql b/sql/schema/002_create_click_logs_table.sql new file mode 100644 index 0000000..0fa0b88 --- /dev/null +++ b/sql/schema/002_create_click_logs_table.sql @@ -0,0 +1,16 @@ +-- +goose Up +CREATE TABLE click_logs ( + click_id BIGSERIAL PRIMARY KEY, + url_id INT NOT NULL REFERENCES urls(url_id) ON DELETE CASCADE, + clicked_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + referrer VARCHAR(255), + user_agent TEXT, + ip_address VARCHAR(45) -- Accommodates both IPv4 and IPv6 +); + +-- Index to optimize analytics queries for a specific link +CREATE INDEX idx_click_logs_url_id ON click_logs(url_id); + +-- +goose Down +DROP INDEX idx_click_logs_url_id; +DROP TABLE click_logs; \ No newline at end of file diff --git a/sqlc.yaml b/sqlc.yaml new file mode 100644 index 0000000..c20ea46 --- /dev/null +++ b/sqlc.yaml @@ -0,0 +1,8 @@ +version: "2" +sql: + - schema: "sql/schema" + queries: "sql/queries" + engine: "postgresql" + gen: + go: + out: "internal/database" \ No newline at end of file