From 5dcc21de277209ef8bc38004a909b3170f473473 Mon Sep 17 00:00:00 2001 From: Bowen Date: Tue, 24 Dec 2024 18:28:21 +0800 Subject: [PATCH 1/2] feat: sqlserver driver supports setting schema --- database/gorm/query_test.go | 22 ++++- database/gorm/test_models.go | 9 +++ database/gorm/test_utils.go | 90 ++++++++++++++++++--- database/migration/default_migrator_test.go | 64 +++++++++++++-- database/schema/grammars/wrap.go | 3 +- database/schema/grammars/wrap_test.go | 43 +++++----- database/schema/schema.go | 12 ++- database/schema/schema_test.go | 32 +++++++- 8 files changed, 231 insertions(+), 44 deletions(-) diff --git a/database/gorm/query_test.go b/database/gorm/query_test.go index 19f99d03a..3c88a2914 100644 --- a/database/gorm/query_test.go +++ b/database/gorm/query_test.go @@ -3938,7 +3938,7 @@ func TestTablePrefixAndSingular(t *testing.T) { } } -func TestSchema(t *testing.T) { +func TestPostgresSchema(t *testing.T) { if env.IsWindows() { t.Skip("Skip test that using Docker") } @@ -3958,6 +3958,26 @@ func TestSchema(t *testing.T) { assert.True(t, user1.ID > 0) } +func TestSqlserverSchema(t *testing.T) { + if env.IsWindows() { + t.Skip("Skip test that using Docker") + } + + sqlserverDocker := supportdocker.Sqlserver() + require.NoError(t, sqlserverDocker.Ready()) + + testQuery := NewTestQueryWithSchema(sqlserverDocker, "goravel") + testQuery.CreateTable(TestTableSchema) + + schema := Schema{Name: "first_schema"} + assert.Nil(t, testQuery.Query().Create(&schema)) + assert.True(t, schema.ID > 0) + + var schema1 Schema + assert.Nil(t, testQuery.Query().Where("name", "first_schema").First(&schema1)) + assert.True(t, schema1.ID > 0) +} + func paginator(page string, limit string) func(methods contractsorm.Query) contractsorm.Query { return func(query contractsorm.Query) contractsorm.Query { page, _ := strconv.Atoi(page) diff --git a/database/gorm/test_models.go b/database/gorm/test_models.go index 8c82f681f..4725d6276 100644 --- a/database/gorm/test_models.go +++ b/database/gorm/test_models.go @@ -420,3 +420,12 @@ type Box struct { func (p *Box) Connection() string { return "postgres" } + +type Schema struct { + Model + Name string +} + +func (r *Schema) TableName() string { + return "goravel.schemas" +} diff --git a/database/gorm/test_utils.go b/database/gorm/test_utils.go index c5b67c8ad..2f4c7ce55 100644 --- a/database/gorm/test_utils.go +++ b/database/gorm/test_utils.go @@ -28,6 +28,7 @@ const ( TestTableRoleUser TestTableUsers TestTableGoravelUser + TestTableSchema ) var testContext = context.Background() @@ -215,39 +216,48 @@ func NewTestQueryWithSchema(docker testing.DatabaseDriver, schema string) *TestQ // Create schema before build query with the schema mockConfig := &mocksconfig.Config{} mockDriver := getMockDriver(docker, mockConfig, docker.Driver().String()) - mockDriver.WithPrefixAndSingular() + mockDriver.Common() query, err := BuildQuery(testContext, mockConfig, docker.Driver().String(), utils.NewTestLog(), nil) if err != nil { panic(fmt.Sprintf("connect to %s failed: %v", docker.Driver().String(), err)) } - if _, err := query.Exec(fmt.Sprintf("CREATE SCHEMA %s", schema)); err != nil { - panic(fmt.Sprintf("create schema %s failed: %v", schema, err)) - } - - mockConfig = &mocksconfig.Config{} - mockDriver = getMockDriver(docker, mockConfig, docker.Driver().String()) testQuery := &TestQuery{ docker: docker, mockConfig: mockConfig, mockDriver: mockDriver, + query: query, } - mockDriver.WithSchema(schema) + if _, err := query.Exec(fmt.Sprintf("CREATE SCHEMA %s", schema)); err != nil { + panic(fmt.Sprintf("create schema %s failed: %v", schema, err)) + } + if docker.Driver() == contractsdatabase.DriverSqlserver { + return testQuery + } + + mockConfig = &mocksconfig.Config{} + mockDriver = getMockDriver(docker, mockConfig, docker.Driver().String()) + mockDriver.WithSchema(schema) query, err = BuildQuery(testContext, mockConfig, docker.Driver().String(), utils.NewTestLog(), nil) if err != nil { panic(fmt.Sprintf("connect to %s failed: %v", docker.Driver().String(), err)) } - testQuery.query = query + testQuery = &TestQuery{ + docker: docker, + mockConfig: mockConfig, + mockDriver: mockDriver, + query: query, + } return testQuery } func (r *TestQuery) CreateTable(testTables ...TestTable) { for table, sql := range newTestTables(r.docker.Driver()).All() { - if len(testTables) == 0 || slices.Contains(testTables, table) { + if (len(testTables) == 0 && table != TestTableSchema) || slices.Contains(testTables, table) { if _, err := r.query.Exec(sql()); err != nil { panic(fmt.Sprintf("create table %v failed: %v", table, err)) } @@ -540,6 +550,8 @@ func NewMockSqlserver(mockConfig *mocksconfig.Config, connection, database, user func (r *MockSqlserver) Common() { r.mockConfig.EXPECT().GetString(fmt.Sprintf("database.connections.%s.prefix", r.connection)).Return("") r.mockConfig.EXPECT().GetBool(fmt.Sprintf("database.connections.%s.singular", r.connection)).Return(false) + r.mockConfig.EXPECT().GetString(fmt.Sprintf("database.connections.%s.schema", r.connection), "public").Return("public") + r.single() r.basic() } @@ -554,18 +566,26 @@ func (r *MockSqlserver) ReadWrite(readDatabaseConfig testing.DatabaseConfig) { r.mockConfig.EXPECT().GetString(fmt.Sprintf("database.connections.%s.dsn", r.connection)).Return("") r.mockConfig.EXPECT().GetString(fmt.Sprintf("database.connections.%s.prefix", r.connection)).Return("") r.mockConfig.EXPECT().GetBool(fmt.Sprintf("database.connections.%s.singular", r.connection)).Return(false) + r.mockConfig.EXPECT().GetString(fmt.Sprintf("database.connections.%s.schema", r.connection), "public").Return("public") + r.basic() } func (r *MockSqlserver) WithPrefixAndSingular() { r.mockConfig.EXPECT().GetString(fmt.Sprintf("database.connections.%s.prefix", r.connection)).Return("goravel_") r.mockConfig.EXPECT().GetBool(fmt.Sprintf("database.connections.%s.singular", r.connection)).Return(true) + r.mockConfig.EXPECT().GetString(fmt.Sprintf("database.connections.%s.schema", r.connection), "public").Return("public") + r.single() r.basic() } func (r *MockSqlserver) WithSchema(schema string) { - panic("sqlserver does not support schema for now") + r.mockConfig.EXPECT().GetString(fmt.Sprintf("database.connections.%s.prefix", r.connection)).Return("") + r.mockConfig.EXPECT().GetBool(fmt.Sprintf("database.connections.%s.singular", r.connection)).Return(false) + r.mockConfig.EXPECT().GetString(fmt.Sprintf("database.connections.%s.schema", r.connection), "public").Return(schema) + r.single() + r.basic() } func (r *MockSqlserver) basic() { @@ -611,6 +631,7 @@ func (r *testTables) All() map[TestTable]func() string { TestTableRoleUser: r.roleUser, TestTableUsers: r.users, TestTableGoravelUser: r.goravelUser, + TestTableSchema: r.schema, } } @@ -1240,6 +1261,53 @@ CREATE TABLE role_user ( } } +func (r *testTables) schema() string { + switch r.driver { + case contractsdatabase.DriverMysql: + return ` +CREATE TABLE goravel.schemas ( + id bigint(20) unsigned NOT NULL AUTO_INCREMENT, + name varchar(255) NOT NULL, + created_at datetime(3) NOT NULL, + updated_at datetime(3) NOT NULL, + PRIMARY KEY (id), + KEY idx_schemas_created_at (created_at), + KEY idx_schemas_updated_at (updated_at) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; +` + case contractsdatabase.DriverPostgres: + return ` +CREATE TABLE goravel.schemas ( + id SERIAL PRIMARY KEY NOT NULL, + name varchar(255) NOT NULL, + created_at timestamp NOT NULL, + updated_at timestamp NOT NULL +); +` + case contractsdatabase.DriverSqlite: + return ` +CREATE TABLE goravel.schemas ( + id integer PRIMARY KEY AUTOINCREMENT NOT NULL, + name varchar(255) NOT NULL, + created_at datetime NOT NULL, + updated_at datetime NOT NULL +); +` + case contractsdatabase.DriverSqlserver: + return ` +CREATE TABLE goravel.schemas ( + id bigint NOT NULL IDENTITY(1,1), + name varchar(255) NOT NULL, + created_at datetime NOT NULL, + updated_at datetime NOT NULL, + PRIMARY KEY (id) +); +` + default: + return "" + } +} + func mockPool(mockConfig *mocksconfig.Config) { mockConfig.EXPECT().GetInt("database.pool.max_idle_conns", 10).Return(10) mockConfig.EXPECT().GetInt("database.pool.max_open_conns", 100).Return(100) diff --git a/database/migration/default_migrator_test.go b/database/migration/default_migrator_test.go index 4127e34ca..3036d92f6 100644 --- a/database/migration/default_migrator_test.go +++ b/database/migration/default_migrator_test.go @@ -162,7 +162,7 @@ func (s *DefaultMigratorWithDBSuite) TestStatus() { } } -func TestDefaultMigratorWithWithSchema(t *testing.T) { +func TestDefaultMigratorWithPostgresSchema(t *testing.T) { if env.IsWindows() { t.Skip("Skip test that using Docker") } @@ -182,6 +182,32 @@ func TestDefaultMigratorWithWithSchema(t *testing.T) { assert.NoError(t, migrator.Run()) assert.True(t, schema.HasTable("users")) + assert.NoError(t, migrator.Rollback(1, 0)) + assert.False(t, schema.HasTable("users")) +} + +func TestDefaultMigratorWithSqlserverSchema(t *testing.T) { + if env.IsWindows() { + t.Skip("Skip test that using Docker") + } + + sqlserverDocker := docker.Sqlserver() + require.NoError(t, sqlserverDocker.Ready()) + + sqlserverQuery := gorm.NewTestQueryWithSchema(sqlserverDocker, "goravel") + schema := databaseschema.GetTestSchema(sqlserverQuery, map[contractsdatabase.Driver]*gorm.TestQuery{ + contractsdatabase.DriverSqlserver: sqlserverQuery, + }) + testMigration := NewTestMigrationWithSqlserverSchema(schema) + schema.Register([]contractsschema.Migration{ + testMigration, + }) + migrator := NewDefaultMigrator(nil, schema, "migrations") + + assert.NoError(t, migrator.Run()) + assert.True(t, schema.HasTable("goravel.users")) + assert.NoError(t, migrator.Rollback(1, 0)) + assert.False(t, schema.HasTable("goravel.users")) } type DefaultMigratorSuite struct { @@ -861,9 +887,9 @@ func (s *DefaultMigratorSuite) TestStatus() { } func (s *DefaultMigratorSuite) mockRunDown( - mockOrm *mocksorm.Orm, - previousConnection, migrationSignature, table string, - err error, + mockOrm *mocksorm.Orm, + previousConnection, migrationSignature, table string, + err error, ) { s.mockSchema.EXPECT().GetConnection().Return(previousConnection).Once() s.mockSchema.EXPECT().Orm().Return(mockOrm).Times(5) @@ -888,10 +914,10 @@ func (s *DefaultMigratorSuite) mockRunDown( } func (s *DefaultMigratorSuite) mockRunUp( - mockOrm *mocksorm.Orm, - previousConnection, migrationSignature, table string, - batch int, - err error, + mockOrm *mocksorm.Orm, + previousConnection, migrationSignature, table string, + batch int, + err error, ) { s.mockSchema.EXPECT().GetConnection().Return(previousConnection).Once() s.mockSchema.EXPECT().Orm().Return(mockOrm).Times(5) @@ -937,6 +963,28 @@ func (r *TestMigration) Down() error { return r.schema.DropIfExists("users") } +type TestMigrationWithSqlserverSchema struct { + schema contractsschema.Schema +} + +func NewTestMigrationWithSqlserverSchema(schema contractsschema.Schema) *TestMigrationWithSqlserverSchema { + return &TestMigrationWithSqlserverSchema{schema: schema} +} + +func (r *TestMigrationWithSqlserverSchema) Signature() string { + return "20240817214501_create_users_table" +} + +func (r *TestMigrationWithSqlserverSchema) Up() error { + return r.schema.Create("goravel.users", func(table contractsschema.Blueprint) { + table.String("name") + }) +} + +func (r *TestMigrationWithSqlserverSchema) Down() error { + return r.schema.DropIfExists("goravel.users") +} + type TestConnectionMigration struct { schema contractsschema.Schema } diff --git a/database/schema/grammars/wrap.go b/database/schema/grammars/wrap.go index 6e18d457e..ef2e1a598 100644 --- a/database/schema/grammars/wrap.go +++ b/database/schema/grammars/wrap.go @@ -88,9 +88,8 @@ func (r *Wrap) Table(table string) string { } if strings.Contains(table, ".") { lastDotIndex := strings.LastIndex(table, ".") - newTable := table[:lastDotIndex] + "." + r.tablePrefix + table[lastDotIndex+1:] - return r.Value(newTable) + return r.Value(table[:lastDotIndex]) + "." + r.Value(r.tablePrefix+table[lastDotIndex+1:]) } return r.Value(r.tablePrefix + table) diff --git a/database/schema/grammars/wrap_test.go b/database/schema/grammars/wrap_test.go index 2a438e7f6..d234a745f 100644 --- a/database/schema/grammars/wrap_test.go +++ b/database/schema/grammars/wrap_test.go @@ -21,28 +21,28 @@ func (s *WrapTestSuite) SetupTest() { s.wrap = NewWrap(database.DriverPostgres, "prefix_") } -func (s *WrapTestSuite) TestColumnWithAlias() { +func (s *WrapTestSuite) TestColumn() { + // With alias result := s.wrap.Column("column as alias") s.Equal(`"column" as "prefix_alias"`, result) -} -func (s *WrapTestSuite) TestColumnWithoutAlias() { - result := s.wrap.Column("column") + // Without alias + result = s.wrap.Column("column") s.Equal(`"column"`, result) } -func (s *WrapTestSuite) TestColumnsWithMultipleColumns() { +func (s *WrapTestSuite) TestColumnize() { result := s.wrap.Columnize([]string{"column1", "column2 as alias2"}) s.Equal(`"column1", "column2" as "prefix_alias2"`, result) } -func (s *WrapTestSuite) TestQuoteWithNonEmptyValue() { +func (s *WrapTestSuite) TestQuote() { + // With non empty value result := s.wrap.Quote("value") s.Equal("'value'", result) -} -func (s *WrapTestSuite) TestQuoteWithEmptyValue() { - result := s.wrap.Quote("") + // With empty value + result = s.wrap.Quote("") s.Equal("", result) } @@ -60,28 +60,31 @@ func (s *WrapTestSuite) TestSegmentsWithMultipleSegments() { s.Equal(`"prefix_table"."column"`, result) } -func (s *WrapTestSuite) TestTableWithAlias() { +func (s *WrapTestSuite) TestTable() { + // With alias result := s.wrap.Table("table as alias") s.Equal(`"prefix_table" as "prefix_alias"`, result) -} -func (s *WrapTestSuite) TestTableWithoutAlias() { - result := s.wrap.Table("table") + // With schema + result = s.wrap.Table("goravel.table") + s.Equal(`"goravel"."prefix_table"`, result) + + // Without alias + result = s.wrap.Table("table") s.Equal(`"prefix_table"`, result) } -func (s *WrapTestSuite) TestValueWithAsterisk() { +func (s *WrapTestSuite) TestValue() { + // With asterisk result := s.wrap.Value("*") s.Equal("*", result) -} -func (s *WrapTestSuite) TestValueWithNonAsterisk() { - result := s.wrap.Value("value") + // Without asterisk + result = s.wrap.Value("value") s.Equal(`"value"`, result) -} -func (s *WrapTestSuite) TestValueOfMysql() { + // With mysql s.wrap.driver = database.DriverMysql - result := s.wrap.Value("value") + result = s.wrap.Value("value") s.Equal("`value`", result) } diff --git a/database/schema/schema.go b/database/schema/schema.go index 047a13639..edb56cf59 100644 --- a/database/schema/schema.go +++ b/database/schema/schema.go @@ -3,6 +3,7 @@ package schema import ( "fmt" "slices" + "strings" "github.com/goravel/framework/contracts/config" contractsdatabase "github.com/goravel/framework/contracts/database" @@ -217,6 +218,13 @@ func (r *Schema) HasIndex(table, index string) bool { } func (r *Schema) HasTable(name string) bool { + var schema string + if strings.Contains(name, ".") { + lastDotIndex := strings.LastIndex(name, ".") + schema = name[:lastDotIndex] + name = name[lastDotIndex+1:] + } + tableName := r.prefix + name tables, err := r.GetTables() @@ -227,7 +235,9 @@ func (r *Schema) HasTable(name string) bool { for _, table := range tables { if table.Name == tableName { - return true + if schema == "" || schema == table.Schema { + return true + } } } diff --git a/database/schema/schema_test.go b/database/schema/schema_test.go index f760a3bc1..5d79800bd 100644 --- a/database/schema/schema_test.go +++ b/database/schema/schema_test.go @@ -1,6 +1,7 @@ package schema import ( + "fmt" "testing" "time" @@ -2331,7 +2332,7 @@ func (s *SchemaSuite) createTableAndAssertColumnsForColumnMethods(schema contrac s.Contains(columnListing, "updated_at") } -func TestSpecificSchema(t *testing.T) { +func TestPostgresSchema(t *testing.T) { if env.IsWindows() { t.Skip("Skip test that using Docker") } @@ -2355,4 +2356,33 @@ func TestSpecificSchema(t *testing.T) { assert.Len(t, tables, 1) assert.Equal(t, "table", tables[0].Name) assert.Equal(t, schema, tables[0].Schema) + assert.True(t, testSchema.HasTable(fmt.Sprintf("%s.%s", schema, table))) + assert.True(t, testSchema.HasTable(table)) +} + +func TestSqlserverSchema(t *testing.T) { + if env.IsWindows() { + t.Skip("Skip test that using Docker") + } + + schema := "goravel" + table := "table" + sqlserverDocker := docker.Sqlserver() + require.NoError(t, sqlserverDocker.Ready()) + + sqlserverQuery := gorm.NewTestQueryWithSchema(sqlserverDocker, schema) + testSchema := GetTestSchema(sqlserverQuery, map[database.Driver]*gorm.TestQuery{ + database.DriverSqlserver: sqlserverQuery, + }) + + assert.NoError(t, testSchema.Create(fmt.Sprintf("%s.%s", schema, table), func(table contractsschema.Blueprint) { + table.String("name") + })) + tables, err := testSchema.GetTables() + + assert.NoError(t, err) + assert.Len(t, tables, 1) + assert.Equal(t, "table", tables[0].Name) + assert.Equal(t, schema, tables[0].Schema) + assert.True(t, testSchema.HasTable(fmt.Sprintf("%s.%s", schema, table))) } From 7202c4159f70be0b761e83cdbe4e4224ea55122c Mon Sep 17 00:00:00 2001 From: Bowen Date: Tue, 24 Dec 2024 18:56:04 +0800 Subject: [PATCH 2/2] fix typo --- database/gorm/test_utils.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/database/gorm/test_utils.go b/database/gorm/test_utils.go index 2f4c7ce55..50574b336 100644 --- a/database/gorm/test_utils.go +++ b/database/gorm/test_utils.go @@ -229,7 +229,7 @@ func NewTestQueryWithSchema(docker testing.DatabaseDriver, schema string) *TestQ query: query, } - if _, err := query.Exec(fmt.Sprintf("CREATE SCHEMA %s", schema)); err != nil { + if _, err := query.Exec(fmt.Sprintf(`CREATE SCHEMA "%s"`, schema)); err != nil { panic(fmt.Sprintf("create schema %s failed: %v", schema, err)) } @@ -550,7 +550,7 @@ func NewMockSqlserver(mockConfig *mocksconfig.Config, connection, database, user func (r *MockSqlserver) Common() { r.mockConfig.EXPECT().GetString(fmt.Sprintf("database.connections.%s.prefix", r.connection)).Return("") r.mockConfig.EXPECT().GetBool(fmt.Sprintf("database.connections.%s.singular", r.connection)).Return(false) - r.mockConfig.EXPECT().GetString(fmt.Sprintf("database.connections.%s.schema", r.connection), "public").Return("public") + r.mockConfig.EXPECT().GetString(fmt.Sprintf("database.connections.%s.schema", r.connection), "dbo").Return("dbo") r.single() r.basic() @@ -566,7 +566,7 @@ func (r *MockSqlserver) ReadWrite(readDatabaseConfig testing.DatabaseConfig) { r.mockConfig.EXPECT().GetString(fmt.Sprintf("database.connections.%s.dsn", r.connection)).Return("") r.mockConfig.EXPECT().GetString(fmt.Sprintf("database.connections.%s.prefix", r.connection)).Return("") r.mockConfig.EXPECT().GetBool(fmt.Sprintf("database.connections.%s.singular", r.connection)).Return(false) - r.mockConfig.EXPECT().GetString(fmt.Sprintf("database.connections.%s.schema", r.connection), "public").Return("public") + r.mockConfig.EXPECT().GetString(fmt.Sprintf("database.connections.%s.schema", r.connection), "dbo").Return("dbo") r.basic() } @@ -574,7 +574,7 @@ func (r *MockSqlserver) ReadWrite(readDatabaseConfig testing.DatabaseConfig) { func (r *MockSqlserver) WithPrefixAndSingular() { r.mockConfig.EXPECT().GetString(fmt.Sprintf("database.connections.%s.prefix", r.connection)).Return("goravel_") r.mockConfig.EXPECT().GetBool(fmt.Sprintf("database.connections.%s.singular", r.connection)).Return(true) - r.mockConfig.EXPECT().GetString(fmt.Sprintf("database.connections.%s.schema", r.connection), "public").Return("public") + r.mockConfig.EXPECT().GetString(fmt.Sprintf("database.connections.%s.schema", r.connection), "dbo").Return("dbo") r.single() r.basic()