From 5386c526aec3d08e0cd4bfa83af84006bc3a2422 Mon Sep 17 00:00:00 2001 From: abyxcos Date: Tue, 30 Nov 2021 21:27:53 -0500 Subject: [PATCH] Ready for import --- .sqlh.go.swp | Bin 0 -> 24576 bytes sqlh.go | 314 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 314 insertions(+) create mode 100644 .sqlh.go.swp create mode 100644 sqlh.go diff --git a/.sqlh.go.swp b/.sqlh.go.swp new file mode 100644 index 0000000000000000000000000000000000000000..ff67a3f9c1eb9ad17a07d4b883529885a2eb6848 GIT binary patch literal 24576 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 GGw|P10c+v_ literal 0 HcmV?d00001 diff --git a/sqlh.go b/sqlh.go new file mode 100644 index 0000000..e8c6071 --- /dev/null +++ b/sqlh.go @@ -0,0 +1,314 @@ +package sqlh + +import ( + "fmt" + "reflect" + "strings" + "unicode" + + "github.com/iancoleman/strcase" +) + +// The format character for query parameter placeholders. See FormatCounter if you need the incrementing counter as is the case for PostgreSQL. +// Sqlite accepts both ? and $ +// MySQL accepts only ? +// PostgreSQL accepts only $ +// Using ?: (?, ?, ?) +// Using $: ($1, $2, $3) +var FormatChar = "?" +// Set FormatCounter to `true` if you need an incrementing counter on your parameter placeholders as is the case for PostgreSQL. ($1, $2, $3) vs ($, $, $) +var FormatCounter = false +// Convert struct field names from golang case to SQL case. Setting a db tag on the field will override any name conversions. +// +// type Person struct { +// ID int +// FirstName string +// LastName string +// Email string `db:"eMail"` +// } +// +// Produces the string (id, first_name, last_name, eMail) +var UseSQLCase = true +// TODO: Sort out who is using the global vs who takes an argument directly +// A comma delimited string of key names to skip on inserts. Spaces are stripped. Typically you don't want to insert your ID column, but may want to fetch it. You may also want to ignore date fields such as "CreatedAt" that are automatically set by your schema. Use the golang struct field name here, not the SQL column name. +// +// sqlh.SkipFields = "ID, CreatedAt" +var SkipFields = "ID" + + + +// BuildInsert is a helper function that returns all the variables needed for an INSERT query in one call. BuildInsert returns the column names, the parameter placeholder string, and the values. BuildInsert automatically passes SkipFields to the Columns() call. +// +// columns, params, values := sqlh.BuildInsert(person) +// query := fmt.Sprintf("INSERT INTO people (%s) VALUES (%s)", +// columns, params) +// db.Exec(query, values) +// +// `INSERT INTO people (first_name, last_name, eMail) values ($1, $2, $3)` +func BuildInsert(i interface{}) (string, string, []interface{}) { + return Columns(i), Params(i), Values(i) +} + +// BuildUpdate is a helper function that returns all the variables needed for an UPDATE query in one call. BuildUPdate returns the column names and parameter placeholder strings spliced together in the field=$1 style, and the array of values. BuildUpdate automatically passes the SkipFields to the Columns() call. +// +// columns, values := sqlh.BuildUpdate(person) +// query := fmt.Sprintf("UPDATE people SET %s", columns) +// db.Exec(query, values) +// +// `UPDATE people SET first_name=$1, last_name=$2, eMail=$3` +func BuildUpdate(i interface{}) (string, []interface{}) { + return Set(i), Values(i) +} + + + +// Columns walks through a struct's fields and returns their names in a format suitable for an INSERT query. If any struct db tags are set, those names will be used for the SQL column name. If UseSQLCase is set, golang casing will be converted to SQL casing on the field names (see UseSQLCase for more details). If SkipFields is set, any golang struct fields are omitted (see SkipFields for more details). +func Columns(i interface{}) string { + return strings.Join(getColumns(i), ", ") +} + +// Walk a struct's fields and return their names as an array of strings. This recurses in to embedded structs, flattening them out. +func getColumns(i interface{}) []string { + var columns []string + + v := reflect.ValueOf(i) + switch v.Kind() { + case reflect.Struct: +ParseLoop: + for i := 0; i < v.NumField(); i++ { + var columnName string + f := v.Type().Field(i) + + // Skip unexported fields + if f.IsExported() == false { + continue + } + + // Skip certain fields like "ID" on inserts + for _, skip := range splitFields(SkipFields) { + if f.Name == skip { + continue ParseLoop + } + } + + // Recurse on embedded structs + if v.Field(i).Kind() == reflect.Struct { + columns = append(columns, getColumns(v.Field(i).Interface())...) + continue + } + + // Append the name of the field. If we have a db struct tag, prefer that. If the SQL case conversion flag is set, convert our field names, then use that. Else, just use the struct field name verbatim. + if t := f.Tag.Get("db"); t != "" { + columnName = t + } else if UseSQLCase == true { + columnName = strcase.ToSnake(f.Name) + } else { + columnName = f.Name + } + columns = append(columns, columnName) + } + } + + return columns +} + +// Params walks through a struct's fields and creates a string of parameter placeholders suitable for an INSERT query. Params uses FormatChar to define the character used for parameter placeholders in your database and FormatCounter if your database requires numbered parameter placeholders. See FormatChar and FormatCounter for more details. +func Params(i interface{}) string { + var parameters []string + paramCount := getParams(i) + + for i := 1; i < paramCount; i++ { + var parameterString string + // Append the parameter character and optional counter + if FormatCounter == true { + // Use a 1-based index on our counters + parameterString = fmt.Sprintf("%s%d", FormatChar, i) + } else { + parameterString = FormatChar + } + parameters = append(parameters, parameterString) + } + + return strings.Join(parameters, ", ") +} + +// Walk a struct's fields and count how many there are. This recurses in to embedded structs. +func getParams(i interface{}) int { + var paramCount = 1 + + v := reflect.ValueOf(i) + switch v.Kind() { + case reflect.Struct: +ParseLoop: + for i := 0; i < v.NumField(); i++ { + f := v.Type().Field(i) + + // Skip unexported fields + if f.IsExported() == false { + continue + } + + // Skip certain fields like "ID" on inserts + for _, skip := range splitFields(SkipFields) { + if f.Name == skip { + continue ParseLoop + } + } + + // Recurse on embedded structs + if v.Field(i).Kind() == reflect.Struct { + paramCount += getParams(v.Field(i).Interface()) - 1 + continue + } + + paramCount++ + } + } + + return paramCount +} + +// Values walks through a struct's fields and returns their values in a format suitable for passing to database/sqlwhich does not believe structs exist. The values []interface{} returned by Values is suitable for passing directly in to database/sql's args ...interface{} parameter. +func Values(i interface{}) []interface{} { + return getValues(i) +} + +// Walk a struct's fields and get the values of the fields. This recurses in to embedded structs, flattening them out. +func getValues(i interface{}) []interface{} { + var values []interface{} + + v := reflect.ValueOf(i) + switch v.Kind() { + case reflect.Struct: +ParseLoop: + for i := 0; i < v.NumField(); i++ { + f := v.Type().Field(i) + + // Skip unexported fields + if f.IsExported() == false { + continue + } + + // Skip certain fields like "ID" on inserts + for _, skip := range splitFields(SkipFields) { + if f.Name == skip { + continue ParseLoop + } + } + + // Recurse on embedded structs + if v.Field(i).Kind() == reflect.Struct { + values = append(values, getValues(v.Field(i).Interface())...) + continue + } + + // Append the field's value + values = append(values, v.Field(i).Interface()) + } + } + + return values +} + +// Set walks through a struct's fields and returns their names and parameter placeholders in a format suitable for a SET clause. Set calls out to Columns and Params and thus follows their same rules. Set and Where are similar functions, but with a suitable joiner for the specific clause. +// +// fmt.Sprintf("SET %s", sqlh.Set(person, "ID")) +// `SET first_name=$1, last_name=$2, eMail=$3` +func Set(i interface{}) string { + return strings.Join(getEquals(i), ", ") +} + +// Where walks through a struct's fields and returns their names and parameter placeholders in a format suitable for a WHERE clause. Where calls out to Columns and Params and thus follows their same rules. Where only handles the very simple case of column=value and isn't suitable for more advanced comparisons. Set and Where are similar functions, but with a suitable joiner for the specific clause. +// +// fmt.Sprintf("WHERE %s", sqlh.Where(person, "ID")) +// `WHERE first_name=$1 AND last_name=$2 AND eMail=$3` +func Where(i interface{}) string { + return strings.Join(getEquals(i), " AND ") +} + +// Loop through the columns and values arrays and splice them together in to an array of field=$1 style strings. +func getEquals(i interface{}) []string { + var equals []string + var parameters []string + columns := getColumns(i) + paramCount := getParams(i) + + for i := 1; i < paramCount; i++ { + var parameterString string + // Append the parameter character and optional counter + if FormatCounter == true { + // Use a 1-based index on our counters + parameterString = fmt.Sprintf("%s%d", FormatChar, i) + } else { + parameterString = FormatChar + } + parameters = append(parameters, parameterString) + } + + for i :=0; i < len(columns); i++ { + equals = append(equals, fmt.Sprintf("%s=%s", columns[i], parameters[i])) + } + + return equals +} + + + +// A helper function to take a comma delimited string and split it in to an array while trimming the spaces. +func splitFields(fields string) []string { + return strings.FieldsFunc(fields, func(c rune) bool { + return unicode.IsSpace(c) || c == ',' + }) +} + +/* +func main() { + FormatChar = "$" + FormatCounter = true + SkipFields = "ID, C,Name" + + type Test2 struct { + A string + B int + C string + } + + type Test struct { + Name string + unexportedInt int + Count int + Blah Test2 + Test2 string `db:"TEST3"` + DB int + ID int + APITestFunc string + } + + test := Test{ + Name: "Test", + unexportedInt: 12, + Count: 5, + Test2: "Hello", + APITestFunc: "1234", + ID: 14, + Blah: Test2{ + A: "123", + B: 4, + C: "567", + }, + } + columns, params, values := BuildInsert(test) + fmt.Printf("(%s)\n", columns) + fmt.Printf("(%s)\n", params) + fmt.Printf("%v\n", values) + for _, v := range values { + fmt.Printf("%s ", reflect.ValueOf(v).Kind()) + } + fmt.Println() + fmt.Println() + + columns, values = BuildUpdate(test) + fmt.Printf("%s\n", columns) + fmt.Printf("%v\n", values) +} +*/ -- 2.45.2