Created
March 26, 2025 01:29
-
-
Save caseylmanus/ded4273875fc262f5f09280b84f4bee1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
1. descriptorTemplate:const descriptorTemplate = ` | |
package generated | |
import "fmt" | |
{{$allTables := .Tables}} | |
{{$allColumns := .Columns}} | |
{{$allIndexes := .Indexes}} | |
// TableDescriptor describes a database table. | |
type TableDescriptor struct { | |
TableName string | |
Indexes []string // Add this line | |
} | |
// ColumnDescriptor describes a column in a database table. | |
type ColumnDescriptor[T any] struct { | |
ColumnName string | |
ParentTable string | |
IndexName string // Add this line | |
} | |
// TableDescriptors maps table names to their descriptors. | |
var TableDescriptors = map[string]TableDescriptor{ | |
{{range $table := $allTables}} | |
"{{$table.TableName}}": { | |
TableName: "{{$table.TableName}}", | |
Indexes: []string{ // Initialize the Indexes field | |
{{range $index := $allIndexes}} | |
{{if eq $index.TableName $table.TableName}} | |
"{{$index.IndexName}}", | |
{{end}} | |
{{end}} | |
}, | |
}, | |
{{end}} | |
} | |
{{range $table := $allTables}} | |
// {{$table.TableName}}TableDescriptor provides access to {{$table.TableName}} table columns. | |
type {{$table.TableName}}TableDescriptor struct { | |
} | |
// {{$table.TableName}} provides access to {{$table.TableName}} table columns. | |
func {{$table.TableName}}() {{$table.TableName}}TableDescriptor { | |
return {{$table.TableName}}TableDescriptor{} | |
} | |
{{range $col := $allColumns}} | |
{{if eq $col.TableName $table.TableName}} | |
// {{$col.ColumnName}} returns a ColumnDescriptor for the {{$col.ColumnName}} column. | |
func ({{$table.TableName}}TableDescriptor) {{$col.ColumnName}}() ColumnDescriptor[{{$col.DataType}}] { | |
return ColumnDescriptor[{{$col.DataType}}]{ | |
ColumnName: "{{$col.ColumnName}}", | |
ParentTable: "{{$table.TableName}}", | |
IndexName: "{{$col.IndexName}}", // Set the IndexName | |
} | |
} | |
{{end}} | |
{{end}} | |
{{end}} | |
// ExpressionBuilder builds where clauses for columns. This is now exported. | |
type ExpressionBuilder[T any] struct { | |
column string | |
clause string | |
params map[string]interface{} | |
} | |
// Equal creates an equality clause. | |
func (b ExpressionBuilder[T]) Equal(value T) *ExpressionBuilder[T] { | |
b.clause = fmt.Sprintf("%s = @%s", b.column, strings.ReplaceAll(b.column, ".", "_")) | |
b.params = map[string]interface{}{strings.ReplaceAll(b.column, ".", "_"): value} | |
return &b | |
} | |
// NotEqual creates a not-equal clause. | |
func (b ExpressionBuilder[T]) NotEqual(value T) *ExpressionBuilder[T] { | |
b.clause = fmt.Sprintf("%s != @%s", b.column, strings.ReplaceAll(b.column, ".", "_")) | |
b.params = map[string]interface{}{strings.ReplaceAll(b.column, ".", "_"): value} | |
return &b | |
} | |
// GreaterThan creates a greater-than clause. | |
func (b ExpressionBuilder[T]) GreaterThan(value T) *ExpressionBuilder[T] { | |
b.clause = fmt.Sprintf("%s > @%s", b.column, strings.ReplaceAll(b.column, ".", "_")) | |
b.params = map[string]interface{}{strings.ReplaceAll(b.column, ".", "_"): value} | |
return &b | |
} | |
// LessThan creates a less-than clause. | |
func (b ExpressionBuilder[T]) LessThan(value T) *ExpressionBuilder[T] { | |
b.clause = fmt.Sprintf("%s < @%s", b.column, strings.ReplaceAll(b.column, ".", "_")) | |
b.params = map[string]interface{}{strings.ReplaceAll(b.column, ".", "_"): value} | |
return &b | |
} | |
// GreaterThanOrEqual creates a greater-than-or-equal clause. | |
func (b ExpressionBuilder[T]) GreaterThanOrEqual(value T) *ExpressionBuilder[T] { | |
b.clause = fmt.Sprintf("%s >= @%s", b.column, strings.ReplaceAll(b.column, ".", "_")) | |
b.params = map[string]interface{}{strings.ReplaceAll(b.column, ".", "_"): value} | |
return &b | |
} | |
// LessThanOrEqual creates a less-than-or-equal clause. | |
func (b ExpressionBuilder[T]) LessThanOrEqual(value T) *ExpressionBuilder[T] { | |
b.clause = fmt.Sprintf("%s <= @%s", b.column, strings.ReplaceAll(b.column, ".", "_")) | |
b.params = map[string]interface{}{strings.ReplaceAll(b.column, ".", "_"): value} | |
return &b | |
} | |
// Like creates a like clause. | |
func (b ExpressionBuilder[T]) Like(value T) *ExpressionBuilder[T] { | |
b.clause = fmt.Sprintf("%s LIKE @%s", b.column, strings.ReplaceAll(b.column, ".", "_")) | |
b.params = map[string]interface{}{strings.ReplaceAll(b.column, ".", "_"): value} | |
return &b | |
} | |
// In creates an in clause. | |
func (b ExpressionBuilder[T]) In(values ...T) *ExpressionBuilder[T] { | |
b.clause = fmt.Sprintf("%s IN (@%s)", b.column, strings.ReplaceAll(b.column, ".", "_")) | |
b.params = map[string]interface{}{strings.ReplaceAll(b.column, ".", "_"): values} | |
return &b | |
} | |
// IsNull creates an is null clause. | |
func (b ExpressionBuilder[T]) IsNull() *ExpressionBuilder[T] { | |
b.clause = fmt.Sprintf("%s IS NULL", b.column) | |
return &b | |
} | |
// IsNotNull creates an is not null clause. | |
func (b ExpressionBuilder[T]) IsNotNull() *ExpressionBuilder[T] { | |
b.clause = fmt.Sprintf("%s IS NOT NULL", b.column) | |
return &b | |
} | |
// And combines two expressions with AND. | |
func (b ExpressionBuilder[T]) And(other *ExpressionBuilder[T]) *ExpressionBuilder[T] { | |
b.clause = fmt.Sprintf("(%s AND %s)", b.clause, other.clause) | |
for k, v := range other.params { | |
b.params[k] = v | |
} | |
return &b | |
} | |
// Or combines two expressions with OR. | |
func (b ExpressionBuilder[T]) Or(other *ExpressionBuilder[T]) *ExpressionBuilder[T] { | |
b.clause = fmt.Sprintf("(%s OR %s)", b.clause, other.clause) | |
for k, v := range other.params { | |
b.params[k] = v | |
} | |
return &b | |
} | |
// Group creates a parenthesized group. | |
func (b ExpressionBuilder[T]) Group() *ExpressionBuilder[T] { | |
b.clause = fmt.Sprintf("(%s)", b.clause) | |
return &b | |
} | |
// GetClause returns the clause and params. | |
func (b ExpressionBuilder[T]) GetClause() (string, map[string]interface{}) { | |
return b.clause, b.params | |
} | |
// Expression creates a new ExpressionBuilder. | |
func (c ColumnDescriptor[T]) Expression() ExpressionBuilder[T] { | |
return ExpressionBuilder[T]{ | |
column: fmt.Sprintf("%s.%s", c.ParentTable, c.ColumnName), | |
params: make(map[string]interface{}), | |
} | |
} | |
` | |
2. selectBuilderTemplate:const selectBuilderTemplate = ` | |
package generated | |
import ( | |
"cloud.google.com/go/spanner" | |
"fmt" | |
"strings" | |
) | |
{{$allTables := .Tables}} | |
{{$allColumns := .Columns}} | |
{{$allConstraints := .Constraints}} | |
{{range $table := $allTables}} | |
// Select{{$table.TableName}}StatementBuilder is a builder for creating select statements for the {{$table.TableName}} table. | |
type Select{{$table.TableName}}StatementBuilder struct { | |
columns []string | |
where string | |
limit int64 | |
offset int64 | |
orderBy string | |
joins []string | |
params map[string]interface{} | |
related map[string]bool | |
forcedIdx string | |
} | |
// Select{{$table.TableName}} creates a new Select{{$table.TableName}}StatementBuilder. | |
func Select{{$table.TableName}}() *Select{{$table.TableName}}StatementBuilder { | |
return &Select{{$table.TableName}}StatementBuilder{ | |
params: make(map[string]interface{}), | |
related: make(map[string]bool), | |
} | |
} | |
// Columns sets the columns to select. | |
func (b *Select{{$table.TableName}}StatementBuilder) Columns(cols ...string) *Select{{$table.TableName}}StatementBuilder { | |
b.columns = append(b.columns, cols...) | |
return b | |
} | |
// Where adds a where clause. | |
func (b *Select{{$table.TableName}}StatementBuilder) Where(expressionBuilder *ExpressionBuilder[any]) *Select{{$table.TableName}}StatementBuilder { | |
if b.where != "" { | |
b.where += " AND " | |
} | |
clause, params := expressionBuilder.GetClause() | |
b.where += clause | |
for k, v := range params { | |
b.params[k] = v | |
} | |
return b | |
} | |
// Limit sets the limit. | |
func (b *Select{{$table.TableName}}StatementBuilder) Limit(limit int64) *Select{{$table.TableName}}StatementBuilder { | |
b.limit = limit | |
return b | |
} | |
// Offset sets the offset. | |
func (b *Select{{$table.TableName}}StatementBuilder) Offset(offset int64) *Select{{$table.TableName}}StatementBuilder { | |
b.offset = offset | |
return b | |
} | |
// OrderBy sets the order by clause. | |
func (b *Select{{$table.TableName}}StatementBuilder) OrderBy(orderBy string) *Select{{$table.TableName}}StatementBuilder { | |
b.orderBy = orderBy | |
return b | |
} | |
// Join adds a join clause. | |
func (b *Select{{$table.TableName}}StatementBuilder) Join(join string) *Select{{$table.TableName}}StatementBuilder { | |
b.joins = append(b.joins, join) | |
return b | |
} | |
{{range $constraint := $allConstraints}} | |
{{if eq $constraint.TableName $table.TableName}} | |
{{if eq $constraint.ConstraintType "FOREIGN_KEY"}} | |
{{$relatedTable := index $allTables (index (where $allTables "TableName" $constraint.ConstraintName) 0).TableName}} | |
// Include{{$relatedTable.TableName}} adds a join to include related {{$relatedTable.TableName}} data. | |
func (b *Select{{$table.TableName}}StatementBuilder) Include{{$relatedTable.TableName}}() *Select{{$table.TableName}}StatementBuilder { | |
b.related["{{$relatedTable.TableName}}"] = true | |
return b | |
} | |
{{end}} | |
{{end}} | |
{{end}} | |
// ForcedIndex sets the forced index. | |
func (b *Select{{$table.TableName}}StatementBuilder) ForcedIndex(idx string) *Select{{$table.TableName}}StatementBuilder { | |
b.forcedIdx = idx | |
return b | |
} | |
// BuildStatement builds a spanner.Statement. | |
func (b *Select{{$table.TableName}}StatementBuilder) BuildStatement() spanner.Statement { | |
sql := "SELECT " | |
if len(b.columns) == 0 { | |
{{range $col := $allColumns}} | |
{{if eq $col.TableName $table.TableName}} | |
b.columns = append(b.columns, "{{$table.TableName}}.{{$col.ColumnName}}") | |
{{end}} | |
{{end}} | |
} | |
{{range $constraint := $allConstraints}} | |
{{if eq $constraint.TableName $table.TableName}} | |
{{if eq $constraint.ConstraintType "FOREIGN_KEY"}} | |
{{$relatedTable := index $allTables (index (where $allTables "TableName" $constraint.ConstraintName) 0).TableName}} | |
if b.related["{{$relatedTable.TableName}}"] { | |
{{range $relatedCol := $allColumns}} | |
{{if eq $relatedCol.TableName $relatedTable.TableName}} | |
b.columns = append(b.columns, "{{$relatedTable.TableName}}.{{$relatedCol.ColumnName}}") | |
{{end}} | |
{{end}} | |
b.joins = append(b.joins, fmt.Sprintf("JOIN %s ON %s.%s = %s.%s", "{{$relatedTable.TableName}}", "{{$table.TableName}}", "{{$constraint.Columns}}", "{{$relatedTable.TableName}}", "{{$constraint.ReferencedColumns}}")) | |
} | |
{{end}} | |
{{end}} | |
{{end}} | |
sql += strings.Join(b.columns, ", ") | |
sql += " FROM {{$table.TableName}}" | |
if len(b.joins) > 0 { | |
sql += " " + strings.Join(b.joins, " ") | |
} | |
if b.where != "" { | |
sql += " WHERE " + b.where | |
} | |
if b.orderBy != "" { | |
sql += " ORDER BY " + b.orderBy | |
} | |
if b.limit > 0 { | |
sql += " LIMIT " + fmt.Sprint(b.limit) | |
} | |
if b.offset > 0 { | |
sql += " OFFSET " + fmt.Sprint(b.offset) | |
} | |
if b.forcedIdx != "" { | |
sql += "@{FORCE_INDEX=" + b.forcedIdx + "}" | |
} | |
return spanner.Statement{ | |
SQL: sql, | |
Params: b.params, | |
} | |
} | |
{{end}} | |
` | |
3. insertBuilderTemplate:const insertBuilderTemplate = ` | |
package generated | |
import ( | |
"cloud.google.com/go/spanner" | |
"reflect" | |
) | |
{{$allTables := .Tables}} | |
{{$allColumns := .Columns}} | |
{{range $table := $allTables}} | |
// Insert{{$table.TableName}}StatementBuilder is a builder for creating insert statements for the {{$table.TableName}} table. | |
type Insert{{$table.TableName}}StatementBuilder struct { | |
values map[string]interface{} | |
} | |
// Insert{{$table.TableName}} creates a new Insert{{$table.TableName}}StatementBuilder. | |
func Insert{{$table.TableName}}() *Insert{{$table.TableName}}StatementBuilder { | |
return &Insert{{$table.TableName}}StatementBuilder{ | |
values: make(map[string]interface{}), | |
} | |
} | |
{{range $col := $allColumns}} | |
{{if eq $col.TableName $table.TableName}} | |
// With{{$col.ColumnName}} sets the value for the {{$col.ColumnName}} column. | |
func (b *Insert{{$table.TableName}}StatementBuilder) With{{$col.ColumnName}}(value {{$col.DataType}}) *Insert{{$table.TableName}}StatementBuilder { | |
b.values["{{$col.ColumnName}}"] = value | |
return b | |
} | |
{{end}} | |
{{end}} | |
// WithStruct sets the values from a struct. | |
func (b *Insert{{$table.TableName}}StatementBuilder) WithStruct(s *{{$table.TableName}}) *Insert{{$table.TableName}}StatementBuilder { | |
val := reflect.ValueOf(s).Elem() | |
typ := val.Type() | |
for i := 0; i < val.NumField(); i++ { | |
field := val.Field(i) | |
fieldName := typ.Field(i).Tag.Get("spanner") // Use the spanner tag | |
if fieldName != "" && fieldName != "-" { | |
b.values[fieldName] = field.Interface() | |
} | |
} | |
return b | |
} | |
// BuildStatement builds a spanner.Statement. | |
func (b *Insert{{$table.TableName}}StatementBuilder) BuildStatement() spanner.Statement { | |
columns := make([]string, 0, len(b.values)) | |
params := make([]string, 0, len(b.values)) | |
for column := range b.values { | |
columns = append(columns, column) | |
params = append(params, "@" + column) | |
} | |
sql := fmt.Sprintf("INSERT INTO {{$table.TableName}} (%s) VALUES (%s)", | |
strings.Join(columns, ", "), | |
strings.Join(params, ", ")) | |
return spanner.Statement{ | |
SQL: sql, | |
Params: b.values, | |
} | |
} | |
{{end}} | |
` | |
4. updateBuilderTemplate:const updateBuilderTemplate = ` | |
package generated | |
import ( | |
"cloud.google.com/go/spanner" | |
"fmt" | |
"strings" | |
"reflect" | |
) | |
{{$allTables := .Tables}} | |
{{$allColumns := .Columns}} | |
{{range $table := $allTables}} | |
// Update{{$table.TableName}}StatementBuilder is a builder for creating update statements for the {{$table.TableName}} table. | |
type Update{{$table.TableName}}StatementBuilder struct { | |
set map[string]interface{} | |
where string | |
params map[string]interface{} | |
forcedIdx string | |
} | |
// Update{{$table.TableName}} creates a new Update{{$table.TableName}}StatementBuilder. | |
func Update{{$table.TableName}}() *Update{{$table.TableName}}StatementBuilder { | |
return &Update{{$table.TableName}}StatementBuilder{ | |
set: make(map[string]interface{}), | |
params: make(map[string]interface{}), | |
} | |
} | |
{{range $col := $allColumns}} | |
{{if eq $col.TableName $table.TableName}} | |
// With{{$col.ColumnName}} sets the value for the {{$col.ColumnName}} column in the update. | |
func (b *Update{{$table.TableName}}StatementBuilder) With{{$col.ColumnName}}(value {{$col.DataType}}) *Update{{$table.TableName}}StatementBuilder { | |
b.set["{{$col.ColumnName}}"] = value | |
return b | |
} | |
{{end}} | |
{{end}} | |
// WithStruct sets the values from a struct. | |
func (b *Update{{$table.TableName}}StatementBuilder) WithStruct(s *{{$table.TableName}}) *Update{{$table.TableName}}StatementBuilder { | |
val := reflect.ValueOf(s).Elem() | |
typ := val.Type() | |
for i := 0; i < val.NumField(); i++ { | |
field := val.Field(i) | |
fieldName := typ.Field(i).Tag.Get("spanner") // Use the spanner tag | |
if fieldName != "" && fieldName != "-" { | |
b.set[fieldName] = field.Interface() | |
} | |
} | |
return b | |
} | |
// Where adds a where clause to the update statement. | |
func (b *Update{{$table.TableName}}StatementBuilder) Where(expressionBuilder *ExpressionBuilder[any]) *Update{{$table.TableName}}StatementBuilder { | |
clause, params := expressionBuilder.GetClause() | |
b.where = clause | |
for k, v := range params { | |
b.params[k] = v | |
} | |
return b | |
} | |
// ForceIndex sets the index to use. | |
func (b *Update{{$table.TableName}}StatementBuilder) ForceIndex(index string) *Update{{$table.TableName}}StatementBuilder { | |
b.forcedIdx = index | |
return b | |
} | |
// BuildStatement builds a spanner.Statement. | |
func (b *Update{{$table.TableName}}StatementBuilder) BuildStatement() spanner.Statement { | |
sets := make([]string, 0, len(b.set)) | |
for column, value := range b.set { | |
sets = append(sets, fmt.Sprintf("%s = @%s", column, column)) | |
b.params[column] = value // Add to the params map | |
} | |
sql := fmt.Sprintf("UPDATE {{$table.TableName}} SET %s", strings.Join(sets, ", ")) | |
if b.where != "" { | |
sql += " WHERE " + b.where | |
} | |
if b.forcedIdx != ""{ | |
sql += "@{FORCE_INDEX=" + b.forcedIdx + "}" | |
} | |
return spanner.Statement{ | |
SQL: sql, | |
Params: b.params, | |
} | |
} | |
{{end}} | |
` | |
5. structTemplate:const structTemplate = ` | |
package generated | |
{{$allTables := .Tables}} | |
{{$allColumns := .Columns}} | |
{{$allConstraints := .Constraints}} | |
{{range $table := $allTables}} | |
// {{$table.TableName}} represents the {{$table.TableName}} table. | |
type {{$table.TableName}} struct { | |
{{range $col := $allColumns}} | |
{{if eq $col.TableName $table.TableName}} | |
{{$col.ColumnName}} {{$col.DataType}} ` + "`spanner:\"{{$col.ColumnName}}\"`" + ` | |
{{end}} | |
{{end}} | |
{{range $constraint := $allConstraints}} | |
{{if eq $constraint.TableName $table.TableName}} | |
{{if eq $constraint.ConstraintType "FOREIGN_KEY"}} | |
{{$relatedTable := index $allTables (index (where $allTables "TableName" $constraint.ConstraintName) 0).TableName}} | |
{{$relatedTable.TableName}}s []{{$relatedTable.TableName}} ` + "`spanner:\"{{$relatedTable.TableName}}s\"`" + ` | |
{{end}} | |
{{end}} | |
{{end}} | |
{{$parentTable := index $allTables (index (where $allTables "TableName" $table.SelfReferencingColumnName) 0).TableName}} | |
{{if $table.SelfReferencingColumnName}} | |
{{$parentTable.TableName}} *{{$parentTable.TableName}} ` + "`spanner:\"{{$parentTable.TableName}}\"`" + ` | |
{{end}} | |
} | |
{{end}} | |
` |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Yes, I can. Let's retrieve the necessary information schema data from Spanner to generate the templates.
1. Spanner Schema Query:
2. Go Code to Execute the Query and Process the Results:
This code does the following:
Important Notes:
projects/your-project/instances/your-instance/databases/your-database
placeholder with your actual Spanner database path.cloud.google.com/go/spanner
,google.golang.org/api/iterator
,cloud.google.com/go/civil
).executeTemplate
function. Use thetableSlice
,columnSlice
,constraintSlice
, andindexSlice
to pass the data to your templates.