From 8a3a08baa0beb574b53e0318d1c1f1ede9bedc20 Mon Sep 17 00:00:00 2001 From: Patrick O'brien Date: Fri, 5 Aug 2016 00:20:17 +1000 Subject: [PATCH] Finish upsert, most of upsert tests * Fix Upsert hooks * Rename singleton template files --- imports.go | 7 +- templates/02_hooks.tpl | 2 + templates/09_update.tpl | 27 +--- templates/10_upsert.tpl | 115 ++++++++++++------ .../{00_helpers.tpl => boil_helpers.tpl} | 0 templates/singleton/boil_types.tpl | 6 + .../{helper_funcs.tpl => boil_helpers.tpl} | 0 ...helper_funcs.tpl => boil_main_helpers.tpl} | 0 templates_test/upsert.tpl | 83 +++++++++++++ 9 files changed, 176 insertions(+), 64 deletions(-) rename templates/singleton/{00_helpers.tpl => boil_helpers.tpl} (100%) create mode 100644 templates/singleton/boil_types.tpl rename templates_test/singleton/{helper_funcs.tpl => boil_helpers.tpl} (100%) rename templates_test/singleton/{main_helper_funcs.tpl => boil_main_helpers.tpl} (100%) diff --git a/imports.go b/imports.go index ee27d00..e2baa56 100644 --- a/imports.go +++ b/imports.go @@ -150,11 +150,12 @@ var defaultTemplateImports = imports{ thirdParty: importList{ `"github.com/nullbio/sqlboiler/boil"`, `"github.com/nullbio/sqlboiler/boil/qm"`, + `"github.com/nullbio/sqlboiler/strmangle"`, }, } var defaultSingletonTemplateImports = map[string]imports{ - "helpers": imports{ + "boil_helpers": imports{ standard: importList{}, thirdParty: importList{ `"github.com/nullbio/sqlboiler/boil"`, @@ -178,7 +179,7 @@ var defaultTestTemplateImports = imports{ } var defaultSingletonTestTemplateImports = map[string]imports{ - "main_helper_funcs": imports{ + "boil_main_helpers": imports{ standard: importList{ `"database/sql"`, `"os"`, @@ -188,7 +189,7 @@ var defaultSingletonTestTemplateImports = map[string]imports{ `"github.com/spf13/viper"`, }, }, - "helper_funcs": imports{ + "boil_helpers": imports{ standard: importList{ `"crypto/md5"`, `"fmt"`, diff --git a/templates/02_hooks.tpl b/templates/02_hooks.tpl index 71d6f82..525dfc5 100644 --- a/templates/02_hooks.tpl +++ b/templates/02_hooks.tpl @@ -2,8 +2,10 @@ {{- $varNameSingular := .Table.Name | singular | camelCase -}} var {{$varNameSingular}}BeforeCreateHooks []{{$tableNameSingular}}Hook var {{$varNameSingular}}BeforeUpdateHooks []{{$tableNameSingular}}Hook +var {{$varNameSingular}}BeforeUpsertHooks []{{$tableNameSingular}}Hook var {{$varNameSingular}}AfterCreateHooks []{{$tableNameSingular}}Hook var {{$varNameSingular}}AfterUpdateHooks []{{$tableNameSingular}}Hook +var {{$varNameSingular}}AfterUpsertHooks []{{$tableNameSingular}}Hook // doBeforeCreateHooks executes all "before create" hooks. func (o *{{$tableNameSingular}}) doBeforeCreateHooks() (err error) { diff --git a/templates/09_update.tpl b/templates/09_update.tpl index e90f910..506afb0 100644 --- a/templates/09_update.tpl +++ b/templates/09_update.tpl @@ -21,7 +21,7 @@ func (o *{{$tableNameSingular}}) UpdateGP(whitelist ...string) { // UpdateP uses an executor to update the {{$tableNameSingular}}, and panics on error. // See Update for whitelist behavior description. func (o *{{$tableNameSingular}}) UpdateP(exec boil.Executor, whitelist ... string) { - err := o.UpdateAt(exec, {{.Table.PKey.Columns | stringMap .StringFuncs.titleCase | prefixStringSlice "o." | join ", "}}, whitelist...) + err := o.Update(exec, whitelist...) if err != nil { panic(boil.WrapErr(err)) } @@ -33,23 +33,6 @@ func (o *{{$tableNameSingular}}) UpdateP(exec boil.Executor, whitelist ... strin // - All columns are inferred to start with // - All primary keys are subtracted from this set func (o *{{$tableNameSingular}}) Update(exec boil.Executor, whitelist ... string) error { - return o.UpdateAt(exec, {{.Table.PKey.Columns | stringMap .StringFuncs.titleCase | prefixStringSlice "o." | join ", "}}, whitelist...) -} - -// UpdateAtG updates the {{$tableNameSingular}} using the primary key to find the row to update. -func (o *{{$tableNameSingular}}) UpdateAtG({{$pkArgs}}, whitelist ...string) error { - return o.UpdateAt(boil.GetDB(), {{$pkNames | join ", "}}, whitelist...) -} - -// UpdateAtGP updates the {{$tableNameSingular}} using the primary key to find the row to update. Panics on error. -func (o *{{$tableNameSingular}}) UpdateAtGP({{$pkArgs}}, whitelist ...string) { - if err := o.UpdateAt(boil.GetDB(), {{$pkNames | join ", "}}, whitelist...); err != nil { - panic(boil.WrapErr(err)) - } -} - -// UpdateAt uses an executor to update the {{$tableNameSingular}} using the primary key to find the row to update. -func (o *{{$tableNameSingular}}) UpdateAt(exec boil.Executor, {{$pkArgs}}, whitelist ...string) error { if err := o.doBeforeUpdateHooks(); err != nil { return err } @@ -85,14 +68,6 @@ func (o *{{$tableNameSingular}}) UpdateAt(exec boil.Executor, {{$pkArgs}}, white return nil } -// UpdateAtP uses an executor to update the {{$tableNameSingular}} using the primary key to find the row to update. -// Panics on error. -func (o *{{$tableNameSingular}}) UpdateAtP(exec boil.Executor, {{$pkArgs}}, whitelist ...string) { - if err := o.UpdateAt(exec, {{$pkNames | join ", "}}, whitelist...); err != nil { - panic(boil.WrapErr(err)) - } -} - // UpdateAll updates all rows with matching column names. func (q {{$varNameSingular}}Query) UpdateAll(cols M) error { boil.SetUpdate(q.Query, cols) diff --git a/templates/10_upsert.tpl b/templates/10_upsert.tpl index 90e3eba..28f4443 100644 --- a/templates/10_upsert.tpl +++ b/templates/10_upsert.tpl @@ -12,51 +12,36 @@ func (o *{{$tableNameSingular}}) UpsertGP(update bool, conflictColumns []string, } } +// UpsertP attempts an insert using an executor, and does an update or ignore on conflict. +// UpsertP panics on error. +func (o *{{$tableNameSingular}}) UpsertP(exec boil.Executor, update bool, conflictColumns []string, updateColumns []string, whitelist ...string) { + if err := o.Upsert(exec, update, conflictColumns, updateColumns, whitelist...); err != nil { + panic(boil.WrapErr(err)) + } +} + // Upsert attempts an insert using an executor, and does an update or ignore on conflict. func (o *{{$tableNameSingular}}) Upsert(exec boil.Executor, update bool, conflictColumns []string, updateColumns []string, whitelist ...string) error { if o == nil { return errors.New("{{.PkgName}}: no {{.Table.Name}} provided for upsert") } - wl, returnColumns := o.generateInsertColumns(whitelist...) - - conflict := make([]string, len(conflictColumns)) - update := make([]string, len(updateColumns)) - - copy(conflict, conflictColumns) - copy(update, updateColumns) - - for i, v := range conflict { - conflict[i] = strmangle.IdentQuote(v) - } - - for i, v := range update { - update[i] = strmangle.IdentQuote(v) - } + columns := o.generateUpsertColumns(conflictColumns, updateColumns, whitelist) + query := o.generateUpsertQuery(update, columns) var err error if err := o.doBeforeUpsertHooks(); err != nil { return err } - ins := fmt.Sprintf(`INSERT INTO {{.Table.Name}} ("%s") VALUES (%s) ON CONFLICT `, strings.Join(wl, `","`), boil.GenerateParamFlags(len(wl), 1)) - if !update { - ins := ins + "DO NOTHING" - } else if len(conflict) != 0 { - ins := ins + fmt.Sprintf(`("%s") DO UPDATE SET %s`, strings.Join(conflict, `","`)) + if len(columns.returning) != 0 { + err = exec.QueryRow(query, boil.GetStructValues(o, columns.whitelist...)...).Scan(boil.GetStructPointers(o, columns.returning...)...) } else { - ins := ins + fmt.Sprintf(`("%s") DO UPDATE SET %s`, strings.Join({{$varNameSingular}}PrimaryKeyColumns, `","`)) - } - - if len(returnColumns) != 0 { - ins = ins + fmt.Sprintf(` RETURNING %s`, strings.Join(returnColumns, ",")) - err = exec.QueryRow(ins, boil.GetStructValues(o, wl...)...).Scan(boil.GetStructPointers(o, returnColumns...)...) - } else { - _, err = exec.Exec(ins, {{.Table.Columns | columnNames | stringMap .StringFuncs.titleCase | prefixStringSlice "o." | join ", "}}) + _, err = exec.Exec(query, {{.Table.Columns | columnNames | stringMap .StringFuncs.titleCase | prefixStringSlice "o." | join ", "}}) } if boil.DebugMode { - fmt.Fprintln(boil.DebugWriter, ins, boil.GetStructValues(o, wl...)) + fmt.Fprintln(boil.DebugWriter, query, boil.GetStructValues(o, columns.whitelist...)) } if err != nil { @@ -70,10 +55,70 @@ func (o *{{$tableNameSingular}}) Upsert(exec boil.Executor, update bool, conflic return nil } -// UpsertP attempts an insert using an executor, and does an update or ignore on conflict. -// UpsertP panics on error. -func (o *{{$tableNameSingular}}) UpsertP(exec boil.Executor, update bool, conflictColumns []string, updateColumns []string, whitelist ...string) { - if err := o.Upsert(exec, update, conflictColumns, updateColumns, whitelist...); err != nil { - panic(boil.WrapErr(err)) - } +// generateUpsertColumns builds an upsertData object, using generated values when necessary. +func (o *{{$tableNameSingular}}) generateUpsertColumns(conflict []string, update []string, whitelist []string) upsertData { + var upsertCols upsertData + + upsertCols.whitelist, upsertCols.returning = o.generateInsertColumns(whitelist...) + + upsertCols.conflict = make([]string, len(conflict)) + upsertCols.update = make([]string, len(update)) + + // generates the ON CONFLICT() columns if none are provided + upsertCols.conflict = o.generateConflictColumns(conflict...) + + // generate the UPDATE SET columns if none are provided + upsertCols.update = o.generateUpdateColumns(update...) + + return upsertCols +} + +// generateConflictColumns returns the user provided columns. +// If no columns are provided, it returns the primary key columns. +func (o *{{$tableNameSingular}}) generateConflictColumns(columns ...string) []string { + if len(columns) != 0 { + return columns + } + + c := make([]string, len({{$varNameSingular}}PrimaryKeyColumns)) + copy(c, {{$varNameSingular}}PrimaryKeyColumns) + + return c +} + +// generateUpsertQuery builds a SQL statement string using the upsertData provided. +func (o *{{$tableNameSingular}}) generateUpsertQuery(update bool, columns upsertData) string { + var set, query string + + for i, v := range columns.conflict { + columns.conflict[i] = strmangle.IdentQuote(v) + } + + var sets []string + // Generate the UPDATE SET clause + for _, v := range columns.update { + quoted := strmangle.IdentQuote(v) + sets = append(sets, fmt.Sprintf("%s = EXCLUDED.%s", quoted, quoted)) + } + set = strings.Join(sets, ", ") + + query = fmt.Sprintf( + `INSERT INTO {{.Table.Name}} ("%s") VALUES (%s) ON CONFLICT DO`, + strings.Join(columns.whitelist, `","`), + boil.GenerateParamFlags(len(columns.whitelist), 1), + ) + + if !update { + query = query + " NOTHING" + } else if len(columns.conflict) != 0 { + query = fmt.Sprintf(`%s("%s") UPDATE SET %s`, query, strings.Join(columns.conflict, `","`), set) + } else { + query = fmt.Sprintf(`%s("%s") UPDATE SET %s`, query, strings.Join({{$varNameSingular}}PrimaryKeyColumns, `","`), set) + } + + if len(columns.returning) != 0 { + query = fmt.Sprintf(`%s RETURNING %s`, query, strings.Join(columns.returning, ",")) + } + + return query } diff --git a/templates/singleton/00_helpers.tpl b/templates/singleton/boil_helpers.tpl similarity index 100% rename from templates/singleton/00_helpers.tpl rename to templates/singleton/boil_helpers.tpl diff --git a/templates/singleton/boil_types.tpl b/templates/singleton/boil_types.tpl new file mode 100644 index 0000000..cb83863 --- /dev/null +++ b/templates/singleton/boil_types.tpl @@ -0,0 +1,6 @@ +type upsertData struct { + conflict []string + update []string + whitelist []string + returning []string +} diff --git a/templates_test/singleton/helper_funcs.tpl b/templates_test/singleton/boil_helpers.tpl similarity index 100% rename from templates_test/singleton/helper_funcs.tpl rename to templates_test/singleton/boil_helpers.tpl diff --git a/templates_test/singleton/main_helper_funcs.tpl b/templates_test/singleton/boil_main_helpers.tpl similarity index 100% rename from templates_test/singleton/main_helper_funcs.tpl rename to templates_test/singleton/boil_main_helpers.tpl diff --git a/templates_test/upsert.tpl b/templates_test/upsert.tpl index e69de29..74259d2 100644 --- a/templates_test/upsert.tpl +++ b/templates_test/upsert.tpl @@ -0,0 +1,83 @@ +{{- $tableNameSingular := .Table.Name | singular | titleCase -}} +{{- $dbName := singular .Table.Name -}} +{{- $tableNamePlural := .Table.Name | plural | titleCase -}} +{{- $varNamePlural := .Table.Name | plural | camelCase -}} +{{- $varNameSingular := .Table.Name | singular | camelCase -}} +{{- $parent := . -}} +func Test{{$tableNamePlural}}Upsert(t *testing.T) { + //var err error + + o := {{$tableNameSingular}}{} + + columns := o.generateUpsertColumns([]string{"one", "two"}, []string{"three", "four"}, []string{"five", "six"}) + if columns.conflict[0] != "one" || columns.conflict[1] != "two" { + t.Errorf("Expected conflict to be %v, got %v", []string{"one", "two"}, columns.conflict) + } + + if columns.update[0] != "three" || columns.update[1] != "four" { + t.Errorf("Expected update to be %v, got %v", []string{"three", "four"}, columns.update) + } + + if columns.whitelist[0] != "five" || columns.whitelist[1] != "six" { + t.Errorf("Expected whitelist to be %v, got %v", []string{"five", "six"}, columns.whitelist) + } + + columns = o.generateUpsertColumns(nil, nil, nil) + if len(columns.whitelist) == 0 { + t.Errorf("Expected whitelist to contain columns, but got len 0") + } + + if len(columns.conflict) == 0 { + t.Errorf("Expected conflict to contain columns, but got len 0") + } + + if len(columns.update) == 0 { + t.Errorf("expected update to contain columns, but got len 0") + } + + upsertCols := upsertData{ + conflict: []string{}, + update: []string{}, + whitelist: []string{"thing"}, + returning: []string{}, + } + + query := o.generateUpsertQuery(false, upsertCols) + expectedQuery := `INSERT INTO {{.Table.Name}} ("thing") VALUES ($1) ON CONFLICT DO NOTHING` + + if query != expectedQuery { + t.Errorf("Expected query mismatch:\n\n%s\n%s\n", query, expectedQuery) + } + + /* + query = o.generateUpsertQuery(true, upsertCols) + primKeys := strings.Join(strmangle.IdentQuote()) + expectedQuery = `INSERT INTO {{.Table.Name}} ("thing") VALUES ($1) ON CONFLICT DO UPDATE()` + + if query != expectedQuery { + t.Errorf("Expected query mismatch:\n\n%s\n%s\n", query, expectedQuery) + } + */ + + /* + create empty row + assign random values to it + + attempt to insert it using upsert + make sure values come back appropriately + + attempt to upsert row again, make sure comes back as prim key error + attempt upsert again, set update to false, ensure it ignores error + + attempt to randomize everything except primary keys on duplicate row + attempt upsert again, set update to true, nil, nil + perform a find on the the row + check if the found row matches the upsert object to ensure returning cols worked appropriately and update worked appropriately + + + + + */ + + {{$varNamePlural}}DeleteAllRows(t) +}