From cc020b27847991c651dcfbb4c0a204596198e28e Mon Sep 17 00:00:00 2001 From: almas-x Date: Fri, 23 May 2025 11:03:49 +0800 Subject: [PATCH] feat: [#669] Migration support GENERATED --- go.mod | 8 ++-- go.sum | 5 +++ grammar.go | 51 +++++++++++++++++++---- grammar_test.go | 108 +++++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 155 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index c0c2d59..5540b89 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.24.3 require ( github.com/Masterminds/squirrel v1.5.4 - github.com/goravel/framework v1.15.2-0.20250513024952-5c6bf415f2ed + github.com/goravel/framework v1.15.2-0.20250523025922-88c15e8bda02 github.com/spf13/cast v1.8.0 github.com/stretchr/testify v1.10.0 gorm.io/driver/postgres v1.5.11 @@ -23,7 +23,7 @@ require ( github.com/dave/dst v0.27.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/dromara/carbon/v2 v2.6.4 // indirect + github.com/dromara/carbon/v2 v2.6.6 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect @@ -75,8 +75,8 @@ require ( golang.org/x/term v0.32.0 // indirect golang.org/x/text v0.25.0 // indirect golang.org/x/tools v0.33.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect - google.golang.org/grpc v1.72.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 // indirect + google.golang.org/grpc v1.72.1 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 27045be..e6b52a0 100644 --- a/go.sum +++ b/go.sum @@ -89,6 +89,7 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dromara/carbon/v2 v2.6.4 h1:cpIansyiEIEed3OlEIqo1IXj86qu0x6pf/E2keL2wYo= github.com/dromara/carbon/v2 v2.6.4/go.mod h1:Baj3A1uBBctJmpZWJd6/+WWnmIuY2pobR6IOpB6xigc= +github.com/dromara/carbon/v2 v2.6.6/go.mod h1:7GXqCUplwN1s1b4whGk2zX4+g4CMCoDIZzmjlyt0vLY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= @@ -147,6 +148,8 @@ github.com/goravel/framework v1.15.2-0.20250512040745-cf77c8a8e8c0 h1:ajQElV0Ieo github.com/goravel/framework v1.15.2-0.20250512040745-cf77c8a8e8c0/go.mod h1:8guNAbVJAc08KttpkwSPBUvVnbGWkoRDO8QIRoAL3f8= github.com/goravel/framework v1.15.2-0.20250513024952-5c6bf415f2ed h1:GHtrocwQ7MmEXgDf9K6vM6KBcT1nbvuyEmQVra7b4N4= github.com/goravel/framework v1.15.2-0.20250513024952-5c6bf415f2ed/go.mod h1:8guNAbVJAc08KttpkwSPBUvVnbGWkoRDO8QIRoAL3f8= +github.com/goravel/framework v1.15.2-0.20250523025922-88c15e8bda02 h1:7uPGKrApEH+uc9t8HN0D1Qwz/rJw4xBlI1B5SY09VPo= +github.com/goravel/framework v1.15.2-0.20250523025922-88c15e8bda02/go.mod h1:dJtMqSQaFcIoICTmCgkNooq5ax+BUn5OF/60UfbtJT8= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= @@ -408,8 +411,10 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 h1: google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI= google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs= google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/grammar.go b/grammar.go index 832521f..2113077 100644 --- a/grammar.go +++ b/grammar.go @@ -36,6 +36,8 @@ func NewGrammar(prefix string) *Grammar { grammar.ModifyDefault, grammar.ModifyIncrement, grammar.ModifyNullable, + grammar.ModifyGeneratedAsForChange, + grammar.ModifyGeneratedAs, } return grammar @@ -435,9 +437,9 @@ func (r *Grammar) GetAttributeCommands() []string { return r.attributeCommands } -func (r *Grammar) ModifyDefault(blueprint driver.Blueprint, column driver.ColumnDefinition) string { +func (r *Grammar) ModifyDefault(_ driver.Blueprint, column driver.ColumnDefinition) string { if column.IsChange() { - if column.GetAutoIncrement() { + if column.GetAutoIncrement() || column.IsSetGeneratedAs() { return "" } if column.GetDefault() != nil { @@ -452,7 +454,39 @@ func (r *Grammar) ModifyDefault(blueprint driver.Blueprint, column driver.Column return "" } -func (r *Grammar) ModifyNullable(blueprint driver.Blueprint, column driver.ColumnDefinition) string { +func (r *Grammar) ModifyGeneratedAs(_ driver.Blueprint, column driver.ColumnDefinition) string { + if !column.IsSetGeneratedAs() { + return "" + } + + option := "by default" + if column.IsAlways() { + option = "always" + } + + identity := "" + if generatedAs := column.GetGeneratedAs(); len(generatedAs) > 0 { + identity = " (" + generatedAs + ")" + } + + sql := fmt.Sprintf(" generated %s as identity%s", option, identity) + if column.IsChange() { + sql = " add" + sql + } + + return sql + +} + +func (r *Grammar) ModifyGeneratedAsForChange(_ driver.Blueprint, column driver.ColumnDefinition) string { + if column.IsChange() && column.IsSetGeneratedAs() && !column.GetAutoIncrement() { + return " drop identity if exists" + } + + return "" +} + +func (r *Grammar) ModifyNullable(_ driver.Blueprint, column driver.ColumnDefinition) string { if column.IsChange() { if column.GetNullable() { return " drop not null" @@ -466,7 +500,10 @@ func (r *Grammar) ModifyNullable(blueprint driver.Blueprint, column driver.Colum } func (r *Grammar) ModifyIncrement(blueprint driver.Blueprint, column driver.ColumnDefinition) string { - if !column.IsChange() && !blueprint.HasCommand("primary") && slices.Contains(r.serials, column.GetType()) && column.GetAutoIncrement() { + if !column.IsChange() && + !blueprint.HasCommand("primary") && + (slices.Contains(r.serials, column.GetType()) || column.IsSetGeneratedAs()) && + column.GetAutoIncrement() { return " primary key" } @@ -474,7 +511,7 @@ func (r *Grammar) ModifyIncrement(blueprint driver.Blueprint, column driver.Colu } func (r *Grammar) TypeBigInteger(column driver.ColumnDefinition) string { - if column.GetAutoIncrement() { + if column.GetAutoIncrement() && !column.IsChange() && !column.IsSetGeneratedAs() { return "bigserial" } @@ -528,7 +565,7 @@ func (r *Grammar) TypeFloat(column driver.ColumnDefinition) string { } func (r *Grammar) TypeInteger(column driver.ColumnDefinition) string { - if column.GetAutoIncrement() { + if column.GetAutoIncrement() && !column.IsChange() && !column.IsSetGeneratedAs() { return "serial" } @@ -556,7 +593,7 @@ func (r *Grammar) TypeMediumText(column driver.ColumnDefinition) string { } func (r *Grammar) TypeSmallInteger(column driver.ColumnDefinition) string { - if column.GetAutoIncrement() { + if column.GetAutoIncrement() && !column.IsChange() && !column.IsSetGeneratedAs() { return "smallserial" } diff --git a/grammar_test.go b/grammar_test.go index d568ffc..de3ab4b 100644 --- a/grammar_test.go +++ b/grammar_test.go @@ -36,7 +36,8 @@ func (s *GrammarSuite) TestCompileAdd() { mockColumn.EXPECT().GetDefault().Return("goravel").Twice() mockColumn.EXPECT().GetNullable().Return(false).Once() mockColumn.EXPECT().GetLength().Return(1).Once() - mockColumn.EXPECT().IsChange().Return(false).Times(3) + mockColumn.EXPECT().IsChange().Return(false).Times(4) + mockColumn.EXPECT().IsSetGeneratedAs().Return(false).Twice() mockBlueprint.EXPECT().HasCommand("primary").Return(false).Once() sql := s.grammar.CompileAdd(mockBlueprint, &contractsdriver.Command{ @@ -56,7 +57,8 @@ func (s *GrammarSuite) TestCompileChange() { mockColumn.EXPECT().GetDefault().Return("goravel").Twice() mockColumn.EXPECT().GetNullable().Return(false).Once() mockColumn.EXPECT().GetLength().Return(1).Once() - mockColumn.EXPECT().IsChange().Return(true).Times(3) + mockColumn.EXPECT().IsChange().Return(true).Times(4) + mockColumn.EXPECT().IsSetGeneratedAs().Return(false).Times(3) mockColumn.EXPECT().GetAutoIncrement().Return(false).Once() sql := s.grammar.CompileChange(mockBlueprint, &contractsdriver.Command{ @@ -159,7 +161,8 @@ func (s *GrammarSuite) TestCompileCreate() { mockColumn1.EXPECT().GetAutoIncrement().Return(true).Once() // postgres.go::ModifyNullable mockColumn1.EXPECT().GetNullable().Return(false).Once() - mockColumn1.EXPECT().IsChange().Return(false).Times(3) + mockColumn1.EXPECT().IsChange().Return(false).Times(5) + mockColumn1.EXPECT().IsSetGeneratedAs().Return(false).Twice() // utils.go::getColumns mockColumn2.EXPECT().GetName().Return("name").Once() @@ -174,7 +177,8 @@ func (s *GrammarSuite) TestCompileCreate() { mockColumn2.EXPECT().GetType().Return("string").Once() // postgres.go::ModifyNullable mockColumn2.EXPECT().GetNullable().Return(true).Once() - mockColumn2.EXPECT().IsChange().Return(false).Times(3) + mockColumn2.EXPECT().IsChange().Return(false).Times(4) + mockColumn2.EXPECT().IsSetGeneratedAs().Return(false).Twice() s.Equal(`create table "goravel_users" ("id" serial primary key not null, "name" varchar(100) null)`, s.grammar.CompileCreate(mockBlueprint)) @@ -415,14 +419,16 @@ func (s *GrammarSuite) TestGetColumns() { mockColumn1.EXPECT().GetDefault().Return(nil).Once() mockColumn1.EXPECT().GetNullable().Return(false).Once() mockColumn1.EXPECT().GetAutoIncrement().Return(true).Twice() - mockColumn1.EXPECT().IsChange().Return(false).Times(3) + mockColumn1.EXPECT().IsChange().Return(false).Times(5) + mockColumn1.EXPECT().IsSetGeneratedAs().Return(false).Twice() mockColumn2.EXPECT().GetName().Return("name").Once() mockColumn2.EXPECT().GetType().Return("string").Twice() mockColumn2.EXPECT().GetDefault().Return("goravel").Twice() mockColumn2.EXPECT().GetNullable().Return(true).Once() mockColumn2.EXPECT().GetLength().Return(10).Once() - mockColumn2.EXPECT().IsChange().Return(false).Times(3) + mockColumn2.EXPECT().IsChange().Return(false).Times(4) + mockColumn2.EXPECT().IsSetGeneratedAs().Return(false).Twice() s.Equal([]string{"\"id\" serial primary key not null", "\"name\" varchar(10) default 'goravel' null"}, s.grammar.getColumns(mockBlueprint)) } @@ -475,6 +481,7 @@ func (s *GrammarSuite) TestModifyDefault() { mockColumn.EXPECT().IsChange().Return(true).Once() mockColumn.EXPECT().GetAutoIncrement().Return(false).Once() mockColumn.EXPECT().GetDefault().Return(nil).Once() + mockColumn.EXPECT().IsSetGeneratedAs().Return(false).Once() }, expectSql: " drop default", }, @@ -492,6 +499,7 @@ func (s *GrammarSuite) TestModifyDefault() { mockColumn.EXPECT().IsChange().Return(true).Once() mockColumn.EXPECT().GetAutoIncrement().Return(false).Once() mockColumn.EXPECT().GetDefault().Return("goravel").Twice() + mockColumn.EXPECT().IsSetGeneratedAs().Return(false).Once() }, expectSql: " set default 'goravel'", }, @@ -511,6 +519,90 @@ func (s *GrammarSuite) TestModifyDefault() { } } +func (s *GrammarSuite) TestModifyGeneratedAs() { + var ( + mockBlueprint *mocksdriver.Blueprint + mockColumn *mocksdriver.ColumnDefinition + ) + + tests := []struct { + name string + setup func() + expectSql []string + }{ + { + name: "generated by default", + setup: func() { + mockColumn.EXPECT().IsChange().Return(false).Twice() + mockColumn.EXPECT().IsSetGeneratedAs().Return(true).Once() + mockColumn.EXPECT().IsAlways().Return(false).Once() + mockColumn.EXPECT().GetGeneratedAs().Return("").Once() + }, + expectSql: []string{" generated by default as identity"}, + }, + { + name: "generated always", + setup: func() { + mockColumn.EXPECT().IsChange().Return(false).Twice() + mockColumn.EXPECT().IsSetGeneratedAs().Return(true).Once() + mockColumn.EXPECT().IsAlways().Return(true).Once() + mockColumn.EXPECT().GetGeneratedAs().Return("").Once() + }, + expectSql: []string{" generated always as identity"}, + }, + { + name: "generated by default with expression", + setup: func() { + mockColumn.EXPECT().IsChange().Return(false).Twice() + mockColumn.EXPECT().IsSetGeneratedAs().Return(true).Once() + mockColumn.EXPECT().IsAlways().Return(false).Once() + mockColumn.EXPECT().GetGeneratedAs().Return("START WITH 1000 INCREMENT BY 5").Once() + }, + expectSql: []string{" generated by default as identity (START WITH 1000 INCREMENT BY 5)"}, + }, + { + name: "generated always with expression", + setup: func() { + mockColumn.EXPECT().IsChange().Return(false).Twice() + mockColumn.EXPECT().IsSetGeneratedAs().Return(true).Once() + mockColumn.EXPECT().IsAlways().Return(true).Once() + mockColumn.EXPECT().GetGeneratedAs().Return("START WITH 1000 INCREMENT BY 5").Once() + }, + expectSql: []string{" generated always as identity (START WITH 1000 INCREMENT BY 5)"}, + }, + { + name: "generated always with expression for change", + setup: func() { + mockColumn.EXPECT().IsChange().Return(true).Twice() + mockColumn.EXPECT().GetAutoIncrement().Return(false).Once() + mockColumn.EXPECT().IsSetGeneratedAs().Return(true).Twice() + mockColumn.EXPECT().IsAlways().Return(true).Once() + mockColumn.EXPECT().GetGeneratedAs().Return("START WITH 1000 INCREMENT BY 5").Once() + }, + expectSql: []string{" drop identity if exists", " add generated always as identity (START WITH 1000 INCREMENT BY 5)"}, + }, + } + + for _, test := range tests { + s.Run(test.name, func() { + mockBlueprint = mocksdriver.NewBlueprint(s.T()) + mockColumn = mocksdriver.NewColumnDefinition(s.T()) + + test.setup() + + var actualSql []string + if sql := s.grammar.ModifyGeneratedAsForChange(mockBlueprint, mockColumn); len(sql) > 0 { + actualSql = append(actualSql, sql) + } + if sql := s.grammar.ModifyGeneratedAs(mockBlueprint, mockColumn); len(sql) > 0 { + actualSql = append(actualSql, sql) + } + + s.Equal(test.expectSql, actualSql) + }) + } +} + func (s *GrammarSuite) TestModifyNullable() { var ( mockBlueprint *mocksdriver.Blueprint @@ -594,6 +686,8 @@ func (s *GrammarSuite) TestTableComment() { func (s *GrammarSuite) TestTypeBigInteger() { mockColumn1 := mocksdriver.NewColumnDefinition(s.T()) mockColumn1.EXPECT().GetAutoIncrement().Return(true).Once() + mockColumn1.EXPECT().IsChange().Return(false).Once() + mockColumn1.EXPECT().IsSetGeneratedAs().Return(false).Once() s.Equal("bigserial", s.grammar.TypeBigInteger(mockColumn1)) @@ -633,6 +727,8 @@ func (s *GrammarSuite) TestTypeFloat() { func (s *GrammarSuite) TestTypeInteger() { mockColumn1 := mocksdriver.NewColumnDefinition(s.T()) mockColumn1.EXPECT().GetAutoIncrement().Return(true).Once() + mockColumn1.EXPECT().IsChange().Return(false).Once() + mockColumn1.EXPECT().IsSetGeneratedAs().Return(false).Once() s.Equal("serial", s.grammar.TypeInteger(mockColumn1))