init - add project files

This commit is contained in:
2025-03-06 23:54:11 -05:00
commit e724ff1120
1363 changed files with 897467 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
package metadata
// Column struct
type Column struct {
Name string `sql:"primary_key"`
IsPrimaryKey bool
IsNullable bool
IsGenerated bool
HasDefault bool
DataType DataType
Comment string
}
// DataTypeKind is database type kind(base, enum, user-defined, array)
type DataTypeKind string
// DataTypeKind possible values
const (
BaseType DataTypeKind = "base"
EnumType DataTypeKind = "enum"
UserDefinedType DataTypeKind = "user-defined"
ArrayType DataTypeKind = "array"
RangeType DataTypeKind = "range"
)
// DataType contains information about column data type
type DataType struct {
Name string
Kind DataTypeKind
IsUnsigned bool
}

View File

@@ -0,0 +1,51 @@
package metadata
import (
"database/sql"
"fmt"
)
// TableType is type of database table(view or base)
type TableType string
// SQL table types
const (
BaseTable TableType = "BASE TABLE"
ViewTable TableType = "VIEW"
)
// DialectQuerySet is set of methods necessary to retrieve dialect metadata information
type DialectQuerySet interface {
GetTablesMetaData(db *sql.DB, schemaName string, tableType TableType) ([]Table, error)
GetEnumsMetaData(db *sql.DB, schemaName string) ([]Enum, error)
}
// GetSchema retrieves Schema information from database
func GetSchema(db *sql.DB, querySet DialectQuerySet, schemaName string) (Schema, error) {
tablesMetaData, err := querySet.GetTablesMetaData(db, schemaName, BaseTable)
if err != nil {
return Schema{}, fmt.Errorf("failed to get %s tables metadata: %w", schemaName, err)
}
viewMetaData, err := querySet.GetTablesMetaData(db, schemaName, ViewTable)
if err != nil {
return Schema{}, fmt.Errorf("failed to get %s view metadata: %w", schemaName, err)
}
enumsMetaData, err := querySet.GetEnumsMetaData(db, schemaName)
if err != nil {
return Schema{}, fmt.Errorf("failed to get %s enum metadata: %w", schemaName, err)
}
ret := Schema{
Name: schemaName,
TablesMetaData: tablesMetaData,
ViewsMetaData: viewMetaData,
EnumsMetaData: enumsMetaData,
}
fmt.Println(" FOUND", len(ret.TablesMetaData), "table(s),", len(ret.ViewsMetaData), "view(s),",
len(ret.EnumsMetaData), "enum(s)")
return ret, nil
}

View File

@@ -0,0 +1,8 @@
package metadata
// Enum metadata struct
type Enum struct {
Name string `sql:"primary_key"`
Comment string
Values []string
}

View File

@@ -0,0 +1,14 @@
package metadata
// Schema struct
type Schema struct {
Name string
TablesMetaData []Table
ViewsMetaData []Table
EnumsMetaData []Enum
}
// IsEmpty returns true if schema info does not contain any table, views or enums metadata
func (s Schema) IsEmpty() bool {
return len(s.TablesMetaData) == 0 && len(s.ViewsMetaData) == 0 && len(s.EnumsMetaData) == 0
}

View File

@@ -0,0 +1,23 @@
package metadata
// Table metadata struct
type Table struct {
Name string `sql:"primary_key"`
Comment string
Columns []Column
}
// MutableColumns returns list of mutable columns for table
func (t Table) MutableColumns() []Column {
var ret []Column
for _, column := range t.Columns {
if column.IsPrimaryKey || column.IsGenerated {
continue
}
ret = append(ret, column)
}
return ret
}

View File

@@ -0,0 +1,118 @@
package mysql
import (
"database/sql"
"errors"
"fmt"
"strings"
"github.com/go-jet/jet/v2/generator/metadata"
"github.com/go-jet/jet/v2/generator/template"
"github.com/go-jet/jet/v2/mysql"
mysqldr "github.com/go-sql-driver/mysql"
)
const mysqlMaxConns = 10
// DBConnection contains MySQL connection details
type DBConnection struct {
Host string
Port int
User string
Password string
Params string
DBName string
}
// Generate generates jet files at destination dir from database connection details
func Generate(destDir string, dbConn DBConnection, generatorTemplate ...template.Template) error {
connectionString := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", dbConn.User, dbConn.Password, dbConn.Host, dbConn.Port, dbConn.DBName)
if dbConn.Params != "" {
connectionString += "?" + dbConn.Params
}
db, err := openConnection(connectionString)
if err != nil {
return fmt.Errorf("failed to open db connection: %w", err)
}
defer db.Close()
err = generate(db, dbConn.DBName, destDir, generatorTemplate...)
if err != nil {
return err
}
return nil
}
// GenerateDSN opens connection via DSN string and does everything what Generate does.
func GenerateDSN(dsn, destDir string, templates ...template.Template) error {
// Special case for go mysql driver. It does not understand schema,
// so we need to trim it before passing to generator
// https://github.com/go-sql-driver/mysql#dsn-data-source-name
idx := strings.Index(dsn, "://")
if idx != -1 {
dsn = dsn[idx+len("://"):]
}
cfg, err := mysqldr.ParseDSN(dsn)
if err != nil {
return fmt.Errorf("failed to parse DSN: %w", err)
}
if cfg.DBName == "" {
return errors.New("database name is required")
}
db, err := openConnection(dsn)
if err != nil {
return fmt.Errorf("failed to open db connection: %w", err)
}
defer db.Close()
err = generate(db, cfg.DBName, destDir, templates...)
if err != nil {
return fmt.Errorf("failed to generate: %w", err)
}
return nil
}
func openConnection(connectionString string) (*sql.DB, error) {
fmt.Println("Connecting to MySQL database...")
db, err := sql.Open("mysql", connectionString)
if err != nil {
return nil, fmt.Errorf("failed to open mysql connection: %w", err)
}
db.SetMaxOpenConns(mysqlMaxConns)
db.SetMaxIdleConns(mysqlMaxConns)
err = db.Ping()
if err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
return db, nil
}
func generate(db *sql.DB, dbName, destDir string, templates ...template.Template) error {
fmt.Println("Retrieving database information...")
// No schemas in MySQL
schemaMetaData, err := metadata.GetSchema(db, &mySqlQuerySet{}, dbName)
if err != nil {
return fmt.Errorf("failed to get '%s' database metadata: %w", dbName, err)
}
genTemplate := template.Default(mysql.Dialect)
if len(templates) > 0 {
genTemplate = templates[0]
}
err = template.ProcessSchema(destDir, schemaMetaData, genTemplate)
if err != nil {
return fmt.Errorf("failed to process '%s' database: %w", schemaMetaData.Name, err)
}
return nil
}

View File

@@ -0,0 +1,91 @@
package mysql
import (
"context"
"database/sql"
"fmt"
"strings"
"github.com/go-jet/jet/v2/generator/metadata"
"github.com/go-jet/jet/v2/qrm"
)
// mySqlQuerySet is dialect query set for MySQL
type mySqlQuerySet struct{}
func (m mySqlQuerySet) GetTablesMetaData(db *sql.DB, schemaName string, tableType metadata.TableType) ([]metadata.Table, error) {
query := `
SELECT
t.table_name as "table.name",
col.COLUMN_NAME AS "column.Name",
col.COLUMN_DEFAULT IS NOT NULL as "column.HasDefault",
col.IS_NULLABLE = "YES" AS "column.IsNullable",
col.COLUMN_COMMENT AS "column.Comment",
COALESCE(pk.IsPrimaryKey, 0) AS "column.IsPrimaryKey",
IF (col.COLUMN_TYPE = 'tinyint(1)',
'boolean',
IF (col.DATA_TYPE = 'enum',
CONCAT(col.TABLE_NAME, '_', col.COLUMN_NAME),
col.DATA_TYPE)
) AS "dataType.Name",
IF (col.DATA_TYPE = 'enum', 'enum', 'base') AS "dataType.Kind",
col.COLUMN_TYPE LIKE '%unsigned%' AS "dataType.IsUnsigned"
FROM INFORMATION_SCHEMA.tables AS t
INNER JOIN
information_schema.columns AS col
ON t.table_schema = col.table_schema AND t.table_name = col.table_name
LEFT JOIN (
SELECT k.column_name, 1 AS IsPrimaryKey, k.table_name
FROM information_schema.table_constraints t
JOIN information_schema.key_column_usage k USING(constraint_name, table_schema, table_name)
WHERE t.table_schema = ?
AND t.constraint_type = 'PRIMARY KEY'
) AS pk ON col.COLUMN_NAME = pk.column_name AND col.table_name = pk.table_name
WHERE t.table_schema = ?
AND t.table_type = ?
ORDER BY
t.table_name,
col.ordinal_position;
`
var tables []metadata.Table
_, err := qrm.Query(context.Background(), db, query, []interface{}{schemaName, schemaName, tableType}, &tables)
if err != nil {
return nil, fmt.Errorf("failed to query column meta data: %w", err)
}
return tables, nil
}
func (m mySqlQuerySet) GetEnumsMetaData(db *sql.DB, schemaName string) ([]metadata.Enum, error) {
query := `
SELECT (CASE c.DATA_TYPE WHEN 'enum' then CONCAT(c.TABLE_NAME, '_', c.COLUMN_NAME) ELSE '' END ) as "name",
SUBSTRING(c.COLUMN_TYPE,5) as "values"
FROM information_schema.columns as c
INNER JOIN information_schema.tables as t on (t.table_schema = c.table_schema AND t.table_name = c.table_name)
WHERE c.table_schema = ? AND DATA_TYPE = 'enum';
`
var queryResult []struct {
Name string
Values string
}
_, err := qrm.Query(context.Background(), db, query, []interface{}{schemaName}, &queryResult)
if err != nil {
return nil, fmt.Errorf("failed to query enums meta data: %w", err)
}
var ret []metadata.Enum
for _, result := range queryResult {
enumValues := strings.Replace(result.Values[1:len(result.Values)-1], "'", "", -1)
ret = append(ret, metadata.Enum{
Name: result.Name,
Values: strings.Split(enumValues, ","),
})
}
return ret, nil
}

View File

@@ -0,0 +1,93 @@
package postgres
import (
"database/sql"
"fmt"
"net/url"
"path"
"strconv"
"github.com/go-jet/jet/v2/generator/metadata"
"github.com/go-jet/jet/v2/generator/template"
"github.com/go-jet/jet/v2/postgres"
"github.com/jackc/pgconn"
)
// DBConnection contains postgres connection details
type DBConnection struct {
Host string
Port int
User string
Password string
SslMode string
Params string
DBName string
SchemaName string
}
// Generate generates jet files at destination dir from database connection details
func Generate(destDir string, dbConn DBConnection, genTemplate ...template.Template) (err error) {
dsn := fmt.Sprintf("postgresql://%s:%s@%s:%s/%s?sslmode=%s",
url.PathEscape(dbConn.User),
url.PathEscape(dbConn.Password),
dbConn.Host,
strconv.Itoa(dbConn.Port),
url.PathEscape(dbConn.DBName),
dbConn.SslMode,
)
return GenerateDSN(dsn, dbConn.SchemaName, destDir, genTemplate...)
}
// GenerateDSN generates jet files using dsn connection string
func GenerateDSN(dsn, schema, destDir string, templates ...template.Template) error {
cfg, err := pgconn.ParseConfig(dsn)
if err != nil {
return fmt.Errorf("failed to parse config: %w", err)
}
if cfg.Database == "" {
return fmt.Errorf("database name is required")
}
db, err := openConnection(dsn)
if err != nil {
return fmt.Errorf("failed to open db connection: %w", err)
}
defer db.Close()
fmt.Println("Retrieving schema information...")
generatorTemplate := template.Default(postgres.Dialect)
if len(templates) > 0 {
generatorTemplate = templates[0]
}
schemaMetadata, err := metadata.GetSchema(db, &postgresQuerySet{}, schema)
if err != nil {
return fmt.Errorf("failed to get '%s' schema metadata: %w", schema, err)
}
dirPath := path.Join(destDir, cfg.Database)
err = template.ProcessSchema(dirPath, schemaMetadata, generatorTemplate)
if err != nil {
return fmt.Errorf("failed to generate schema %s: %d", schemaMetadata.Name, err)
}
return nil
}
func openConnection(dsn string) (*sql.DB, error) {
fmt.Println("Connecting to postgres database...")
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, fmt.Errorf("failed to open db connection: %w", err)
}
err = db.Ping()
if err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
return db, nil
}

View File

@@ -0,0 +1,121 @@
package postgres
import (
"context"
"database/sql"
"fmt"
"github.com/go-jet/jet/v2/generator/metadata"
"github.com/go-jet/jet/v2/qrm"
)
// postgresQuerySet is dialect query set for PostgreSQL
type postgresQuerySet struct{}
func (p postgresQuerySet) GetTablesMetaData(db *sql.DB, schemaName string, tableType metadata.TableType) ([]metadata.Table, error) {
query := `
SELECT table_name as "table.name", obj_description((quote_ident(table_schema)||'.'||quote_ident(table_name))::regclass) as "table.comment"
FROM information_schema.tables
WHERE table_schema = $1 and table_type = $2
ORDER BY table_name;
`
var tables []metadata.Table
_, err := qrm.Query(context.Background(), db, query, []interface{}{schemaName, tableType}, &tables)
if err != nil {
return nil, fmt.Errorf("failed to query %s metadata: %w", tableType, err)
}
// add materialized views separately, because materialized views are not part of standard information schema
if tableType == metadata.ViewTable {
matViewQuery := `
select matviewname as "table.name"
from pg_matviews
where schemaname = $1;
`
var matViews []metadata.Table
_, err := qrm.Query(context.Background(), db, matViewQuery, []interface{}{schemaName}, &matViews)
if err != nil {
return nil, fmt.Errorf("failed to query materialized view metadata: %w", err)
}
tables = append(tables, matViews...)
}
for i := range tables {
tables[i].Columns, err = getColumnsMetaData(db, schemaName, tables[i].Name)
if err != nil {
return nil, fmt.Errorf("failed to query %s columns metadata: %w", tableType, err)
}
}
return tables, nil
}
func getColumnsMetaData(db *sql.DB, schemaName string, tableName string) ([]metadata.Column, error) {
query := `
select
attr.attname as "column.Name",
col_description(attr.attrelid, attr.attnum) as "column.Comment",
exists(
select 1
from pg_catalog.pg_index indx
where attr.attrelid = indx.indrelid and attr.attnum = any(indx.indkey) and indx.indisprimary
) as "column.IsPrimaryKey",
not attr.attnotnull as "column.isNullable",
attr.attgenerated = 's' as "column.isGenerated",
attr.atthasdef as "column.hasDefault",
(case
when tp.typtype = 'b' AND tp.typcategory <> 'A' then 'base'
when tp.typtype = 'b' AND tp.typcategory = 'A' then 'array'
when tp.typtype = 'd' then 'base'
when tp.typtype = 'e' then 'enum'
when tp.typtype = 'r' then 'range'
end) as "dataType.Kind",
(case when tp.typtype = 'd' then (select pg_type.typname from pg_catalog.pg_type where pg_type.oid = tp.typbasetype)
when tp.typcategory = 'A' then pg_catalog.format_type(attr.atttypid, attr.atttypmod)
else tp.typname
end) as "dataType.Name",
false as "dataType.isUnsigned"
from pg_catalog.pg_attribute as attr
join pg_catalog.pg_class as cls on cls.oid = attr.attrelid
join pg_catalog.pg_namespace as ns on ns.oid = cls.relnamespace
join pg_catalog.pg_type as tp on tp.oid = attr.atttypid
where
ns.nspname = $1 and
cls.relname = $2 and
not attr.attisdropped and
attr.attnum > 0
order by
attr.attnum;
`
var columns []metadata.Column
_, err := qrm.Query(context.Background(), db, query, []interface{}{schemaName, tableName}, &columns)
if err != nil {
return nil, fmt.Errorf("failed to query '%s' columns metadata: %w", tableName, err)
}
return columns, nil
}
func (p postgresQuerySet) GetEnumsMetaData(db *sql.DB, schemaName string) ([]metadata.Enum, error) {
query := `
SELECT t.typname as "enum.name",
obj_description(t.oid) as "enum.comment",
e.enumlabel as "values"
FROM pg_catalog.pg_type t
JOIN pg_catalog.pg_enum e on t.oid = e.enumtypid
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
WHERE n.nspname = $1
ORDER BY n.nspname, t.typname, e.enumsortorder;`
var result []metadata.Enum
_, err := qrm.Query(context.Background(), db, query, []interface{}{schemaName}, &result)
if err != nil {
return nil, fmt.Errorf("failed to query enums metadata for schema '%s': %w", schemaName, err)
}
return result, nil
}

View File

@@ -0,0 +1,122 @@
package sqlite
import (
"context"
"database/sql"
"fmt"
"strings"
"github.com/go-jet/jet/v2/generator/metadata"
"github.com/go-jet/jet/v2/internal/utils/semantic"
"github.com/go-jet/jet/v2/qrm"
)
// sqliteQuerySet is dialect query set for SQLite
type sqliteQuerySet struct{}
func (p sqliteQuerySet) GetTablesMetaData(db *sql.DB, schemaName string, tableType metadata.TableType) ([]metadata.Table, error) {
query := `
SELECT name as "table.name"
FROM sqlite_master
WHERE type=? AND name != 'sqlite_sequence'
ORDER BY name;
`
sqlTableType := "table"
if tableType == metadata.ViewTable {
sqlTableType = "view"
}
var tables []metadata.Table
_, err := qrm.Query(context.Background(), db, query, []interface{}{sqlTableType}, &tables)
if err != nil {
return nil, fmt.Errorf("failed to query %s metadata: %w", schemaName, err)
}
for i := range tables {
tables[i].Columns, err = p.GetTableColumnsMetaData(db, schemaName, tables[i].Name)
if err != nil {
return nil, fmt.Errorf("failed to query column metadata: %w", err)
}
}
return tables, nil
}
func getTableInfoQuery(db *sql.DB) (string, error) {
var version string
err := db.QueryRow("select sqlite_version();").Scan(&version)
if err != nil {
return "", fmt.Errorf("failed to get sqlite version: %w", err)
}
sqliteVersion, err := semantic.VersionFromString(version)
if err != nil {
return "", fmt.Errorf("can't parse sqlite version: %w", err)
}
// generated columns were added in version 3.26.0
if sqliteVersion.Lt(semantic.Version{Major: 3, Minor: 26, Patch: 0}) {
return `select * from pragma_table_info(?);`, nil
}
return `select * from pragma_table_xinfo(?);`, nil
}
func (p sqliteQuerySet) GetTableColumnsMetaData(db *sql.DB, schemaName string, tableName string) ([]metadata.Column, error) {
tableInfoQuery, err := getTableInfoQuery(db)
if err != nil {
return nil, err
}
var columnInfos []struct {
Name string
Type string
NotNull int32
DfltValue string
Pk int32
Hidden int32
}
_, err = qrm.Query(context.Background(), db, tableInfoQuery, []interface{}{tableName}, &columnInfos)
if err != nil {
return nil, fmt.Errorf("failed to query '%s' column metadata: %w", tableName, err)
}
var columns []metadata.Column
for _, columnInfo := range columnInfos {
columnType := strings.TrimSuffix(getColumnType(columnInfo.Type), " GENERATED ALWAYS")
isGenerated := columnInfo.Hidden == 2 || columnInfo.Hidden == 3 // stored or virtual column
hasDefault := columnInfo.DfltValue != ""
columns = append(columns, metadata.Column{
Name: columnInfo.Name,
IsPrimaryKey: columnInfo.Pk != 0,
IsNullable: columnInfo.NotNull != 1,
IsGenerated: isGenerated,
HasDefault: hasDefault,
DataType: metadata.DataType{
Name: columnType,
Kind: metadata.BaseType,
IsUnsigned: false,
},
})
}
return columns, nil
}
// will convert VARCHAR(10) -> VARCHAR, etc...
func getColumnType(columnType string) string {
return strings.TrimSpace(strings.Split(columnType, "(")[0])
}
func (p sqliteQuerySet) GetEnumsMetaData(db *sql.DB, schemaName string) ([]metadata.Enum, error) {
return nil, nil
}

View File

@@ -0,0 +1,37 @@
package sqlite
import (
"database/sql"
"fmt"
"github.com/go-jet/jet/v2/generator/metadata"
"github.com/go-jet/jet/v2/generator/template"
"github.com/go-jet/jet/v2/sqlite"
)
// GenerateDSN generates jet files using dsn connection string
func GenerateDSN(dsn, destDir string, templates ...template.Template) error {
db, err := sql.Open("sqlite3", dsn)
if err != nil {
return fmt.Errorf("failed to open sqlite connection: %w", err)
}
defer db.Close()
fmt.Println("Retrieving schema information...")
generatorTemplate := template.Default(sqlite.Dialect)
if len(templates) > 0 {
generatorTemplate = templates[0]
}
schemaMetadata, err := metadata.GetSchema(db, &sqliteQuerySet{}, "")
if err != nil {
return fmt.Errorf("failed to query database metadata: %w", err)
}
err = template.ProcessSchema(destDir, schemaMetadata, generatorTemplate)
if err != nil {
return fmt.Errorf("failed to process database %s: %w", schemaMetadata.Name, err)
}
return nil
}

View File

@@ -0,0 +1,196 @@
package template
var autoGenWarningTemplate = `
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
`
var tableSQLBuilderTemplate = `
{{define "column-list" -}}
{{- range $i, $c := . }}
{{- $field := columnField $c}}
{{- if gt $i 0 }}, {{end}}{{$field.Name}}Column
{{- end}}
{{- end}}
package {{package}}
import (
"github.com/go-jet/jet/v2/{{dialect.PackageName}}"
)
var {{tableTemplate.InstanceName}} = new{{tableTemplate.TypeName}}("{{schemaName}}", "{{.Name}}", "{{tableTemplate.DefaultAlias}}")
{{golangComment .Comment}}
type {{structImplName}} struct {
{{dialect.PackageName}}.Table
// Columns
{{- range $i, $c := .Columns}}
{{- $field := columnField $c}}
{{$field.Name}} {{dialect.PackageName}}.Column{{$field.Type}} {{golangComment .Comment}}
{{- end}}
AllColumns {{dialect.PackageName}}.ColumnList
MutableColumns {{dialect.PackageName}}.ColumnList
}
type {{tableTemplate.TypeName}} struct {
{{structImplName}}
{{toUpper insertedRowAlias}} {{structImplName}}
}
// AS creates new {{tableTemplate.TypeName}} with assigned alias
func (a {{tableTemplate.TypeName}}) AS(alias string) *{{tableTemplate.TypeName}} {
return new{{tableTemplate.TypeName}}(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new {{tableTemplate.TypeName}} with assigned schema name
func (a {{tableTemplate.TypeName}}) FromSchema(schemaName string) *{{tableTemplate.TypeName}} {
return new{{tableTemplate.TypeName}}(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new {{tableTemplate.TypeName}} with assigned table prefix
func (a {{tableTemplate.TypeName}}) WithPrefix(prefix string) *{{tableTemplate.TypeName}} {
return new{{tableTemplate.TypeName}}(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new {{tableTemplate.TypeName}} with assigned table suffix
func (a {{tableTemplate.TypeName}}) WithSuffix(suffix string) *{{tableTemplate.TypeName}} {
return new{{tableTemplate.TypeName}}(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func new{{tableTemplate.TypeName}}(schemaName, tableName, alias string) *{{tableTemplate.TypeName}} {
return &{{tableTemplate.TypeName}}{
{{structImplName}}: new{{tableTemplate.TypeName}}Impl(schemaName, tableName, alias),
{{toUpper insertedRowAlias}}: new{{tableTemplate.TypeName}}Impl("", "{{insertedRowAlias}}", ""),
}
}
func new{{tableTemplate.TypeName}}Impl(schemaName, tableName, alias string) {{structImplName}} {
var (
{{- range $i, $c := .Columns}}
{{- $field := columnField $c}}
{{$field.Name}}Column = {{dialect.PackageName}}.{{$field.Type}}Column("{{$c.Name}}")
{{- end}}
allColumns = {{dialect.PackageName}}.ColumnList{ {{template "column-list" .Columns}} }
mutableColumns = {{dialect.PackageName}}.ColumnList{ {{template "column-list" .MutableColumns}} }
)
return {{structImplName}}{
Table: {{dialect.PackageName}}.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
{{- range $i, $c := .Columns}}
{{- $field := columnField $c}}
{{$field.Name}}: {{$field.Name}}Column,
{{- end}}
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}
`
var tableSqlBuilderSetSchemaTemplate = `package {{package}}
// UseSchema sets a new schema name for all generated {{type}} SQL builder types. It is recommended to invoke
// this method only once at the beginning of the program.
func UseSchema(schema string) {
{{- range .}}
{{ .InstanceName }} = {{ .InstanceName }}.FromSchema(schema)
{{- end}}
}
`
var tableModelFileTemplate = `package {{package}}
{{ with modelImports }}
import (
{{- range .}}
"{{.}}"
{{- end}}
)
{{end}}
{{$modelTableTemplate := tableTemplate}}
{{golangComment .Comment}}
type {{$modelTableTemplate.TypeName}} struct {
{{- range .Columns}}
{{- $field := structField .}}
{{$field.Name}} {{$field.Type.Name}} ` + "{{$field.TagsString}}" + ` {{golangComment .Comment}}
{{- end}}
}
`
var enumSQLBuilderTemplate = `package {{package}}
import "github.com/go-jet/jet/v2/{{dialect.PackageName}}"
{{golangComment .Comment}}
var {{enumTemplate.InstanceName}} = &struct {
{{- range $index, $value := .Values}}
{{enumValueName $value}} {{dialect.PackageName}}.StringExpression
{{- end}}
} {
{{- range $index, $value := .Values}}
{{enumValueName $value}}: {{dialect.PackageName}}.NewEnumValue("{{$value}}"),
{{- end}}
}
`
var enumModelTemplate = `package {{package}}
{{- $enumTemplate := enumTemplate}}
import "errors"
{{golangComment .Comment}}
type {{$enumTemplate.TypeName}} string
const (
{{- range $_, $value := .Values}}
{{valueName $value}} {{$enumTemplate.TypeName}} = "{{$value}}"
{{- end}}
)
var {{$enumTemplate.TypeName}}AllValues = []{{$enumTemplate.TypeName}} {
{{- range $_, $value := .Values}}
{{valueName $value}},
{{- end}}
}
func (e *{{$enumTemplate.TypeName}}) Scan(value interface{}) error {
var enumValue string
switch val := value.(type) {
case string:
enumValue = val
case []byte:
enumValue = string(val)
default:
return errors.New("jet: Invalid scan value for AllTypesEnum enum. Enum value has to be of type string or []byte")
}
switch enumValue {
{{- range $_, $value := .Values}}
case "{{$value}}":
*e = {{valueName $value}}
{{- end}}
default:
return errors.New("jet: Invalid scan value '" + enumValue + "' for {{$enumTemplate.TypeName}} enum")
}
return nil
}
func (e {{$enumTemplate.TypeName}}) String() string {
return string(e)
}
`

View File

@@ -0,0 +1,13 @@
package template
import "regexp"
// Returns the provided string as golang comment without ascii control characters
func formatGolangComment(comment string) string {
if len(comment) == 0 {
return ""
}
// Format as colang comment and remove ascii control characters from string
return "// " + regexp.MustCompile(`[[:cntrl:]]+`).ReplaceAllString(comment, "")
}

View File

@@ -0,0 +1,27 @@
package template
import "testing"
func Test_formatGolangComment(t *testing.T) {
type args struct {
comment string
}
tests := []struct {
name string
args args
want string
}{
{name: "Empty string", args: args{comment: ""}, want: ""},
{name: "Non-empty string", args: args{comment: "This is a comment"}, want: "// This is a comment"},
{name: "String with control characters", args: args{comment: "This is a comment with control characters \x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f and text after"}, want: "// This is a comment with control characters and text after"},
{name: "String with escape characters", args: args{comment: "This is a comment with escape characters \n\r\t and text after"}, want: "// This is a comment with escape characters and text after"},
{name: "String with unicode characters", args: args{comment: "This is a comment with unicode characters ₲鬼佬℧⇄↻ and text after"}, want: "// This is a comment with unicode characters ₲鬼佬℧⇄↻ and text after"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := formatGolangComment(tt.args.comment); got != tt.want {
t.Errorf("formatGoLangComment() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -0,0 +1,60 @@
package template
import (
"github.com/go-jet/jet/v2/generator/metadata"
"github.com/go-jet/jet/v2/internal/jet"
)
// Template is generator template used for file generation
type Template struct {
Dialect jet.Dialect
Schema func(schemaMetaData metadata.Schema) Schema
}
// Default is default generator template implementation
func Default(dialect jet.Dialect) Template {
return Template{
Dialect: dialect,
Schema: DefaultSchema,
}
}
// UseSchema replaces current schema generate function with a new implementation and returns new generator template
func (t Template) UseSchema(schemaFunc func(schemaMetaData metadata.Schema) Schema) Template {
t.Schema = schemaFunc
return t
}
// Schema is schema generator template used to generate schema(model and sql builder) files
type Schema struct {
Path string
Model Model
SQLBuilder SQLBuilder
}
// UsePath replaces path and returns new schema template
func (s Schema) UsePath(path string) Schema {
s.Path = path
return s
}
// UseModel returns new schema template with replaced template for model files generation
func (s Schema) UseModel(model Model) Schema {
s.Model = model
return s
}
// UseSQLBuilder returns new schema with replaced template for sql builder files generation
func (s Schema) UseSQLBuilder(sqlBuilder SQLBuilder) Schema {
s.SQLBuilder = sqlBuilder
return s
}
// DefaultSchema returns default schema template implementation
func DefaultSchema(schemaMetaData metadata.Schema) Schema {
return Schema{
Path: schemaMetaData.Name,
Model: DefaultModel(),
SQLBuilder: DefaultSQLBuilder(),
}
}

View File

@@ -0,0 +1,340 @@
package template
import (
"fmt"
"github.com/go-jet/jet/v2/generator/metadata"
"github.com/go-jet/jet/v2/internal/utils/dbidentifier"
"github.com/google/uuid"
"github.com/jackc/pgtype"
"path"
"reflect"
"strings"
"time"
)
// Model is template for model files generation
type Model struct {
Skip bool
Path string
Table func(table metadata.Table) TableModel
View func(table metadata.Table) ViewModel
Enum func(enum metadata.Enum) EnumModel
}
// PackageName returns package name of model types
func (m Model) PackageName() string {
return path.Base(m.Path)
}
// UsePath returns new Model template with replaced file path
func (m Model) UsePath(path string) Model {
m.Path = path
return m
}
// UseTable returns new Model template with replaced template for table model files generation
func (m Model) UseTable(tableModelFunc func(table metadata.Table) TableModel) Model {
m.Table = tableModelFunc
return m
}
// UseView returns new Model template with replaced template for view model files generation
func (m Model) UseView(tableModelFunc func(table metadata.Table) TableModel) Model {
m.View = tableModelFunc
return m
}
// UseEnum returns new Model template with replaced template for enum model files generation
func (m Model) UseEnum(enumFunc func(enumMetaData metadata.Enum) EnumModel) Model {
m.Enum = enumFunc
return m
}
// DefaultModel returns default Model template implementation
func DefaultModel() Model {
return Model{
Skip: false,
Path: "/model",
Table: DefaultTableModel,
View: DefaultViewModel,
Enum: DefaultEnumModel,
}
}
// TableModel is template for table model files generation
type TableModel struct {
Skip bool
FileName string
TypeName string
Field func(columnMetaData metadata.Column) TableModelField
}
// ViewModel is template for view model files generation
type ViewModel = TableModel
// DefaultViewModel is default view template implementation
var DefaultViewModel = DefaultTableModel
// DefaultTableModel is default table template implementation
func DefaultTableModel(tableMetaData metadata.Table) TableModel {
return TableModel{
FileName: dbidentifier.ToGoFileName(tableMetaData.Name),
TypeName: dbidentifier.ToGoIdentifier(tableMetaData.Name),
Field: DefaultTableModelField,
}
}
// UseFileName returns new TableModel with new file name set
func (t TableModel) UseFileName(fileName string) TableModel {
t.FileName = fileName
return t
}
// UseTypeName returns new TableModel with new type name set
func (t TableModel) UseTypeName(typeName string) TableModel {
t.TypeName = typeName
return t
}
// UseField returns new TableModel with new TableModelField template function
func (t TableModel) UseField(structFieldFunc func(columnMetaData metadata.Column) TableModelField) TableModel {
t.Field = structFieldFunc
return t
}
func getTableModelImports(modelType TableModel, tableMetaData metadata.Table) []string {
importPaths := map[string]bool{}
for _, columnMetaData := range tableMetaData.Columns {
field := modelType.Field(columnMetaData)
importPath := field.Type.ImportPath
if importPath != "" {
importPaths[importPath] = true
}
}
var ret []string
for importPath := range importPaths {
ret = append(ret, importPath)
}
return ret
}
// EnumModel is template for enum model files generation
type EnumModel struct {
Skip bool
FileName string
TypeName string
ValueName func(value string) string
}
// UseFileName returns new EnumModel with new file name set
func (em EnumModel) UseFileName(fileName string) EnumModel {
em.FileName = fileName
return em
}
// UseTypeName returns new EnumModel with new type name set
func (em EnumModel) UseTypeName(typeName string) EnumModel {
em.TypeName = typeName
return em
}
// DefaultEnumModel returns default implementation for EnumModel
func DefaultEnumModel(enumMetaData metadata.Enum) EnumModel {
typeName := dbidentifier.ToGoIdentifier(enumMetaData.Name)
return EnumModel{
FileName: dbidentifier.ToGoFileName(enumMetaData.Name),
TypeName: typeName,
ValueName: func(value string) string {
return typeName + "_" + dbidentifier.ToGoIdentifier(value)
},
}
}
// TableModelField is template for table model field generation
type TableModelField struct {
Name string
Type Type
Tags []string
}
// DefaultTableModelField returns default TableModelField implementation
func DefaultTableModelField(columnMetaData metadata.Column) TableModelField {
var tags []string
if columnMetaData.IsPrimaryKey {
tags = append(tags, `sql:"primary_key"`)
}
return TableModelField{
Name: dbidentifier.ToGoIdentifier(columnMetaData.Name),
Type: getType(columnMetaData),
Tags: tags,
}
}
// UseType returns new TypeModelField with a new field type set
func (f TableModelField) UseType(t Type) TableModelField {
f.Type = t
return f
}
// UseName returns new TableModelField implementation with new field name set
func (f TableModelField) UseName(name string) TableModelField {
f.Name = name
return f
}
// UseTags returns new TableModelField implementation with additional tags added.
func (f TableModelField) UseTags(tags ...string) TableModelField {
f.Tags = append(f.Tags, tags...)
return f
}
// TagsString returns tags string representation
func (f TableModelField) TagsString() string {
if len(f.Tags) == 0 {
return ""
}
return fmt.Sprintf("`%s`", strings.Join(f.Tags, " "))
}
// Type represents type of the struct field
type Type struct {
ImportPath string
Name string
}
// NewType creates new type for dummy object
func NewType(dummyObject interface{}) Type {
return Type{
ImportPath: getImportPath(dummyObject),
Name: getTypeName(dummyObject),
}
}
func getTypeName(t interface{}) string {
typeStr := reflect.TypeOf(t).String()
typeStr = strings.Replace(typeStr, "[]uint8", "[]byte", -1)
return typeStr
}
func getImportPath(dummyData interface{}) string {
dataType := reflect.TypeOf(dummyData)
if dataType.Kind() == reflect.Ptr {
return dataType.Elem().PkgPath()
}
return dataType.PkgPath()
}
func getType(columnMetadata metadata.Column) Type {
userDefinedType := getUserDefinedType(columnMetadata)
if userDefinedType != "" {
if columnMetadata.IsNullable {
return Type{Name: "*" + userDefinedType}
}
return Type{Name: userDefinedType}
}
return NewType(getGoType(columnMetadata))
}
func getUserDefinedType(column metadata.Column) string {
switch column.DataType.Kind {
case metadata.EnumType:
return dbidentifier.ToGoIdentifier(column.DataType.Name)
case metadata.UserDefinedType, metadata.ArrayType:
return "string"
}
return ""
}
func getGoType(column metadata.Column) interface{} {
defaultGoType := toGoType(column)
if column.IsNullable {
return reflect.New(reflect.TypeOf(defaultGoType)).Interface()
}
return defaultGoType
}
// toGoType returns model type for column info.
func toGoType(column metadata.Column) interface{} {
switch strings.ToLower(column.DataType.Name) {
case "user-defined", "enum":
return ""
case "boolean", "bool":
return false
case "tinyint":
if column.DataType.IsUnsigned {
return uint8(0)
}
return int8(0)
case "smallint", "int2",
"year":
if column.DataType.IsUnsigned {
return uint16(0)
}
return int16(0)
case "integer", "int4",
"mediumint", "int": //MySQL
if column.DataType.IsUnsigned {
return uint32(0)
}
return int32(0)
case "bigint", "int8":
if column.DataType.IsUnsigned {
return uint64(0)
}
return int64(0)
case "date",
"timestamp without time zone", "timestamp",
"timestamp with time zone", "timestamptz",
"time without time zone", "time",
"time with time zone", "timetz",
"datetime": // MySQL
return time.Time{}
case "bytea",
"binary", "varbinary", "tinyblob", "blob", "mediumblob", "longblob": //MySQL
return []byte("")
case "text",
"character", "bpchar",
"character varying", "varchar", "nvarchar",
"tsvector", "bit", "bit varying", "varbit",
"money", "json", "jsonb",
"xml", "point", "interval", "line", "array",
"char", "tinytext", "mediumtext", "longtext": // MySQL
return ""
case "real", "float4":
return float32(0.0)
case "numeric", "decimal",
"double precision", "float8", "float",
"double": // MySQL
return float64(0.0)
case "uuid":
return uuid.UUID{}
case "daterange":
return pgtype.Daterange{}
case "tsrange":
return pgtype.Tsrange{}
case "tstzrange":
return pgtype.Tstzrange{}
case "int4range":
return pgtype.Int4range{}
case "int8range":
return pgtype.Int8range{}
case "numrange":
return pgtype.Numrange{}
default:
fmt.Println("- [Model ] Unsupported sql column '" + column.Name + " " + column.DataType.Name + "', using string instead.")
return ""
}
}

View File

@@ -0,0 +1,45 @@
package template
import (
"github.com/go-jet/jet/v2/generator/metadata"
"github.com/stretchr/testify/require"
"testing"
)
func Test_TableModelField(t *testing.T) {
require.Equal(t, DefaultTableModelField(metadata.Column{
Name: "col_name",
IsPrimaryKey: true,
IsNullable: true,
DataType: metadata.DataType{
Name: "smallint",
Kind: "base",
IsUnsigned: true,
},
}), TableModelField{
Name: "ColName",
Type: Type{
ImportPath: "",
Name: "*uint16",
},
Tags: []string{"sql:\"primary_key\""},
})
require.Equal(t, DefaultTableModelField(metadata.Column{
Name: "time_column_1",
IsPrimaryKey: false,
IsNullable: true,
DataType: metadata.DataType{
Name: "timestamp with time zone",
Kind: "base",
IsUnsigned: false,
},
}), TableModelField{
Name: "TimeColumn1",
Type: Type{
ImportPath: "time",
Name: "*time.Time",
},
Tags: nil,
})
}

View File

@@ -0,0 +1,382 @@
package template
import (
"bytes"
"errors"
"fmt"
"github.com/go-jet/jet/v2/internal/utils/filesys"
"path"
"strings"
"text/template"
"github.com/go-jet/jet/v2/generator/metadata"
"github.com/go-jet/jet/v2/internal/jet"
)
// ProcessSchema will process schema metadata and constructs go files using generator Template
func ProcessSchema(dirPath string, schemaMetaData metadata.Schema, generatorTemplate Template) error {
if schemaMetaData.IsEmpty() {
return nil
}
schemaTemplate := generatorTemplate.Schema(schemaMetaData)
schemaPath := path.Join(dirPath, schemaTemplate.Path)
fmt.Println("Destination directory:", schemaPath)
fmt.Println("Cleaning up destination directory...")
err := filesys.RemoveDir(schemaPath)
if err != nil {
return errors.New("failed to cleanup generated files")
}
err = processModel(schemaPath, schemaMetaData, schemaTemplate)
if err != nil {
return fmt.Errorf("failed to generate model types: %w", err)
}
err = processSQLBuilder(schemaPath, generatorTemplate.Dialect, schemaMetaData, schemaTemplate)
if err != nil {
return fmt.Errorf("failed to generate sql builder types: %w", err)
}
return nil
}
func processModel(dirPath string, schemaMetaData metadata.Schema, schemaTemplate Schema) error {
modelTemplate := schemaTemplate.Model
if modelTemplate.Skip {
fmt.Println("Skipping the generation of model types.")
return nil
}
modelDirPath := path.Join(dirPath, modelTemplate.Path)
err := filesys.EnsureDirPathExist(modelDirPath)
if err != nil {
return fmt.Errorf("destination dir path does not exist: %w", err)
}
err = processTableModels("table", modelDirPath, schemaMetaData.TablesMetaData, modelTemplate)
if err != nil {
return fmt.Errorf("failed to generate table model types: %w", err)
}
err = processTableModels("view", modelDirPath, schemaMetaData.ViewsMetaData, modelTemplate)
if err != nil {
return fmt.Errorf("failed to generate view model types: %w", err)
}
err = processEnumModels(modelDirPath, schemaMetaData.EnumsMetaData, modelTemplate)
if err != nil {
return fmt.Errorf("failed to process enum types: %w", err)
}
return nil
}
func processSQLBuilder(dirPath string, dialect jet.Dialect, schemaMetaData metadata.Schema, schemaTemplate Schema) error {
sqlBuilderTemplate := schemaTemplate.SQLBuilder
if sqlBuilderTemplate.Skip {
fmt.Println("Skipping the generation of SQL Builder types.")
return nil
}
sqlBuilderPath := path.Join(dirPath, sqlBuilderTemplate.Path)
err := processTableSQLBuilder("table", sqlBuilderPath, dialect, schemaMetaData, schemaMetaData.TablesMetaData, sqlBuilderTemplate)
if err != nil {
return fmt.Errorf("failed to process table sql builder types: %w", err)
}
err = processTableSQLBuilder("view", sqlBuilderPath, dialect, schemaMetaData, schemaMetaData.ViewsMetaData, sqlBuilderTemplate)
if err != nil {
return fmt.Errorf("failed to process view sql builder types: %w", err)
}
err = processEnumSQLBuilder(sqlBuilderPath, dialect, schemaMetaData.EnumsMetaData, sqlBuilderTemplate)
if err != nil {
return fmt.Errorf("failed to process enum types: %w", err)
}
return nil
}
func processEnumSQLBuilder(dirPath string, dialect jet.Dialect, enumsMetaData []metadata.Enum, sqlBuilder SQLBuilder) error {
if len(enumsMetaData) == 0 {
return nil
}
fmt.Printf("Generating enum sql builder files\n")
for _, enumMetaData := range enumsMetaData {
enumTemplate := sqlBuilder.Enum(enumMetaData)
if enumTemplate.Skip {
continue
}
enumSQLBuilderPath := path.Join(dirPath, enumTemplate.Path)
err := filesys.EnsureDirPathExist(enumSQLBuilderPath)
if err != nil {
return fmt.Errorf("failed to create enum sql builder directory - %s: %w", enumSQLBuilderPath, err)
}
text, err := generateTemplate(
autoGenWarningTemplate+enumSQLBuilderTemplate,
enumMetaData,
template.FuncMap{
"package": func() string {
return enumTemplate.PackageName()
},
"dialect": func() jet.Dialect {
return dialect
},
"enumTemplate": func() EnumSQLBuilder {
return enumTemplate
},
"enumValueName": func(enumValue string) string {
return enumTemplate.ValueName(enumValue)
},
"golangComment": formatGolangComment,
})
if err != nil {
return fmt.Errorf("failed to generete enum type %s: %w", enumTemplate.FileName, err)
}
err = filesys.FormatAndSaveGoFile(enumSQLBuilderPath, enumTemplate.FileName, text)
if err != nil {
return fmt.Errorf("failed to format and save '%s' enum type : %w", enumTemplate.FileName, err)
}
}
return nil
}
func processTableSQLBuilder(fileTypes, dirPath string,
dialect jet.Dialect,
schemaMetaData metadata.Schema,
tablesMetaData []metadata.Table,
sqlBuilderTemplate SQLBuilder) error {
if len(tablesMetaData) == 0 {
return nil
}
fmt.Printf("Generating %s sql builder files\n", fileTypes)
var generatedBuilders []TableSQLBuilder
for _, tableMetaData := range tablesMetaData {
var tableSQLBuilder TableSQLBuilder
if fileTypes == "view" {
tableSQLBuilder = sqlBuilderTemplate.View(tableMetaData)
} else {
tableSQLBuilder = sqlBuilderTemplate.Table(tableMetaData)
}
if tableSQLBuilder.Skip {
continue
}
tableSQLBuilderPath := path.Join(dirPath, tableSQLBuilder.Path)
err := filesys.EnsureDirPathExist(tableSQLBuilderPath)
if err != nil {
return fmt.Errorf("failed to create table sql builder directory - %s: %w", tableSQLBuilderPath, err)
}
text, err := generateTemplate(
autoGenWarningTemplate+tableSQLBuilderTemplate,
tableMetaData,
template.FuncMap{
"package": func() string {
return tableSQLBuilder.PackageName()
},
"dialect": func() jet.Dialect {
return dialect
},
"schemaName": func() string {
return schemaMetaData.Name
},
"tableTemplate": func() TableSQLBuilder {
return tableSQLBuilder
},
"structImplName": func() string { // postgres only
structName := tableSQLBuilder.TypeName
return string(strings.ToLower(structName)[0]) + structName[1:]
},
"columnField": func(columnMetaData metadata.Column) TableSQLBuilderColumn {
return tableSQLBuilder.Column(columnMetaData)
},
"toUpper": strings.ToUpper,
"insertedRowAlias": func() string {
return insertedRowAlias(dialect)
},
"golangComment": formatGolangComment,
})
if err != nil {
return fmt.Errorf("failed to generate table sql builder type %s: %w", tableSQLBuilder.TypeName, err)
}
err = filesys.FormatAndSaveGoFile(tableSQLBuilderPath, tableSQLBuilder.FileName, text)
if err != nil {
return fmt.Errorf("failed to format and save generated sql builder type '%s': %w", tableSQLBuilder.FileName, err)
}
generatedBuilders = append(generatedBuilders, tableSQLBuilder)
}
err := generateUseSchemaFunc(dirPath, fileTypes, generatedBuilders)
if err != nil {
return fmt.Errorf("failed to generate UseSchema function")
}
return nil
}
func generateUseSchemaFunc(dirPath, fileTypes string, builders []TableSQLBuilder) error {
if len(builders) == 0 {
return nil
}
text, err := generateTemplate(
autoGenWarningTemplate+tableSqlBuilderSetSchemaTemplate,
builders,
template.FuncMap{
"package": func() string { return builders[0].PackageName() },
"type": func() string { return fileTypes },
},
)
if err != nil {
return fmt.Errorf("failed to generate use schema template: %w", err)
}
basePath := path.Join(dirPath, builders[0].Path)
fileName := fileTypes + "_use_schema"
err = filesys.FormatAndSaveGoFile(basePath, fileName, text)
if err != nil {
return fmt.Errorf("failed to save %s file: %w", fileName, err)
}
return nil
}
func insertedRowAlias(dialect jet.Dialect) string {
if dialect.Name() == "MySQL" {
return "new"
}
return "excluded"
}
func processTableModels(fileTypes, modelDirPath string, tablesMetaData []metadata.Table, modelTemplate Model) error {
if len(tablesMetaData) == 0 {
return nil
}
fmt.Printf("Generating %s model files...\n", fileTypes)
for _, tableMetaData := range tablesMetaData {
var tableTemplate TableModel
if fileTypes == "table" {
tableTemplate = modelTemplate.Table(tableMetaData)
} else {
tableTemplate = modelTemplate.View(tableMetaData)
}
if tableTemplate.Skip {
continue
}
text, err := generateTemplate(
autoGenWarningTemplate+tableModelFileTemplate,
tableMetaData,
template.FuncMap{
"package": func() string {
return modelTemplate.PackageName()
},
"modelImports": func() []string {
return getTableModelImports(tableTemplate, tableMetaData)
},
"tableTemplate": func() TableModel {
return tableTemplate
},
"structField": func(columnMetaData metadata.Column) TableModelField {
return tableTemplate.Field(columnMetaData)
},
"golangComment": formatGolangComment,
})
if err != nil {
return fmt.Errorf("failed to generate model type '%s': %w", tableMetaData.Name, err)
}
err = filesys.FormatAndSaveGoFile(modelDirPath, tableTemplate.FileName, text)
if err != nil {
return fmt.Errorf("failed to save '%s' model type: %w", tableTemplate.FileName, err)
}
}
return nil
}
func processEnumModels(modelDir string, enumsMetaData []metadata.Enum, modelTemplate Model) error {
if len(enumsMetaData) == 0 {
return nil
}
fmt.Print("Generating enum model files...\n")
for _, enumMetaData := range enumsMetaData {
enumTemplate := modelTemplate.Enum(enumMetaData)
if enumTemplate.Skip {
continue
}
text, err := generateTemplate(
autoGenWarningTemplate+enumModelTemplate,
enumMetaData,
template.FuncMap{
"package": func() string {
return modelTemplate.PackageName()
},
"enumTemplate": func() EnumModel {
return enumTemplate
},
"valueName": func(value string) string {
return enumTemplate.ValueName(value)
},
"golangComment": formatGolangComment,
})
if err != nil {
return fmt.Errorf("failed to generate enum type '%s': %w", enumMetaData.Name, err)
}
err = filesys.FormatAndSaveGoFile(modelDir, enumTemplate.FileName, text)
if err != nil {
return fmt.Errorf("failed to save '%s' enum type: %w", enumTemplate.FileName, err)
}
}
return nil
}
func generateTemplate(templateText string, templateData interface{}, funcMap template.FuncMap) ([]byte, error) {
t, err := template.New("sqlBuilderTableTemplate").Funcs(funcMap).Parse(templateText)
if err != nil {
return nil, fmt.Errorf("failed to parse template: %w", err)
}
var buf bytes.Buffer
if err := t.Execute(&buf, templateData); err != nil {
return nil, fmt.Errorf("failed to generate template: %w", err)
}
return buf.Bytes(), nil
}

View File

@@ -0,0 +1,259 @@
package template
import (
"fmt"
"github.com/go-jet/jet/v2/generator/metadata"
"github.com/go-jet/jet/v2/internal/utils/dbidentifier"
"path"
"slices"
"strings"
"unicode"
)
// SQLBuilder is template for generating sql builder files
type SQLBuilder struct {
Skip bool
Path string
Table func(table metadata.Table) TableSQLBuilder
View func(view metadata.Table) TableSQLBuilder
Enum func(enum metadata.Enum) EnumSQLBuilder
}
// DefaultSQLBuilder returns default SQLBuilder implementation
func DefaultSQLBuilder() SQLBuilder {
return SQLBuilder{
Path: "",
Table: DefaultTableSQLBuilder,
View: DefaultViewSQLBuilder,
Enum: DefaultEnumSQLBuilder,
}
}
// UsePath returns new SQLBuilder with new relative path set
func (sb SQLBuilder) UsePath(path string) SQLBuilder {
sb.Path = path
return sb
}
// UseTable returns new SQLBuilder with new TableSQLBuilder template function set
func (sb SQLBuilder) UseTable(tableFunc func(table metadata.Table) TableSQLBuilder) SQLBuilder {
sb.Table = tableFunc
return sb
}
// UseView returns new SQLBuilder with new ViewSQLBuilder template function set
func (sb SQLBuilder) UseView(viewFunc func(table metadata.Table) ViewSQLBuilder) SQLBuilder {
sb.View = viewFunc
return sb
}
// UseEnum returns new SQLBuilder with new EnumSQLBuilder template function set
func (sb SQLBuilder) UseEnum(enumFunc func(enum metadata.Enum) EnumSQLBuilder) SQLBuilder {
sb.Enum = enumFunc
return sb
}
// TableSQLBuilder is template for generating table SQLBuilder files
type TableSQLBuilder struct {
Skip bool
Path string
FileName string
InstanceName string
TypeName string
DefaultAlias string
Column func(columnMetaData metadata.Column) TableSQLBuilderColumn
}
// ViewSQLBuilder is template for generating view SQLBuilder files
type ViewSQLBuilder = TableSQLBuilder
// DefaultTableSQLBuilder returns default implementation for TableSQLBuilder
func DefaultTableSQLBuilder(tableMetaData metadata.Table) TableSQLBuilder {
tableNameGoIdentifier := dbidentifier.ToGoIdentifier(tableMetaData.Name)
return TableSQLBuilder{
Path: "/table",
FileName: dbidentifier.ToGoFileName(tableMetaData.Name),
InstanceName: tableNameGoIdentifier,
TypeName: tableNameGoIdentifier + "Table",
DefaultAlias: "",
Column: DefaultTableSQLBuilderColumn,
}
}
// DefaultViewSQLBuilder returns default implementation for ViewSQLBuilder
func DefaultViewSQLBuilder(viewMetaData metadata.Table) ViewSQLBuilder {
tableSQLBuilder := DefaultTableSQLBuilder(viewMetaData)
tableSQLBuilder.Path = "/view"
return tableSQLBuilder
}
// PackageName returns package name of table sql builder types
func (tb TableSQLBuilder) PackageName() string {
return path.Base(tb.Path)
}
// UsePath returns new TableSQLBuilder with new relative path set
func (tb TableSQLBuilder) UsePath(path string) TableSQLBuilder {
tb.Path = path
return tb
}
// UseFileName returns new TableSQLBuilder with new file name set
func (tb TableSQLBuilder) UseFileName(name string) TableSQLBuilder {
tb.FileName = name
return tb
}
// UseInstanceName returns new TableSQLBuilder with new instance name set
func (tb TableSQLBuilder) UseInstanceName(name string) TableSQLBuilder {
tb.InstanceName = name
return tb
}
// UseTypeName returns new TableSQLBuilder with new type name set
func (tb TableSQLBuilder) UseTypeName(name string) TableSQLBuilder {
tb.TypeName = name
return tb
}
// UseDefaultAlias returns new TableSQLBuilder with new default alias set
func (tb TableSQLBuilder) UseDefaultAlias(defaultAlias string) TableSQLBuilder {
tb.DefaultAlias = defaultAlias
return tb
}
// UseColumn returns new TableSQLBuilder with new column template function set
func (tb TableSQLBuilder) UseColumn(columnsFunc func(column metadata.Column) TableSQLBuilderColumn) TableSQLBuilder {
tb.Column = columnsFunc
return tb
}
// TableSQLBuilderColumn is template for table sql builder column
type TableSQLBuilderColumn struct {
Name string
Type string
}
var reservedKeywords = []string{"TableName", "Table", "SchemaName", "Alias", "AllColumns", "MutableColumns"}
func renameIfReserved(name string) string {
if slices.Contains(reservedKeywords, name) {
return name + "_"
}
return name
}
// DefaultTableSQLBuilderColumn returns default implementation of TableSQLBuilderColumn
func DefaultTableSQLBuilderColumn(columnMetaData metadata.Column) TableSQLBuilderColumn {
return TableSQLBuilderColumn{
Name: renameIfReserved(dbidentifier.ToGoIdentifier(columnMetaData.Name)),
Type: getSqlBuilderColumnType(columnMetaData),
}
}
// getSqlBuilderColumnType returns type of jet sql builder column
func getSqlBuilderColumnType(columnMetaData metadata.Column) string {
if columnMetaData.DataType.Kind != metadata.BaseType &&
columnMetaData.DataType.Kind != metadata.RangeType {
return "String"
}
switch strings.ToLower(columnMetaData.DataType.Name) {
case "boolean", "bool":
return "Bool"
case "smallint", "integer", "bigint", "int2", "int4", "int8",
"tinyint", "mediumint", "int", "year": //MySQL
return "Integer"
case "date":
return "Date"
case "timestamp without time zone",
"timestamp", "datetime": //MySQL:
return "Timestamp"
case "timestamp with time zone", "timestamptz":
return "Timestampz"
case "time without time zone",
"time": //MySQL
return "Time"
case "time with time zone", "timetz":
return "Timez"
case "interval":
return "Interval"
case "user-defined", "enum", "text", "character", "character varying", "bytea", "uuid",
"tsvector", "bit", "bit varying", "money", "json", "jsonb", "xml", "point", "line", "ARRAY",
"char", "varchar", "nvarchar", "binary", "varbinary", "bpchar", "varbit",
"tinyblob", "blob", "mediumblob", "longblob", "tinytext", "mediumtext", "longtext": // MySQL
return "String"
case "real", "numeric", "decimal", "double precision", "float", "float4", "float8",
"double": // MySQL
return "Float"
case "daterange":
return "DateRange"
case "tsrange":
return "TimestampRange"
case "tstzrange":
return "TimestampzRange"
case "int4range":
return "Int4Range"
case "int8range":
return "Int8Range"
case "numrange":
return "NumericRange"
default:
fmt.Println("- [SQL Builder] Unsupported sql column '" + columnMetaData.Name + " " + columnMetaData.DataType.Name + "', using StringColumn instead.")
return "String"
}
}
// EnumSQLBuilder is template for generating enum SQLBuilder files
type EnumSQLBuilder struct {
Skip bool
Path string
FileName string
InstanceName string
ValueName func(enumValue string) string
}
// DefaultEnumSQLBuilder returns default implementation of EnumSQLBuilder
func DefaultEnumSQLBuilder(enumMetaData metadata.Enum) EnumSQLBuilder {
return EnumSQLBuilder{
Path: "/enum",
FileName: dbidentifier.ToGoFileName(enumMetaData.Name),
InstanceName: dbidentifier.ToGoIdentifier(enumMetaData.Name),
ValueName: func(enumValue string) string {
return defaultEnumValueName(enumMetaData.Name, enumValue)
},
}
}
// PackageName returns enum sql builder package name
func (e EnumSQLBuilder) PackageName() string {
return path.Base(e.Path)
}
// UsePath returns new EnumSQLBuilder with new path set
func (e EnumSQLBuilder) UsePath(path string) EnumSQLBuilder {
e.Path = path
return e
}
// UseFileName returns new EnumSQLBuilder with new file name set
func (e EnumSQLBuilder) UseFileName(name string) EnumSQLBuilder {
e.FileName = name
return e
}
// UseInstanceName returns new EnumSQLBuilder with instance name set
func (e EnumSQLBuilder) UseInstanceName(name string) EnumSQLBuilder {
e.InstanceName = name
return e
}
func defaultEnumValueName(enumName, enumValue string) string {
enumValueName := dbidentifier.ToGoIdentifier(enumValue)
if !unicode.IsLetter([]rune(enumValueName)[0]) {
return dbidentifier.ToGoIdentifier(enumName) + enumValueName
}
return enumValueName
}

View File

@@ -0,0 +1,36 @@
package template
import (
"github.com/go-jet/jet/v2/generator/metadata"
"github.com/stretchr/testify/require"
"testing"
)
func TestToGoEnumValueIdentifier(t *testing.T) {
require.Equal(t, defaultEnumValueName("enum_name", "enum_value"), "EnumValue")
require.Equal(t, defaultEnumValueName("NumEnum", "100"), "NumEnum100")
}
func TestColumnRenameReserved(t *testing.T) {
tests := []struct {
col string
want string
}{
{col: "TableName", want: "TableName_"},
{col: "Table", want: "Table_"},
{col: "SchemaName", want: "SchemaName_"},
{col: "Alias", want: "Alias_"},
{col: "AllColumns", want: "AllColumns_"},
{col: "MutableColumns", want: "MutableColumns_"},
{col: "OtherColumn", want: "OtherColumn"},
}
for _, tt := range tests {
t.Run(tt.col, func(t *testing.T) {
builder := DefaultTableSQLBuilderColumn(metadata.Column{
Name: tt.col,
})
require.Equal(t, builder.Name, tt.want)
})
}
}