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 }
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() }
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" }
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)) }
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) }
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 })
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 })
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 }
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
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 }
You can find the full code example here
Top comments (0)