Automating SQL execution with Go: development experience and setting up notifications

Automating SQL execution with Go: development experience and setting up notifications

In today's world, automating routine processes helps save time and avoid mistakes. In one of my projects, I had a task to automatically execute SQL scripts that appear in a certain folder, and then send a full report on the execution to the administrator's email. To solve this task, I decided to write a specialized microservice in Go language, which constantly monitors the specified directory, executes the found SQL-files and notifies the responsible persons about the results of the work. Next I will detail the steps of implementation, the use of an external configuration file and the peculiarities of the approach.

Step 1: Project initialization and application configuration

The first step was to develop the project structure and create a configuration file. For flexibility of work all settings - database connection parameters, information for SMTP-notifications and paths to working folders - are placed in a separate config.yml file. This makes it easy to change the parameters without changing the source code.

database:
  host: localhost
  port: 3306
  user: root
  password: secret
  name: mydb

smtp:
  server: smtp.example.com
  port: 587
  username: user@example.com
  password: smtp-password
  from: noreply@example.com
  to: admin@example.com

paths:
  input: ./sql/input
  output: ./sql/processed
  error: ./sql/errors

Step 2: Connecting to the database

After loading the settings from the file, you need to establish a connection to the database. For this purpose I used a package for working with MySQL. It is necessary to establish a connection to the database and check the correctness of interaction with the database.

func initDB() {
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true&multiStatements=true",
		config.Database.User,
		config.Database.Password,
		config.Database.Host,
		config.Database.Port,
		config.Database.Name)

	var err error
	db, err = sql.Open("mysql", dsn)
	if err != nil {
		log.Fatalf("Error connecting to database: %v", err)
	}

	err = db.Ping()
	if err != nil {
		log.Fatalf("Error pinging database: %v", err)
	}
}

Step 3: Folder monitoring and file processing

The main functionality of the script is to constantly monitor the specified directory. Every few seconds the application checks for new SQL files, and if they are detected, it reads the contents of the file and tries to execute SQL queries in the database.

Features of this stage:

  • Checking each file for extra characters (e.g., BOM) - this is important for correct query execution.
  • Logging errors in reading or executing SQL queries for crash analysis.
  • Separating files into successfully executed and those that caused an error by moving them to the appropriate folders (done and failed).
func processFiles() {
	files, err := os.ReadDir(config.Paths.Input)
	if err != nil {
		log.Printf("Error reading input directory: %v", err)
		return
	}

	for _, file := range files {
		if file.IsDir() {
			continue
		}

		filePath := filepath.Join(config.Paths.Input, file.Name())
		processFile(filePath)
	}
}

func processFile(filePath string) {
	data, err := os.ReadFile(filePath)
	if err != nil {
		log.Printf("Error reading file %s: %v", filePath, err)
		return
	}

	err = executeSQL(string(data))
	if err != nil {
		handleError(filePath, err)
	} else {
		handleSuccess(filePath)
	}
}

Step 4: Realization of SQL query execution logic

Each SQL file is checked before execution, after which a SQL query is executed using db.Exec. If the query is successfully executed, the file is moved to the archived scripts folder; otherwise, it is moved to the errors folder. This approach avoids repeated execution of the same query and helps to track which files need repeated attention.

func executeSQL(query string) error {
	if strings.Contains(query, "\ufeff") {
		query = strings.ReplaceAll(query, "\ufeff", "")
	}

	_, err := db.Exec(query)
	return err
}

Step 5: Sending the progress report to email

At the end of the script operation, the important point is to send a report to the administrator. Each time an operation is executed (successfully or with an error), a corresponding notification is sent via SMTP. The use of a secure TLS connection ensures the security of data transmission. This allows you to quickly receive feedback on the script operation and prevent possible problems in the system operation.

func handleError(filePath string, err error) {
	log.Printf("Error executing SQL: %v", err)
	sendEmail("SQL Execution Error", fmt.Sprintf("Error: %v\nFile: %s", err, filePath))
	moveFile(filePath, config.Paths.Failed)
}

Advantages and disadvantages of the approach

Advantages:

  • Automation of routine tasks can significantly reduce the time to complete operations and reduce the risk of errors.
  • Flexibility of settings via an external configuration file makes it possible to easily adapt the script to different environments.
  • Sending reports via e-mail ensures that responsible persons are informed promptly.

Disadvantages:

  • Constant disk folder checking can put a strain on the system with a large number of files if an optimal caching or queuing strategy is not implemented.
  • Synchronization issues may arise in case of large volume of SQL queries, especially if the queries require significant resources.
  • The presence of minimal error handling in sprint queries requires additional monitoring for timely response.

Conclusion

Developing this microservice was a unique experience for me in automating routine tasks using Go. Proper separation of logic into configuration, database connectivity, file monitoring and notifications makes the system flexible and scalable. This approach not only minimizes the risks associated with manual execution of SQL scripts, but also allows you to quickly respond to issues through reporting.

If you are facing similar challenges, this example can be a good starting point for creating a reliable and easy-to-use solution.

package main

import (
	"crypto/tls"
	"database/sql"
	"fmt"
	_ "github.com/octoper/go-ray"
	"log"
	"net/smtp"
	"os"
	"path/filepath"
	"strings"
	"time"

	_ "github.com/go-sql-driver/mysql"
	"gopkg.in/yaml.v3"
)

type Config struct {
	Database struct {
		Host     string `yaml:"host"`
		Port     int    `yaml:"port"`
		User     string `yaml:"user"`
		Password string `yaml:"password"`
		Name     string `yaml:"name"`
	} `yaml:"database"`

	SMTP struct {
		Server   string `yaml:"server"`
		Port     int    `yaml:"port"`
		Username string `yaml:"username"`
		Password string `yaml:"password"`
		From     string `yaml:"from"`
		To       string `yaml:"to"`
	} `yaml:"smtp"`

	Paths struct {
		Input  string `yaml:"input"`
		Done   string `yaml:"done"`
		Failed string `yaml:"failed"`
	} `yaml:"paths"`
}

var (
	config Config
	db     *sql.DB
)

func main() {
	loadConfig("config.yml")

	initDB()
	defer db.Close()

	for {
		processFiles()
		time.Sleep(5 * time.Second) // Проверка каждые 5 секунд
	}
}

func loadConfig(filename string) {
	data, err := os.ReadFile(filename)
	if err != nil {
		log.Fatalf("Error reading config file: %v", err)
	}

	err = yaml.Unmarshal(data, &config)
	if err != nil {
		log.Fatalf("Error parsing config file: %v", err)
	}
}

func initDB() {
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true&multiStatements=true",
		config.Database.User,
		config.Database.Password,
		config.Database.Host,
		config.Database.Port,
		config.Database.Name)

	var err error
	db, err = sql.Open("mysql", dsn)
	if err != nil {
		log.Fatalf("Error connecting to database: %v", err)
	}

	err = db.Ping()
	if err != nil {
		log.Fatalf("Error pinging database: %v", err)
	}
}

func processFiles() {
	files, err := os.ReadDir(config.Paths.Input)
	if err != nil {
		log.Printf("Error reading input directory: %v", err)
		return
	}

	for _, file := range files {
		if file.IsDir() {
			continue
		}

		filePath := filepath.Join(config.Paths.Input, file.Name())
		processFile(filePath)
	}
}

func processFile(filePath string) {
	data, err := os.ReadFile(filePath)
	if err != nil {
		log.Printf("Error reading file %s: %v", filePath, err)
		return
	}

	err = executeSQL(string(data))
	if err != nil {
		handleError(filePath, err)
	} else {
		handleSuccess(filePath)
	}
}

func executeSQL(query string) error {
	if strings.Contains(query, "\ufeff") {
		query = strings.ReplaceAll(query, "\ufeff", "")
	}

	_, err := db.Exec(query)
	return err
}

func handleError(filePath string, err error) {
	log.Printf("Error executing SQL: %v", err)
	sendEmail("SQL Execution Error", fmt.Sprintf("Error: %v\nFile: %s", err, filePath))
	moveFile(filePath, config.Paths.Failed)
}

func handleSuccess(filePath string) {
	log.Println("SQL executed successfully")
	sendEmail("SQL Execution Success", fmt.Sprintf("File: %s", filePath))
	moveFile(filePath, config.Paths.Done)
}

func moveFile(source, destDir string) {
	fileName := filepath.Base(source)
	destPath := filepath.Join(destDir, fileName)

	err := os.Rename(source, destPath)
	if err != nil {
		log.Printf("Error moving file: %v", err)
	}
}

func sendEmail(subject, body string) {
	tlsConfig := &tls.Config{
		ServerName:         config.SMTP.Server,
		InsecureSkipVerify: false,
	}

	conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", config.SMTP.Server, config.SMTP.Port), tlsConfig)
	if err != nil {
		log.Printf("Error creating TLS connection: %v", err)
		return
	}
	defer conn.Close()

	client, err := smtp.NewClient(conn, config.SMTP.Server)
	if err != nil {
		log.Printf("Error creating SMTP client: %v", err)
		return
	}
	defer client.Close()

	auth := smtp.PlainAuth("", config.SMTP.Username, config.SMTP.Password, config.SMTP.Server)
	if err := client.Auth(auth); err != nil {
		log.Printf("SMTP auth error: %v", err)
		return
	}

	if err := client.Mail(config.SMTP.From); err != nil {
		log.Printf("Mail command error: %v", err)
		return
	}
	if err := client.Rcpt(config.SMTP.To); err != nil {
		log.Printf("Rcpt command error: %v", err)
		return
	}

	wc, err := client.Data()
	if err != nil {
		log.Printf("Data command error: %v", err)
		return
	}
	defer wc.Close()

	msg := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s",
		config.SMTP.From,
		config.SMTP.To,
		subject,
		body,
	)

	if _, err = fmt.Fprint(wc, msg); err != nil {
		log.Printf("Error writing message: %v", err)
		return
	}
}

 

Popular Posts

My most popular posts

Maximum productivity on remote job
Business

Maximum productivity on remote job

I started my own business and intentionally did my best to work from anywhere in the world. Sometimes I sit with my office with a large 27-inch monitor in my apartment in Cheboksary. Sometimes I’m in the office or in some cafe in another city.

Hello! I am Sergey Emelyanov and I am hardworker
Business PHP

Hello! I am Sergey Emelyanov and I am hardworker

I am a programmer. I am an entrepreneur in my heart. I started making money from the age of 11, in the harsh 90s, handing over glassware to a local store and exchanging it for sweets. I earned so much that was enough for various snacks.

Hire Professional CRM developer for $25 per hour

I will make time for your project. Knowledge of Vtiger CRM, SuiteCRM, Laravel, Vue.js, Wordpress. I offer cooperation options that will help you take advantage of external experience, optimize costs and reduce risks. Full transparency of all stages of work and accounting for time costs. Pay only development working hours after accepting the task. Accept PayPal and Payoneer payment systems. How to hire professional developer? Just fill in the form

Telegram
@sergeyem
Telephone
+4915211100235
Email