DEV Community

Tobias
Tobias

Posted on

An Introduction to Go Ast - Generating SQL Queries Based on Structs

The Go standard library includes an 'ast' package, which provides a set of tools for working with the abstract syntax tree (AST). The AST represents the structure of a Go program in a tree-like data structure and can be used to analyze, modify, or generate code programmatically. In this post, we investigate the possibility of analyzing structs and generating a SQL query based on them.

Goal

Our goal involves generating a SQL query based on a given struct with specific formatting requirements. The struct name should be pluralized and lowercase, while the field names should be in snake_case and also lowercase.

For example, given the following struct

type User struct { ID int Name string InvitationCode string } 
Enter fullscreen mode Exit fullscreen mode

The generated SQL query should be an INSERT statement into a table named users with columns id, name, and invitation_code. The values to be inserted should be represented using placeholders in the query, such as :id, :name, and :invitation_code.

Formatting Requirements

Let's create some helper functions to normalize the structs and field names.
The first requirement we look at is to transform UpperCamelCase into snake_case

We can do this by simply iterating over the string and anytime we see a uppercase character and the previous character is lower case we insert a underscore _

func SnakeCase(s string) string { var str strings.Builder var prev rune for i, r := range s { // check if we should insert a underscore if i > 0 && unicode.IsUpper(r) && unicode.IsLower(prev) { str.WriteRune('_') } // lower case all characters str.WriteRune(unicode.ToLower(r)) prev = r } return str.String() } 
Enter fullscreen mode Exit fullscreen mode

Next up we want to pluralize the string,
this could be done in similar fashion to this

func Pluralize(s string) string { if strings.HasSuffix(s, "y") { return strings.TrimSuffix(s, "y") + "ies" } return s + "s" } 
Enter fullscreen mode Exit fullscreen mode

As a little helper function lets add a NormalizeTableName() function which combines the above into one

// normalize the struct name to lowercase, pluralize it and apply snakeCase // for example, User -> users, ReviewPost -> review_posts func normalizeTableName(name string) string { return pluralize(snakeCase(name)) } 
Enter fullscreen mode Exit fullscreen mode

With those helper functions out of the way let's have a look on how to parse and analyse the file.
To do this we can use the go/ast package, the ast package provides us with a set of tools for working with the abstract syntax tree.

File Parsing

The Go standard library comes with a handy package to parse Go source code files and generate a AST based on it.

func ExtractStructs(filePath string) []QueryBuilder { // create a file set, this keeps track of file positions for error messages and other diagnostic output fset := token.NewFileSet() // parse the whole file including errors parsedAST, err := parser.ParseFile(fset, filePath, nil, parser.AllErrors) } 
Enter fullscreen mode Exit fullscreen mode

After parsing the file and checking for potential errors we use the go/ast package to inspect the abstract syntax tree.

Inspect traverses an AST in depth first order. If the function returns true, Inspect is called recursively.

ast.Inspect(parsedAST, func(n ast.Node) bool { return true }) 
Enter fullscreen mode Exit fullscreen mode

In our case we are only interested if our node of type ast.TypeSpec and ast.StructType.
TypeSpec is needed to get the actual struct name and ast.StructType let's us iterate over the struct fields

ast.Inspect(parsedAST, func(n ast.Node) bool { // try to convert n to ast.TypeSpec if typeSpec, isTypeSpec := n.(*ast.TypeSpec); isTypeSpec { s, isStructType := typeSpec.Type.(*ast.StructType) // check if conversion was successful if !isStructType { return true } // get the struct name structName := normalizeTableName(typeSpec.Name.Name) // get Fields helper function fields := getFields(s) } return true }) 
Enter fullscreen mode Exit fullscreen mode

As you've probably noticed there is another helper function getFields
The implementation looks like this.

func getFields(s *ast.StructType) []string { fields := make([]string, len(s.Fields.List)) for i, field := range s.Fields.List { if len(field.Names) == 0 { continue } fields[i] = SnakeCase(field.Names[0].Name) } return fields } 
Enter fullscreen mode Exit fullscreen mode

With this function we got all we need. We are able to extract the struct name and the field names additionally apply snake_case and pluralization respectively.

Query Generation

As we already extracted all the needed data we can introduce a simple function to generate the needed queries. To keep this nice and clean we use another struct which defines the needed data for a query and a struct method to return a insert query.

// define the QueryBuilder struct type QueryBuilder struct { TableName string Fields []string } // Generate a insert query func (q QueryBuilder) InsertQuery() string { return fmt.Sprintf("INSERT INTO %s (%s) VALUES (:%s)", strings.ToLower(q.TableName), strings.Join(q.Fields, ", "), strings.Join(q.Fields, ", :")) } // add more implementation for the needed queries 
Enter fullscreen mode Exit fullscreen mode

and at last let's update the ExtractStructs function and combine everything

func ExtractStructs(filePath string) []QueryBuilder { fset := token.NewFileSet() parsedAST, err := parser.ParseFile(fset, filePath, nil, parser.AllErrors) if err != nil { fmt.Println(err) return nil } // Find all struct declarations var structs []QueryBuilder ast.Inspect(parsedAST, func(n ast.Node) bool { // try to convert n to ast.TypeSpec if typeSpec, isTypeSpec := n.(*ast.TypeSpec); isTypeSpec { s, isStructType := typeSpec.Type.(*ast.StructType) // check if conversion was successful if !isStructType { return true } // get the struct name structName := typeSpec.Name.Name // get Fields helper function fields := getFields(s) structs = append(structs, QueryBuilder{TableName: normalizeTableName(structName), Fields: fields}) } return true }) return structs } 
Enter fullscreen mode Exit fullscreen mode

You can find the full code example here

Top comments (0)