diff --git a/contracts/database/schema/column.go b/contracts/database/schema/column.go index e267563cc..fbd929992 100644 --- a/contracts/database/schema/column.go +++ b/contracts/database/schema/column.go @@ -3,6 +3,8 @@ package schema type ColumnDefinition interface { // AutoIncrement set the column as auto increment AutoIncrement() ColumnDefinition + // Change the column + Change() ColumnDefinition // Comment sets the comment value Comment(comment string) ColumnDefinition // Default set the default value @@ -37,6 +39,8 @@ type ColumnDefinition interface { GetUseCurrent() bool // GetUseCurrentOnUpdate returns the useCurrentOnUpdate value GetUseCurrentOnUpdate() bool + // IsChange returns true if the column has changed + IsChange() bool // IsSetComment returns true if the comment value is set IsSetComment() bool // OnUpdate sets the column to use the value on update (Mysql only) diff --git a/contracts/database/schema/grammar.go b/contracts/database/schema/grammar.go index 12edd680f..4fd9e0f8a 100644 --- a/contracts/database/schema/grammar.go +++ b/contracts/database/schema/grammar.go @@ -3,12 +3,16 @@ package schema type Grammar interface { // CompileAdd Compile an add column command. CompileAdd(blueprint Blueprint, command *Command) string + // CompileChange Compile a change column command. + CompileChange(blueprint Blueprint, command *Command) []string // CompileColumns Compile the query to determine the columns. CompileColumns(schema, table string) string // CompileComment Compile a column comment command. CompileComment(blueprint Blueprint, command *Command) string // CompileCreate Compile a create table command. CompileCreate(blueprint Blueprint) string + // CompileDefault Compile a default value command. + CompileDefault(blueprint Blueprint, command *Command) string // CompileDrop Compile a drop table command. CompileDrop(blueprint Blueprint) string // CompileDropAllDomains Compile the SQL needed to drop all domains. diff --git a/database/schema/blueprint.go b/database/schema/blueprint.go index 7e46b06e5..1b3234733 100644 --- a/database/schema/blueprint.go +++ b/database/schema/blueprint.go @@ -438,6 +438,12 @@ func (r *Blueprint) ToSql(grammar schema.Grammar) []string { switch command.Name { case constants.CommandAdd: + if command.Column.IsChange() { + if statement := grammar.CompileChange(r, command); len(statement) > 0 { + statements = append(statements, statement...) + } + continue + } statements = append(statements, grammar.CompileAdd(r, command)) case constants.CommandComment: if statement := grammar.CompileComment(r, command); statement != "" { @@ -445,6 +451,10 @@ func (r *Blueprint) ToSql(grammar schema.Grammar) []string { } case constants.CommandCreate: statements = append(statements, grammar.CompileCreate(r)) + case constants.CommandDefault: + if statement := grammar.CompileDefault(r, command); statement != "" { + statements = append(statements, statement) + } case constants.CommandDrop: statements = append(statements, grammar.CompileDrop(r)) case constants.CommandDropColumn: @@ -511,12 +521,18 @@ func (r *Blueprint) addAttributeCommands(grammar schema.Grammar) { attributeCommands := grammar.GetAttributeCommands() for _, column := range r.columns { for _, command := range attributeCommands { - if command == constants.CommandComment && column.comment != nil { + if command == constants.CommandComment && (column.comment != nil || column.change) { r.addCommand(&schema.Command{ Column: column, Name: constants.CommandComment, }) } + if command == constants.CommandDefault && column.def != nil { + r.addCommand(&schema.Command{ + Column: column, + Name: constants.CommandDefault, + }) + } } } } diff --git a/database/schema/blueprint_test.go b/database/schema/blueprint_test.go index 1249f8c41..a7bcfd65b 100644 --- a/database/schema/blueprint_test.go +++ b/database/schema/blueprint_test.go @@ -395,6 +395,15 @@ func (s *BlueprintTestSuite) TestToSql() { } else { s.Empty(s.blueprint.ToSql(grammar)) } + + // Change column + s.SetupTest() + s.blueprint.String("name", 100).Change() + if driver == database.DriverPostgres { + s.Len(s.blueprint.ToSql(grammar), 2) + } else { + s.Empty(s.blueprint.ToSql(grammar)) + } } } @@ -447,3 +456,15 @@ func (s *BlueprintTestSuite) TestUnsignedTinyInteger() { unsigned: convert.Pointer(true), }) } + +func (s *BlueprintTestSuite) TestChange() { + column := "name" + customLength := 100 + s.blueprint.String(column, customLength).Change() + s.Contains(s.blueprint.GetAddedColumns(), &ColumnDefinition{ + length: &customLength, + name: &column, + change: true, + ttype: convert.Pointer("string"), + }) +} diff --git a/database/schema/column.go b/database/schema/column.go index a9e512d88..af5152fef 100644 --- a/database/schema/column.go +++ b/database/schema/column.go @@ -8,6 +8,7 @@ import ( type ColumnDefinition struct { allowed []any autoIncrement *bool + change bool comment *string def any length *int @@ -29,6 +30,12 @@ func (r *ColumnDefinition) AutoIncrement() schema.ColumnDefinition { return r } +func (r *ColumnDefinition) Change() schema.ColumnDefinition { + r.change = true + + return r +} + func (r *ColumnDefinition) Comment(comment string) schema.ColumnDefinition { r.comment = &comment @@ -149,6 +156,10 @@ func (r *ColumnDefinition) GetUseCurrentOnUpdate() bool { return false } +func (r *ColumnDefinition) IsChange() bool { + return r.change +} + func (r *ColumnDefinition) IsSetComment() bool { return r != nil && r.comment != nil } diff --git a/database/schema/column_test.go b/database/schema/column_test.go index c95bf770a..9b2cfdd3b 100644 --- a/database/schema/column_test.go +++ b/database/schema/column_test.go @@ -63,6 +63,13 @@ func (s *ColumnDefinitionTestSuite) GetType() { s.Equal("string", s.columnDefinition.GetType()) } +func (s *ColumnDefinitionTestSuite) IsChange() { + s.False(s.columnDefinition.IsChange()) + + s.columnDefinition.Change() + s.True(s.columnDefinition.IsChange()) +} + func (s *ColumnDefinitionTestSuite) Unsigned() { s.columnDefinition.Unsigned() s.True(*s.columnDefinition.unsigned) diff --git a/database/schema/constants/constants.go b/database/schema/constants/constants.go index f7a0d2d00..b8d4a906b 100644 --- a/database/schema/constants/constants.go +++ b/database/schema/constants/constants.go @@ -4,6 +4,7 @@ const ( CommandAdd = "add" CommandComment = "comment" CommandCreate = "create" + CommandDefault = "default" CommandDrop = "drop" CommandDropColumn = "dropColumn" CommandDropForeign = "dropForeign" diff --git a/database/schema/grammars/mysql.go b/database/schema/grammars/mysql.go index 5f5c59061..9f8a29cc0 100644 --- a/database/schema/grammars/mysql.go +++ b/database/schema/grammars/mysql.go @@ -41,6 +41,12 @@ func (r *Mysql) CompileAdd(blueprint schema.Blueprint, command *schema.Command) return fmt.Sprintf("alter table %s add %s", r.wrap.Table(blueprint.GetTableName()), r.getColumn(blueprint, command.Column)) } +func (r *Mysql) CompileChange(blueprint schema.Blueprint, command *schema.Command) []string { + return []string{ + fmt.Sprintf("alter table %s modify %s", r.wrap.Table(blueprint.GetTableName()), r.getColumn(blueprint, command.Column)), + } +} + func (r *Mysql) CompileColumns(schema, table string) string { return fmt.Sprintf( "select column_name as `name`, data_type as `type_name`, column_type as `type`, "+ @@ -71,6 +77,10 @@ func (r *Mysql) CompileCreate(blueprint schema.Blueprint) string { return fmt.Sprintf("create table %s (%s)", r.wrap.Table(blueprint.GetTableName()), strings.Join(columns, ", ")) } +func (r *Mysql) CompileDefault(_ schema.Blueprint, _ *schema.Command) string { + return "" +} + func (r *Mysql) CompileDisableForeignKeyConstraints() string { return "SET FOREIGN_KEY_CHECKS=0;" } diff --git a/database/schema/grammars/mysql_test.go b/database/schema/grammars/mysql_test.go index 8f3fbfc4f..9f86c141f 100644 --- a/database/schema/grammars/mysql_test.go +++ b/database/schema/grammars/mysql_test.go @@ -43,6 +43,27 @@ func (s *MysqlSuite) TestCompileAdd() { s.Equal("alter table `goravel_users` add `name` varchar(1) not null default 'goravel' comment 'comment'", sql) } +func (s *MysqlSuite) TestCompileChange() { + mockBlueprint := mocksschema.NewBlueprint(s.T()) + mockColumn := mocksschema.NewColumnDefinition(s.T()) + + mockBlueprint.EXPECT().GetTableName().Return("users").Once() + mockColumn.EXPECT().GetName().Return("name").Once() + mockColumn.EXPECT().GetType().Return("string").Twice() + mockColumn.EXPECT().GetDefault().Return("goravel").Twice() + mockColumn.EXPECT().GetNullable().Return(false).Once() + mockColumn.EXPECT().GetLength().Return(1).Once() + mockColumn.EXPECT().GetOnUpdate().Return(nil).Once() + mockColumn.EXPECT().GetComment().Return("comment").Once() + mockColumn.EXPECT().GetUnsigned().Return(false).Once() + + sql := s.grammar.CompileChange(mockBlueprint, &contractsschema.Command{ + Column: mockColumn, + }) + + s.Equal([]string{"alter table `goravel_users` modify `name` varchar(1) not null default 'goravel' comment 'comment'"}, sql) +} + func (s *MysqlSuite) TestCompileCreate() { mockColumn1 := mocksschema.NewColumnDefinition(s.T()) mockColumn2 := mocksschema.NewColumnDefinition(s.T()) diff --git a/database/schema/grammars/postgres.go b/database/schema/grammars/postgres.go index 2e541df94..d22b33000 100644 --- a/database/schema/grammars/postgres.go +++ b/database/schema/grammars/postgres.go @@ -39,6 +39,19 @@ func (r *Postgres) CompileAdd(blueprint schema.Blueprint, command *schema.Comman return fmt.Sprintf("alter table %s add column %s", r.wrap.Table(blueprint.GetTableName()), r.getColumn(blueprint, command.Column)) } +func (r *Postgres) CompileChange(blueprint schema.Blueprint, command *schema.Command) []string { + changes := []string{fmt.Sprintf("alter column %s type %s", r.wrap.Column(command.Column.GetName()), getType(r, command.Column))} + for _, modifier := range r.modifiers { + if change := modifier(blueprint, command.Column); change != "" { + changes = append(changes, fmt.Sprintf("alter column %s%s", r.wrap.Column(command.Column.GetName()), change)) + } + } + + return []string{ + fmt.Sprintf("alter table %s %s", r.wrap.Table(blueprint.GetTableName()), strings.Join(changes, ", ")), + } +} + func (r *Postgres) CompileColumns(schema, table string) string { return fmt.Sprintf( "select a.attname as name, t.typname as type_name, format_type(a.atttypid, a.atttypmod) as type, "+ @@ -67,6 +80,10 @@ func (r *Postgres) CompileCreate(blueprint schema.Blueprint) string { return fmt.Sprintf("create table %s (%s)", r.wrap.Table(blueprint.GetTableName()), strings.Join(r.getColumns(blueprint), ", ")) } +func (r *Postgres) CompileDefault(_ schema.Blueprint, _ *schema.Command) string { + return "" +} + func (r *Postgres) CompileDrop(blueprint schema.Blueprint) string { return fmt.Sprintf("drop table %s", r.wrap.Table(blueprint.GetTableName())) } @@ -291,6 +308,15 @@ func (r *Postgres) GetAttributeCommands() []string { } func (r *Postgres) ModifyDefault(blueprint schema.Blueprint, column schema.ColumnDefinition) string { + if column.IsChange() { + if column.GetAutoIncrement() { + return "" + } + if column.GetDefault() != nil { + return fmt.Sprintf(" set default %s", getDefaultValue(column.GetDefault())) + } + return " drop default" + } if column.GetDefault() != nil { return fmt.Sprintf(" default %s", getDefaultValue(column.GetDefault())) } @@ -299,15 +325,20 @@ func (r *Postgres) ModifyDefault(blueprint schema.Blueprint, column schema.Colum } func (r *Postgres) ModifyNullable(blueprint schema.Blueprint, column schema.ColumnDefinition) string { + if column.IsChange() { + if column.GetNullable() { + return " drop not null" + } + return " set not null" + } if column.GetNullable() { return " null" - } else { - return " not null" } + return " not null" } func (r *Postgres) ModifyIncrement(blueprint schema.Blueprint, column schema.ColumnDefinition) string { - if !blueprint.HasCommand("primary") && slices.Contains(r.serials, column.GetType()) && column.GetAutoIncrement() { + if !column.IsChange() && !blueprint.HasCommand("primary") && slices.Contains(r.serials, column.GetType()) && column.GetAutoIncrement() { return " primary key" } diff --git a/database/schema/grammars/postgres_test.go b/database/schema/grammars/postgres_test.go index d7c048f1b..5c4116964 100644 --- a/database/schema/grammars/postgres_test.go +++ b/database/schema/grammars/postgres_test.go @@ -33,6 +33,7 @@ func (s *PostgresSuite) 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) mockBlueprint.EXPECT().HasCommand("primary").Return(false).Once() sql := s.grammar.CompileAdd(mockBlueprint, &contractsschema.Command{ @@ -42,6 +43,28 @@ func (s *PostgresSuite) TestCompileAdd() { s.Equal(`alter table "goravel_users" add column "name" varchar(1) default 'goravel' not null`, sql) } +func (s *PostgresSuite) TestCompileChange() { + mockBlueprint := mocksschema.NewBlueprint(s.T()) + mockColumn := mocksschema.NewColumnDefinition(s.T()) + + mockBlueprint.EXPECT().GetTableName().Return("users").Once() + mockColumn.EXPECT().GetName().Return("name").Times(3) + mockColumn.EXPECT().GetType().Return("string").Once() + 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().GetAutoIncrement().Return(false).Once() + + sql := s.grammar.CompileChange(mockBlueprint, &contractsschema.Command{ + Column: mockColumn, + }) + + s.Equal([]string{ + `alter table "goravel_users" alter column "name" type varchar(1), alter column "name" set default 'goravel', alter column "name" set not null`, + }, sql) +} + func (s *PostgresSuite) TestCompileComment() { mockBlueprint := mocksschema.NewBlueprint(s.T()) mockColumnDefinition := mocksschema.NewColumnDefinition(s.T()) @@ -82,6 +105,7 @@ func (s *PostgresSuite) TestCompileCreate() { mockColumn1.EXPECT().GetAutoIncrement().Return(true).Once() // postgres.go::ModifyNullable mockColumn1.EXPECT().GetNullable().Return(false).Once() + mockColumn1.EXPECT().IsChange().Return(false).Times(3) // utils.go::getColumns mockColumn2.EXPECT().GetName().Return("name").Once() @@ -96,6 +120,7 @@ func (s *PostgresSuite) TestCompileCreate() { mockColumn2.EXPECT().GetType().Return("string").Once() // postgres.go::ModifyNullable mockColumn2.EXPECT().GetNullable().Return(true).Once() + mockColumn2.EXPECT().IsChange().Return(false).Times(3) s.Equal(`create table "goravel_users" ("id" serial primary key not null, "name" varchar(100) null)`, s.grammar.CompileCreate(mockBlueprint)) @@ -305,12 +330,14 @@ func (s *PostgresSuite) 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) 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) s.Equal([]string{"\"id\" serial primary key not null", "\"name\" varchar(10) default 'goravel' null"}, s.grammar.getColumns(mockBlueprint)) } @@ -346,16 +373,43 @@ func (s *PostgresSuite) TestModifyDefault() { { name: "without change and default is nil", setup: func() { + mockColumn.EXPECT().IsChange().Return(false).Once() + mockColumn.EXPECT().GetDefault().Return(nil).Once() + }, + }, + { + name: "with change and auto increment", + setup: func() { + mockColumn.EXPECT().IsChange().Return(true).Once() + mockColumn.EXPECT().GetAutoIncrement().Return(true).Once() + }, + }, + { + name: "with change and default is nil", + setup: func() { + mockColumn.EXPECT().IsChange().Return(true).Once() + mockColumn.EXPECT().GetAutoIncrement().Return(false).Once() mockColumn.EXPECT().GetDefault().Return(nil).Once() }, + expectSql: " drop default", }, { name: "without change and default is not nil", setup: func() { + mockColumn.EXPECT().IsChange().Return(false).Once() mockColumn.EXPECT().GetDefault().Return("goravel").Twice() }, expectSql: " default 'goravel'", }, + { + name: "with change and default is not nil", + setup: func() { + mockColumn.EXPECT().IsChange().Return(true).Once() + mockColumn.EXPECT().GetAutoIncrement().Return(false).Once() + mockColumn.EXPECT().GetDefault().Return("goravel").Twice() + }, + expectSql: " set default 'goravel'", + }, } for _, test := range tests { @@ -373,17 +427,62 @@ func (s *PostgresSuite) TestModifyDefault() { } func (s *PostgresSuite) TestModifyNullable() { - mockBlueprint := mocksschema.NewBlueprint(s.T()) + var ( + mockBlueprint *mocksschema.Blueprint + mockColumn *mocksschema.ColumnDefinition + ) - mockColumn := mocksschema.NewColumnDefinition(s.T()) + tests := []struct { + name string + setup func() + expectSql string + }{ + { + name: "without change and nullable", + setup: func() { + mockColumn.EXPECT().IsChange().Return(false).Once() + mockColumn.EXPECT().GetNullable().Return(true).Once() + }, + expectSql: " null", + }, + { + name: "with change and and nullable", + setup: func() { + mockColumn.EXPECT().IsChange().Return(true).Once() + mockColumn.EXPECT().GetNullable().Return(true).Once() + }, + expectSql: " drop not null", + }, + { + name: "without change and not nullable", + setup: func() { + mockColumn.EXPECT().IsChange().Return(false).Once() + mockColumn.EXPECT().GetNullable().Return(false).Once() + }, + expectSql: " not null", + }, + { + name: "with change and not nullable", + setup: func() { + mockColumn.EXPECT().IsChange().Return(true).Once() + mockColumn.EXPECT().GetNullable().Return(false).Once() + }, + expectSql: " set not null", + }, + } - mockColumn.EXPECT().GetNullable().Return(true).Once() + for _, test := range tests { + s.Run(test.name, func() { + mockBlueprint = mocksschema.NewBlueprint(s.T()) + mockColumn = mocksschema.NewColumnDefinition(s.T()) - s.Equal(" null", s.grammar.ModifyNullable(mockBlueprint, mockColumn)) + test.setup() - mockColumn.EXPECT().GetNullable().Return(false).Once() + sql := s.grammar.ModifyNullable(mockBlueprint, mockColumn) - s.Equal(" not null", s.grammar.ModifyNullable(mockBlueprint, mockColumn)) + s.Equal(test.expectSql, sql) + }) + } } func (s *PostgresSuite) TestModifyIncrement() { @@ -393,6 +492,7 @@ func (s *PostgresSuite) TestModifyIncrement() { mockBlueprint.EXPECT().HasCommand("primary").Return(false).Once() mockColumn.EXPECT().GetType().Return("bigInteger").Once() mockColumn.EXPECT().GetAutoIncrement().Return(true).Once() + mockColumn.EXPECT().IsChange().Return(false).Once() s.Equal(" primary key", s.grammar.ModifyIncrement(mockBlueprint, mockColumn)) } diff --git a/database/schema/grammars/sqlite.go b/database/schema/grammars/sqlite.go index 3c6dd3362..99250957f 100644 --- a/database/schema/grammars/sqlite.go +++ b/database/schema/grammars/sqlite.go @@ -43,6 +43,10 @@ func (r *Sqlite) CompileAdd(blueprint schema.Blueprint, command *schema.Command) return fmt.Sprintf("alter table %s add column %s", r.wrap.Table(blueprint.GetTableName()), r.getColumn(blueprint, command.Column)) } +func (r *Sqlite) CompileChange(blueprint schema.Blueprint, command *schema.Command) []string { + return nil +} + func (r *Sqlite) CompileColumns(schema, table string) string { return fmt.Sprintf( `select name, type, not "notnull" as "nullable", dflt_value as "default", pk as "primary", hidden as "extra" `+ @@ -61,6 +65,10 @@ func (r *Sqlite) CompileCreate(blueprint schema.Blueprint) string { r.addPrimaryKeys(getCommandByName(blueprint.GetCommands(), "primary"))) } +func (r *Sqlite) CompileDefault(_ schema.Blueprint, _ *schema.Command) string { + return "" +} + func (r *Sqlite) CompileDisableWriteableSchema() string { return r.pragma("writable_schema", "0") } diff --git a/database/schema/grammars/sqlserver.go b/database/schema/grammars/sqlserver.go index d521d94f4..830b3b197 100644 --- a/database/schema/grammars/sqlserver.go +++ b/database/schema/grammars/sqlserver.go @@ -21,7 +21,7 @@ type Sqlserver struct { func NewSqlserver(tablePrefix string) *Sqlserver { sqlserver := &Sqlserver{ - attributeCommands: []string{constants.CommandComment}, + attributeCommands: []string{constants.CommandComment, constants.CommandDefault}, serials: []string{"bigInteger", "integer", "mediumInteger", "smallInteger", "tinyInteger"}, wrap: NewWrap(database.DriverSqlserver, tablePrefix), } @@ -38,6 +38,13 @@ func (r *Sqlserver) CompileAdd(blueprint schema.Blueprint, command *schema.Comma return fmt.Sprintf("alter table %s add %s", r.wrap.Table(blueprint.GetTableName()), r.getColumn(blueprint, command.Column)) } +func (r *Sqlserver) CompileChange(blueprint schema.Blueprint, command *schema.Command) []string { + return []string{ + r.CompileDropDefaultConstraint(blueprint, command), + fmt.Sprintf("alter table %s alter column %s", r.wrap.Table(blueprint.GetTableName()), r.getColumn(blueprint, command.Column)), + } +} + func (r *Sqlserver) CompileColumns(schema, table string) string { newSchema := "schema_name()" if schema != "" { @@ -70,6 +77,18 @@ func (r *Sqlserver) CompileCreate(blueprint schema.Blueprint) string { return fmt.Sprintf("create table %s (%s)", r.wrap.Table(blueprint.GetTableName()), strings.Join(r.getColumns(blueprint), ", ")) } +func (r *Sqlserver) CompileDefault(blueprint schema.Blueprint, command *schema.Command) string { + if command.Column.IsChange() && command.Column.GetDefault() != nil { + return fmt.Sprintf("alter table %s add default %s for %s", + r.wrap.Table(blueprint.GetTableName()), + getDefaultValue(command.Column.GetDefault()), + r.wrap.Column(command.Column.GetName()), + ) + } + + return "" +} + func (r *Sqlserver) CompileDrop(blueprint schema.Blueprint) string { return fmt.Sprintf("drop table %s", r.wrap.Table(blueprint.GetTableName())) } @@ -115,8 +134,10 @@ func (r *Sqlserver) CompileDropColumn(blueprint schema.Blueprint, command *schem } func (r *Sqlserver) CompileDropDefaultConstraint(blueprint schema.Blueprint, command *schema.Command) string { - // TODO Add change logic columns := fmt.Sprintf("'%s'", strings.Join(command.Columns, "','")) + if command.Column != nil && command.Column.IsChange() { + columns = fmt.Sprintf("'%s'", command.Column.GetName()) + } table := r.wrap.Table(blueprint.GetTableName()) tableName := r.wrap.Quote(table) @@ -281,7 +302,7 @@ func (r *Sqlserver) GetAttributeCommands() []string { } func (r *Sqlserver) ModifyDefault(_ schema.Blueprint, column schema.ColumnDefinition) string { - if column.GetDefault() != nil { + if !column.IsChange() && column.GetDefault() != nil { return fmt.Sprintf(" default %s", getDefaultValue(column.GetDefault())) } @@ -291,13 +312,13 @@ func (r *Sqlserver) ModifyDefault(_ schema.Blueprint, column schema.ColumnDefini func (r *Sqlserver) ModifyNullable(_ schema.Blueprint, column schema.ColumnDefinition) string { if column.GetNullable() { return " null" - } else { - return " not null" } + + return " not null" } func (r *Sqlserver) ModifyIncrement(blueprint schema.Blueprint, column schema.ColumnDefinition) string { - if slices.Contains(r.serials, column.GetType()) && column.GetAutoIncrement() { + if !column.IsChange() && slices.Contains(r.serials, column.GetType()) && column.GetAutoIncrement() { if blueprint.HasCommand("primary") { return " identity" } diff --git a/database/schema/grammars/sqlserver_test.go b/database/schema/grammars/sqlserver_test.go index 2e0335bd1..e09380c60 100644 --- a/database/schema/grammars/sqlserver_test.go +++ b/database/schema/grammars/sqlserver_test.go @@ -32,6 +32,7 @@ func (s *SqlserverSuite) TestCompileAdd() { mockColumn.EXPECT().GetDefault().Return("goravel").Twice() mockColumn.EXPECT().GetNullable().Return(false).Once() mockColumn.EXPECT().GetLength().Return(1).Once() + mockColumn.EXPECT().IsChange().Return(false).Twice() sql := s.grammar.CompileAdd(mockBlueprint, &contractsschema.Command{ Column: mockColumn, @@ -40,6 +41,27 @@ func (s *SqlserverSuite) TestCompileAdd() { s.Equal(`alter table "goravel_users" add "name" nvarchar(1) default 'goravel' not null`, sql) } +func (s *SqlserverSuite) TestCompileChange() { + mockBlueprint := mocksschema.NewBlueprint(s.T()) + mockColumn := mocksschema.NewColumnDefinition(s.T()) + + mockBlueprint.EXPECT().GetTableName().Return("users").Twice() + mockColumn.EXPECT().GetName().Return("name").Twice() + mockColumn.EXPECT().GetType().Return("string").Once() + mockColumn.EXPECT().GetNullable().Return(false).Once() + mockColumn.EXPECT().GetLength().Return(1).Once() + mockColumn.EXPECT().IsChange().Return(true).Times(3) + + sql := s.grammar.CompileChange(mockBlueprint, &contractsschema.Command{ + Column: mockColumn, + }) + + s.Equal([]string{ + `DECLARE @sql NVARCHAR(MAX) = '';SELECT @sql += 'ALTER TABLE "goravel_users" DROP CONSTRAINT ' + OBJECT_NAME([default_object_id]) + ';' FROM sys.columns WHERE [object_id] = OBJECT_ID('"goravel_users"') AND [name] in ('name') AND [default_object_id] <> 0;EXEC(@sql);`, + `alter table "goravel_users" alter column "name" nvarchar(1) not null`, + }, sql) +} + func (s *SqlserverSuite) TestCompileCreate() { mockColumn1 := mocksschema.NewColumnDefinition(s.T()) mockColumn2 := mocksschema.NewColumnDefinition(s.T()) @@ -64,6 +86,7 @@ func (s *SqlserverSuite) TestCompileCreate() { mockColumn1.EXPECT().GetType().Return("integer").Once() // postgres.go::ModifyNullable mockColumn1.EXPECT().GetNullable().Return(false).Once() + mockColumn1.EXPECT().IsChange().Return(false).Twice() // utils.go::getColumns mockColumn2.EXPECT().GetName().Return("name").Once() @@ -77,11 +100,28 @@ func (s *SqlserverSuite) TestCompileCreate() { mockColumn2.EXPECT().GetType().Return("string").Once() // postgres.go::ModifyNullable mockColumn2.EXPECT().GetNullable().Return(true).Once() + mockColumn2.EXPECT().IsChange().Return(false).Twice() s.Equal(`create table "goravel_users" ("id" int identity primary key not null, "name" nvarchar(100) null)`, s.grammar.CompileCreate(mockBlueprint)) } +func (s *SqlserverSuite) TestCompileDefault() { + mockBlueprint := mocksschema.NewBlueprint(s.T()) + mockColumnDefinition := mocksschema.NewColumnDefinition(s.T()) + + mockColumnDefinition.EXPECT().IsChange().Return(true).Once() + mockColumnDefinition.EXPECT().GetDefault().Return("default").Twice() + mockColumnDefinition.EXPECT().GetName().Return("id").Once() + mockBlueprint.EXPECT().GetTableName().Return("users").Once() + + sql := s.grammar.CompileDefault(mockBlueprint, &contractsschema.Command{ + Column: mockColumnDefinition, + }) + + s.Equal(`alter table "goravel_users" add default 'default' for "id"`, sql) +} + func (s *SqlserverSuite) TestCompileDropColumn() { mockBlueprint := mocksschema.NewBlueprint(s.T()) mockBlueprint.EXPECT().GetTableName().Return("users").Twice() @@ -212,12 +252,14 @@ func (s *SqlserverSuite) TestGetColumns() { mockColumn1.EXPECT().GetDefault().Return(nil).Once() mockColumn1.EXPECT().GetNullable().Return(false).Once() mockColumn1.EXPECT().GetAutoIncrement().Return(true).Once() + mockColumn1.EXPECT().IsChange().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).Twice() s.Equal([]string{`"id" int identity primary key not null`, `"name" nvarchar(10) default 'goravel' null`}, s.grammar.getColumns(mockBlueprint)) } @@ -236,12 +278,14 @@ func (s *SqlserverSuite) TestModifyDefault() { { name: "without change and default is nil", setup: func() { + mockColumn.EXPECT().IsChange().Return(false).Once() mockColumn.EXPECT().GetDefault().Return(nil).Once() }, }, { name: "without change and default is not nil", setup: func() { + mockColumn.EXPECT().IsChange().Return(false).Once() mockColumn.EXPECT().GetDefault().Return("goravel").Twice() }, expectSql: " default 'goravel'", @@ -281,6 +325,7 @@ func (s *SqlserverSuite) TestModifyIncrement() { mockBlueprint.EXPECT().HasCommand("primary").Return(false).Once() mockColumn.EXPECT().GetType().Return("bigInteger").Once() mockColumn.EXPECT().GetAutoIncrement().Return(true).Once() + mockColumn.EXPECT().IsChange().Return(false).Once() s.Equal(" identity primary key", s.grammar.ModifyIncrement(mockBlueprint, mockColumn)) } diff --git a/database/schema/schema_test.go b/database/schema/schema_test.go index 5fdaef5b4..48d3254c1 100644 --- a/database/schema/schema_test.go +++ b/database/schema/schema_test.go @@ -14,6 +14,7 @@ import ( "github.com/goravel/framework/contracts/database" contractsschema "github.com/goravel/framework/contracts/database/schema" "github.com/goravel/framework/database/gorm" + "github.com/goravel/framework/database/schema/constants" "github.com/goravel/framework/support/carbon" "github.com/goravel/framework/support/docker" "github.com/goravel/framework/support/env" @@ -67,6 +68,121 @@ func (s *SchemaSuite) TearDownTest() { } } +func (s *SchemaSuite) TestColumnChange() { + for driver, testQuery := range s.driverToTestQuery { + if driver == database.DriverSqlite { + continue + } + s.Run(driver.String(), func() { + schema := GetTestSchema(testQuery, s.driverToTestQuery) + table := "column_change" + expectedDefaultStringLength := constants.DefaultStringLength + customStringLength := 100 + expectedCustomStringLength := customStringLength + expectedColumnType := "text" + + if driver == database.DriverSqlserver { + expectedDefaultStringLength = constants.DefaultStringLength * 2 + expectedCustomStringLength = customStringLength * 2 + expectedColumnType = "nvarchar" + } + s.NoError(schema.Create(table, func(table contractsschema.Blueprint) { + table.ID() + table.String("change_length") + table.String("change_type") + table.String("change_to_nullable") + table.String("change_to_not_nullable").Nullable() + table.String("change_add_default") + table.String("change_remove_default").Default("goravel") + table.String("change_modify_default").Default("goravel") + table.String("change_add_comment") + table.String("change_remove_comment").Comment("goravel") + table.String("change_modify_comment").Comment("goravel") + + })) + columns, err := schema.GetColumns(table) + s.Require().Nil(err) + for _, column := range columns { + if column.Name == "change_length" { + s.Contains(column.Type, fmt.Sprintf("(%d)", expectedDefaultStringLength)) + } + if column.Name == "change_type" { + s.Contains(column.TypeName, "varchar") + } + if column.Name == "change_to_nullable" { + s.False(column.Nullable) + } + if column.Name == "change_to_not_nullable" { + s.True(column.Nullable) + } + if column.Name == "change_add_default" { + s.Empty(column.Default) + } + if column.Name == "change_remove_default" || column.Name == "change_modify_default" { + s.Contains(column.Default, "goravel") + } + if driver != database.DriverSqlserver { + if column.Name == "change_add_comment" { + s.Empty(column.Comment) + } + if column.Name == "change_remove_comment" || column.Name == "change_modify_comment" { + s.Contains(column.Comment, "goravel") + } + } + + } + s.NoError(schema.Table(table, func(table contractsschema.Blueprint) { + table.String("change_length", customStringLength).Change() + table.Text("change_type").Change() + table.String("change_to_nullable").Nullable().Change() + table.String("change_to_not_nullable").Change() + table.String("change_add_default").Default("goravel").Change() + table.String("change_remove_default").Change() + table.String("change_modify_default").Default("goravel_again").Change() + table.String("change_add_comment").Comment("goravel").Change() + table.String("change_remove_comment").Change() + table.String("change_modify_comment").Comment("goravel_again").Change() + })) + columns, err = schema.GetColumns(table) + s.Require().Nil(err) + for _, column := range columns { + if column.Name == "change_length" { + s.Contains(column.Type, fmt.Sprintf("(%d)", expectedCustomStringLength)) + } + if column.Name == "change_type" { + s.Contains(column.TypeName, expectedColumnType) + } + if column.Name == "change_to_nullable" { + s.True(column.Nullable) + } + if column.Name == "change_to_not_nullable" { + s.False(column.Nullable) + } + if column.Name == "change_add_default" { + s.Contains(column.Default, "goravel") + } + if column.Name == "change_remove_default" { + s.Empty(column.Default) + } + if column.Name == "change_modify_default" { + s.Contains(column.Default, "goravel_again") + } + if driver != database.DriverSqlserver { + if column.Name == "change_add_comment" { + s.Contains(column.Comment, "goravel") + } + if column.Name == "change_remove_comment" { + s.Empty(column.Comment) + } + if column.Name == "change_modify_comment" { + s.Contains(column.Comment, "goravel_again") + } + } + } + }) + } +} + func (s *SchemaSuite) TestColumnExtraAttributes() { for driver, testQuery := range s.driverToTestQuery { s.Run(driver.String(), func() { diff --git a/mocks/database/schema/ColumnDefinition.go b/mocks/database/schema/ColumnDefinition.go index a65d08523..ed65cd3e4 100644 --- a/mocks/database/schema/ColumnDefinition.go +++ b/mocks/database/schema/ColumnDefinition.go @@ -67,6 +67,53 @@ func (_c *ColumnDefinition_AutoIncrement_Call) RunAndReturn(run func() schema.Co return _c } +// Change provides a mock function with no fields +func (_m *ColumnDefinition) Change() schema.ColumnDefinition { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Change") + } + + var r0 schema.ColumnDefinition + if rf, ok := ret.Get(0).(func() schema.ColumnDefinition); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(schema.ColumnDefinition) + } + } + + return r0 +} + +// ColumnDefinition_Change_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Change' +type ColumnDefinition_Change_Call struct { + *mock.Call +} + +// Change is a helper method to define mock.On call +func (_e *ColumnDefinition_Expecter) Change() *ColumnDefinition_Change_Call { + return &ColumnDefinition_Change_Call{Call: _e.mock.On("Change")} +} + +func (_c *ColumnDefinition_Change_Call) Run(run func()) *ColumnDefinition_Change_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *ColumnDefinition_Change_Call) Return(_a0 schema.ColumnDefinition) *ColumnDefinition_Change_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *ColumnDefinition_Change_Call) RunAndReturn(run func() schema.ColumnDefinition) *ColumnDefinition_Change_Call { + _c.Call.Return(run) + return _c +} + // Comment provides a mock function with given fields: comment func (_m *ColumnDefinition) Comment(comment string) schema.ColumnDefinition { ret := _m.Called(comment) @@ -844,6 +891,51 @@ func (_c *ColumnDefinition_GetUseCurrentOnUpdate_Call) RunAndReturn(run func() b return _c } +// IsChange provides a mock function with no fields +func (_m *ColumnDefinition) IsChange() bool { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for IsChange") + } + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// ColumnDefinition_IsChange_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsChange' +type ColumnDefinition_IsChange_Call struct { + *mock.Call +} + +// IsChange is a helper method to define mock.On call +func (_e *ColumnDefinition_Expecter) IsChange() *ColumnDefinition_IsChange_Call { + return &ColumnDefinition_IsChange_Call{Call: _e.mock.On("IsChange")} +} + +func (_c *ColumnDefinition_IsChange_Call) Run(run func()) *ColumnDefinition_IsChange_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *ColumnDefinition_IsChange_Call) Return(_a0 bool) *ColumnDefinition_IsChange_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *ColumnDefinition_IsChange_Call) RunAndReturn(run func() bool) *ColumnDefinition_IsChange_Call { + _c.Call.Return(run) + return _c +} + // IsSetComment provides a mock function with no fields func (_m *ColumnDefinition) IsSetComment() bool { ret := _m.Called() diff --git a/mocks/database/schema/Grammar.go b/mocks/database/schema/Grammar.go index aa1498c2c..dc387c636 100644 --- a/mocks/database/schema/Grammar.go +++ b/mocks/database/schema/Grammar.go @@ -67,6 +67,55 @@ func (_c *Grammar_CompileAdd_Call) RunAndReturn(run func(schema.Blueprint, *sche return _c } +// CompileChange provides a mock function with given fields: blueprint, command +func (_m *Grammar) CompileChange(blueprint schema.Blueprint, command *schema.Command) []string { + ret := _m.Called(blueprint, command) + + if len(ret) == 0 { + panic("no return value specified for CompileChange") + } + + var r0 []string + if rf, ok := ret.Get(0).(func(schema.Blueprint, *schema.Command) []string); ok { + r0 = rf(blueprint, command) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + return r0 +} + +// Grammar_CompileChange_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CompileChange' +type Grammar_CompileChange_Call struct { + *mock.Call +} + +// CompileChange is a helper method to define mock.On call +// - blueprint schema.Blueprint +// - command *schema.Command +func (_e *Grammar_Expecter) CompileChange(blueprint interface{}, command interface{}) *Grammar_CompileChange_Call { + return &Grammar_CompileChange_Call{Call: _e.mock.On("CompileChange", blueprint, command)} +} + +func (_c *Grammar_CompileChange_Call) Run(run func(blueprint schema.Blueprint, command *schema.Command)) *Grammar_CompileChange_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(schema.Blueprint), args[1].(*schema.Command)) + }) + return _c +} + +func (_c *Grammar_CompileChange_Call) Return(_a0 []string) *Grammar_CompileChange_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Grammar_CompileChange_Call) RunAndReturn(run func(schema.Blueprint, *schema.Command) []string) *Grammar_CompileChange_Call { + _c.Call.Return(run) + return _c +} + // CompileColumns provides a mock function with given fields: _a0, table func (_m *Grammar) CompileColumns(_a0 string, table string) string { ret := _m.Called(_a0, table) @@ -207,6 +256,53 @@ func (_c *Grammar_CompileCreate_Call) RunAndReturn(run func(schema.Blueprint) st return _c } +// CompileDefault provides a mock function with given fields: blueprint, command +func (_m *Grammar) CompileDefault(blueprint schema.Blueprint, command *schema.Command) string { + ret := _m.Called(blueprint, command) + + if len(ret) == 0 { + panic("no return value specified for CompileDefault") + } + + var r0 string + if rf, ok := ret.Get(0).(func(schema.Blueprint, *schema.Command) string); ok { + r0 = rf(blueprint, command) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Grammar_CompileDefault_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CompileDefault' +type Grammar_CompileDefault_Call struct { + *mock.Call +} + +// CompileDefault is a helper method to define mock.On call +// - blueprint schema.Blueprint +// - command *schema.Command +func (_e *Grammar_Expecter) CompileDefault(blueprint interface{}, command interface{}) *Grammar_CompileDefault_Call { + return &Grammar_CompileDefault_Call{Call: _e.mock.On("CompileDefault", blueprint, command)} +} + +func (_c *Grammar_CompileDefault_Call) Run(run func(blueprint schema.Blueprint, command *schema.Command)) *Grammar_CompileDefault_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(schema.Blueprint), args[1].(*schema.Command)) + }) + return _c +} + +func (_c *Grammar_CompileDefault_Call) Return(_a0 string) *Grammar_CompileDefault_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Grammar_CompileDefault_Call) RunAndReturn(run func(schema.Blueprint, *schema.Command) string) *Grammar_CompileDefault_Call { + _c.Call.Return(run) + return _c +} + // CompileDrop provides a mock function with given fields: blueprint func (_m *Grammar) CompileDrop(blueprint schema.Blueprint) string { ret := _m.Called(blueprint)