875 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			875 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			Go
		
	
	
	
| /*
 | |
|  * Copyright 2025 coze-dev Authors
 | |
|  *
 | |
|  * Licensed under the Apache License, Version 2.0 (the "License");
 | |
|  * you may not use this file except in compliance with the License.
 | |
|  * You may obtain a copy of the License at
 | |
|  *
 | |
|  *     http://www.apache.org/licenses/LICENSE-2.0
 | |
|  *
 | |
|  * Unless required by applicable law or agreed to in writing, software
 | |
|  * distributed under the License is distributed on an "AS IS" BASIS,
 | |
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
|  * See the License for the specific language governing permissions and
 | |
|  * limitations under the License.
 | |
|  */
 | |
| 
 | |
| package service
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"os"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/stretchr/testify/assert"
 | |
| 	"go.uber.org/mock/gomock"
 | |
| 	"gorm.io/driver/mysql"
 | |
| 	"gorm.io/gorm"
 | |
| 
 | |
| 	"github.com/coze-dev/coze-studio/backend/api/model/crossdomain/database"
 | |
| 	"github.com/coze-dev/coze-studio/backend/api/model/table"
 | |
| 	entity2 "github.com/coze-dev/coze-studio/backend/domain/memory/database/entity"
 | |
| 	"github.com/coze-dev/coze-studio/backend/domain/memory/database/internal/dal"
 | |
| 	"github.com/coze-dev/coze-studio/backend/domain/memory/database/repository"
 | |
| 	"github.com/coze-dev/coze-studio/backend/infra/contract/rdb"
 | |
| 	rdb2 "github.com/coze-dev/coze-studio/backend/infra/impl/rdb"
 | |
| 	mock "github.com/coze-dev/coze-studio/backend/internal/mock/infra/contract/idgen"
 | |
| 	storageMock "github.com/coze-dev/coze-studio/backend/internal/mock/infra/contract/storage"
 | |
| 	"github.com/coze-dev/coze-studio/backend/pkg/lang/ptr"
 | |
| 	"github.com/coze-dev/coze-studio/backend/pkg/lang/slices"
 | |
| )
 | |
| 
 | |
| func setupTestEnv(t *testing.T) (*gorm.DB, rdb.RDB, *mock.MockIDGenerator, repository.DraftDAO, repository.OnlineDAO, Database) {
 | |
| 	dsn := "root:root@tcp(127.0.0.1:3306)/opencoze?charset=utf8mb4&parseTime=True&loc=Local"
 | |
| 	if os.Getenv("CI_JOB_NAME") != "" {
 | |
| 		dsn = strings.ReplaceAll(dsn, "127.0.0.1", "mysql")
 | |
| 	}
 | |
| 	gormDB, err := gorm.Open(mysql.Open(dsn))
 | |
| 	assert.NoError(t, err)
 | |
| 
 | |
| 	ctrl := gomock.NewController(t)
 | |
| 	idGen := mock.NewMockIDGenerator(ctrl)
 | |
| 
 | |
| 	baseID := time.Now().UnixNano()
 | |
| 	idGen.EXPECT().GenID(gomock.Any()).DoAndReturn(func(ctx context.Context) (int64, error) {
 | |
| 		id := baseID
 | |
| 		baseID++
 | |
| 		return id, nil
 | |
| 	}).AnyTimes()
 | |
| 
 | |
| 	idGen.EXPECT().GenMultiIDs(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, count int) ([]int64, error) {
 | |
| 		ids := make([]int64, count)
 | |
| 		for i := 0; i < count; i++ {
 | |
| 			ids[i] = baseID
 | |
| 			baseID++
 | |
| 		}
 | |
| 		return ids, nil
 | |
| 	}).AnyTimes()
 | |
| 
 | |
| 	rdbService := rdb2.NewService(gormDB, idGen)
 | |
| 	draftDAO := dal.NewDraftDatabaseDAO(gormDB, idGen)
 | |
| 	onlineDAO := dal.NewOnlineDatabaseDAO(gormDB, idGen)
 | |
| 
 | |
| 	mockStorage := storageMock.NewMockStorage(ctrl)
 | |
| 	mockStorage.EXPECT().GetObjectUrl(gomock.Any(), gomock.Any()).Return("URL_ADDRESS", nil).AnyTimes()
 | |
| 	mockStorage.EXPECT().GetObject(gomock.Any(), gomock.Any()).Return([]byte("test text"), nil).AnyTimes()
 | |
| 	mockStorage.EXPECT().PutObject(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
 | |
| 
 | |
| 	dbService := NewService(rdbService, gormDB, idGen, mockStorage, nil)
 | |
| 
 | |
| 	return gormDB, rdbService, idGen, draftDAO, onlineDAO, dbService
 | |
| }
 | |
| 
 | |
| func cleanupTestEnv(t *testing.T, db *gorm.DB, additionalTables ...string) {
 | |
| 	sqlDB, err := db.DB()
 | |
| 	assert.NoError(t, err)
 | |
| 
 | |
| 	daosToClean := []string{"online_database_info", "draft_database_info"}
 | |
| 	for _, tableName := range daosToClean {
 | |
| 		_, err := sqlDB.Exec(fmt.Sprintf("DELETE FROM `%s` WHERE 1=1", tableName))
 | |
| 		if err != nil {
 | |
| 			t.Logf("Failed to clean table %s: %v", tableName, err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	rows, err := sqlDB.Query("SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name LIKE 'table_%'")
 | |
| 	assert.NoError(t, err, "Failed to query tables")
 | |
| 	defer rows.Close()
 | |
| 
 | |
| 	var tablesToDrop []string
 | |
| 	for rows.Next() {
 | |
| 		var tableName string
 | |
| 		if err := rows.Scan(&tableName); err != nil {
 | |
| 			t.Logf("Error scanning table name: %v", err)
 | |
| 			continue
 | |
| 		}
 | |
| 		tablesToDrop = append(tablesToDrop, tableName)
 | |
| 	}
 | |
| 
 | |
| 	tablesToDrop = append(tablesToDrop, additionalTables...)
 | |
| 
 | |
| 	for _, tableName := range tablesToDrop {
 | |
| 		_, err := sqlDB.Exec(fmt.Sprintf("DROP TABLE IF EXISTS `%s`", tableName))
 | |
| 		if err != nil {
 | |
| 			t.Logf("Failed to drop table %s: %v", tableName, err)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestCreateDatabase(t *testing.T) {
 | |
| 	gormDB, _, _, _, onlineDAO, dbService := setupTestEnv(t)
 | |
| 	defer cleanupTestEnv(t, gormDB)
 | |
| 
 | |
| 	req := &CreateDatabaseRequest{
 | |
| 		Database: &database.Database{
 | |
| 			SpaceID:   1,
 | |
| 			CreatorID: 1001,
 | |
| 
 | |
| 			TableName: "test_db_create",
 | |
| 			FieldList: []*database.FieldItem{
 | |
| 				{
 | |
| 					Name:         "id_custom",
 | |
| 					Type:         table.FieldItemType_Number,
 | |
| 					MustRequired: true,
 | |
| 				},
 | |
| 				{
 | |
| 					Name:         "name",
 | |
| 					Type:         table.FieldItemType_Text,
 | |
| 					MustRequired: true,
 | |
| 				},
 | |
| 				{
 | |
| 					Name: "score",
 | |
| 					Type: table.FieldItemType_Float,
 | |
| 				},
 | |
| 				{
 | |
| 					Name: "date",
 | |
| 					Type: table.FieldItemType_Date,
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	resp, err := dbService.CreateDatabase(context.Background(), req)
 | |
| 	assert.NoError(t, err)
 | |
| 	assert.NotNil(t, resp)
 | |
| 	assert.NotNil(t, resp.Database)
 | |
| 
 | |
| 	assert.Equal(t, req.Database.TableName, resp.Database.TableName)
 | |
| 	assert.Equal(t, req.Database.TableType, resp.Database.TableType)
 | |
| 	assert.NotEmpty(t, resp.Database.ActualTableName)
 | |
| 	assert.Len(t, resp.Database.FieldList, 4)
 | |
| 
 | |
| 	savedDB, err := onlineDAO.Get(context.Background(), resp.Database.ID)
 | |
| 	assert.NoError(t, err)
 | |
| 	assert.Equal(t, resp.Database.ID, savedDB.ID)
 | |
| 	assert.Equal(t, resp.Database.TableName, savedDB.TableName)
 | |
| }
 | |
| 
 | |
| func TestUpdateDatabase(t *testing.T) {
 | |
| 	gormDB, _, _, _, onlineDAO, dbService := setupTestEnv(t)
 | |
| 	defer cleanupTestEnv(t, gormDB)
 | |
| 
 | |
| 	resp, err := createDatabase(dbService)
 | |
| 	assert.NoError(t, err)
 | |
| 	assert.NotNil(t, resp)
 | |
| 
 | |
| 	databaseInfo := &entity2.Database{
 | |
| 		ID: resp.Database.ID,
 | |
| 
 | |
| 		SpaceID:   1,
 | |
| 		CreatorID: 1001,
 | |
| 		TableName: "test_db_update",
 | |
| 		TableType: ptr.Of(table.TableType_OnlineTable),
 | |
| 		FieldList: []*database.FieldItem{
 | |
| 			{
 | |
| 				Name:         "age",
 | |
| 				Type:         table.FieldItemType_Float,
 | |
| 				MustRequired: true,
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	updateReq := &UpdateDatabaseRequest{
 | |
| 		Database: databaseInfo,
 | |
| 	}
 | |
| 
 | |
| 	res, err := dbService.UpdateDatabase(context.Background(), updateReq)
 | |
| 	assert.NoError(t, err)
 | |
| 	assert.NotNil(t, res)
 | |
| 
 | |
| 	updatedDB, err := onlineDAO.Get(context.Background(), databaseInfo.ID)
 | |
| 	assert.NoError(t, err)
 | |
| 	assert.Len(t, updatedDB.FieldList, 1)
 | |
| }
 | |
| 
 | |
| func TestDeleteDatabase(t *testing.T) {
 | |
| 	gormDB, _, _, draftDAO, onlineDAO, dbService := setupTestEnv(t)
 | |
| 	defer cleanupTestEnv(t, gormDB)
 | |
| 
 | |
| 	resp, err := createDatabase(dbService)
 | |
| 	assert.NoError(t, err)
 | |
| 	assert.NotNil(t, resp)
 | |
| 
 | |
| 	deleteReq := &DeleteDatabaseRequest{
 | |
| 		ID: resp.Database.ID,
 | |
| 	}
 | |
| 
 | |
| 	err = dbService.DeleteDatabase(context.Background(), deleteReq)
 | |
| 
 | |
| 	assert.NoError(t, err)
 | |
| 
 | |
| 	_, err = draftDAO.Get(context.Background(), resp.Database.ID)
 | |
| 	assert.Error(t, err)
 | |
| 	assert.Contains(t, err.Error(), "not found")
 | |
| 
 | |
| 	_, err = onlineDAO.Get(context.Background(), resp.Database.ID)
 | |
| 	assert.Error(t, err)
 | |
| 	assert.Contains(t, err.Error(), "not found")
 | |
| }
 | |
| 
 | |
| func TestListDatabase(t *testing.T) {
 | |
| 	gormDB, _, _, _, _, dbService := setupTestEnv(t)
 | |
| 	defer cleanupTestEnv(t, gormDB)
 | |
| 
 | |
| 	for i := 0; i < 3; i++ {
 | |
| 		resp, err := createDatabase(dbService)
 | |
| 		assert.NoError(t, err)
 | |
| 		assert.NotNil(t, resp)
 | |
| 	}
 | |
| 
 | |
| 	spaceID := int64(1)
 | |
| 	tableType := table.TableType_OnlineTable
 | |
| 	listReq := &ListDatabaseRequest{
 | |
| 		SpaceID:   &spaceID,
 | |
| 		TableType: tableType,
 | |
| 		Limit:     2,
 | |
| 	}
 | |
| 	resp, err := dbService.ListDatabase(context.Background(), listReq)
 | |
| 	assert.NoError(t, err)
 | |
| 	assert.NotNil(t, resp)
 | |
| 	assert.GreaterOrEqual(t, len(resp.Databases), 2)
 | |
| 
 | |
| 	listReq = &ListDatabaseRequest{
 | |
| 		SpaceID:   &spaceID,
 | |
| 		TableType: tableType,
 | |
| 		Limit:     2,
 | |
| 		Offset:    2,
 | |
| 	}
 | |
| 	resp, err = dbService.ListDatabase(context.Background(), listReq)
 | |
| 	assert.NoError(t, err)
 | |
| 	assert.NotNil(t, resp)
 | |
| 	assert.GreaterOrEqual(t, len(resp.Databases), 1)
 | |
| }
 | |
| 
 | |
| func createDatabase(dbService Database) (*CreateDatabaseResponse, error) {
 | |
| 	req := &CreateDatabaseRequest{
 | |
| 		Database: &entity2.Database{
 | |
| 			SpaceID:   1,
 | |
| 			CreatorID: 1001,
 | |
| 
 | |
| 			TableName: "test_db_table_01",
 | |
| 			FieldList: []*database.FieldItem{
 | |
| 				{
 | |
| 					Name:         "id_custom",
 | |
| 					Type:         table.FieldItemType_Number,
 | |
| 					MustRequired: true,
 | |
| 				},
 | |
| 				{
 | |
| 					Name:         "name",
 | |
| 					Type:         table.FieldItemType_Text,
 | |
| 					MustRequired: true,
 | |
| 				},
 | |
| 				{
 | |
| 					Name: "score",
 | |
| 					Type: table.FieldItemType_Float,
 | |
| 				},
 | |
| 				{
 | |
| 					Name: "date",
 | |
| 					Type: table.FieldItemType_Date,
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	return dbService.CreateDatabase(context.Background(), req)
 | |
| }
 | |
| 
 | |
| func TestCRUDDatabaseRecord(t *testing.T) {
 | |
| 	gormDB, _, _, _, _, dbService := setupTestEnv(t)
 | |
| 	defer cleanupTestEnv(t, gormDB)
 | |
| 
 | |
| 	resp, err := createDatabase(dbService)
 | |
| 	assert.NoError(t, err)
 | |
| 	assert.NotNil(t, resp)
 | |
| 
 | |
| 	addRecordReq := &AddDatabaseRecordRequest{
 | |
| 		DatabaseID: resp.Database.ID,
 | |
| 		TableType:  table.TableType_OnlineTable,
 | |
| 		UserID:     1001,
 | |
| 		Records: []map[string]string{
 | |
| 			{
 | |
| 				"id_custom": "1",
 | |
| 				"name":      "John Doe",
 | |
| 				"score":     "80.5",
 | |
| 				"date":      "2025-01-01 00:00:00",
 | |
| 			},
 | |
| 			{
 | |
| 				"id_custom":  "2",
 | |
| 				"name":       "Jane Smith",
 | |
| 				"score":      "90.5",
 | |
| 				"date":       "2025-01-01 01:00:00",
 | |
| 				"bstudio_id": "1",
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	err = dbService.AddDatabaseRecord(context.Background(), addRecordReq)
 | |
| 	assert.NoError(t, err)
 | |
| 
 | |
| 	listRecordReq := &ListDatabaseRecordRequest{
 | |
| 		DatabaseID: resp.Database.ID,
 | |
| 		TableType:  table.TableType_OnlineTable,
 | |
| 		UserID:     1001,
 | |
| 		Limit:      50,
 | |
| 	}
 | |
| 
 | |
| 	listResp, err := dbService.ListDatabaseRecord(context.Background(), listRecordReq)
 | |
| 	assert.NoError(t, err)
 | |
| 	assert.NotNil(t, listResp)
 | |
| 	assert.True(t, len(listResp.Records) == 2)
 | |
| 
 | |
| 	foundJohn := false
 | |
| 	foundSmith := false
 | |
| 	bsID := ""
 | |
| 	for _, record := range listResp.Records {
 | |
| 		if record["name"] == "John Doe" && record["score"] == "80.5" {
 | |
| 			foundJohn = true
 | |
| 			bsID = record[database.DefaultIDColName]
 | |
| 		}
 | |
| 		if record["name"] == "Jane Smith" && record["score"] == "90.5" {
 | |
| 			foundSmith = true
 | |
| 		}
 | |
| 	}
 | |
| 	assert.True(t, foundJohn, "John Doe record not found")
 | |
| 	assert.True(t, foundSmith, "Jane Smith record not found")
 | |
| 
 | |
| 	updateRecordReq := &UpdateDatabaseRecordRequest{
 | |
| 		DatabaseID: resp.Database.ID,
 | |
| 		TableType:  table.TableType_OnlineTable,
 | |
| 		UserID:     1001,
 | |
| 		Records: []map[string]string{
 | |
| 			{
 | |
| 				database.DefaultIDColName: bsID,
 | |
| 				"name":                    "John Updated",
 | |
| 				"score":                   "90",
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	err = dbService.UpdateDatabaseRecord(context.Background(), updateRecordReq)
 | |
| 	assert.NoError(t, err)
 | |
| 
 | |
| 	listReq := &ListDatabaseRecordRequest{
 | |
| 		DatabaseID: resp.Database.ID,
 | |
| 		TableType:  table.TableType_OnlineTable,
 | |
| 		Limit:      50,
 | |
| 		UserID:     1001,
 | |
| 	}
 | |
| 	listRespAfterUpdate, err := dbService.ListDatabaseRecord(context.Background(), listReq)
 | |
| 	assert.NoError(t, err)
 | |
| 	assert.NotNil(t, listRespAfterUpdate)
 | |
| 	assert.True(t, len(listRespAfterUpdate.Records) > 0)
 | |
| 
 | |
| 	foundJohnUpdate := false
 | |
| 	for _, record := range listRespAfterUpdate.Records {
 | |
| 		if record[database.DefaultIDColName] == bsID {
 | |
| 			foundJohnUpdate = true
 | |
| 			assert.Equal(t, "90", record["score"])
 | |
| 		}
 | |
| 	}
 | |
| 	assert.True(t, foundJohnUpdate, "John Doe update record not found")
 | |
| 
 | |
| 	deleteRecordReq := &DeleteDatabaseRecordRequest{
 | |
| 		DatabaseID: resp.Database.ID,
 | |
| 		TableType:  table.TableType_OnlineTable,
 | |
| 		UserID:     1001,
 | |
| 		Records: []map[string]string{
 | |
| 			{
 | |
| 				database.DefaultIDColName: bsID,
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	err = dbService.DeleteDatabaseRecord(context.Background(), deleteRecordReq)
 | |
| 	assert.NoError(t, err)
 | |
| 
 | |
| 	listRecordAfterDeleteReq := &ListDatabaseRecordRequest{
 | |
| 		DatabaseID: resp.Database.ID,
 | |
| 		TableType:  table.TableType_OnlineTable,
 | |
| 		Limit:      50,
 | |
| 		UserID:     1001,
 | |
| 	}
 | |
| 	listRespAfterDelete, err := dbService.ListDatabaseRecord(context.Background(), listRecordAfterDeleteReq)
 | |
| 	assert.NoError(t, err)
 | |
| 	assert.NotNil(t, listRespAfterDelete)
 | |
| 	assert.Equal(t, len(listRespAfterDelete.Records), 1)
 | |
| }
 | |
| 
 | |
| func TestExecuteSQLWithOperations(t *testing.T) {
 | |
| 	gormDB, _, _, _, _, dbService := setupTestEnv(t)
 | |
| 	defer cleanupTestEnv(t, gormDB)
 | |
| 
 | |
| 	resp, err := createDatabase(dbService)
 | |
| 	assert.NoError(t, err)
 | |
| 	assert.NotNil(t, resp)
 | |
| 
 | |
| 	fieldMap := slices.ToMap(resp.Database.FieldList, func(e *database.FieldItem) (string, *database.FieldItem) {
 | |
| 		return e.Name, e
 | |
| 	})
 | |
| 
 | |
| 	upsertRows := []*database.UpsertRow{
 | |
| 		{
 | |
| 			Records: []*database.Record{
 | |
| 				{
 | |
| 					FieldId:    strconv.FormatInt(fieldMap["id_custom"].AlterID, 10),
 | |
| 					FieldValue: "?",
 | |
| 				},
 | |
| 				{
 | |
| 					FieldId:    strconv.FormatInt(fieldMap["name"].AlterID, 10),
 | |
| 					FieldValue: "?",
 | |
| 				},
 | |
| 				{
 | |
| 					FieldId:    strconv.FormatInt(fieldMap["score"].AlterID, 10),
 | |
| 					FieldValue: "?",
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			Records: []*database.Record{
 | |
| 				{
 | |
| 					FieldId:    strconv.FormatInt(fieldMap["id_custom"].AlterID, 10),
 | |
| 					FieldValue: "?",
 | |
| 				},
 | |
| 				{
 | |
| 					FieldId:    strconv.FormatInt(fieldMap["name"].AlterID, 10),
 | |
| 					FieldValue: "?",
 | |
| 				},
 | |
| 				{
 | |
| 					FieldId:    strconv.FormatInt(fieldMap["score"].AlterID, 10),
 | |
| 					FieldValue: "?",
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	executeInsertReq := &ExecuteSQLRequest{
 | |
| 		DatabaseID:  resp.Database.ID,
 | |
| 		TableType:   table.TableType_OnlineTable,
 | |
| 		OperateType: database.OperateType_Insert,
 | |
| 		UpsertRows:  upsertRows,
 | |
| 		SQLParams: []*database.SQLParamVal{
 | |
| 			{
 | |
| 				Value: ptr.Of("111"),
 | |
| 			},
 | |
| 			{
 | |
| 				Value: ptr.Of("Alice"),
 | |
| 			},
 | |
| 			{
 | |
| 				Value: ptr.Of("85.5"),
 | |
| 			},
 | |
| 			{
 | |
| 				Value: ptr.Of("112"),
 | |
| 			},
 | |
| 			{
 | |
| 				Value: ptr.Of("Bob"),
 | |
| 			},
 | |
| 			{
 | |
| 				Value: ptr.Of("90.5"),
 | |
| 			},
 | |
| 		},
 | |
| 		UserID:  "1001",
 | |
| 		SpaceID: 1,
 | |
| 	}
 | |
| 
 | |
| 	insertResp, err := dbService.ExecuteSQL(context.Background(), executeInsertReq)
 | |
| 	assert.NoError(t, err)
 | |
| 	assert.NotNil(t, insertResp)
 | |
| 	assert.NotNil(t, insertResp.RowsAffected)
 | |
| 	assert.Equal(t, 2, len(insertResp.Records))
 | |
| 	assert.Equal(t, 1, len(insertResp.Records[0]))
 | |
| 	assert.Equal(t, int64(2), *insertResp.RowsAffected)
 | |
| 
 | |
| 	limit := int64(10)
 | |
| 	selectFields := &database.SelectFieldList{
 | |
| 		FieldID: []string{strconv.FormatInt(fieldMap["name"].AlterID, 10), strconv.FormatInt(fieldMap["score"].AlterID, 10)},
 | |
| 	}
 | |
| 
 | |
| 	executeSelectReq := &ExecuteSQLRequest{
 | |
| 		DatabaseID:  resp.Database.ID,
 | |
| 		TableType:   table.TableType_OnlineTable,
 | |
| 		OperateType: database.OperateType_Select,
 | |
| 		// SelectFieldList: selectFields,
 | |
| 		Limit:   &limit,
 | |
| 		UserID:  "1001",
 | |
| 		SpaceID: 1,
 | |
| 		OrderByList: []database.OrderBy{
 | |
| 			{
 | |
| 				Field:     "id_custom",
 | |
| 				Direction: table.SortDirection_Desc,
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	selectResp, err := dbService.ExecuteSQL(context.Background(), executeSelectReq)
 | |
| 	assert.NoError(t, err)
 | |
| 	assert.NotNil(t, selectResp)
 | |
| 	assert.NotNil(t, selectResp.Records)
 | |
| 	assert.True(t, len(selectResp.Records) == 2)
 | |
| 	assert.Equal(t, string(selectResp.Records[0]["name"].([]uint8)), "Bob")
 | |
| 	assert.NotNil(t, selectResp.Records[0][database.DefaultUidDisplayColName])
 | |
| 	assert.NotNil(t, selectResp.Records[0][database.DefaultIDDisplayColName])
 | |
| 	assert.NotNil(t, selectResp.Records[0][database.DefaultCreateTimeDisplayColName])
 | |
| 
 | |
| 	executeNotNullSelectReq := &ExecuteSQLRequest{
 | |
| 		DatabaseID:      resp.Database.ID,
 | |
| 		TableType:       table.TableType_OnlineTable,
 | |
| 		OperateType:     database.OperateType_Select,
 | |
| 		SelectFieldList: selectFields,
 | |
| 		Limit:           &limit,
 | |
| 		UserID:          "1001",
 | |
| 		SpaceID:         1,
 | |
| 		OrderByList: []database.OrderBy{
 | |
| 			{
 | |
| 				Field:     "id_custom",
 | |
| 				Direction: table.SortDirection_Desc,
 | |
| 			},
 | |
| 		},
 | |
| 		Condition: &database.ComplexCondition{
 | |
| 			Conditions: []*database.Condition{
 | |
| 				{
 | |
| 					Left:      "name",
 | |
| 					Operation: database.Operation_IS_NOT_NULL,
 | |
| 				},
 | |
| 			},
 | |
| 			Logic: database.Logic_And,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	selectNotNullResp, err := dbService.ExecuteSQL(context.Background(), executeNotNullSelectReq)
 | |
| 	assert.NoError(t, err)
 | |
| 	assert.NotNil(t, selectNotNullResp)
 | |
| 	assert.NotNil(t, selectNotNullResp.Records)
 | |
| 	assert.True(t, len(selectNotNullResp.Records) == 2)
 | |
| 	assert.Equal(t, string(selectNotNullResp.Records[0]["name"].([]uint8)), "Bob")
 | |
| 
 | |
| 	executeNullSelectReq := &ExecuteSQLRequest{
 | |
| 		DatabaseID:      resp.Database.ID,
 | |
| 		TableType:       table.TableType_OnlineTable,
 | |
| 		OperateType:     database.OperateType_Select,
 | |
| 		SelectFieldList: selectFields,
 | |
| 		Limit:           &limit,
 | |
| 		UserID:          "1001",
 | |
| 		SpaceID:         1,
 | |
| 		OrderByList: []database.OrderBy{
 | |
| 			{
 | |
| 				Field:     "id_custom",
 | |
| 				Direction: table.SortDirection_Desc,
 | |
| 			},
 | |
| 		},
 | |
| 		Condition: &database.ComplexCondition{
 | |
| 			Conditions: []*database.Condition{
 | |
| 				{
 | |
| 					Left:      "name",
 | |
| 					Operation: database.Operation_IS_NULL,
 | |
| 				},
 | |
| 			},
 | |
| 			Logic: database.Logic_And,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	selectNullResp, err := dbService.ExecuteSQL(context.Background(), executeNullSelectReq)
 | |
| 	assert.NoError(t, err)
 | |
| 	assert.NotNil(t, selectNullResp)
 | |
| 	assert.True(t, len(selectNullResp.Records) == 0)
 | |
| 
 | |
| 	executeINSelectReq := &ExecuteSQLRequest{
 | |
| 		DatabaseID:      resp.Database.ID,
 | |
| 		TableType:       table.TableType_OnlineTable,
 | |
| 		OperateType:     database.OperateType_Select,
 | |
| 		SelectFieldList: selectFields,
 | |
| 		Limit:           &limit,
 | |
| 		UserID:          "1001",
 | |
| 		SpaceID:         1,
 | |
| 		OrderByList: []database.OrderBy{
 | |
| 			{
 | |
| 				Field:     "id_custom",
 | |
| 				Direction: table.SortDirection_Desc,
 | |
| 			},
 | |
| 		},
 | |
| 		SQLParams: []*database.SQLParamVal{
 | |
| 			{
 | |
| 				Value: ptr.Of("Alice"),
 | |
| 			},
 | |
| 			{
 | |
| 				Value: ptr.Of("Bob"),
 | |
| 			},
 | |
| 		},
 | |
| 		Condition: &database.ComplexCondition{
 | |
| 			Conditions: []*database.Condition{
 | |
| 				{
 | |
| 					Left:      "name",
 | |
| 					Operation: database.Operation_IN,
 | |
| 					Right:     "(?,?)",
 | |
| 				},
 | |
| 			},
 | |
| 			Logic: database.Logic_And,
 | |
| 		},
 | |
| 	}
 | |
| 	selectINResp, err := dbService.ExecuteSQL(context.Background(), executeINSelectReq)
 | |
| 	assert.NoError(t, err)
 | |
| 	assert.NotNil(t, selectINResp)
 | |
| 	assert.True(t, len(selectINResp.Records) == 2)
 | |
| 
 | |
| 	updateRows := []*database.UpsertRow{
 | |
| 		{
 | |
| 			Records: []*database.Record{
 | |
| 				{
 | |
| 					FieldId:    strconv.FormatInt(fieldMap["id_custom"].AlterID, 10),
 | |
| 					FieldValue: "?",
 | |
| 				},
 | |
| 				{
 | |
| 					FieldId:    strconv.FormatInt(fieldMap["name"].AlterID, 10),
 | |
| 					FieldValue: "?",
 | |
| 				},
 | |
| 				{
 | |
| 					FieldId:    strconv.FormatInt(fieldMap["score"].AlterID, 10),
 | |
| 					FieldValue: "?",
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	executeUpdateReq := &ExecuteSQLRequest{
 | |
| 		DatabaseID:  resp.Database.ID,
 | |
| 		TableType:   table.TableType_OnlineTable,
 | |
| 		OperateType: database.OperateType_Update,
 | |
| 		UpsertRows:  updateRows,
 | |
| 		Limit:       &limit,
 | |
| 		SQLParams: []*database.SQLParamVal{
 | |
| 			{
 | |
| 				Value: ptr.Of("111"),
 | |
| 			},
 | |
| 			{
 | |
| 				Value: ptr.Of("Alice2"),
 | |
| 			},
 | |
| 			{
 | |
| 				Value: ptr.Of("99"),
 | |
| 			},
 | |
| 			{
 | |
| 				Value: ptr.Of("111"),
 | |
| 			},
 | |
| 		},
 | |
| 		UserID:  "1001",
 | |
| 		SpaceID: 1,
 | |
| 		Condition: &database.ComplexCondition{
 | |
| 			Conditions: []*database.Condition{
 | |
| 				{
 | |
| 					Left:      "id_custom",
 | |
| 					Operation: database.Operation_EQUAL,
 | |
| 					Right:     "?",
 | |
| 				},
 | |
| 			},
 | |
| 			Logic: database.Logic_And,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	updateResp, err := dbService.ExecuteSQL(context.Background(), executeUpdateReq)
 | |
| 	assert.NoError(t, err)
 | |
| 	assert.NotNil(t, updateResp)
 | |
| 	assert.NotNil(t, updateResp.RowsAffected)
 | |
| 
 | |
| 	executeDeleteReq := &ExecuteSQLRequest{
 | |
| 		DatabaseID:  resp.Database.ID,
 | |
| 		TableType:   table.TableType_OnlineTable,
 | |
| 		OperateType: database.OperateType_Delete,
 | |
| 		Limit:       &limit,
 | |
| 		UserID:      "1001",
 | |
| 		SpaceID:     1,
 | |
| 		SQLParams: []*database.SQLParamVal{
 | |
| 			{
 | |
| 				Value: ptr.Of("111"),
 | |
| 			},
 | |
| 		},
 | |
| 		Condition: &database.ComplexCondition{
 | |
| 			Conditions: []*database.Condition{
 | |
| 				{
 | |
| 					Left:      "id_custom",
 | |
| 					Operation: database.Operation_EQUAL,
 | |
| 					Right:     "?",
 | |
| 				},
 | |
| 			},
 | |
| 			Logic: database.Logic_And,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	dResp, err := dbService.ExecuteSQL(context.Background(), executeDeleteReq)
 | |
| 	assert.NoError(t, err)
 | |
| 	assert.NotNil(t, dResp)
 | |
| 	assert.NotNil(t, dResp.RowsAffected)
 | |
| 
 | |
| 	selectCustom := &ExecuteSQLRequest{
 | |
| 		DatabaseID:  resp.Database.ID,
 | |
| 		TableType:   table.TableType_OnlineTable,
 | |
| 		OperateType: database.OperateType_Custom,
 | |
| 		Limit:       &limit,
 | |
| 		UserID:      "1001",
 | |
| 		SpaceID:     1,
 | |
| 		SQL:         ptr.Of(fmt.Sprintf("SELECT * FROM %s WHERE score > ?", "test_db_table_01")),
 | |
| 		SQLParams: []*database.SQLParamVal{
 | |
| 			{
 | |
| 				Value: ptr.Of("85"),
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	selectCustomResp, err := dbService.ExecuteSQL(context.Background(), selectCustom)
 | |
| 	assert.NoError(t, err)
 | |
| 	assert.NotNil(t, selectCustomResp)
 | |
| 	assert.NotNil(t, selectCustomResp.Records)
 | |
| 	assert.True(t, len(selectCustomResp.Records) > 0)
 | |
| 
 | |
| 	// Test custom SQL UPDATE
 | |
| 	updateCustom := &ExecuteSQLRequest{
 | |
| 		DatabaseID:  resp.Database.ID,
 | |
| 		TableType:   table.TableType_OnlineTable,
 | |
| 		OperateType: database.OperateType_Custom,
 | |
| 		UserID:      "1001",
 | |
| 		SpaceID:     1,
 | |
| 		SQL:         ptr.Of(fmt.Sprintf("UPDATE %s SET name = 'Bob Updated' WHERE id_custom = ?", "test_db_table_01")),
 | |
| 		SQLParams: []*database.SQLParamVal{
 | |
| 			{
 | |
| 				Value: ptr.Of("112"),
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	updateCustomResp, err := dbService.ExecuteSQL(context.Background(), updateCustom)
 | |
| 	assert.NoError(t, err)
 | |
| 	assert.NotNil(t, updateCustomResp)
 | |
| 	assert.NotNil(t, updateCustomResp.RowsAffected)
 | |
| 	assert.Equal(t, *updateCustomResp.RowsAffected, int64(1))
 | |
| 
 | |
| 	// Test custom SQL DELETE
 | |
| 	deleteCustom := &ExecuteSQLRequest{
 | |
| 		DatabaseID:  resp.Database.ID,
 | |
| 		TableType:   table.TableType_OnlineTable,
 | |
| 		OperateType: database.OperateType_Custom,
 | |
| 		UserID:      "1001",
 | |
| 		SpaceID:     1,
 | |
| 		SQL:         ptr.Of(fmt.Sprintf("DELETE FROM %s WHERE id_custom = ?", "test_db_table_01")),
 | |
| 		SQLParams: []*database.SQLParamVal{
 | |
| 			{
 | |
| 				Value: ptr.Of("112"),
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	deleteCustomResp, err := dbService.ExecuteSQL(context.Background(), deleteCustom)
 | |
| 	assert.NoError(t, err)
 | |
| 	assert.NotNil(t, deleteCustomResp)
 | |
| 	assert.NotNil(t, deleteCustomResp.RowsAffected)
 | |
| 	assert.Equal(t, *deleteCustomResp.RowsAffected, int64(1))
 | |
| }
 | |
| 
 | |
| func TestDeleteDatabaseByAppID(t *testing.T) {
 | |
| 	gormDB, _, _, draftDAO, onlineDAO, dbService := setupTestEnv(t)
 | |
| 	defer cleanupTestEnv(t, gormDB)
 | |
| 
 | |
| 	appID := int64(123456)
 | |
| 	dbCount := 3
 | |
| 	var onlineIDs []int64
 | |
| 	var draftIDs []int64
 | |
| 	var physicalTables []string
 | |
| 
 | |
| 	for i := 0; i < dbCount; i++ {
 | |
| 		dbName := fmt.Sprintf("test_appid_db_%d_%d", appID, i)
 | |
| 		req := &CreateDatabaseRequest{
 | |
| 			Database: &entity2.Database{
 | |
| 				AppID:     appID,
 | |
| 				SpaceID:   1,
 | |
| 				CreatorID: 1001,
 | |
| 				TableName: dbName,
 | |
| 				FieldList: []*database.FieldItem{
 | |
| 					{
 | |
| 						Name:         "id_custom",
 | |
| 						Type:         table.FieldItemType_Number,
 | |
| 						MustRequired: true,
 | |
| 					},
 | |
| 					{
 | |
| 						Name:         "name",
 | |
| 						Type:         table.FieldItemType_Text,
 | |
| 						MustRequired: true,
 | |
| 					},
 | |
| 					{
 | |
| 						Name: "score",
 | |
| 						Type: table.FieldItemType_Float,
 | |
| 					},
 | |
| 					{
 | |
| 						Name: "date",
 | |
| 						Type: table.FieldItemType_Date,
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 		}
 | |
| 		resp, err := dbService.CreateDatabase(context.Background(), req)
 | |
| 		assert.NoError(t, err)
 | |
| 		assert.NotNil(t, resp)
 | |
| 		assert.NotNil(t, resp.Database)
 | |
| 		assert.Equal(t, appID, resp.Database.AppID)
 | |
| 		assert.NotEmpty(t, resp.Database.ActualTableName)
 | |
| 		physicalTables = append(physicalTables, resp.Database.ActualTableName)
 | |
| 		if resp.Database.ID != 0 {
 | |
| 			onlineIDs = append(onlineIDs, resp.Database.ID)
 | |
| 		}
 | |
| 		if resp.Database.DraftID != nil {
 | |
| 			draftIDs = append(draftIDs, *resp.Database.DraftID)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	for _, id := range onlineIDs {
 | |
| 		_, err := onlineDAO.Get(context.Background(), id)
 | |
| 		assert.NoError(t, err)
 | |
| 	}
 | |
| 	for _, id := range draftIDs {
 | |
| 		_, err := draftDAO.Get(context.Background(), id)
 | |
| 		assert.NoError(t, err)
 | |
| 	}
 | |
| 
 | |
| 	_, err := dbService.DeleteDatabaseByAppID(context.Background(), &DeleteDatabaseByAppIDRequest{AppID: appID})
 | |
| 	assert.NoError(t, err)
 | |
| 
 | |
| 	for _, id := range onlineIDs {
 | |
| 		_, err := onlineDAO.Get(context.Background(), id)
 | |
| 		assert.Error(t, err)
 | |
| 		assert.Contains(t, err.Error(), "not found")
 | |
| 	}
 | |
| 	for _, id := range draftIDs {
 | |
| 		_, err := draftDAO.Get(context.Background(), id)
 | |
| 		assert.Error(t, err)
 | |
| 		assert.Contains(t, err.Error(), "not found")
 | |
| 	}
 | |
| 
 | |
| 	sqlDB, err := gormDB.DB()
 | |
| 	assert.NoError(t, err)
 | |
| 	for _, tableName := range physicalTables {
 | |
| 		var cnt int
 | |
| 		err := sqlDB.QueryRow(fmt.Sprintf("SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = '%s'", tableName)).Scan(&cnt)
 | |
| 		assert.NoError(t, err)
 | |
| 		assert.Equal(t, 0, cnt, "physical table %s should be deleted", tableName)
 | |
| 	}
 | |
| }
 |