diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1777f72b4..1a2e72e2f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,6 +57,50 @@ jobs: run: | go test -tags sqlite -race -cover ./... + tidb-tests: + name: TiDB tests - Go v${{ matrix.go-version }} + runs-on: ubuntu-latest + strategy: + matrix: + go-version: + - "1.25" + + services: + tidb: + image: pingcap/tidb:latest + ports: + - 4000:4000 + + steps: + - uses: actions/checkout@v3 + - name: Setup Go ${{ matrix.go }} + uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.go-version }} + + - name: Setup TiDB environment + run: | + mysqldump -u root --port 4000 --version + echo $HOME + echo -e "[mysqldump]\ncolumn-statistics=0" > $HOME/.my.cnf + + - name: Build and run soda + env: + SODA_DIALECT: "tidb" + TIDB_PORT: 4000 + run: | + go build -v -tags sqlite -o tsoda ./soda + ./tsoda drop -e $SODA_DIALECT -p ./testdata/migrations + ./tsoda create -e $SODA_DIALECT -p ./testdata/migrations + ./tsoda migrate -e $SODA_DIALECT -p ./testdata/migrations + + - name: Test + env: + SODA_DIALECT: "tidb" + TIDB_PORT: 4000 + run: | + go test -tags sqlite -race -cover ./... + pg-tests: name: PostgreSQL tests - Go v${{ matrix.go-version }} runs-on: ubuntu-latest diff --git a/config_test.go b/config_test.go index 58d20a1ae..88c5b294a 100644 --- a/config_test.go +++ b/config_test.go @@ -13,7 +13,7 @@ func Test_LoadsConnectionsFromConfig(t *testing.T) { r.NoError(LoadConfigFile()) if DialectSupported("sqlite3") { - r.Equal(5, len(Connections)) + r.Equal(6, len(Connections)) } else { r.Equal(4, len(Connections)) } diff --git a/database.yml b/database.yml index 794e6540b..86dd05ee0 100644 --- a/database.yml +++ b/database.yml @@ -8,6 +8,16 @@ mysql: options: readTimeout: 5s +tidb: + dialect: "tidb" + database: "pop_test" + host: '{{ envOr "TIDB_HOST" "127.0.0.1" }}' + port: '{{ envOr "TIDB_PORT" "4000" }}' + user: '{{ envOr "TIDB_USER" "root" }}' + password: '{{ envOr "TIDB_PASSWORD" "" }}' + options: + readTimeout: 10s + postgres: url: '{{ envOr "POSTGRESQL_URL" "postgres://postgres:postgres%23@localhost:5433/pop_test?sslmode=disable" }}' pool: 25 diff --git a/dialect_tidb.go b/dialect_tidb.go new file mode 100644 index 000000000..ff18f1caa --- /dev/null +++ b/dialect_tidb.go @@ -0,0 +1,305 @@ +package pop + +import ( + "bytes" + "context" + "fmt" + "io" + "os/exec" + "strings" + + _mysql "github.com/go-sql-driver/mysql" // Load MySQL Go driver + "github.com/gobuffalo/fizz" + "github.com/gobuffalo/fizz/translators" + "github.com/jmoiron/sqlx" + "github.com/ory/pop/v6/columns" + "github.com/ory/pop/v6/internal/defaults" + "github.com/ory/pop/v6/logging" +) + +const nameTiDB = "tidb" +const hostTiDB = "127.0.0.1" +const portTiDB = "4000" + +func init() { + AvailableDialects = append(AvailableDialects, nameTiDB) + urlParser[nameTiDB] = urlParserTiDB + finalizer[nameTiDB] = finalizerTiDB + newConnection[nameTiDB] = newTiDB +} + +var _ dialect = &tidb{} + +type tidb struct { + commonDialect +} + +func (m *tidb) Name() string { + return nameTiDB +} + +func (m *tidb) DefaultDriver() string { + return nameMySQL +} + +func (tidb) Quote(key string) string { + return fmt.Sprintf("`%s`", key) +} + +func (m *tidb) Details() *ConnectionDetails { + return m.ConnectionDetails +} + +func (m *tidb) URL() string { + cd := m.ConnectionDetails + if cd.URL != "" { + url := strings.TrimPrefix(cd.URL, "tidb://") + url = strings.TrimPrefix(url, "mysql://") + return url + } + + user := fmt.Sprintf("%s:%s@", cd.User, cd.Password) + user = strings.Replace(user, ":@", "@", 1) + if user == "@" || strings.HasPrefix(user, ":") { + user = "" + } + + addr := fmt.Sprintf("(%s:%s)", cd.Host, cd.Port) + // in case of unix domain socket, tricky. + // it is better to check Host is not valid inet address or has '/'. + if cd.Port == "socket" { + addr = fmt.Sprintf("unix(%s)", cd.Host) + } + + s := "%s%s/%s?%s" + return fmt.Sprintf(s, user, addr, cd.Database, cd.OptionsString("")) +} + +func (m *tidb) urlWithoutDB() string { + cd := m.ConnectionDetails + return strings.Replace(m.URL(), "/"+cd.Database+"?", "/?", 1) +} + +func (m *tidb) MigrationURL() string { + return m.URL() +} + +func (m *tidb) Create(c *Connection, model *Model, cols columns.Columns) error { + if err := genericCreate(c, model, cols, m); err != nil { + return fmt.Errorf("tidb create: %w", err) + } + return nil +} + +func (m *tidb) Update(c *Connection, model *Model, cols columns.Columns) error { + if err := genericUpdate(c, model, cols, m); err != nil { + return fmt.Errorf("tidb update: %w", err) + } + return nil +} + +func (m *tidb) UpdateQuery(c *Connection, model *Model, cols columns.Columns, query Query) (int64, error) { + if n, err := genericUpdateQuery(c, model, cols, m, query, sqlx.QUESTION); err != nil { + return n, fmt.Errorf("tidb update query: %w", err) + } else { + return n, nil + } +} + +func (m *tidb) Destroy(c *Connection, model *Model) error { + stmt := fmt.Sprintf("DELETE FROM %s WHERE %s = ?", m.Quote(model.TableName()), model.IDField()) + _, err := genericExec(c, stmt, model.ID()) + if err != nil { + return fmt.Errorf("tidb destroy: %w", err) + } + return nil +} + +func (m *tidb) Delete(c *Connection, model *Model, query Query) error { + sqlQuery, args := query.ToSQL(model) + // * MySQL does not support table alias for DELETE syntax until 8.0. + // * Do not generate SQL manually if they may have `WHERE IN`. + // * Spaces are intentionally added to make it easy to see on the log. + sqlQuery = asRegex.ReplaceAllString(sqlQuery, " ") + + _, err := genericExec(c, sqlQuery, args...) + return err +} + +func (m *tidb) SelectOne(c *Connection, model *Model, query Query) error { + if err := genericSelectOne(c, model, query); err != nil { + return fmt.Errorf("tidb select one: %w", err) + } + return nil +} + +func (m *tidb) SelectMany(c *Connection, models *Model, query Query) error { + if err := genericSelectMany(c, models, query); err != nil { + return fmt.Errorf("tidb select many: %w", err) + } + return nil +} + +// CreateDB creates a new database, from the given connection credentials +func (m *tidb) CreateDB() error { + deets := m.ConnectionDetails + db, _, err := openPotentiallyInstrumentedConnection(context.Background(), m, m.urlWithoutDB()) + if err != nil { + return fmt.Errorf("error creating TiDB database %s: %w", deets.Database, err) + } + defer db.Close() + charset := defaults.String(deets.option("charset"), "utf8mb4") + encoding := defaults.String(deets.option("collation"), "utf8mb4_general_ci") + query := fmt.Sprintf("CREATE DATABASE `%s` DEFAULT CHARSET `%s` DEFAULT COLLATE `%s`", deets.Database, charset, encoding) + log(logging.SQL, query) + + _, err = db.Exec(query) + if err != nil { + return fmt.Errorf("error creating TiDB database %s: %w", deets.Database, err) + } + + log(logging.Info, "created database %s", deets.Database) + return nil +} + +// DropDB drops an existing database, from the given connection credentials +func (m *tidb) DropDB() error { + deets := m.ConnectionDetails + db, _, err := openPotentiallyInstrumentedConnection(context.Background(), m, m.urlWithoutDB()) + if err != nil { + return fmt.Errorf("error dropping TiDB database %s: %w", deets.Database, err) + } + defer db.Close() + query := fmt.Sprintf("DROP DATABASE `%s`", deets.Database) + log(logging.SQL, query) + + _, err = db.Exec(query) + if err != nil { + return fmt.Errorf("error dropping TiDB database %s: %w", deets.Database, err) + } + + log(logging.Info, "dropped database %s", deets.Database) + return nil +} + +func (m *tidb) TranslateSQL(sql string) string { + return sql +} + +func (m *tidb) FizzTranslator() fizz.Translator { + t := translators.NewMySQL(m.URL(), m.Details().Database) + return t +} + +func (m *tidb) DumpSchema(w io.Writer) error { + deets := m.Details() + cmd := exec.Command("mysqldump", "--protocol", "TCP", "-d", "-h", deets.Host, "-P", deets.Port, "-u", deets.User, fmt.Sprintf("--password=%s", deets.Password), deets.Database) + if deets.Port == "socket" { + cmd = exec.Command("mysqldump", "-d", "-S", deets.Host, "-u", deets.User, fmt.Sprintf("--password=%s", deets.Password), deets.Database) + } + return genericDumpSchema(deets, cmd, w) +} + +// LoadSchema executes a schema sql file against the configured database. +func (m *tidb) LoadSchema(r io.Reader) error { + return genericLoadSchema(m, r) +} + +// TruncateAll truncates all tables for the given connection. +func (m *tidb) TruncateAll(tx *Connection) error { + var stmts []string + err := tx.RawQuery(tidbTruncate, m.Details().Database, tx.MigrationTableName()).All(&stmts) + if err != nil { + return err + } + if len(stmts) == 0 { + return nil + } + + var qb bytes.Buffer + // #49: Disable foreign keys before truncation + qb.WriteString("SET SESSION FOREIGN_KEY_CHECKS = 0; ") + qb.WriteString(strings.Join(stmts, " ")) + // #49: Re-enable foreign keys after truncation + qb.WriteString(" SET SESSION FOREIGN_KEY_CHECKS = 1;") + + return tx.RawQuery(qb.String()).Exec() +} + +func (m *tidb) AfterOpen(c *Connection) error { + // ref: ory/kratos#1551 + err := c.RawQuery("SET SESSION transaction_isolation = 'REPEATABLE-READ';").Exec() + if err != nil { + return fmt.Errorf("tidb: setting transaction isolation level: %w", err) + } + return nil +} + +func newTiDB(deets *ConnectionDetails) (dialect, error) { + cd := &tidb{ + commonDialect: commonDialect{ConnectionDetails: deets}, + } + return cd, nil +} + +func urlParserTiDB(cd *ConnectionDetails) error { + dsn := cd.URL + dsn = strings.TrimPrefix(dsn, "tidb://") + dsn = strings.TrimPrefix(dsn, "mysql://") + cfg, err := _mysql.ParseDSN(dsn) + if err != nil { + return fmt.Errorf("the URL '%s' is not supported by MySQL/TiDB driver: %w", cd.URL, err) + } + + cd.User = cfg.User + cd.Password = cfg.Passwd + cd.Database = cfg.DBName + + // NOTE: use cfg.Params if want to fill options with full parameters + cd.setOption("collation", cfg.Collation) + + if cfg.Net == "unix" { + cd.Port = "socket" // trick. see: `URL()` + cd.Host = cfg.Addr + } else { + tmp := strings.Split(cfg.Addr, ":") + cd.Host = tmp[0] + if len(tmp) > 1 { + cd.Port = tmp[1] + } + } + + return nil +} + +func finalizerTiDB(cd *ConnectionDetails) { + cd.Host = defaults.String(cd.Host, hostTiDB) + cd.Port = defaults.String(cd.Port, portTiDB) + + defs := map[string]string{ + "readTimeout": "3s", + "collation": "utf8mb4_general_ci", + } + forced := map[string]string{ + "parseTime": "true", + "multiStatements": "true", + } + + for k, def := range defs { + cd.setOptionWithDefault(k, cd.option(k), def) + } + + for k, v := range forced { + // respect user specified options but print warning! + cd.setOptionWithDefault(k, cd.option(k), v) + if cd.option(k) != v { // when user-defined option exists + log(logging.Warn, "IMPORTANT! '%s: %s' option is required to work properly but your current setting is '%v: %v'.", k, v, k, cd.option(k)) + log(logging.Warn, "It is highly recommended to remove '%v: %v' option from your config!", k, cd.option(k)) + } // or override with `cd.Options[k] = v`? + if cd.URL != "" && !strings.Contains(cd.URL, k+"="+v) { + log(logging.Warn, "IMPORTANT! '%s=%s' option is required to work properly. Please add it to the database URL in the config!", k, v) + } // or fix user specified url? + } +} + +const tidbTruncate = "SELECT concat('TRUNCATE TABLE `', TABLE_NAME, '`;') as stmt FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = ? AND table_name <> ? AND table_type <> 'VIEW'" diff --git a/dialect_tidb_test.go b/dialect_tidb_test.go new file mode 100644 index 000000000..81d79aaa6 --- /dev/null +++ b/dialect_tidb_test.go @@ -0,0 +1,243 @@ +package pop + +import ( + "os" + "strings" + "testing" + + "github.com/gobuffalo/fizz" + "github.com/gobuffalo/fizz/translators" + "github.com/stretchr/testify/require" +) + +func Test_TiDB_URL_As_Is(t *testing.T) { + r := require.New(t) + + cd := &ConnectionDetails{ + URL: "mysql://user:pass@(host:port)/dbase?opt=value", + } + err := cd.Finalize() + r.NoError(err) + + m := &mysql{commonDialect{ConnectionDetails: cd}} + r.Equal("user:pass@(host:port)/dbase?opt=value", m.URL()) + r.Equal("user:pass@(host:port)/?opt=value", m.urlWithoutDB()) + r.Equal("user:pass@(host:port)/dbase?opt=value", m.MigrationURL()) +} + +func Test_TiDB_URL_Override_withURL(t *testing.T) { + r := require.New(t) + + cd := &ConnectionDetails{ + Database: "xx", + Host: "xx", + Port: "xx", + User: "xx", + Password: "xx", + URL: "mysql://user:pass@(host:port)/dbase?opt=value", + } + err := cd.Finalize() + r.NoError(err) + + m := &mysql{commonDialect{ConnectionDetails: cd}} + r.Equal("user:pass@(host:port)/dbase?opt=value", m.URL()) + r.Equal("user:pass@(host:port)/?opt=value", m.urlWithoutDB()) + r.Equal("user:pass@(host:port)/dbase?opt=value", m.MigrationURL()) +} + +func Test_TiDB_URL_With_Values(t *testing.T) { + r := require.New(t) + m := &mysql{commonDialect{ConnectionDetails: &ConnectionDetails{ + Database: "dbase", + Host: "host", + Port: "port", + User: "user", + Password: "pass", + Options: map[string]string{"opt": "value"}, + }}} + r.Equal("user:pass@(host:port)/dbase?opt=value", m.URL()) + r.Equal("user:pass@(host:port)/?opt=value", m.urlWithoutDB()) + r.Equal("user:pass@(host:port)/dbase?opt=value", m.MigrationURL()) +} + +func Test_TiDB_URL_Without_User(t *testing.T) { + r := require.New(t) + m := &mysql{commonDialect{ConnectionDetails: &ConnectionDetails{ + Password: "pass", + Database: "dbase", + }}} + // finalizerTiDB fills address part in real world. + // without user, password cannot live alone + r.Equal("(:)/dbase?", m.URL()) +} + +func Test_TiDB_URL_Without_Password(t *testing.T) { + r := require.New(t) + m := &mysql{commonDialect{ConnectionDetails: &ConnectionDetails{ + User: "user", + Database: "dbase", + }}} + // finalizerTiDB fills address part in real world. + r.Equal("user@(:)/dbase?", m.URL()) +} + +func Test_TiDB_URL_urlParserTiDB_Standard(t *testing.T) { + r := require.New(t) + cd := &ConnectionDetails{ + URL: "mysql://user:pass@(host:port)/database?collation=utf8¶m2=value2", + } + err := urlParserTiDB(cd) + r.NoError(err) + r.Equal("user", cd.User) + r.Equal("pass", cd.Password) + r.Equal("host", cd.Host) + r.Equal("port", cd.Port) + r.Equal("database", cd.Database) + // only collation is stored as options by urlParserTiDB() + r.Equal("utf8", cd.Options["collation"]) + r.Equal("", cd.Options["param2"]) +} + +func Test_TiDB_URL_urlParserTiDB_With_Protocol(t *testing.T) { + r := require.New(t) + cd := &ConnectionDetails{ + URL: "user:pass@tcp(host:port)/dbase", + } + err := urlParserTiDB(cd) + r.NoError(err) + r.Equal("user", cd.User) + r.Equal("pass", cd.Password) + r.Equal("host", cd.Host) + r.Equal("port", cd.Port) + r.Equal("dbase", cd.Database) +} + +func Test_TiDB_URL_urlParserTiDB_Socket(t *testing.T) { + r := require.New(t) + cd := &ConnectionDetails{ + URL: "unix(/tmp/socket)/dbase", + } + err := urlParserTiDB(cd) + r.NoError(err) + r.Equal("/tmp/socket", cd.Host) + r.Equal("socket", cd.Port) + + // additional test without URL + cd.URL = "" + m := &mysql{commonDialect{ConnectionDetails: cd}} + r.True(strings.HasPrefix(m.URL(), "unix(/tmp/socket)/dbase?")) + r.True(strings.HasPrefix(m.urlWithoutDB(), "unix(/tmp/socket)/?")) +} + +func Test_TiDB_URL_urlParserTiDB_Unsupported(t *testing.T) { + r := require.New(t) + cd := &ConnectionDetails{ + URL: "mysql://user:pass@host:port/dbase?opt=value", + } + err := urlParserTiDB(cd) + r.Error(err) +} + +func Test_TiDB_Database_Open_Failure(t *testing.T) { + r := require.New(t) + m := &mysql{commonDialect{ConnectionDetails: &ConnectionDetails{}}} + err := m.CreateDB() + r.Error(err) + err = m.DropDB() + r.Error(err) +} + +func Test_TiDB_FizzTranslator(t *testing.T) { + r := require.New(t) + cd := &ConnectionDetails{} + m := &mysql{commonDialect{ConnectionDetails: cd}} + ft := m.FizzTranslator() + r.IsType(&translators.MySQL{}, ft) + r.Implements((*fizz.Translator)(nil), ft) +} + +func Test_TiDB_Finalizer_Default_CD(t *testing.T) { + r := require.New(t) + m := &mysql{commonDialect{ConnectionDetails: &ConnectionDetails{}}} + finalizerTiDB(m.ConnectionDetails) + r.Equal(hostTiDB, m.ConnectionDetails.Host) + r.Equal(portTiDB, m.ConnectionDetails.Port) +} + +func Test_TiDB_Finalizer_Default_Options(t *testing.T) { + r := require.New(t) + m := &mysql{commonDialect{ConnectionDetails: &ConnectionDetails{}}} + finalizerTiDB(m.ConnectionDetails) + r.Contains(m.URL(), "multiStatements=true") + r.Contains(m.URL(), "parseTime=true") + r.Contains(m.URL(), "readTimeout=3s") + r.Contains(m.URL(), "collation=utf8mb4_general_ci") +} + +func Test_TiDB_Finalizer_Preserve_User_Defined_Options(t *testing.T) { + r := require.New(t) + m := &mysql{commonDialect{ConnectionDetails: &ConnectionDetails{ + Options: map[string]string{ + "multiStatements": "false", + "parseTime": "false", + "readTimeout": "1h", + "collation": "utf8", + }, + }}} + finalizerTiDB(m.ConnectionDetails) + r.Contains(m.URL(), "multiStatements=false") + r.Contains(m.URL(), "parseTime=false") + r.Contains(m.URL(), "readTimeout=1h") + r.Contains(m.URL(), "collation=utf8") +} + +func (s *TiDBSuite) Test_TiDB_DDL_Operations() { + r := s.Require() + + origDatabase := PDB.Dialect.Details().Database + PDB.Dialect.Details().Database = "pop_test_mysql_extra" + defer func() { + PDB.Dialect.Details().Database = origDatabase + }() + + PDB.Dialect.DropDB() // clean up + err := PDB.Dialect.CreateDB() + r.NoError(err) + err = PDB.Dialect.CreateDB() + r.Error(err) + err = PDB.Dialect.DropDB() + r.NoError(err) + err = PDB.Dialect.DropDB() + r.Error(err) +} + +func (s *TiDBSuite) Test_TiDB_DDL_Schema() { + r := s.Require() + f, err := os.CreateTemp(s.T().TempDir(), "pop_test_mysql_dump") + r.NoError(err) + s.T().Cleanup(func() { + _ = f.Close() + }) + + // do it against "pop_test" + err = PDB.Dialect.DumpSchema(f) + r.NoError(err) + _, err = f.Seek(0, 0) + r.NoError(err) + err = PDB.Dialect.LoadSchema(f) + r.NoError(err) + + origDatabase := PDB.Dialect.Details().Database + PDB.Dialect.Details().Database = "pop_test_not_exist" + defer func() { + PDB.Dialect.Details().Database = origDatabase + }() + + // do it against "pop_test_not_exist" + _, err = f.Seek(0, 0) + r.NoError(err) + err = PDB.Dialect.LoadSchema(f) + r.Error(err) + err = PDB.Dialect.DumpSchema(f) + r.Error(err) +} diff --git a/executors_test.go b/executors_test.go index 05ac4ab28..4580642a7 100644 --- a/executors_test.go +++ b/executors_test.go @@ -1597,7 +1597,7 @@ func Test_UpdateQuery(t *testing.T) { r.Equal(u1b.Bio.String, "must-not-change-1") r.Equal(u2b.Bio.String, "must-not-change-2") r.Equal(u3b.Bio.String, "must-not-change-3") - if tx.Dialect.Name() != nameMySQL { // MySQL timestamps are in seconds + if tx.Dialect.Name() != nameMySQL && tx.Dialect.Name() != nameMariaDB && tx.Dialect.Name() != nameTiDB { // MySQL/MariaDB/TiDB timestamps are in seconds r.NotEqual(u1.UpdatedAt, u1b.UpdatedAt) r.NotEqual(u2.UpdatedAt, u2b.UpdatedAt) } @@ -1606,8 +1606,8 @@ func Test_UpdateQuery(t *testing.T) { // ID is ignored count, err = tx.Where("true").UpdateQuery(&User{ID: 123, Name: nulls.NewString("Bar")}, "id", "name") r.NoError(err) - if tx.Dialect.Name() == nameMySQL || tx.Dialect.Name() == nameMariaDB { - r.EqualValues(1, count) // on UPDATE, MySQL/MariaDB count only rows with changes, not all matched rows + if tx.Dialect.Name() == nameMySQL || tx.Dialect.Name() == nameMariaDB || tx.Dialect.Name() == nameTiDB { + r.EqualValues(1, count) // on UPDATE, MySQL/MariaDB/TiDB count only rows with changes, not all matched rows } else { r.EqualValues(3, count) } diff --git a/genny/config/templates/tidb.yml.tmpl b/genny/config/templates/tidb.yml.tmpl new file mode 100644 index 000000000..438dc64bc --- /dev/null +++ b/genny/config/templates/tidb.yml.tmpl @@ -0,0 +1,49 @@ +--- +development: + dialect: "tidb" + database: "{{.opts.Prefix}}_development" + host: "127.0.0.1" + port: "4000" + user: "root" + password: "root" + +test: + dialect: "tidb" + # + # You can use a single URL string for the same configuration: + # + #url: "mysql://root:root@(127.0.0.1:4000)/{{.opts.Prefix}}_test?parseTime=true&multiStatements=true&readTimeout=3s" + # + # Note that if you use `url`, other configurations are silently ignored. + # In this case, the URL must contain all required connection parameters. + # + database: "{{.opts.Prefix}}_test" + host: "127.0.0.1" + port: "4000" + user: "root" + password: "root" + +production: + # + # You can also use environmental variables to override values in this config. + # + #url: {{"{{"}}envOr "DATABASE_URL" "mysql://root:root@(localhost:4000)/{{.opts.Prefix}}_production?parseTime=true&multiStatements=true&readTimeout=3s"}} + # + dialect: "tidb" + database: "{{.opts.Prefix}}_production" + host: {{"{{"}}envOr "DATABASE_HOST" "localhost"}} + port: {{"{{"}}envOr "DATABASE_PORT" "4000"}} + user: {{"{{"}}envOr "DATABASE_USER" "root"}} + password: {{"{{"}}envOr "DATABASE_PASSWORD" "root"}} + # + # And you can also override connection parameters by setting it under options. + # + #options: + # parseTime: true + # multiStatements: true + # readTimeout: 3s + # collation: "utf8mb4_general_ci" + # + # CAUTION! + # `parseTime` and` multiStatements` must be set to `true` to work properly. + # If you are not sure, do not change (or set) these values. diff --git a/pop_test.go b/pop_test.go index 43ce318f9..3dc35175f 100644 --- a/pop_test.go +++ b/pop_test.go @@ -24,6 +24,10 @@ type MySQLSuite struct { suite.Suite } +type TiDBSuite struct { + suite.Suite +} + type SQLiteSuite struct { suite.Suite } @@ -42,6 +46,8 @@ func TestSpecificSuites(t *testing.T) { suite.Run(t, &SQLiteSuite{}) case "cockroach": suite.Run(t, &CockroachSuite{}) + case "tidb": + suite.Run(t, &TiDBSuite{}) } } diff --git a/testdata/migrations/20210104145901_context_tables.up.fizz b/testdata/migrations/20210104145901_context_tables.up.fizz index 25d07163f..16e5cdd72 100644 --- a/testdata/migrations/20210104145901_context_tables.up.fizz +++ b/testdata/migrations/20210104145901_context_tables.up.fizz @@ -10,7 +10,7 @@ } {{ end }} -{{ if eq .Dialect "mysql" }} +{{ if or (eq .Dialect "mysql") (eq .Dialect "tidb") }} create_table("context_prefix_a_table") { t.Column("id", "string", { primary: true }) t.Column("value", "string")