siteviewcounter/countersql/database.go
Steven Polley 7a7993abd8
All checks were successful
continuous-integration/drone/push Build is passing
initial commit
2020-06-03 20:40:13 -06:00

641 lines
19 KiB
Go

package countersql
import (
"database/sql"
"fmt"
)
// Configuration holds the DSN connection string and a resource Semaphore to limit the number of active connections
type Configuration struct {
DSN string
}
// Connection represents a single connection to the database, however there may be many instances / connections
type Connection struct {
DB *sql.DB
}
// HasIPVisited returns true only if the IP address is present in the database
func (conn Connection) HasIPVisited(ipAddress string) (bool, error) {
rows, err := conn.DB.Query(`SELECT id FROM visit WHERE ip_address = ? LIMIT 1;`, ipAddress)
if err != nil {
return false, fmt.Errorf("SELECT query failed: %v", err)
}
defer rows.Close()
if !rows.Next() {
return false, nil
}
return true, nil
}
// GetUniqueVisits counts the number of entires in the visits table, representing one unique source IP address per row
func (conn Connection) GetUniqueVisits() (int, error) {
rows, err := conn.DB.Query(`SELECT COUNT(*) FROM visit`)
if err != nil {
return 0, fmt.Errorf("SELECT query failed: %v", err)
}
defer rows.Close()
if !rows.Next() {
return 0, nil
}
var uniqueVists int
if err := rows.Scan(&uniqueVists); err != nil {
return 0, fmt.Errorf("failed to scan database row: %v", err)
}
return uniqueVists, nil
}
// IncrementVisitor accepts an IP address and updates the row matching that IP address
// It does not check if the row matching the IP address supplied exists or not
func (conn Connection) IncrementVisitor(ipAddress string) error {
_, err := conn.DB.Exec(`UPDATE visit SET visits = visits + 1, last_visited = NOW() WHERE ip_address = ?`, ipAddress)
if err != nil {
return fmt.Errorf("UPDATE query failed: %v", err)
}
return nil
}
// AddVisitor accepts an IP address and inserts a new row into the database as this represents a new unique visitor
func (conn Connection) AddVisitor(ipAddress string) error {
_, err := conn.DB.Exec(`INSERT INTO visit (ip_address, visits, last_visited) VALUES (?, '0', NOW())`, ipAddress)
if err != nil {
return fmt.Errorf("INSERT query failed: %v", err)
}
return nil
}
//
/*
// ************************************
// Task Database Functions
// ************************************
// GetTasks returns a slice of all tasks in the database. It requires
// a boolean parameter. If true, it only returns active tasks. If false
// it will return all tasks. A task is active if the time now is past
// the planned_start date.
func (conn Connection) GetTasks(activeTasksOnly bool) ([]Task, error) {
var rows *sql.Rows
var err error
if activeTasksOnly {
rows, err = conn.DB.Query(`SELECT
tasks.id, tasks.name, tasks.weight, tasks.timecost, tasks.planned_start, tasks.planned_end, tasks.actual_start, tasks.actual_end, tasks.expire_action, tasks.close_code, lists.id, lists.name
FROM tasks
INNER JOIN lists
ON tasks.list_id = lists.id
WHERE tasks.close_code IS NULL AND tasks.planned_start < ?
ORDER BY tasks.actual_start DESC, tasks.weight DESC`, time.Now().Add(-time.Hour*6))
} else {
rows, err = conn.DB.Query(`SELECT
tasks.id, tasks.name, tasks.weight, tasks.timecost, tasks.planned_start, tasks.planned_end, tasks.actual_start, tasks.actual_end, tasks.expire_action, tasks.close_code, lists.id, lists.name
FROM tasks
INNER JOIN lists
ON tasks.list_id = lists.id
ORDER BY tasks.actual_start DESC, tasks.weight DESC`)
}
if err != nil {
return nil, fmt.Errorf("SELECT query failed: %v", err)
}
defer rows.Close()
if !rows.Next() {
return nil, nil
}
var tasks []Task
for {
task := Task{}
if err := rows.Scan(&task.ID, &task.Name, &task.Weight, &task.TimeCost, &task.PlannedStart, &task.PlannedEnd, &task.ActualStart, &task.ActualEnd, &task.ExpireAction, &task.CloseCode, &task.List.ID, &task.List.Name); err != nil {
return nil, fmt.Errorf("failed to scan database row: %v", err)
}
tasks = append(tasks, task)
if !rows.Next() {
break
}
}
return tasks, nil
}
// GetTasksByListID returns a slice of all tasks on a specific list in the database
func (conn Connection) GetTasksByListID(listID int, activeTasksOnly bool) ([]Task, error) {
activeTasksQuery := ""
if activeTasksOnly {
activeTasksQuery = " AND tasks.close_code IS NULL"
}
rows, err := conn.DB.Query(`SELECT
tasks.id, tasks.name, tasks.weight, tasks.timecost, tasks.planned_start, tasks.planned_end, tasks.actual_start, tasks.actual_end, tasks.expire_action, tasks.close_code, lists.id, lists.name
FROM tasks
INNER JOIN lists ON tasks.list_id = list.id
WHERE lists.id = ? `+activeTasksQuery, listID)
if err != nil {
return nil, fmt.Errorf("SELECT query failed: %v", err)
}
defer rows.Close()
if !rows.Next() {
return nil, nil
}
var tasks []Task
for {
task := Task{}
if err := rows.Scan(&task.ID, &task.Name, &task.Weight, &task.TimeCost, &task.PlannedStart, &task.PlannedEnd, &task.ActualStart, &task.ActualEnd, &task.ExpireAction, &task.CloseCode, &task.List.ID, &task.List.Name); err != nil {
return nil, fmt.Errorf("failed to scan database row: %v", err)
}
tasks = append(tasks, task)
if !rows.Next() {
break
}
}
return tasks, nil
}
// GetTaskByID returns a task matching the ID provided
func (conn Connection) GetTaskByID(taskID int) (*Task, error) {
rows, err := conn.DB.Query(`SELECT
tasks.id, tasks.name, tasks.weight, tasks.timecost, tasks.planned_start, tasks.planned_end, tasks.actual_start, tasks.actual_end, tasks.expire_action, tasks.close_code, lists.id, lists.name
FROM tasks
INNER JOIN lists ON tasks.list_id = list.id
WHERE tasks.id = ? `, taskID)
if err != nil {
return nil, fmt.Errorf("SELECT query failed: %v", err)
}
defer rows.Close()
if !rows.Next() {
return nil, fmt.Errorf("no results returned for taskID '%d'", taskID)
}
task := Task{}
if err := rows.Scan(&task.ID, &task.Name, &task.Weight, &task.TimeCost, &task.PlannedStart, &task.PlannedEnd, &task.ActualStart, &task.ActualEnd, &task.ExpireAction, &task.CloseCode, &task.List.ID, &task.List.Name); err != nil {
return nil, fmt.Errorf("failed to scan database row: %v", err)
}
return &task, nil
}
// NewTask will insert a row into the database, given a pre-instantiated task
func (conn Connection) NewTask(task *Task) error {
_, err := conn.DB.Exec(`INSERT INTO tasks
(list_id, name, weight, timecost, planned_start, planned_end, actual_start, actual_end, expire_action, close_code)
VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, ?, NULL)`, task.List.ID, task.Name, task.Weight, task.TimeCost, task.PlannedStart, task.PlannedEnd, task.ExpireAction)
if err != nil {
return fmt.Errorf("INSERT query failed: %v", err)
}
return nil
}
// DeleteTaskByID will remove a row from the database, given a list ID
func (conn Connection) DeleteTaskByID(taskID int) error {
_, err := conn.DB.Exec("DELETE FROM tasks WHERE tasks.id = ?", taskID)
if err != nil {
return fmt.Errorf("Delete query failed: %v", err)
}
return nil
}
// StartTask will set the actual_start to now, given a task ID. This marks the task as active and started
func (conn Connection) StartTask(taskID int) error {
_, err := conn.DB.Exec("UPDATE tasks SET actual_start = now() WHERE tasks.id = ?", taskID)
if err != nil {
return fmt.Errorf("Delete query failed: %v", err)
}
return nil
}
// UpdateTask will update a task row in the database, given a pre-instantiated task
func (conn Connection) UpdateTask(task *Task) error {
var err error
_, err = conn.DB.Exec(`UPDATE tasks SET
list_id = ?,
name = ?,
weight = ?,
timecost = ?,
planned_start = ?,
planned_end = ?,
actual_start = ?,
actual_end = ?,
expire_action = ?,
close_code = ?
WHERE id = ?`,
task.List.ID, task.Name, task.Weight, task.TimeCost, task.PlannedStart, task.PlannedEnd, task.ActualStart, task.ActualEnd, task.ExpireAction, task.CloseCode, task.ID)
if err != nil {
return fmt.Errorf("UPDATE task query failed for %d: %v", task.ID, err)
}
return nil
}
// StopTask will stop a task by setting the close code
// closeCode can be: completed, cancelled or reset
// completed is self explanatory.
// cancelled indicates the task itself is being canelled / skipped
// reset is not an official close code, but indicates that actual_start should be cleared (timer reset)
func (conn Connection) StopTask(taskID int, closeCode string) error {
var err error
if closeCode == "reset" {
_, err = conn.DB.Exec("UPDATE tasks SET actual_start = NULL WHERE tasks.id = ?", taskID)
} else if closeCode == "cancelled" {
_, err = conn.DB.Exec("UPDATE tasks SET actual_end = now(), close_code = 'cancelled' WHERE tasks.id = ?", taskID)
} else if closeCode == "completed" {
_, err = conn.DB.Exec("UPDATE tasks SET actual_end = now(), close_code = 'completed' WHERE tasks.id = ?", taskID)
} else {
err = fmt.Errorf("invalid close code: '%s' - must be reset, cancelled or completed", closeCode)
}
if err != nil {
return fmt.Errorf("StopTask query failed: %v", err)
}
return nil
}
// ************************************
// List Database Functions
// ************************************
// GetLists returns a slice of all lists in the database.
// If joinLocations is true, it will grab the locations as well
func (conn Connection) GetLists(getListLocations bool) ([]List, error) {
rows, err := conn.DB.Query(`SELECT
lists.id, lists.name
FROM lists`)
if err != nil {
return nil, fmt.Errorf("SELECT query failed: %v", err)
}
defer rows.Close()
if !rows.Next() {
return nil, nil
}
var lists []List
for {
list := List{}
if err := rows.Scan(&list.ID, &list.Name); err != nil {
return nil, fmt.Errorf("failed to scan database row: %v", err)
}
lists = append(lists, list)
if !rows.Next() {
break
}
}
if getListLocations {
wg := sync.WaitGroup{}
m := sync.Mutex{}
for i := range lists {
wg.Add(1)
go func(i int) {
defer wg.Done()
rows, err := conn.DB.Query(`SELECT
listlocations.location_id, locations.name
FROM listlocations
INNER JOIN locations
ON listlocations.location_id = locations.id
WHERE list_id = ?`, lists[i].ID)
defer rows.Close()
if err != nil {
log.Printf("SELECT listlocations query failed: %v", err)
return
}
if !rows.Next() {
return
}
var locations []Location
for {
var location Location
if err := rows.Scan(&location.ID, &location.Name); err != nil {
log.Printf("failed to scan database row: %v", err)
continue
}
locations = append(locations, location)
if !rows.Next() {
break
}
}
m.Lock()
lists[i].Locations = locations
m.Unlock()
}(i)
}
wg.Wait() // wait
}
return lists, nil
}
// GetListByID will return a single list in the database with the given ID
func (conn Connection) GetListByID(listID int, getListLocations bool) (*List, error) {
rows, err := conn.DB.Query(`SELECT
lists.name
FROM lists
WHERE lists.id = ?
LIMIT 1`, listID)
if err != nil {
return nil, fmt.Errorf("SELECT query failed: %v", err)
}
defer rows.Close()
if !rows.Next() {
return nil, fmt.Errorf("no results returned for listID '%d'", listID)
}
list := List{}
if err := rows.Scan(&list.Name); err != nil {
return nil, fmt.Errorf("failed to scan database row: %v", err)
}
list.ID = listID
if getListLocations {
rows, err := conn.DB.Query(`SELECT
listlocations.location_id
FROM listlocations
WHERE list_id = ?`, list.ID)
defer rows.Close()
if err != nil {
return nil, fmt.Errorf("SELECT listlocations query failed: %v", err)
}
if !rows.Next() {
return nil, fmt.Errorf("no results returned for listID '%d'", listID)
}
var locations []Location
for {
var location Location
if err := rows.Scan(&location.ID); err != nil {
log.Printf("failed to scan database row: %v", err)
break
}
locations = append(locations, location)
if !rows.Next() {
break
}
}
list.Locations = locations
}
return &list, nil
}
// NewList will insert a row into the database, given a pre-instantiated list
func (conn Connection) NewList(list *List) error {
result, err := conn.DB.Exec(`INSERT
INTO lists
(name)
VALUES (?)`, list.Name)
if err != nil {
return fmt.Errorf("INSERT list query failed: %v", err)
}
lastInsertID, _ := result.LastInsertId()
query := ` INSERT
INTO listlocations
(list_id, location_id)
VALUES `
// TBD: Handle list location mapping inserts here, loop through and build query, then execute it.
values := []interface{}{}
for _, listLocation := range list.Locations {
values = append(values, lastInsertID, listLocation.ID)
// the rest of this loop is magic lifted from https://play.golang.org/p/YqNJKybpwWB
numFields := 2
query += `(`
for j := 0; j < numFields; j++ {
query += `?,`
}
query = query[:len(query)-1] + `),`
}
query = query[:len(query)-1] // this is also part of the above mentioned magic
_, err = conn.DB.Exec(query, values...)
if err != nil {
return fmt.Errorf("INSERT listlocations query failed: %v", err)
}
return nil
}
// UpdateList will update a list row in the database, given a pre-instantiated list
func (conn Connection) UpdateList(list *List) error {
wg := sync.WaitGroup{}
wg.Add(2)
var err error
go func() {
defer wg.Done()
_, err = conn.DB.Exec(`UPDATE lists
(name)
VALUES (?)`, list.Name)
if err != nil {
err = fmt.Errorf("INSERT list query failed: %v", err)
return
}
}()
// Handle list locations... Have fun
go func() {
defer wg.Done()
var rows *sql.Rows
rows, err = conn.DB.Query(`SELECT
listlocations.location_id
WHERE list_id = ?`, list.ID)
if err != nil {
err = fmt.Errorf("SELECT listlocations query failed: %v", err)
return
}
defer rows.Close()
var locationIDsToRemove, locationIDsToAdd, dbIDs []int
if rows.Next() { // If this is false, the list has no assigned locations in the database currently
// We loop through databaseRows and determine locations that need to be removed
for {
var currDBID int
if err := rows.Scan(&currDBID); err != nil {
err = fmt.Errorf("failed to scan listlocations database row: %v", err)
return
}
matchFound := false
for _, currNewLocation := range list.Locations {
if currNewLocation.ID == currDBID { // If we find a match, we know this is a location that both exists in the database, and is set on the new object, so we wouldn't take action
matchFound = true
continue
}
}
if !matchFound { // If we don't find a match, we know that this row in the database should be removed because it's no longer set on the new listObject
locationIDsToRemove = append(locationIDsToRemove, currDBID)
}
// We collect the locationIDs while looping through the database rows so we can re-use them below
dbIDs = append(dbIDs, currDBID)
if !rows.Next() {
break
}
}
// We loop through the newly set locations and determine any that need to be added to the database
for _, currNewLocation := range list.Locations {
matchFound := false
for _, currDBID := range dbIDs {
if currNewLocation.ID == currDBID { // If we find a match, we know the location is set on the new object and already exists in the database, so we take no action
matchFound = true
continue
}
}
if !matchFound { // If we don't find a match, we know the location is set on the new object, but doesn't exist in the database, so we need to add it
locationIDsToAdd = append(locationIDsToAdd, currNewLocation.ID)
}
}
} else { // If there are no rows in the database then we know we just need to all all locations set on this new list object and put them in the database
for _, currNewLocation := range list.Locations {
locationIDsToAdd = append(locationIDsToAdd, currNewLocation.ID)
}
}
// We now have populated locationIDsToAdd and locationIDsToRemove, so we just need to build and execute the queries
wg.Add(2)
go func() {
defer wg.Done()
query := ` INSERT
INTO listlocations
(list_id, location_id)
VALUES `
// TBD: Handle list location mapping inserts here, loop through and build query, then execute it.
values := []interface{}{}
for _, locationID := range locationIDsToAdd {
values = append(values, list.ID, locationID)
// the rest of this loop is magic lifted from https://play.golang.org/p/YqNJKybpwWB
numFields := 2
query += `(`
for j := 0; j < numFields; j++ {
query += `?,`
}
query = query[:len(query)-1] + `),`
}
query = query[:len(query)-1] // this is also part of the above mentioned magic
_, err = conn.DB.Exec(query, values...)
if err != nil {
err = fmt.Errorf("INSERT listlocations query failed: %v", err)
return
}
}()
go func() {
defer wg.Done()
query := ` DELETE
FROM listlocations
WHERE `
values := []interface{}{}
for _, locationID := range locationIDsToRemove {
values = append(values, list.ID, locationID)
query += `(list_id = ? AND location_id = ?) OR`
}
query = query[:len(query)-3]
_, err = conn.DB.Exec(query, values...)
if err != nil {
err = fmt.Errorf("DELETE listlocations query failed: %v", err)
return
}
}()
}()
wg.Wait()
if err != nil {
return fmt.Errorf("a goroutine in UpdateList failed: %v", err)
}
return nil
}
// DeleteListByID will remove a row from the database, given a list ID
func (conn Connection) DeleteListByID(listID int) error {
_, err := conn.DB.Exec("DELETE FROM lists WHERE lists.id = ?", listID)
if err != nil {
return fmt.Errorf("Delete query failed: %v", err)
}
return nil
}
// ************************************
// Location Database Functions
// ************************************
// GetLocations returns a slice of all locations in the database
func (conn Connection) GetLocations() ([]Location, error) {
rows, err := conn.DB.Query(`SELECT
locations.id, locations.name, locations.mac_address
FROM locations`)
if err != nil {
return nil, fmt.Errorf("SELECT query failed: %v", err)
}
defer rows.Close()
if !rows.Next() {
return nil, nil
}
var locations []Location
for {
location := Location{}
if err := rows.Scan(&location.ID, &location.Name, &location.MACAddress); err != nil {
return nil, fmt.Errorf("failed to scan database row: %v", err)
}
locations = append(locations, location)
if !rows.Next() {
break
}
}
return locations, nil
}
// DeleteLocationByID will remove a row from the database, given a list ID
func (conn Connection) DeleteLocationByID(locationID int) error {
_, err := conn.DB.Exec(`DELETE
FROM locations
WHERE locations.id = ?`, locationID)
if err != nil {
return fmt.Errorf("Delete query failed: %v", err)
}
return nil
}
*/
// Connect will open a TCP connection to the database with the given DSN configuration
func (conf Configuration) Connect() (*Connection, error) {
conn := &Connection{}
var err error
conn.DB, err = sql.Open("mysql", conf.DSN)
if err != nil {
return nil, fmt.Errorf("failed to open db: %v", err)
}
return conn, nil
}