github.com/NpoolPlatform/chain-middleware@v0.0.0-20240228100535-eb1bcf896eb9/pkg/migrator/migrator.go (about)

     1  //nolint
     2  package migrator
     3  
     4  import (
     5  	"context"
     6  	"database/sql"
     7  	"fmt"
     8  	"strings"
     9  	"time"
    10  
    11  	servicename "github.com/NpoolPlatform/chain-middleware/pkg/servicename"
    12  	"github.com/NpoolPlatform/go-service-framework/pkg/config"
    13  	"github.com/NpoolPlatform/go-service-framework/pkg/logger"
    14  	constant "github.com/NpoolPlatform/go-service-framework/pkg/mysql/const"
    15  	redis2 "github.com/NpoolPlatform/go-service-framework/pkg/redis"
    16  	basetypes "github.com/NpoolPlatform/message/npool/basetypes/v1"
    17  
    18  	"github.com/google/uuid"
    19  )
    20  
    21  const (
    22  	keyUsername  = "username"
    23  	keyPassword  = "password"
    24  	keyDBName    = "database_name"
    25  	maxOpen      = 5
    26  	maxIdle      = 2
    27  	MaxLife      = 0
    28  	keyServiceID = "serviceid"
    29  )
    30  
    31  func lockKey() string {
    32  	serviceID := config.GetStringValueWithNameSpace(servicename.ServiceDomain, keyServiceID)
    33  	return fmt.Sprintf("%v:%v", basetypes.Prefix_PrefixMigrate, serviceID)
    34  }
    35  
    36  func dsn(hostname string) (string, error) {
    37  	username := config.GetStringValueWithNameSpace(constant.MysqlServiceName, keyUsername)
    38  	password := config.GetStringValueWithNameSpace(constant.MysqlServiceName, keyPassword)
    39  	dbname := config.GetStringValueWithNameSpace(hostname, keyDBName)
    40  
    41  	svc, err := config.PeekService(constant.MysqlServiceName)
    42  	if err != nil {
    43  		logger.Sugar().Warnw("dsn", "error", err)
    44  		return "", err
    45  	}
    46  
    47  	return fmt.Sprintf("%v:%v@tcp(%v:%v)/%v?parseTime=true&interpolateParams=true",
    48  		username, password,
    49  		svc.Address,
    50  		svc.Port,
    51  		dbname,
    52  	), nil
    53  }
    54  
    55  func open(hostname string) (conn *sql.DB, err error) {
    56  	hdsn, err := dsn(hostname)
    57  	if err != nil {
    58  		return nil, err
    59  	}
    60  
    61  	logger.Sugar().Infow("open", "hdsn", hdsn)
    62  
    63  	conn, err = sql.Open("mysql", hdsn)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  
    68  	// https://github.com/go-sql-driver/mysql
    69  	// See "Important settings" section.
    70  
    71  	conn.SetConnMaxLifetime(time.Minute * MaxLife)
    72  	conn.SetMaxOpenConns(maxOpen)
    73  	conn.SetMaxIdleConns(maxIdle)
    74  
    75  	return conn, nil
    76  }
    77  
    78  func tables(ctx context.Context, dbName string, tx *sql.DB) ([]string, error) {
    79  	tables := []string{}
    80  	rows, err := tx.QueryContext(
    81  		ctx,
    82  		fmt.Sprintf("select table_name from information_schema.tables where table_schema = '%v'", dbName),
    83  	)
    84  	if err != nil {
    85  		return nil, err
    86  	}
    87  	for rows.Next() {
    88  		table := []byte{}
    89  		if err := rows.Scan(&table); err != nil {
    90  			return nil, err
    91  		}
    92  		tables = append(tables, string(table))
    93  	}
    94  	logger.Sugar().Infow(
    95  		"tables",
    96  		"Tables", tables,
    97  	)
    98  	return tables, nil
    99  }
   100  
   101  func existIDInt(ctx context.Context, dbName, table string, tx *sql.DB) (bool, bool, bool, error) {
   102  	_type := []byte{}
   103  	rc := 0
   104  
   105  	rows, err := tx.QueryContext(
   106  		ctx,
   107  		fmt.Sprintf("select column_type,1 from information_schema.columns where table_name='%v' and column_name='id' and table_schema='%v'", table, dbName),
   108  	)
   109  	if err != nil {
   110  		return false, false, false, err
   111  	}
   112  	for rows.Next() {
   113  		if err := rows.Scan(&_type, &rc); err != nil {
   114  			return false, false, false, err
   115  		}
   116  	}
   117  	return rc == 1, strings.Contains(string(_type), "int"), strings.Contains(string(_type), "unsigned"), nil
   118  }
   119  
   120  func setIDUnsigned(ctx context.Context, dbName, table string, tx *sql.DB) error {
   121  	logger.Sugar().Infow(
   122  		"setIDUnsigned",
   123  		"db", dbName,
   124  		"table", table,
   125  		"State", "INT UNSIGNED",
   126  	)
   127  	_, err := tx.ExecContext(
   128  		ctx,
   129  		fmt.Sprintf("alter table %v.%v change id id int unsigned not null auto_increment", dbName, table),
   130  	)
   131  	if err != nil {
   132  		return err
   133  	}
   134  	return nil
   135  }
   136  
   137  func existEntIDUnique(ctx context.Context, dbName, table string, tx *sql.DB) (bool, bool, error) {
   138  	key := ""
   139  	rc := 0
   140  	rows, err := tx.QueryContext(
   141  		ctx,
   142  		fmt.Sprintf("select column_key,1 from information_schema.columns where table_schema='%v' and table_name='%v' and column_name='ent_id'", dbName, table),
   143  	)
   144  	if err != nil {
   145  		return false, false, err
   146  	}
   147  	for rows.Next() {
   148  		if err := rows.Scan(&key, &rc); err != nil {
   149  			return false, false, err
   150  		}
   151  	}
   152  	return rc == 1, key == "UNI", nil
   153  }
   154  
   155  func setEmptyEntID(ctx context.Context, dbName, table string, tx *sql.DB) error {
   156  	logger.Sugar().Infow(
   157  		"setEmptyEntID",
   158  		"db", dbName,
   159  		"table", table,
   160  		"State", "ENT_ID UUID",
   161  	)
   162  	rows, err := tx.QueryContext(
   163  		ctx,
   164  		fmt.Sprintf("select id, ent_id from %v.%v", dbName, table),
   165  	)
   166  	if err != nil {
   167  		return err
   168  	}
   169  	for rows.Next() {
   170  		var id uint32
   171  		var entID string
   172  		if err := rows.Scan(&id, &entID); err != nil {
   173  			return err
   174  		}
   175  		if _, err := uuid.Parse(entID); err == nil {
   176  			continue
   177  		}
   178  		if _, err := tx.ExecContext(
   179  			ctx,
   180  			fmt.Sprintf("update %v.%v set ent_id='%v' where id=%v", dbName, table, uuid.New(), id),
   181  		); err != nil {
   182  			return err
   183  		}
   184  	}
   185  	return nil
   186  }
   187  
   188  func setEntIDUnique(ctx context.Context, dbName, table string, tx *sql.DB) error {
   189  	logger.Sugar().Infow(
   190  		"setEntIDUnique",
   191  		"db", dbName,
   192  		"table", table,
   193  		"State", "ENT_ID UNIQUE",
   194  	)
   195  	_, err := tx.ExecContext(
   196  		ctx,
   197  		fmt.Sprintf("alter table %v.%v change column ent_id ent_id char(36) unique", dbName, table),
   198  	)
   199  	if err != nil {
   200  		return err
   201  	}
   202  	return nil
   203  }
   204  
   205  func id2EntID(ctx context.Context, dbName, table string, tx *sql.DB) error {
   206  	logger.Sugar().Infow(
   207  		"id2EntID",
   208  		"db", dbName,
   209  		"table", table,
   210  		"State", "ID -> EntID",
   211  	)
   212  	_, err := tx.ExecContext(
   213  		ctx,
   214  		fmt.Sprintf("alter table %v.%v change column id ent_id char(36) unique", dbName, table),
   215  	)
   216  	if err != nil {
   217  		return err
   218  	}
   219  	return nil
   220  }
   221  
   222  func addIDColumn(ctx context.Context, dbName, table string, tx *sql.DB) error {
   223  	logger.Sugar().Infow(
   224  		"addIDColumn",
   225  		"db", dbName,
   226  		"table", table,
   227  		"State", "ID INT",
   228  	)
   229  	_, err := tx.ExecContext(
   230  		ctx,
   231  		fmt.Sprintf("alter table %v.%v add id int unsigned not null auto_increment, add primary key(id)", dbName, table),
   232  	)
   233  	if err != nil {
   234  		return err
   235  	}
   236  	return nil
   237  }
   238  
   239  func dropPrimaryKey(ctx context.Context, dbName, table string, tx *sql.DB) error {
   240  	logger.Sugar().Infow(
   241  		"dropPrimaryKey",
   242  		"db", dbName,
   243  		"table", table,
   244  		"State", "DROP PRIMARY",
   245  	)
   246  	_, err := tx.ExecContext(
   247  		ctx,
   248  		fmt.Sprintf("alter table %v.%v drop primary key", dbName, table),
   249  	)
   250  	if err != nil {
   251  		logger.Sugar().Warnw(
   252  			"dropPrimaryKey",
   253  			"db", dbName,
   254  			"table", table,
   255  			"Error", err,
   256  		)
   257  	}
   258  	return nil
   259  }
   260  
   261  func addEntIDColumn(ctx context.Context, dbName, table string, tx *sql.DB) error {
   262  	logger.Sugar().Infow(
   263  		"addEntIDColumn",
   264  		"db", dbName,
   265  		"table", table,
   266  		"State", "ADD ENT_ID",
   267  	)
   268  	_, err := tx.ExecContext(
   269  		ctx,
   270  		fmt.Sprintf("alter table %v.%v add ent_id char(36) not null", dbName, table),
   271  	)
   272  	if err != nil {
   273  		return err
   274  	}
   275  	return nil
   276  }
   277  
   278  func migrateEntID(ctx context.Context, dbName, table string, tx *sql.DB) error {
   279  	logger.Sugar().Infow(
   280  		"migrateEntID",
   281  		"db", dbName,
   282  		"table", table,
   283  	)
   284  
   285  	idExist, idInt, idUnsigned, err := existIDInt(ctx, dbName, table, tx)
   286  	if err != nil {
   287  		return err
   288  	}
   289  
   290  	if idInt && !idUnsigned {
   291  		if err := setIDUnsigned(ctx, dbName, table, tx); err != nil {
   292  			return err
   293  		}
   294  	}
   295  
   296  	entIDExist, entIDUnique, err := existEntIDUnique(ctx, dbName, table, tx)
   297  	if err != nil {
   298  		return err
   299  	}
   300  
   301  	if entIDExist && idInt {
   302  		if err := setEmptyEntID(ctx, dbName, table, tx); err != nil {
   303  			return err
   304  		}
   305  		if !entIDUnique {
   306  			if err := setEntIDUnique(ctx, dbName, table, tx); err != nil {
   307  				return err
   308  			}
   309  		}
   310  		return nil
   311  	}
   312  	if idExist && !idInt {
   313  		if err := id2EntID(ctx, dbName, table, tx); err != nil {
   314  			return err
   315  		}
   316  	}
   317  	if !idInt {
   318  		if err := dropPrimaryKey(ctx, dbName, table, tx); err != nil {
   319  			return err
   320  		}
   321  		if err := addIDColumn(ctx, dbName, table, tx); err != nil {
   322  			return err
   323  		}
   324  	}
   325  
   326  	entIDExist, _, err = existEntIDUnique(ctx, dbName, table, tx)
   327  	if err != nil {
   328  		return err
   329  	}
   330  	if !entIDExist {
   331  		if err := addEntIDColumn(ctx, dbName, table, tx); err != nil {
   332  			return err
   333  		}
   334  	}
   335  	if err := setEmptyEntID(ctx, dbName, table, tx); err != nil {
   336  		return err
   337  	}
   338  	if err := setEntIDUnique(ctx, dbName, table, tx); err != nil {
   339  		return err
   340  	}
   341  	logger.Sugar().Infow(
   342  		"migrateEntID",
   343  		"db", dbName,
   344  		"table", table,
   345  		"State", "Migrated",
   346  	)
   347  	return err
   348  }
   349  
   350  func Migrate(ctx context.Context) error {
   351  	var err error
   352  	var conn *sql.DB
   353  
   354  	logger.Sugar().Infow("Migrate", "Start", "...")
   355  	err = redis2.TryLock(lockKey(), 0)
   356  	if err != nil {
   357  		return err
   358  	}
   359  	defer func(err *error) {
   360  		_ = redis2.Unlock(lockKey())
   361  		logger.Sugar().Infow("Migrate", "Done", "...", "error", *err)
   362  	}(&err)
   363  
   364  	conn, err = open(servicename.ServiceDomain)
   365  	if err != nil {
   366  		return err
   367  	}
   368  	defer func() {
   369  		if err := conn.Close(); err != nil {
   370  			logger.Sugar().Errorw("Close", "Error", err)
   371  		}
   372  	}()
   373  
   374  	dbname := config.GetStringValueWithNameSpace(servicename.ServiceDomain, keyDBName)
   375  	_tables, err := tables(ctx, dbname, conn)
   376  	if err != nil {
   377  		return err
   378  	}
   379  
   380  	logger.Sugar().Infow(
   381  		"Migrate",
   382  		"Round", 1,
   383  	)
   384  	for _, table := range _tables {
   385  		if err = migrateEntID(ctx, dbname, table, conn); err != nil {
   386  			return err
   387  		}
   388  	}
   389  
   390  	logger.Sugar().Infow(
   391  		"Migrate",
   392  		"Round", 2,
   393  	)
   394  	for _, table := range _tables {
   395  		if err = migrateEntID(ctx, dbname, table, conn); err != nil {
   396  			return err
   397  		}
   398  	}
   399  	return nil
   400  }