From 808f7d6ce2fb093f57435a4adfff2890fa635b6c Mon Sep 17 00:00:00 2001 From: almas1992 Date: Thu, 9 Jan 2025 18:57:39 +0800 Subject: [PATCH 1/5] feat: migrator support to change table column --- contracts/database/schema/column.go | 4 + contracts/database/schema/grammar.go | 2 + database/schema/blueprint.go | 6 ++ database/schema/blueprint_test.go | 21 +++++ database/schema/column.go | 11 +++ database/schema/column_test.go | 7 ++ database/schema/grammars/mysql.go | 6 ++ database/schema/grammars/mysql_test.go | 21 +++++ database/schema/grammars/postgres.go | 73 +++++++++++------ database/schema/grammars/postgres_test.go | 56 +++++++++++++ database/schema/grammars/sqlite.go | 4 + database/schema/grammars/sqlserver.go | 19 +++-- database/schema/grammars/sqlserver_test.go | 29 +++++++ mocks/database/schema/ColumnDefinition.go | 92 ++++++++++++++++++++++ mocks/database/schema/Grammar.go | 49 ++++++++++++ 15 files changed, 372 insertions(+), 28 deletions(-) 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 740de13ef..42c967ae1 100644 --- a/contracts/database/schema/grammar.go +++ b/contracts/database/schema/grammar.go @@ -3,6 +3,8 @@ 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. diff --git a/database/schema/blueprint.go b/database/schema/blueprint.go index d46c55d9c..83b2b4693 100644 --- a/database/schema/blueprint.go +++ b/database/schema/blueprint.go @@ -430,6 +430,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); statement != nil { + statements = append(statements, statement...) + } + continue + } statements = append(statements, grammar.CompileAdd(r, command)) case constants.CommandComment: if statement := grammar.CompileComment(r, command); statement != "" { diff --git a/database/schema/blueprint_test.go b/database/schema/blueprint_test.go index d4f777220..3226f1204 100644 --- a/database/schema/blueprint_test.go +++ b/database/schema/blueprint_test.go @@ -386,6 +386,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), 1) + } else { + s.Empty(s.blueprint.ToSql(grammar)) + } } } @@ -438,3 +447,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/grammars/mysql.go b/database/schema/grammars/mysql.go index ce4b7db9c..df3904ad7 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`, "+ diff --git a/database/schema/grammars/mysql_test.go b/database/schema/grammars/mysql_test.go index 40eb82d88..327a7f2d7 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 f77a2a267..8c3fe3e35 100644 --- a/database/schema/grammars/postgres.go +++ b/database/schema/grammars/postgres.go @@ -39,16 +39,29 @@ 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, "+ - "(select tc.collcollate from pg_catalog.pg_collation tc where tc.oid = a.attcollation) as collation, "+ - "not a.attnotnull as nullable, "+ - "(select pg_get_expr(adbin, adrelid) from pg_attrdef where c.oid = pg_attrdef.adrelid and pg_attrdef.adnum = a.attnum) as default, "+ - "col_description(c.oid, a.attnum) as comment "+ - "from pg_attribute a, pg_class c, pg_type t, pg_namespace n "+ - "where c.relname = %s and n.nspname = %s and a.attnum > 0 and a.attrelid = c.oid and a.atttypid = t.oid and n.oid = c.relnamespace "+ - "order by a.attnum", r.wrap.Quote(table), r.wrap.Quote(schema)) + "(select tc.collcollate from pg_catalog.pg_collation tc where tc.oid = a.attcollation) as collation, "+ + "not a.attnotnull as nullable, "+ + "(select pg_get_expr(adbin, adrelid) from pg_attrdef where c.oid = pg_attrdef.adrelid and pg_attrdef.adnum = a.attnum) as default, "+ + "col_description(c.oid, a.attnum) as comment "+ + "from pg_attribute a, pg_class c, pg_type t, pg_namespace n "+ + "where c.relname = %s and n.nspname = %s and a.attnum > 0 and a.attrelid = c.oid and a.atttypid = t.oid and n.oid = c.relnamespace "+ + "order by a.attnum", r.wrap.Quote(table), r.wrap.Quote(schema)) } func (r *Postgres) CompileComment(blueprint schema.Blueprint, command *schema.Command) string { @@ -194,16 +207,16 @@ func (r *Postgres) CompileIndex(blueprint schema.Blueprint, command *schema.Comm func (r *Postgres) CompileIndexes(schema, table string) string { return fmt.Sprintf( "select ic.relname as name, string_agg(a.attname, ',' order by indseq.ord) as columns, "+ - "am.amname as \"type\", i.indisunique as \"unique\", i.indisprimary as \"primary\" "+ - "from pg_index i "+ - "join pg_class tc on tc.oid = i.indrelid "+ - "join pg_namespace tn on tn.oid = tc.relnamespace "+ - "join pg_class ic on ic.oid = i.indexrelid "+ - "join pg_am am on am.oid = ic.relam "+ - "join lateral unnest(i.indkey) with ordinality as indseq(num, ord) on true "+ - "left join pg_attribute a on a.attrelid = i.indrelid and a.attnum = indseq.num "+ - "where tc.relname = %s and tn.nspname = %s "+ - "group by ic.relname, am.amname, i.indisunique, i.indisprimary", + "am.amname as \"type\", i.indisunique as \"unique\", i.indisprimary as \"primary\" "+ + "from pg_index i "+ + "join pg_class tc on tc.oid = i.indrelid "+ + "join pg_namespace tn on tn.oid = tc.relnamespace "+ + "join pg_class ic on ic.oid = i.indexrelid "+ + "join pg_am am on am.oid = ic.relam "+ + "join lateral unnest(i.indkey) with ordinality as indseq(num, ord) on true "+ + "left join pg_attribute a on a.attrelid = i.indrelid and a.attnum = indseq.num "+ + "where tc.relname = %s and tn.nspname = %s "+ + "group by ic.relname, am.amname, i.indisunique, i.indisprimary", r.wrap.Quote(table), r.wrap.Quote(schema), ) @@ -225,9 +238,9 @@ func (r *Postgres) CompileRenameIndex(_ schema.Schema, _ schema.Blueprint, comma func (r *Postgres) CompileTables(_ string) string { return "select c.relname as name, n.nspname as schema, pg_total_relation_size(c.oid) as size, " + - "obj_description(c.oid, 'pg_class') as comment from pg_class c, pg_namespace n " + - "where c.relkind in ('r', 'p') and n.oid = c.relnamespace and n.nspname not in ('pg_catalog', 'information_schema') " + - "order by c.relname" + "obj_description(c.oid, 'pg_class') as comment from pg_class c, pg_namespace n " + + "where c.relkind in ('r', 'p') and n.oid = c.relnamespace and n.nspname not in ('pg_catalog', 'information_schema') " + + "order by c.relname" } func (r *Postgres) CompileTypes() string { @@ -291,6 +304,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 fmt.Sprintf(" drop default") + } if column.GetDefault() != nil { return fmt.Sprintf(" default %s", getDefaultValue(column.GetDefault())) } @@ -299,15 +321,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 42764efab..d17a8efd7 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 { @@ -377,6 +431,7 @@ func (s *PostgresSuite) TestModifyNullable() { mockColumn := mocksschema.NewColumnDefinition(s.T()) + mockColumn.EXPECT().IsChange().Return(false).Twice() mockColumn.EXPECT().GetNullable().Return(true).Once() s.Equal(" null", s.grammar.ModifyNullable(mockBlueprint, mockColumn)) @@ -393,6 +448,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 cc19654df..1ad5b640d 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" `+ diff --git a/database/schema/grammars/sqlserver.go b/database/schema/grammars/sqlserver.go index ae0dae88a..b5e5fb704 100644 --- a/database/schema/grammars/sqlserver.go +++ b/database/schema/grammars/sqlserver.go @@ -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 != "" { @@ -115,8 +122,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 +290,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 +300,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 0dd4beb05..6b84ce564 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,6 +100,7 @@ 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)) @@ -212,12 +236,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 +262,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 +309,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/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 a251255cb..005945c55 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) From 44e1425cf509f11e15515548a23ab6835a0ff645 Mon Sep 17 00:00:00 2001 From: ALMAS Date: Thu, 9 Jan 2025 19:21:19 +0800 Subject: [PATCH 2/5] fix lint issue --- database/schema/grammars/postgres.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/schema/grammars/postgres.go b/database/schema/grammars/postgres.go index 8c3fe3e35..14a19462b 100644 --- a/database/schema/grammars/postgres.go +++ b/database/schema/grammars/postgres.go @@ -311,7 +311,7 @@ func (r *Postgres) ModifyDefault(blueprint schema.Blueprint, column schema.Colum if column.GetDefault() != nil { return fmt.Sprintf(" set default %s", getDefaultValue(column.GetDefault())) } - return fmt.Sprintf(" drop default") + return " drop default" } if column.GetDefault() != nil { return fmt.Sprintf(" default %s", getDefaultValue(column.GetDefault())) From 9b9c1c7623b3dcc6d3273ddf8a31f4be4ff60f08 Mon Sep 17 00:00:00 2001 From: almas1992 Date: Fri, 10 Jan 2025 14:07:56 +0800 Subject: [PATCH 3/5] improve unit test --- database/schema/blueprint.go | 2 +- database/schema/grammars/postgres_test.go | 58 ++++++++++++++-- database/schema/schema_test.go | 82 +++++++++++++++++++++++ 3 files changed, 134 insertions(+), 8 deletions(-) diff --git a/database/schema/blueprint.go b/database/schema/blueprint.go index 83b2b4693..a69bfafdc 100644 --- a/database/schema/blueprint.go +++ b/database/schema/blueprint.go @@ -431,7 +431,7 @@ func (r *Blueprint) ToSql(grammar schema.Grammar) []string { switch command.Name { case constants.CommandAdd: if command.Column.IsChange() { - if statement := grammar.CompileChange(r, command); statement != nil { + if statement := grammar.CompileChange(r, command); len(statement) > 0 { statements = append(statements, statement...) } continue diff --git a/database/schema/grammars/postgres_test.go b/database/schema/grammars/postgres_test.go index d17a8efd7..ca5f8f192 100644 --- a/database/schema/grammars/postgres_test.go +++ b/database/schema/grammars/postgres_test.go @@ -427,18 +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().IsChange().Return(false).Twice() - 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() { diff --git a/database/schema/schema_test.go b/database/schema/schema_test.go index 17189113f..39208e8d0 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,87 @@ 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 + + if driver == database.DriverSqlserver { + expectedDefaultStringLength = constants.DefaultStringLength * 2 + expectedCustomStringLength = customStringLength * 2 + } + 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") + })) + 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" { + s.Contains(column.Default, "goravel") + } + } + s.NoError(schema.Table(table, func(table contractsschema.Blueprint) { + table.String("change_length", customStringLength).Change() + table.Integer("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() + })) + 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, "int") + } + 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) + } + } + }) + } +} + func (s *SchemaSuite) TestColumnExtraAttributes() { for driver, testQuery := range s.driverToTestQuery { s.Run(driver.String(), func() { From 60fcc728281ea64f129d210d4e3405a17c3f76a8 Mon Sep 17 00:00:00 2001 From: almas1992 Date: Fri, 10 Jan 2025 16:21:51 +0800 Subject: [PATCH 4/5] add compile default for sql server and improve unit test --- contracts/database/schema/grammar.go | 2 + database/schema/blueprint.go | 12 +++++- database/schema/blueprint_test.go | 2 +- database/schema/constants/constants.go | 1 + database/schema/grammars/mysql.go | 4 ++ database/schema/grammars/postgres.go | 44 +++++++++++--------- database/schema/grammars/sqlite.go | 4 ++ database/schema/grammars/sqlserver.go | 14 ++++++- database/schema/grammars/sqlserver_test.go | 16 ++++++++ database/schema/schema_test.go | 6 ++- mocks/database/schema/Grammar.go | 47 ++++++++++++++++++++++ 11 files changed, 127 insertions(+), 25 deletions(-) diff --git a/contracts/database/schema/grammar.go b/contracts/database/schema/grammar.go index 42c967ae1..2a3342f12 100644 --- a/contracts/database/schema/grammar.go +++ b/contracts/database/schema/grammar.go @@ -11,6 +11,8 @@ type Grammar interface { 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 a69bfafdc..0da1f2c91 100644 --- a/database/schema/blueprint.go +++ b/database/schema/blueprint.go @@ -443,6 +443,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: @@ -509,12 +513,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 3226f1204..034bdabd0 100644 --- a/database/schema/blueprint_test.go +++ b/database/schema/blueprint_test.go @@ -391,7 +391,7 @@ func (s *BlueprintTestSuite) TestToSql() { s.SetupTest() s.blueprint.String("name", 100).Change() if driver == database.DriverPostgres { - s.Len(s.blueprint.ToSql(grammar), 1) + s.Len(s.blueprint.ToSql(grammar), 2) } else { s.Empty(s.blueprint.ToSql(grammar)) } 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 df3904ad7..fdb99eab1 100644 --- a/database/schema/grammars/mysql.go +++ b/database/schema/grammars/mysql.go @@ -77,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/postgres.go b/database/schema/grammars/postgres.go index 14a19462b..236553b05 100644 --- a/database/schema/grammars/postgres.go +++ b/database/schema/grammars/postgres.go @@ -55,13 +55,13 @@ func (r *Postgres) CompileChange(blueprint schema.Blueprint, command *schema.Com 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, "+ - "(select tc.collcollate from pg_catalog.pg_collation tc where tc.oid = a.attcollation) as collation, "+ - "not a.attnotnull as nullable, "+ - "(select pg_get_expr(adbin, adrelid) from pg_attrdef where c.oid = pg_attrdef.adrelid and pg_attrdef.adnum = a.attnum) as default, "+ - "col_description(c.oid, a.attnum) as comment "+ - "from pg_attribute a, pg_class c, pg_type t, pg_namespace n "+ - "where c.relname = %s and n.nspname = %s and a.attnum > 0 and a.attrelid = c.oid and a.atttypid = t.oid and n.oid = c.relnamespace "+ - "order by a.attnum", r.wrap.Quote(table), r.wrap.Quote(schema)) + "(select tc.collcollate from pg_catalog.pg_collation tc where tc.oid = a.attcollation) as collation, "+ + "not a.attnotnull as nullable, "+ + "(select pg_get_expr(adbin, adrelid) from pg_attrdef where c.oid = pg_attrdef.adrelid and pg_attrdef.adnum = a.attnum) as default, "+ + "col_description(c.oid, a.attnum) as comment "+ + "from pg_attribute a, pg_class c, pg_type t, pg_namespace n "+ + "where c.relname = %s and n.nspname = %s and a.attnum > 0 and a.attrelid = c.oid and a.atttypid = t.oid and n.oid = c.relnamespace "+ + "order by a.attnum", r.wrap.Quote(table), r.wrap.Quote(schema)) } func (r *Postgres) CompileComment(blueprint schema.Blueprint, command *schema.Command) string { @@ -80,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())) } @@ -207,16 +211,16 @@ func (r *Postgres) CompileIndex(blueprint schema.Blueprint, command *schema.Comm func (r *Postgres) CompileIndexes(schema, table string) string { return fmt.Sprintf( "select ic.relname as name, string_agg(a.attname, ',' order by indseq.ord) as columns, "+ - "am.amname as \"type\", i.indisunique as \"unique\", i.indisprimary as \"primary\" "+ - "from pg_index i "+ - "join pg_class tc on tc.oid = i.indrelid "+ - "join pg_namespace tn on tn.oid = tc.relnamespace "+ - "join pg_class ic on ic.oid = i.indexrelid "+ - "join pg_am am on am.oid = ic.relam "+ - "join lateral unnest(i.indkey) with ordinality as indseq(num, ord) on true "+ - "left join pg_attribute a on a.attrelid = i.indrelid and a.attnum = indseq.num "+ - "where tc.relname = %s and tn.nspname = %s "+ - "group by ic.relname, am.amname, i.indisunique, i.indisprimary", + "am.amname as \"type\", i.indisunique as \"unique\", i.indisprimary as \"primary\" "+ + "from pg_index i "+ + "join pg_class tc on tc.oid = i.indrelid "+ + "join pg_namespace tn on tn.oid = tc.relnamespace "+ + "join pg_class ic on ic.oid = i.indexrelid "+ + "join pg_am am on am.oid = ic.relam "+ + "join lateral unnest(i.indkey) with ordinality as indseq(num, ord) on true "+ + "left join pg_attribute a on a.attrelid = i.indrelid and a.attnum = indseq.num "+ + "where tc.relname = %s and tn.nspname = %s "+ + "group by ic.relname, am.amname, i.indisunique, i.indisprimary", r.wrap.Quote(table), r.wrap.Quote(schema), ) @@ -238,9 +242,9 @@ func (r *Postgres) CompileRenameIndex(_ schema.Schema, _ schema.Blueprint, comma func (r *Postgres) CompileTables(_ string) string { return "select c.relname as name, n.nspname as schema, pg_total_relation_size(c.oid) as size, " + - "obj_description(c.oid, 'pg_class') as comment from pg_class c, pg_namespace n " + - "where c.relkind in ('r', 'p') and n.oid = c.relnamespace and n.nspname not in ('pg_catalog', 'information_schema') " + - "order by c.relname" + "obj_description(c.oid, 'pg_class') as comment from pg_class c, pg_namespace n " + + "where c.relkind in ('r', 'p') and n.oid = c.relnamespace and n.nspname not in ('pg_catalog', 'information_schema') " + + "order by c.relname" } func (r *Postgres) CompileTypes() string { diff --git a/database/schema/grammars/sqlite.go b/database/schema/grammars/sqlite.go index 1ad5b640d..128524fcc 100644 --- a/database/schema/grammars/sqlite.go +++ b/database/schema/grammars/sqlite.go @@ -65,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 b5e5fb704..595ca0dc7 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), } @@ -77,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())) } diff --git a/database/schema/grammars/sqlserver_test.go b/database/schema/grammars/sqlserver_test.go index 6b84ce564..132229545 100644 --- a/database/schema/grammars/sqlserver_test.go +++ b/database/schema/grammars/sqlserver_test.go @@ -106,6 +106,22 @@ func (s *SqlserverSuite) TestCompileCreate() { 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() diff --git a/database/schema/schema_test.go b/database/schema/schema_test.go index 39208e8d0..cbaa496c3 100644 --- a/database/schema/schema_test.go +++ b/database/schema/schema_test.go @@ -79,10 +79,12 @@ func (s *SchemaSuite) TestColumnChange() { 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() @@ -117,7 +119,7 @@ func (s *SchemaSuite) TestColumnChange() { } s.NoError(schema.Table(table, func(table contractsschema.Blueprint) { table.String("change_length", customStringLength).Change() - table.Integer("change_type").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() @@ -130,7 +132,7 @@ func (s *SchemaSuite) TestColumnChange() { s.Contains(column.Type, fmt.Sprintf("(%d)", expectedCustomStringLength)) } if column.Name == "change_type" { - s.Contains(column.TypeName, "int") + s.Contains(column.TypeName, expectedColumnType) } if column.Name == "change_to_nullable" { s.True(column.Nullable) diff --git a/mocks/database/schema/Grammar.go b/mocks/database/schema/Grammar.go index 005945c55..3be08d049 100644 --- a/mocks/database/schema/Grammar.go +++ b/mocks/database/schema/Grammar.go @@ -256,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) From 5e1e1e53119b8797eb1f027943194e6e47012ecf Mon Sep 17 00:00:00 2001 From: almas1992 Date: Fri, 10 Jan 2025 18:51:53 +0800 Subject: [PATCH 5/5] add more test case --- database/schema/schema_test.go | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/database/schema/schema_test.go b/database/schema/schema_test.go index cbaa496c3..7909f3220 100644 --- a/database/schema/schema_test.go +++ b/database/schema/schema_test.go @@ -94,6 +94,11 @@ func (s *SchemaSuite) TestColumnChange() { 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) @@ -113,9 +118,18 @@ func (s *SchemaSuite) TestColumnChange() { if column.Name == "change_add_default" { s.Empty(column.Default) } - if column.Name == "change_remove_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() @@ -124,6 +138,10 @@ func (s *SchemaSuite) TestColumnChange() { 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) @@ -146,6 +164,20 @@ func (s *SchemaSuite) TestColumnChange() { 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") + } + } } }) }