// Copyright 2017 The Xorm Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package tests

import (
	"database/sql"
	"errors"
	"fmt"
	"slices"
	"strings"
	"testing"
	"time"

	"code.forgejo.org/xorm/xorm"
	"code.forgejo.org/xorm/xorm/schemas"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestStoreEngine(t *testing.T) {
	assert.NoError(t, PrepareEngine())

	assert.NoError(t, testEngine.DropTables("user_store_engine"))

	type UserinfoStoreEngine struct {
		Id   int64
		Name string
	}

	assert.NoError(t, testEngine.StoreEngine("InnoDB").Table("user_store_engine").CreateTable(&UserinfoStoreEngine{}))
}

func TestCreateTable(t *testing.T) {
	assert.NoError(t, PrepareEngine())

	assert.NoError(t, testEngine.DropTables("user_user"))

	type UserinfoCreateTable struct {
		Id   int64
		Name string
	}

	assert.NoError(t, testEngine.Table("user_user").CreateTable(&UserinfoCreateTable{}))
}

func TestCreateTable2(t *testing.T) {
	type BaseModelLogicalDel struct {
		Id        string     `xorm:"varchar(46) pk"`
		CreatedAt time.Time  `xorm:"created"`
		UpdatedAt time.Time  `xorm:"updated"`
		DeletedAt *time.Time `xorm:"deleted"`
	}
	type TestPerson struct {
		BaseModelLogicalDel `xorm:"extends"`
		UserId              string `xorm:"varchar(46) notnull"`
		PersonId            string `xorm:"varchar(46) notnull"`
		Star                bool
		SortNo              int
		DispName            string `xorm:"varchar(100)"`
		FirstName           string
		LastName            string
		FirstNameKana       string
		LastNameKana        string
		BirthYear           *int
		BirthMonth          *int
		BirthDay            *int
		ImageId             string `xorm:"varchar(46)"`
		ImageDefaultId      string `xorm:"varchar(46)"`
		UserText            string `xorm:"varchar(2000)"`
		GenderId            *int
		At1                 string `xorm:"varchar(10)"`
		At1Rate             int
		At2                 string `xorm:"varchar(10)"`
		At2Rate             int
		At3                 string `xorm:"varchar(10)"`
		At3Rate             int
		At4                 string `xorm:"varchar(10)"`
		At4Rate             int
		At5                 string `xorm:"varchar(10)"`
		At5Rate             int
		At6                 string `xorm:"varchar(10)"`
		At6Rate             int
	}

	assert.NoError(t, PrepareEngine())

	tb1, err := testEngine.TableInfo(TestPerson{})
	assert.NoError(t, err)
	tb2, err := testEngine.TableInfo(new(TestPerson))
	assert.NoError(t, err)
	cols1, cols2 := tb1.ColumnsSeq(), tb2.ColumnsSeq()
	assert.EqualValues(t, len(cols1), len(cols2))
	for i, col := range cols1 {
		assert.EqualValues(t, col, cols2[i])
	}

	result, err := testEngine.IsTableExist(new(TestPerson))
	assert.NoError(t, err)
	if result {
		assert.NoError(t, testEngine.DropTables(new(TestPerson)))
	}

	assert.NoError(t, testEngine.CreateTables(new(TestPerson)))
	tables1, err := testEngine.DBMetas()
	assert.NoError(t, err)
	assert.Len(t, tables1, 1)
	assert.EqualValues(t, len(cols1), len(tables1[0].Columns()))

	result, err = testEngine.IsTableExist(new(TestPerson))
	assert.NoError(t, err)
	if result {
		assert.NoError(t, testEngine.DropTables(new(TestPerson)))
	}

	assert.NoError(t, testEngine.CreateTables(TestPerson{}))
	tables2, err := testEngine.DBMetas()
	assert.NoError(t, err)
	assert.Len(t, tables2, 1)
	assert.EqualValues(t, len(cols1), len(tables2[0].Columns()))
}

func TestCreateMultiTables(t *testing.T) {
	assert.NoError(t, PrepareEngine())

	session := testEngine.NewSession()
	defer session.Close()

	type UserinfoMultiTable struct {
		Id   int64
		Name string
	}

	user := &UserinfoMultiTable{}
	assert.NoError(t, session.Begin())

	for i := range 10 {
		tableName := fmt.Sprintf("user_%v", i)

		assert.NoError(t, session.DropTable(tableName))

		assert.NoError(t, session.Table(tableName).CreateTable(user))
	}

	assert.NoError(t, session.Commit())
}

type SyncTable1 struct {
	Id   int64
	Name string
	Dev  int `xorm:"index"`
}

type SyncTable2 struct {
	Id     int64
	Name   string `xorm:"unique"`
	Number string `xorm:"index"`
	Dev    int
	Age    int
}

func (SyncTable2) TableName() string {
	return "sync_table1"
}

type SyncTable3 struct {
	Id     int64
	Name   string `xorm:"unique"`
	Number string `xorm:"index"`
	Dev    int
	Age    int
}

func (s *SyncTable3) TableName() string {
	return "sync_table1"
}

func TestSyncTable(t *testing.T) {
	assert.NoError(t, PrepareEngine())

	assert.NoError(t, testEngine.Sync(new(SyncTable1)))

	tables, err := testEngine.DBMetas()
	assert.NoError(t, err)
	assert.EqualValues(t, 1, len(tables))
	assert.EqualValues(t, "sync_table1", tables[0].Name)
	tableInfo, err := testEngine.TableInfo(new(SyncTable1))
	assert.NoError(t, err)
	assert.EqualValues(t, testEngine.Dialect().SQLType(tables[0].GetColumn("name")), testEngine.Dialect().SQLType(tableInfo.GetColumn("name")))

	assert.NoError(t, testEngine.Sync(new(SyncTable2)))

	tables, err = testEngine.DBMetas()
	assert.NoError(t, err)
	assert.EqualValues(t, 1, len(tables))
	assert.EqualValues(t, "sync_table1", tables[0].Name)
	tableInfo, err = testEngine.TableInfo(new(SyncTable2))
	assert.NoError(t, err)
	assert.EqualValues(t, testEngine.Dialect().SQLType(tables[0].GetColumn("name")), testEngine.Dialect().SQLType(tableInfo.GetColumn("name")))

	assert.NoError(t, testEngine.Sync(new(SyncTable3)))

	tables, err = testEngine.DBMetas()
	assert.NoError(t, err)
	assert.EqualValues(t, 1, len(tables))
	assert.EqualValues(t, "sync_table1", tables[0].Name)
	tableInfo, err = testEngine.TableInfo(new(SyncTable3))
	assert.NoError(t, err)
	assert.EqualValues(t, testEngine.Dialect().SQLType(tables[0].GetColumn("name")), testEngine.Dialect().SQLType(tableInfo.GetColumn("name")))
}

func TestSyncTable2(t *testing.T) {
	assert.NoError(t, PrepareEngine())

	assert.NoError(t, testEngine.Table("sync_tablex").Sync(new(SyncTable1)))

	tables, err := testEngine.DBMetas()
	assert.NoError(t, err)
	assert.EqualValues(t, 1, len(tables))
	assert.EqualValues(t, "sync_tablex", tables[0].Name)
	assert.EqualValues(t, 3, len(tables[0].Columns()))

	type SyncTable4 struct {
		SyncTable1 `xorm:"extends"`
		NewCol     string
	}

	assert.NoError(t, testEngine.Table("sync_tablex").Sync(new(SyncTable4)))
	tables, err = testEngine.DBMetas()
	assert.NoError(t, err)
	assert.EqualValues(t, 1, len(tables))
	assert.EqualValues(t, "sync_tablex", tables[0].Name)
	assert.EqualValues(t, 4, len(tables[0].Columns()))
	assert.EqualValues(t, colMapper.Obj2Table("NewCol"), tables[0].Columns()[3].Name)
}

func TestSyncTable3(t *testing.T) {
	type SyncTable5 struct {
		Id         int64
		Name       string
		Text       string   `xorm:"TEXT"`
		Char       byte     `xorm:"CHAR(1)"`
		TenChar    [10]byte `xorm:"CHAR(10)"`
		TenVarChar string   `xorm:"VARCHAR(10)"`
	}

	assert.NoError(t, PrepareEngine())

	assert.NoError(t, testEngine.Sync(new(SyncTable5)))

	tables, err := testEngine.DBMetas()
	assert.NoError(t, err)
	tableInfo, err := testEngine.TableInfo(new(SyncTable5))
	assert.NoError(t, err)
	assert.EqualValues(t, testEngine.Dialect().SQLType(tableInfo.GetColumn("name")), testEngine.Dialect().SQLType(tables[0].GetColumn("name")))
	/* Engine.DBMetas() returns the size of the column from the database but Engine.TableInfo() might not be able to guess the column size.
	For example using MySQL/MariaDB: when utf-8 charset is used, "`xorm:"TEXT(21846)`" creates a MEDIUMTEXT column not a TEXT column. */
	assert.True(t, testEngine.Dialect().SQLType(tables[0].GetColumn("text")) == testEngine.Dialect().SQLType(tableInfo.GetColumn("text")) || strings.HasPrefix(testEngine.Dialect().SQLType(tables[0].GetColumn("text")), testEngine.Dialect().SQLType(tableInfo.GetColumn("text"))+"("))
	assert.EqualValues(t, testEngine.Dialect().SQLType(tableInfo.GetColumn("char")), testEngine.Dialect().SQLType(tables[0].GetColumn("char")))
	assert.EqualValues(t, testEngine.Dialect().SQLType(tableInfo.GetColumn("ten_char")), testEngine.Dialect().SQLType(tables[0].GetColumn("ten_char")))
	assert.EqualValues(t, testEngine.Dialect().SQLType(tableInfo.GetColumn("ten_var_char")), testEngine.Dialect().SQLType(tables[0].GetColumn("ten_var_char")))

	if *doNVarcharTest {
		var oldDefaultVarchar string
		var oldDefaultChar string
		oldDefaultVarchar, *defaultVarchar = *defaultVarchar, "nvarchar"
		oldDefaultChar, *defaultChar = *defaultChar, "nchar"
		testEngine.Dialect().SetParams(map[string]string{
			"DEFAULT_VARCHAR": *defaultVarchar,
			"DEFAULT_CHAR":    *defaultChar,
		})
		defer func() {
			*defaultVarchar = oldDefaultVarchar
			*defaultChar = oldDefaultChar
			testEngine.Dialect().SetParams(map[string]string{
				"DEFAULT_VARCHAR": *defaultVarchar,
				"DEFAULT_CHAR":    *defaultChar,
			})
		}()
		assert.NoError(t, PrepareEngine())

		assert.NoError(t, testEngine.Sync(new(SyncTable5)))

		tables, err := testEngine.DBMetas()
		assert.NoError(t, err)
		tableInfo, err := testEngine.TableInfo(new(SyncTable5))
		assert.NoError(t, err)
		assert.EqualValues(t, testEngine.Dialect().SQLType(tableInfo.GetColumn("name")), testEngine.Dialect().SQLType(tables[0].GetColumn("name")))
		assert.EqualValues(t, testEngine.Dialect().SQLType(tableInfo.GetColumn("text")), testEngine.Dialect().SQLType(tables[0].GetColumn("text")))
		assert.EqualValues(t, testEngine.Dialect().SQLType(tableInfo.GetColumn("char")), testEngine.Dialect().SQLType(tables[0].GetColumn("char")))
		assert.EqualValues(t, testEngine.Dialect().SQLType(tableInfo.GetColumn("ten_char")), testEngine.Dialect().SQLType(tables[0].GetColumn("ten_char")))
		assert.EqualValues(t, testEngine.Dialect().SQLType(tableInfo.GetColumn("ten_var_char")), testEngine.Dialect().SQLType(tables[0].GetColumn("ten_var_char")))
	}
}

func TestSyncTable4(t *testing.T) {
	type SyncTable6 struct {
		Id  int64
		Qty float64 `xorm:"numeric(36,2)"`
	}

	assert.NoError(t, PrepareEngine())

	assert.NoError(t, testEngine.Sync(new(SyncTable6)))

	assert.NoError(t, testEngine.Sync(new(SyncTable6)))
}

func TestIsTableExist(t *testing.T) {
	assert.NoError(t, PrepareEngine())

	exist, err := testEngine.IsTableExist(new(CustomTableName))
	assert.NoError(t, err)
	assert.False(t, exist)

	assert.NoError(t, testEngine.CreateTables(new(CustomTableName)))

	exist, err = testEngine.IsTableExist(new(CustomTableName))
	assert.NoError(t, err)
	assert.True(t, exist)
}

func TestIsTableEmpty(t *testing.T) {
	assert.NoError(t, PrepareEngine())

	type NumericEmpty struct {
		Numeric float64 `xorm:"numeric(26,2)"`
	}

	type PictureEmpty struct {
		Id          int64
		Url         string `xorm:"unique"` // image's url
		Title       string
		Description string
		Created     time.Time `xorm:"created"`
		ILike       int
		PageView    int
		From_url    string // nolint
		Pre_url     string `xorm:"unique"` // pre view image's url
		Uid         int64
	}

	assert.NoError(t, testEngine.DropTables(&PictureEmpty{}, &NumericEmpty{}))

	assert.NoError(t, testEngine.Sync(new(PictureEmpty), new(NumericEmpty)))

	isEmpty, err := testEngine.IsTableEmpty(&PictureEmpty{})
	assert.NoError(t, err)
	assert.True(t, isEmpty)

	tbName := testEngine.GetTableMapper().Obj2Table("PictureEmpty")
	isEmpty, err = testEngine.IsTableEmpty(tbName)
	assert.NoError(t, err)
	assert.True(t, isEmpty)
}

type CustomTableName struct {
	Id   int64
	Name string
}

func (c *CustomTableName) TableName() string {
	return "customtablename"
}

func TestCustomTableName(t *testing.T) {
	assert.NoError(t, PrepareEngine())

	c := new(CustomTableName)
	assert.NoError(t, testEngine.DropTables(c))

	assert.NoError(t, testEngine.CreateTables(c))
}

type IndexOrUnique struct {
	Id        int64
	Index     int `xorm:"index"`
	Unique    int `xorm:"unique"`
	Group1    int `xorm:"index(ttt)"`
	Group2    int `xorm:"index(ttt)"`
	UniGroup1 int `xorm:"unique(lll)"`
	UniGroup2 int `xorm:"unique(lll)"`
}

func TestIndexAndUnique(t *testing.T) {
	assert.NoError(t, PrepareEngine())

	assert.NoError(t, testEngine.CreateTables(&IndexOrUnique{}))

	assert.NoError(t, testEngine.DropTables(&IndexOrUnique{}))

	assert.NoError(t, testEngine.CreateTables(&IndexOrUnique{}))

	assert.NoError(t, testEngine.CreateIndexes(&IndexOrUnique{}))

	assert.NoError(t, testEngine.CreateUniques(&IndexOrUnique{}))

	assert.NoError(t, testEngine.DropIndexes(&IndexOrUnique{}))
}

func TestMetaInfo(t *testing.T) {
	assert.NoError(t, PrepareEngine())
	assert.NoError(t, testEngine.Sync(new(CustomTableName), new(IndexOrUnique)))

	tables, err := testEngine.DBMetas()
	assert.NoError(t, err)
	assert.EqualValues(t, 2, len(tables))
	tableNames := []string{tables[0].Name, tables[1].Name}
	assert.Contains(t, tableNames, "customtablename")
	assert.Contains(t, tableNames, "index_or_unique")
}

func TestCharst(t *testing.T) {
	assert.NoError(t, PrepareEngine())

	err := testEngine.DropTables("user_charset")
	assert.NoError(t, err)

	err = testEngine.Charset("utf8").Table("user_charset").CreateTable(&Userinfo{})
	assert.NoError(t, err)
}

func TestSync2_1(t *testing.T) {
	type WxTest struct {
		Id                 int   `xorm:"not null pk autoincr INT(64)"`
		Passport_user_type int16 `xorm:"null int"`
		Id_delete          int8  `xorm:"null int default 1"`
	}

	assert.NoError(t, PrepareEngine())

	assert.NoError(t, testEngine.DropTables("wx_test"))
	assert.NoError(t, testEngine.Sync(new(WxTest)))
	assert.NoError(t, testEngine.Sync(new(WxTest)))
}

func TestUnique_1(t *testing.T) {
	type UserUnique struct {
		Id        int64
		UserName  string    `xorm:"unique varchar(25) not null"`
		Password  string    `xorm:"varchar(255) not null"`
		Admin     bool      `xorm:"not null"`
		CreatedAt time.Time `xorm:"created"`
		UpdatedAt time.Time `xorm:"updated"`
	}

	assert.NoError(t, PrepareEngine())

	assert.NoError(t, testEngine.DropTables("user_unique"))
	assert.NoError(t, testEngine.Sync(new(UserUnique)))

	assert.NoError(t, testEngine.DropTables("user_unique"))
	assert.NoError(t, testEngine.CreateTables(new(UserUnique)))
	assert.NoError(t, testEngine.CreateUniques(new(UserUnique)))
}

func TestSync2_2(t *testing.T) {
	type TestSync2Index struct {
		Id     int64
		UserId int64 `xorm:"index"`
	}

	assert.NoError(t, PrepareEngine())

	tableNames := make(map[string]bool)
	for i := range 10 {
		tableName := fmt.Sprintf("test_sync2_index_%d", i)
		tableNames[tableName] = true
		assert.NoError(t, testEngine.Table(tableName).Sync(new(TestSync2Index)))

		exist, err := testEngine.IsTableExist(tableName)
		assert.NoError(t, err)
		assert.True(t, exist)
	}

	tables, err := testEngine.DBMetas()
	assert.NoError(t, err)

	for _, table := range tables {
		assert.True(t, tableNames[table.Name])
	}
}

func TestSync2_Default(t *testing.T) {
	type TestSync2Default struct {
		Id       int64
		UserId   int64  `xorm:"default(1)"`
		IsMember bool   `xorm:"default(true)"`
		Name     string `xorm:"default('my_name')"`
	}

	assert.NoError(t, PrepareEngine())
	assertSync(t, new(TestSync2Default))
	assert.NoError(t, testEngine.Sync(new(TestSync2Default)))
}

func TestSync2_Default2(t *testing.T) {
	type TestSync2Default2 struct {
		Id       int64
		UserId   int64  `xorm:"default(1)"`
		IsMember bool   `xorm:"default(true)"`
		Name     string `xorm:"default('')"`
	}

	assert.NoError(t, PrepareEngine())
	assertSync(t, new(TestSync2Default2))
	assert.NoError(t, testEngine.Sync(new(TestSync2Default2)))
	assert.NoError(t, testEngine.Sync(new(TestSync2Default2)))
	assert.NoError(t, testEngine.Sync(new(TestSync2Default2)))

	assert.NoError(t, testEngine.Sync(new(TestSync2Default2)))
	assert.NoError(t, testEngine.Sync(new(TestSync2Default2)))
	assert.NoError(t, testEngine.Sync(new(TestSync2Default2)))
}

func TestModifyColum(t *testing.T) {
	// Since SQLITE don't support modify column SQL, currrently just ignore
	if testEngine.Dialect().URI().DBType == schemas.SQLITE {
		return
	}
	type TestModifyColumn struct {
		Id       int64
		UserId   int64  `xorm:"default(1)"`
		IsMember bool   `xorm:"default(true)"`
		Name     string `xorm:"char(10)"`
	}

	assert.NoError(t, PrepareEngine())
	assertSync(t, new(TestModifyColumn))

	alterSQL := testEngine.Dialect().ModifyColumnSQL("test_modify_column", &schemas.Column{
		Name: "name",
		SQLType: schemas.SQLType{
			Name: "VARCHAR",
		},
		Length:         16,
		Nullable:       false,
		DefaultIsEmpty: true,
	})
	_, err := testEngine.Exec(alterSQL)
	assert.NoError(t, err)
}

type TestCollateColumn struct {
	Id     int64
	UserId int64  `xorm:"unique(s)"`
	Name   string `xorm:"varchar(20) unique(s)"`
	dbtype string `xorm:"-"`
}

func (t TestCollateColumn) TableCollations() []*schemas.Collation {
	if t.dbtype == string(schemas.MYSQL) {
		return []*schemas.Collation{
			{
				Name:   "utf8mb4_general_ci",
				Column: "name",
			},
		}
	}
	return nil
}

func TestCollate(t *testing.T) {
	assert.NoError(t, PrepareEngine())
	assertSync(t, &TestCollateColumn{
		dbtype: string(testEngine.Dialect().URI().DBType),
	})

	_, err := testEngine.Insert(&TestCollateColumn{
		UserId: 1,
		Name:   "test",
	})
	assert.NoError(t, err)
	_, err = testEngine.Insert(&TestCollateColumn{
		UserId: 1,
		Name:   "Test",
	})
	if testEngine.Dialect().URI().DBType == schemas.MYSQL {
		ver, err1 := testEngine.DBVersion()
		assert.NoError(t, err1)

		tables, err1 := testEngine.DBMetas()
		assert.NoError(t, err1)
		for _, table := range tables {
			if table.Name == "test_collate_column" {
				col := table.GetColumn("name")
				if col == nil {
					assert.Error(t, errors.New("not found column"))
					return
				}
				// tidb doesn't follow utf8mb4_general_ci
				if col.Collation == "utf8mb4_general_ci" && ver.Edition != "TiDB" {
					assert.Error(t, err)
				} else {
					assert.NoError(t, err)
				}
				break
			}
		}
	} else {
		assert.NoError(t, err)
	}

	// Since SQLITE don't support modify column SQL, currrently just ignore
	if testEngine.Dialect().URI().DBType != schemas.MYSQL {
		return
	}

	var newCollation string
	if testEngine.Dialect().URI().DBType == schemas.MYSQL {
		newCollation = "utf8mb4_bin"
	} else {
		return
	}

	alterSQL := testEngine.Dialect().ModifyColumnSQL("test_collate_column", &schemas.Column{
		Name: "name",
		SQLType: schemas.SQLType{
			Name: "VARCHAR",
		},
		Length:         20,
		Nullable:       true,
		DefaultIsEmpty: true,
		Collation:      newCollation,
	})
	_, err = testEngine.Exec(alterSQL)
	assert.NoError(t, err)

	_, err = testEngine.Insert(&TestCollateColumn{
		UserId: 1,
		Name:   "test1",
	})
	assert.NoError(t, err)
	_, err = testEngine.Insert(&TestCollateColumn{
		UserId: 1,
		Name:   "Test1",
	})
	assert.NoError(t, err)
}

type SyncWithOpts1 struct {
	Id        int64
	Index     int `xorm:"index"`
	Unique    int `xorm:"unique"`
	Group1    int `xorm:"index(ttt)"`
	Group2    int `xorm:"index(ttt)"`
	UniGroup1 int `xorm:"unique(lll)"`
	UniGroup2 int `xorm:"unique(lll)"`
}

func (*SyncWithOpts1) TableName() string {
	return "sync_with_opts"
}

type SyncWithOpts2 struct {
	Id        int64
	Index     int `xorm:"index"`
	Unique    int `xorm:""`
	Group1    int `xorm:"index(ttt)"`
	Group2    int `xorm:"index(ttt)"`
	UniGroup1 int `xorm:""`
	UniGroup2 int `xorm:"unique(lll)"`
}

func (*SyncWithOpts2) TableName() string {
	return "sync_with_opts"
}

type SyncWithOpts3 struct {
	Id        int64
	Index     int `xorm:""`
	Unique    int `xorm:"unique"`
	Group1    int `xorm:""`
	Group2    int `xorm:"index(ttt)"`
	UniGroup1 int `xorm:"unique(lll)"`
	UniGroup2 int `xorm:"unique(lll)"`
}

func (*SyncWithOpts3) TableName() string {
	return "sync_with_opts"
}

func TestSyncWithOptions(t *testing.T) {
	assert.NoError(t, PrepareEngine())

	// ignore indices and constrains
	result, err := testEngine.SyncWithOptions(xorm.SyncOptions{IgnoreIndices: true, IgnoreConstrains: true}, &SyncWithOpts1{})
	assert.NoError(t, err)
	assert.NotNil(t, result)
	assert.Len(t, getIndicesOfBeanFromDB(t, &SyncWithOpts1{}), 0)

	// only ignore constrains
	result, err = testEngine.SyncWithOptions(xorm.SyncOptions{IgnoreConstrains: true}, &SyncWithOpts2{})
	assert.NoError(t, err)
	assert.NotNil(t, result)
	indices := getIndicesOfBeanFromDB(t, &SyncWithOpts1{})
	assert.Len(t, indices, 2)
	assert.ElementsMatch(t, []string{"ttt", "index"}, getKeysFromMap(indices))

	// only ignore indices
	result, err = testEngine.SyncWithOptions(xorm.SyncOptions{IgnoreIndices: true}, &SyncWithOpts3{})
	assert.NoError(t, err)
	assert.NotNil(t, result)
	indices = getIndicesOfBeanFromDB(t, &SyncWithOpts1{})
	assert.Len(t, indices, 4)
	assert.ElementsMatch(t, []string{"ttt", "index", "unique", "lll"}, getKeysFromMap(indices))

	// only ignore drop indices
	result, err = testEngine.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, &SyncWithOpts3{})
	assert.NoError(t, err)
	assert.NotNil(t, result)
	indices = getIndicesOfBeanFromDB(t, &SyncWithOpts1{})
	assert.Len(t, indices, 4)
	assert.ElementsMatch(t, []string{"ttt", "index", "unique", "lll"}, getKeysFromMap(indices))

	tableInfoFromStruct, _ := testEngine.TableInfo(&SyncWithOpts1{})
	assert.ElementsMatch(t, getKeysFromMap(tableInfoFromStruct.Indexes), getKeysFromMap(getIndicesOfBeanFromDB(t, &SyncWithOpts1{})))
}

func getIndicesOfBeanFromDB(t *testing.T, bean any) map[string]*schemas.Index {
	dbm, err := testEngine.DBMetas()
	assert.NoError(t, err)

	tName := testEngine.TableName(bean)
	var tSchema *schemas.Table
	for _, t := range dbm {
		if t.Name == tName {
			tSchema = t
			break
		}
	}
	if !assert.NotNil(t, tSchema) {
		return nil
	}
	return tSchema.Indexes
}

func getKeysFromMap(m map[string]*schemas.Index) []string {
	var ss []string
	for k := range m {
		ss = append(ss, k)
	}
	return ss
}

func TestInspectForeignKeys(t *testing.T) {
	assert.NoError(t, PrepareEngine())

	defaultBaseTable := "CREATE TABLE %sbase_table (id INT PRIMARY KEY NOT NULL)"
	defaultConstraint := "CONSTRAINT satellite_table_base_table_id_fkey FOREIGN KEY (base_table_id) REFERENCES %sbase_table (id)"
	defaultSatelliteTable := "CREATE TABLE %ssatellite_table (id INT PRIMARY KEY NOT NULL, base_table_id INT, %s)"

	tests := []struct {
		name               string
		baseTable          string
		constraint         string
		satelliteTable     string
		preScript          []string
		tweakScript        []string
		postScript         []string
		skipDB             []schemas.DBType
		expectedForeignKey *schemas.ForeignKey
	}{
		{
			name: "basic match",
			expectedForeignKey: &schemas.ForeignKey{
				Name:            "satellite_table_base_table_id_fkey",
				SourceFieldName: "base_table_id",
				TargetTableName: "base_table",
				TargetFieldName: "id",
			},
		},
		{
			name:               "on delete cascade",
			constraint:         "CONSTRAINT satellite_table_base_table_id_fkey FOREIGN KEY (base_table_id) REFERENCES %sbase_table (id) ON DELETE CASCADE",
			expectedForeignKey: nil,
		},
		{
			name:               "on update cascade",
			constraint:         "CONSTRAINT satellite_table_base_table_id_fkey FOREIGN KEY (base_table_id) REFERENCES %sbase_table (id) ON UPDATE CASCADE",
			expectedForeignKey: nil,
		},
		{
			name:               "deferrable",
			constraint:         "CONSTRAINT satellite_table_base_table_id_fkey FOREIGN KEY (base_table_id) REFERENCES %sbase_table (id) DEFERRABLE",
			expectedForeignKey: nil,
			skipDB: []schemas.DBType{
				schemas.MYSQL,  // MySQL: no deferred constraints
				schemas.SQLITE, // SQLite: no API to retrieve deferrable info from FKs
			},
		},
		{
			name:               "initially deferred",
			constraint:         "CONSTRAINT satellite_table_base_table_id_fkey FOREIGN KEY (base_table_id) REFERENCES %sbase_table (id) DEFERRABLE INITIALLY DEFERRED",
			expectedForeignKey: nil,
			skipDB: []schemas.DBType{
				schemas.MYSQL,  // MySQL: no deferred constraints
				schemas.SQLITE, // SQLite: no API to retrieve deferrable info from FKs
			},
		},
		{
			name: "not valid",
			tweakScript: []string{
				"ALTER TABLE %ssatellite_table DROP CONSTRAINT satellite_table_base_table_id_fkey",
				"ALTER TABLE %[1]ssatellite_table ADD FOREIGN KEY (base_table_id) REFERENCES %[1]sbase_table (id) NOT VALID",
			},
			expectedForeignKey: nil,
			skipDB:             []schemas.DBType{schemas.MYSQL, schemas.SQLITE}, // `NOT VALID` is only part of PostgreSQL
		},
		{
			name:               "multiple columns",
			baseTable:          "CREATE TABLE %sbase_table (id_1 INT NOT NULL, id_2 INT NOT NULL, PRIMARY KEY (id_1, id_2))",
			satelliteTable:     "CREATE TABLE %ssatellite_table (id INT PRIMARY KEY NOT NULL, base_table_id_1 INT, base_table_id_2 INT, %s)",
			constraint:         "CONSTRAINT satellite_table_base_table_id_fkey FOREIGN KEY (base_table_id_1, base_table_id_2) REFERENCES %sbase_table (id_1, id_2)",
			expectedForeignKey: nil,
		},
		{
			name: "mismatched schema",
			preScript: []string{
				"CREATE SCHEMA test_schema_2",
				"CREATE TABLE test_schema_2.base_table (id INT PRIMARY KEY NOT NULL)",
			},
			constraint: "CONSTRAINT satellite_table_base_table_id_fkey FOREIGN KEY (base_table_id) REFERENCES test_schema_2.base_table (id) /* %s */",
			postScript: []string{
				"DROP SCHEMA test_schema_2 CASCADE",
			},
			expectedForeignKey: nil,
			skipDB:             []schemas.DBType{schemas.MYSQL, schemas.SQLITE}, // MySQL & SQLite: no schemas
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if slices.Contains(test.skipDB, testEngine.Dialect().URI().DBType) {
				t.SkipNow()
			}

			schemaMarker := ""
			if schema != nil && *schema != "" {
				schemaMarker = fmt.Sprintf("%s.", *schema)
			}

			// Do a cascade drop to ensure that tables can be cleaned up no matter what happens here
			defer func() {
				// all errors ignored...
				for _, sql := range test.postScript {
					_, _ = testEngine.Exec(sql)
				}
				if testEngine.Dialect().URI().DBType == schemas.SQLITE {
					_, _ = testEngine.Exec(fmt.Sprintf("DROP TABLE %ssatellite_table", schemaMarker))
					_, _ = testEngine.Exec(fmt.Sprintf("DROP TABLE %sbase_table", schemaMarker))
				} else {
					_, _ = testEngine.Exec(fmt.Sprintf("DROP TABLE %ssatellite_table CASCADE", schemaMarker))
					_, _ = testEngine.Exec(fmt.Sprintf("DROP TABLE %sbase_table CASCADE", schemaMarker))
				}
			}()

			for _, sql := range test.preScript {
				_, err := testEngine.Exec(sql)
				require.NoError(t, err, "executing preScript: %q", sql)
			}

			createBaseTable := test.baseTable
			if createBaseTable == "" {
				createBaseTable = defaultBaseTable
			}
			createBaseTable = fmt.Sprintf(createBaseTable, schemaMarker)

			createConstraint := test.constraint
			if createConstraint == "" {
				createConstraint = defaultConstraint
			}
			createConstraint = fmt.Sprintf(createConstraint, schemaMarker)

			createSatelliteTable := test.satelliteTable
			if createSatelliteTable == "" {
				createSatelliteTable = defaultSatelliteTable
			}
			createSatelliteTable = fmt.Sprintf(createSatelliteTable, schemaMarker, createConstraint)

			_, err := testEngine.Exec(createBaseTable)
			require.NoError(t, err, "executing createBaseTable: %q", createBaseTable)
			_, err = testEngine.Exec(createSatelliteTable)
			require.NoError(t, err, "executing createSatelliteTable: %q", createSatelliteTable)

			for _, sql := range test.tweakScript {
				_, err := testEngine.Exec(fmt.Sprintf(sql, schemaMarker))
				require.NoError(t, err, "executing tweakScript: %q", sql)
			}

			tables, err := testEngine.DBMetas()
			require.NoError(t, err)

			var baseTable, satelliteTable *schemas.Table
			for _, tbl := range tables {
				println(fmt.Sprintf("tbl.Name = %q", tbl.Name))
				switch tbl.Name {
				case "base_table":
					baseTable = tbl
				case "satellite_table":
					satelliteTable = tbl
				default:
					require.Fail(t, "unexpected table: %q", t.Name)
				}
			}
			require.NotNil(t, baseTable, "baseTable == nil")
			require.NotNil(t, satelliteTable, "satelliteTable == nil")

			require.Len(t, baseTable.ForeignKeys, 0, "expected 0 foreign keys")

			if test.expectedForeignKey == nil {
				assert.Len(t, satelliteTable.ForeignKeys, 0, "expected 0 foreign keys")
			} else {
				require.Len(t, satelliteTable.ForeignKeys, 1, "expected 1 foreign key")
				fk := satelliteTable.ForeignKeys[0]
				assert.EqualValues(t, *test.expectedForeignKey, fk)
			}
		})
	}
}

func TestSyncNewTableWithForeignKey(t *testing.T) {
	type BaseTable struct {
		Id int `xorm:"pk autoincr"`
	}

	type SatelliteTable struct {
		Id          int16 `xorm:"pk autoincr"`
		BaseTableId int   `xorm:"references(base_table,id)"`
	}

	assert.NoError(t, PrepareEngine())
	assertSync(t, new(BaseTable))
	assertSync(t, new(SatelliteTable))

	tables, err := testEngine.DBMetas()
	require.NoError(t, err)

	satelliteTableIdx := slices.IndexFunc(tables, func(t *schemas.Table) bool {
		return t.Name == "satellite_table"
	})
	require.NotEqual(t, -1, satelliteTableIdx)
	satelliteTableMeta := tables[satelliteTableIdx]

	satelliteTableInfo, err := testEngine.TableInfo(new(SatelliteTable))
	require.NoError(t, err)

	require.Len(t, satelliteTableInfo.ForeignKeys, 1, "TableInfo(SatelliteTable) must have 1 FK")
	require.Len(t, satelliteTableMeta.ForeignKeys, 1, "DBMetas()[SatelliteTable] must have 1 FK")
	assertFK := func(fk *schemas.ForeignKey) {
		assert.EqualValues(t, "base_table_id", fk.SourceFieldName, "SourceFieldName")
		assert.EqualValues(t, "base_table", fk.TargetTableName, "TargetTableName")
		assert.EqualValues(t, "id", fk.TargetFieldName, "TargetFieldName")
	}
	assertFK(&satelliteTableInfo.ForeignKeys[0])
	assertFK(&satelliteTableMeta.ForeignKeys[0])

	// An automatically managed foreign key index will be created as well:
	require.Len(t, satelliteTableInfo.Indexes, 1, "TableInfo(SatelliteTable) must have 1 Index")
	require.Len(t, satelliteTableMeta.Indexes, 1, "DBMetas()[SatelliteTable] must have 1 Index")
	require.Contains(t, satelliteTableInfo.Indexes, "base_table_id_fkey")
	require.Contains(t, satelliteTableMeta.Indexes, "base_table_id_fkey")
	assertIndex := func(fk *schemas.Index) {
		assert.EqualValues(t, true, fk.IsRegular, "IsRegular")
		assert.EqualValues(t, "base_table_id_fkey", fk.Name, "Name")
		assert.EqualValues(t, schemas.IndexType, fk.Type, "Type")
		assert.EqualValues(t, []string{"base_table_id"}, fk.Cols, "Cols")
		assert.EqualValues(t, true, fk.IsForeignKey, "IsForeignKey")
	}
	assertIndex(satelliteTableInfo.Indexes["base_table_id_fkey"])
	assertIndex(satelliteTableMeta.Indexes["base_table_id_fkey"])

	// Regression check: the new constraint should not affect reading columns from the table.
	require.Len(t, satelliteTableMeta.Columns(), 2)
	column := satelliteTableMeta.Columns()[0]
	assert.EqualValues(t, "id", column.Name)
	column = satelliteTableMeta.Columns()[1]
	assert.EqualValues(t, "base_table_id", column.Name)
}

type Sync1BaseTable struct {
	Id int `xorm:"pk autoincr"`
}

type Sync1SatelliteTable struct {
	Id          int16 `xorm:"pk autoincr"`
	BaseTableId int
	Index       string // field with a reserved name to ensure it is quoted
}

func (*Sync1BaseTable) TableName() string {
	return "sync_base_table"
}

func (*Sync1SatelliteTable) TableName() string {
	return "sync_satellite_table"
}

type Sync2BaseTable struct {
	Id int `xorm:"pk autoincr"`
}

type Sync2SatelliteTable struct {
	Id          int16  `xorm:"pk autoincr"`
	BaseTableId int    `xorm:"references(sync_base_table, id)"`
	Index       string // field with a reserved name to ensure it is quoted
}

func (*Sync2BaseTable) TableName() string {
	return "sync_base_table"
}

func (*Sync2SatelliteTable) TableName() string {
	return "sync_satellite_table"
}

func TestAlterTableForeignKeyAdd(t *testing.T) {
	assert.NoError(t, PrepareEngine())

	err := testEngine.Sync(new(Sync1BaseTable))
	require.NoError(t, err)
	err = testEngine.Sync(new(Sync1SatelliteTable))
	require.NoError(t, err)

	err = testEngine.AlterTable(new(Sync1SatelliteTable), schemas.AddForeignKey{
		ForeignKey: schemas.ForeignKey{
			Name:            "test_fk",
			SourceFieldName: "base_table_id",
			TargetTableName: "sync_base_table",
			TargetFieldName: "id",
		},
	})
	require.NoError(t, err)

	tables, err := testEngine.DBMetas()
	require.NoError(t, err)
	satelliteTableIdx := slices.IndexFunc(tables, func(t *schemas.Table) bool {
		return t.Name == "sync_satellite_table"
	})
	require.NotEqual(t, -1, satelliteTableIdx)
	satelliteTableMeta := tables[satelliteTableIdx]

	require.Len(t, satelliteTableMeta.ForeignKeys, 1)
	fk := satelliteTableMeta.ForeignKeys[0]
	assert.EqualValues(t, "base_table_id", fk.SourceFieldName, "SourceFieldName")
	assert.EqualValues(t, "sync_base_table", fk.TargetTableName, "TargetTableName")
	assert.EqualValues(t, "id", fk.TargetFieldName, "TargetFieldName")

	// Unlike Sync(), AlterTable won't tweak the indexes on the database table... unless the database does it itself
	// which we can't control (MySQL):
	if testEngine.Dialect().URI().DBType == schemas.MYSQL {
		require.Len(t, satelliteTableMeta.Indexes, 1, "DBMetas()[SatelliteTable] expected to have 1 Index")
		require.Contains(t, satelliteTableMeta.Indexes, "test_fk")
		index := satelliteTableMeta.Indexes["test_fk"]
		assert.EqualValues(t, false, index.IsRegular, "IsRegular")
		assert.EqualValues(t, "test_fk", index.Name, "Name")
		assert.EqualValues(t, schemas.IndexType, index.Type, "Type")
		assert.EqualValues(t, []string{"base_table_id"}, index.Cols, "Cols")
		assert.EqualValues(t, true, index.IsForeignKey, "IsForeignKey")
	} else {
		assert.Len(t, satelliteTableMeta.Indexes, 0, "DBMetas()[SatelliteTable] expected to have 0 Indexes")
	}

	// Regression check: the alter should leave a table with expected columns.
	require.Len(t, satelliteTableMeta.Columns(), 3)
	column := satelliteTableMeta.Columns()[0]
	assert.EqualValues(t, "id", column.Name)
	column = satelliteTableMeta.Columns()[1]
	assert.EqualValues(t, "base_table_id", column.Name)
	column = satelliteTableMeta.Columns()[2]
	assert.EqualValues(t, "index", column.Name)
}

func TestAlterTableForeignKeyDrop(t *testing.T) {
	assert.NoError(t, PrepareEngine())

	err := testEngine.Sync(new(Sync2BaseTable))
	require.NoError(t, err)
	err = testEngine.Sync(new(Sync2SatelliteTable))
	require.NoError(t, err)

	satelliteTableInfo, err := testEngine.TableInfo(new(Sync2SatelliteTable))
	require.NoError(t, err)

	err = testEngine.AlterTable(new(Sync2SatelliteTable), schemas.DropForeignKey{ForeignKey: satelliteTableInfo.ForeignKeys[0]})
	require.NoError(t, err)

	tables, err := testEngine.DBMetas()
	require.NoError(t, err)
	satelliteTableIdx := slices.IndexFunc(tables, func(t *schemas.Table) bool {
		return t.Name == "sync_satellite_table"
	})
	require.NotEqual(t, -1, satelliteTableIdx)
	satelliteTableMeta := tables[satelliteTableIdx]
	assert.Len(t, satelliteTableMeta.ForeignKeys, 0)

	// Sync will automatically create a foreign key and an index; AlterTable(DropForeignKey) will not touch the index:
	require.Len(t, satelliteTableMeta.Indexes, 1, "DBMetas()[SatelliteTable] expected to have 1 Index")
	require.Contains(t, satelliteTableMeta.Indexes, "base_table_id_fkey")
	index := satelliteTableMeta.Indexes["base_table_id_fkey"]
	assert.EqualValues(t, true, index.IsRegular, "IsRegular")
	assert.EqualValues(t, "base_table_id_fkey", index.Name, "Name")
	assert.EqualValues(t, schemas.IndexType, index.Type, "Type")
	assert.EqualValues(t, []string{"base_table_id"}, index.Cols, "Cols")
	assert.EqualValues(t, false, index.IsForeignKey, "IsForeignKey")

	// Regression check: the alter should leave a table with expected columns.
	require.Len(t, satelliteTableMeta.Columns(), 3)
	column := satelliteTableMeta.Columns()[0]
	assert.EqualValues(t, "id", column.Name)
	column = satelliteTableMeta.Columns()[1]
	assert.EqualValues(t, "base_table_id", column.Name)
	column = satelliteTableMeta.Columns()[2]
	assert.EqualValues(t, "index", column.Name)
}

func TestAlterTableForeignKeyDropAndAdd(t *testing.T) {
	assert.NoError(t, PrepareEngine())

	err := testEngine.Sync(new(Sync2BaseTable))
	require.NoError(t, err)
	err = testEngine.Sync(new(Sync2SatelliteTable))
	require.NoError(t, err)

	satelliteTableInfo, err := testEngine.TableInfo(new(Sync2SatelliteTable))
	require.NoError(t, err)

	// This operation is effectively "renaming" a foreign key by dropping it and creating another with a similar
	// definition but a new name.  This should work, but this test is implemented because this requires special support
	// in MySQL.
	err = testEngine.AlterTable(new(Sync2SatelliteTable),
		schemas.DropForeignKey{ForeignKey: satelliteTableInfo.ForeignKeys[0]},
		schemas.AddForeignKey{
			ForeignKey: schemas.ForeignKey{
				Name:            "test_fk",
				SourceFieldName: "base_table_id",
				TargetTableName: "sync_base_table",
				TargetFieldName: "id",
			},
		},
	)
	require.NoError(t, err)

	tables, err := testEngine.DBMetas()
	require.NoError(t, err)
	satelliteTableIdx := slices.IndexFunc(tables, func(t *schemas.Table) bool {
		return t.Name == "sync_satellite_table"
	})
	require.NotEqual(t, -1, satelliteTableIdx)
	satelliteTableMeta := tables[satelliteTableIdx]

	require.Len(t, satelliteTableMeta.ForeignKeys, 1)
	fk := satelliteTableMeta.ForeignKeys[0]

	// SQLite foreign keys don't have persisted names -- so for SQLite in this "rename FK" case we're OK with just
	// having no errors from multiple operations at once.
	if testEngine.Dialect().URI().DBType != schemas.SQLITE {
		assert.EqualValues(t, "test_fk", fk.Name, "Name")
	}
	assert.EqualValues(t, "base_table_id", fk.SourceFieldName, "SourceFieldName")
	assert.EqualValues(t, "sync_base_table", fk.TargetTableName, "TargetTableName")
	assert.EqualValues(t, "id", fk.TargetFieldName, "TargetFieldName")
}

func TestSyncForeignKeyCreate(t *testing.T) {
	assert.NoError(t, PrepareEngine())

	{
		err := testEngine.Sync(new(Sync1BaseTable))
		require.NoError(t, err)
		err = testEngine.Sync(new(Sync1SatelliteTable))
		require.NoError(t, err)

		tables, err := testEngine.DBMetas()
		require.NoError(t, err)

		satelliteTableIdx := slices.IndexFunc(tables, func(t *schemas.Table) bool {
			return t.Name == "sync_satellite_table"
		})
		require.NotEqual(t, -1, satelliteTableIdx)
		satelliteTableMeta := tables[satelliteTableIdx]

		satelliteTableInfo, err := testEngine.TableInfo(new(Sync1SatelliteTable))
		require.NoError(t, err)

		assert.Len(t, satelliteTableInfo.ForeignKeys, 0, "TableInfo(SatelliteTable) must have 0 FK")
		assert.Len(t, satelliteTableMeta.ForeignKeys, 0, "DBMetas()[SatelliteTable] must have 0 FK")
		assert.Len(t, satelliteTableInfo.Indexes, 0, "TableInfo(SatelliteTable) must have 0 Indexes")
		assert.Len(t, satelliteTableMeta.Indexes, 0, "DBMetas()[SatelliteTable] must have 0 Indexes")
	}

	// Put some data into the table so that we can ensure it isn't lost during the sync, because: (a) it's easy to
	// accidentally change these tests to use the test sync which drops tables and therefore doesn't test the table alter
	// as-expected, and (b) for SQLite we'll be doing this whole recreate table thing.
	_, err := testEngine.Insert(&Sync1BaseTable{Id: 123})
	require.NoError(t, err)
	_, err = testEngine.Insert(&Sync1SatelliteTable{Id: 1, BaseTableId: 123})
	require.NoError(t, err)

	{
		err := testEngine.Sync(new(Sync2BaseTable))
		require.NoError(t, err)
		err = testEngine.Sync(new(Sync2SatelliteTable))
		require.NoError(t, err)

		tables, err := testEngine.DBMetas()
		require.NoError(t, err)

		satelliteTableIdx := slices.IndexFunc(tables, func(t *schemas.Table) bool {
			return t.Name == "sync_satellite_table"
		})
		require.NotEqual(t, -1, satelliteTableIdx)
		satelliteTableMeta := tables[satelliteTableIdx]

		satelliteTableInfo, err := testEngine.TableInfo(new(Sync2SatelliteTable))
		require.NoError(t, err)

		require.Len(t, satelliteTableInfo.ForeignKeys, 1, "TableInfo(SatelliteTable) must have 1 FK")
		require.Len(t, satelliteTableMeta.ForeignKeys, 1, "DBMetas()[SatelliteTable] must have 1 FK")
		assertFK := func(fk *schemas.ForeignKey) {
			assert.EqualValues(t, "base_table_id", fk.SourceFieldName, "SourceFieldName")
			assert.EqualValues(t, "sync_base_table", fk.TargetTableName, "TargetTableName")
			assert.EqualValues(t, "id", fk.TargetFieldName, "TargetFieldName")
		}
		assertFK(&satelliteTableInfo.ForeignKeys[0])
		assertFK(&satelliteTableMeta.ForeignKeys[0])

		// An automatically managed foreign key index will be created as well:
		require.Len(t, satelliteTableInfo.Indexes, 1, "TableInfo(SatelliteTable) must have 1 Index")
		require.Len(t, satelliteTableMeta.Indexes, 1, "DBMetas()[SatelliteTable] must have 1 Index")
		require.Contains(t, satelliteTableInfo.Indexes, "base_table_id_fkey")
		require.Contains(t, satelliteTableMeta.Indexes, "base_table_id_fkey")
		assertIndex := func(fk *schemas.Index) {
			assert.EqualValues(t, true, fk.IsRegular, "IsRegular")
			assert.EqualValues(t, "base_table_id_fkey", fk.Name, "Name")
			assert.EqualValues(t, schemas.IndexType, fk.Type, "Type")
			assert.EqualValues(t, []string{"base_table_id"}, fk.Cols, "Cols")
			assert.EqualValues(t, true, fk.IsForeignKey, "IsForeignKey")
		}
		assertIndex(satelliteTableInfo.Indexes["base_table_id_fkey"])
		assertIndex(satelliteTableMeta.Indexes["base_table_id_fkey"])
	}

	var baseRecord Sync1BaseTable
	has, err := testEngine.ID(123).Get(&baseRecord)
	require.NoError(t, err)
	assert.True(t, has, "Sync1BaseTable[Id=123] must exist")
	assert.EqualValues(t, 123, baseRecord.Id)

	var satelliteRecord Sync2SatelliteTable
	has, err = testEngine.ID(1).Get(&satelliteRecord)
	require.NoError(t, err)
	assert.True(t, has, "Sync2SatelliteTable[Id=1] must exist")
	assert.EqualValues(t, 1, satelliteRecord.Id)
	assert.EqualValues(t, 123, satelliteRecord.BaseTableId)
}

func TestSyncDoesNotDropForeignKeys(t *testing.T) {
	assert.NoError(t, PrepareEngine())

	fkAndIndexAssertions := func() {
		tables, err := testEngine.DBMetas()
		require.NoError(t, err)

		satelliteTableIdx := slices.IndexFunc(tables, func(t *schemas.Table) bool {
			return t.Name == "sync_satellite_table"
		})
		require.NotEqual(t, -1, satelliteTableIdx)
		satelliteTableMeta := tables[satelliteTableIdx]

		satelliteTableInfo, err := testEngine.TableInfo(new(Sync2SatelliteTable))
		require.NoError(t, err)

		require.Len(t, satelliteTableInfo.ForeignKeys, 1, "TableInfo(SatelliteTable) must have 1 FK")
		require.Len(t, satelliteTableMeta.ForeignKeys, 1, "DBMetas()[SatelliteTable] must have 1 FK")
		assertFK := func(fk *schemas.ForeignKey) {
			assert.EqualValues(t, "base_table_id", fk.SourceFieldName, "SourceFieldName")
			assert.EqualValues(t, "sync_base_table", fk.TargetTableName, "TargetTableName")
			assert.EqualValues(t, "id", fk.TargetFieldName, "TargetFieldName")
		}
		assertFK(&satelliteTableInfo.ForeignKeys[0])
		assertFK(&satelliteTableMeta.ForeignKeys[0])

		// An automatically managed foreign key index will be created as well:
		require.Len(t, satelliteTableInfo.Indexes, 1, "TableInfo(SatelliteTable) must have 1 Index")
		require.Len(t, satelliteTableMeta.Indexes, 1, "DBMetas()[SatelliteTable] must have 1 Index")
		require.Contains(t, satelliteTableInfo.Indexes, "base_table_id_fkey")
		require.Contains(t, satelliteTableMeta.Indexes, "base_table_id_fkey")
		assertIndex := func(fk *schemas.Index) {
			assert.EqualValues(t, true, fk.IsRegular, "IsRegular")
			assert.EqualValues(t, "base_table_id_fkey", fk.Name, "Name")
			assert.EqualValues(t, schemas.IndexType, fk.Type, "Type")
			assert.EqualValues(t, []string{"base_table_id"}, fk.Cols, "Cols")
			assert.EqualValues(t, true, fk.IsForeignKey, "IsForeignKey")
		}
		assertIndex(satelliteTableInfo.Indexes["base_table_id_fkey"])
		assertIndex(satelliteTableMeta.Indexes["base_table_id_fkey"])
	}

	// Initial table creation and assertions.
	err := testEngine.Sync(new(Sync2BaseTable))
	require.NoError(t, err)
	err = testEngine.Sync(new(Sync2SatelliteTable))
	require.NoError(t, err)
	fkAndIndexAssertions()

	// Put some data into the table so that we can ensure it isn't lost during the sync, because: (a) it's easy to
	// accidentally change these tests to use the test sync which drops tables and therefore doesn't test the table alter
	// as-expected, and (b) for SQLite we'll be doing this whole recreate table thing.
	_, err = testEngine.Insert(&Sync1BaseTable{Id: 123})
	require.NoError(t, err)
	_, err = testEngine.Insert(&Sync1SatelliteTable{Id: 1, BaseTableId: 123})
	require.NoError(t, err)

	// Now Sync() to the table that does not have a `references` tag.  This should *not* delete the foreign key, the
	// references() automatic index, or (of course) the data in the tables.  By not deleting by default, xorm allows the
	// use of small focused struct during schema migrations with Sync operations, not needing to redeclare the whole
	// table.
	err = testEngine.Sync(new(Sync1BaseTable))
	require.NoError(t, err)
	err = testEngine.Sync(new(Sync1SatelliteTable))
	require.NoError(t, err)
	fkAndIndexAssertions()

	var baseRecord Sync1BaseTable
	has, err := testEngine.ID(123).Get(&baseRecord)
	require.NoError(t, err)
	assert.True(t, has, "Sync1BaseTable[Id=123] must exist")
	assert.EqualValues(t, 123, baseRecord.Id)

	var satelliteRecord Sync2SatelliteTable
	has, err = testEngine.ID(1).Get(&satelliteRecord)
	require.NoError(t, err)
	assert.True(t, has, "Sync2SatelliteTable[Id=1] must exist")
	assert.EqualValues(t, 1, satelliteRecord.Id)
	assert.EqualValues(t, 123, satelliteRecord.BaseTableId)
}

func TestSyncWithDropForeignKeys(t *testing.T) {
	assert.NoError(t, PrepareEngine())

	// Initial table creation and assertions.
	err := testEngine.Sync(new(Sync2BaseTable))
	require.NoError(t, err)
	err = testEngine.Sync(new(Sync2SatelliteTable))
	require.NoError(t, err)

	// Pre-check, make sure FKs are present.  This doesn't check all the details since that overlaps with other test
	// cases, but just makes sure the FK and Index are present so that they can be dropped.
	{
		tables, err := testEngine.DBMetas()
		require.NoError(t, err)

		satelliteTableIdx := slices.IndexFunc(tables, func(t *schemas.Table) bool {
			return t.Name == "sync_satellite_table"
		})
		require.NotEqual(t, -1, satelliteTableIdx)
		satelliteTableMeta := tables[satelliteTableIdx]

		require.Len(t, satelliteTableMeta.ForeignKeys, 1, "DBMetas()[SatelliteTable] must have 1 FK")
		require.Len(t, satelliteTableMeta.Indexes, 1, "DBMetas()[SatelliteTable] must have 1 Index")
	}

	// Put some data into the table so that we can ensure it isn't lost during the sync, because: (a) it's easy to
	// accidentally change these tests to use the test sync which drops tables and therefore doesn't test the table alter
	// as-expected, and (b) for SQLite we'll be doing this whole recreate table thing.
	_, err = testEngine.Insert(&Sync1BaseTable{Id: 123})
	require.NoError(t, err)
	_, err = testEngine.Insert(&Sync1SatelliteTable{Id: 1, BaseTableId: 123})
	require.NoError(t, err)

	// Now Sync() to the table that does not have a `references` tag.  This should *not* delete the foreign key, the
	// references() automatic index, or (of course) the data in the tables.  By not deleting by default, xorm allows the
	// use of small focused struct during schema migrations with Sync operations, not needing to redeclare the whole
	// table.
	err = testEngine.Sync(new(Sync1BaseTable))
	require.NoError(t, err)
	_, err = testEngine.SyncWithOptions(xorm.SyncOptions{DropForeignKeys: true}, new(Sync1SatelliteTable))
	require.NoError(t, err)

	// Post-check, make sure FK and Index are dropped.
	{
		tables, err := testEngine.DBMetas()
		require.NoError(t, err)

		satelliteTableIdx := slices.IndexFunc(tables, func(t *schemas.Table) bool {
			return t.Name == "sync_satellite_table"
		})
		require.NotEqual(t, -1, satelliteTableIdx)
		satelliteTableMeta := tables[satelliteTableIdx]

		require.Len(t, satelliteTableMeta.ForeignKeys, 0, "DBMetas()[SatelliteTable] must have 0 FKs")
		require.Len(t, satelliteTableMeta.Indexes, 0, "DBMetas()[SatelliteTable] must have 0 Indexes")
	}

	// Ensure data still exists as well.
	var baseRecord Sync1BaseTable
	has, err := testEngine.ID(123).Get(&baseRecord)
	require.NoError(t, err)
	assert.True(t, has, "Sync1BaseTable[Id=123] must exist")
	assert.EqualValues(t, 123, baseRecord.Id)

	var satelliteRecord Sync2SatelliteTable
	has, err = testEngine.ID(1).Get(&satelliteRecord)
	require.NoError(t, err)
	assert.True(t, has, "Sync2SatelliteTable[Id=1] must exist")
	assert.EqualValues(t, 1, satelliteRecord.Id)
	assert.EqualValues(t, 123, satelliteRecord.BaseTableId)
}

func TestAlterTableForeignKeyAddExistingViolation(t *testing.T) {
	assert.NoError(t, PrepareEngine())

	err := testEngine.Sync(new(Sync1BaseTable))
	require.NoError(t, err)
	err = testEngine.Sync(new(Sync1SatelliteTable))
	require.NoError(t, err)

	// Put some data into Sync1SatelliteTable that will be an invalid foreign key reference.
	_, err = testEngine.Insert(&Sync1SatelliteTable{Id: 1, BaseTableId: 321})
	require.NoError(t, err)

	err = testEngine.AlterTable(new(Sync1SatelliteTable), schemas.AddForeignKey{
		ForeignKey: schemas.ForeignKey{
			Name:            "test_fk",
			SourceFieldName: "base_table_id",
			TargetTableName: "sync_base_table",
			TargetFieldName: "id",
		},
	})
	require.NotNil(t, err)
	assert.ErrorIs(t, err, xorm.ErrForeignKeyViolation)
}

type AlterTableUniqueKey1 struct {
	Id   int `xorm:"pk autoincr"`
	Text string
}

type AlterTableUniqueKey2 struct {
	Id   int    `xorm:"pk autoincr"`
	Text string `xorm:"unique"`
}

func (*AlterTableUniqueKey1) TableName() string {
	return "alter_table_unique_key"
}

func (*AlterTableUniqueKey2) TableName() string {
	return "alter_table_unique_key"
}

func TestAlterTableUniqueKeyUniqueKeyAddViolation(t *testing.T) {
	assert.NoError(t, PrepareEngine())

	err := testEngine.Sync(new(AlterTableUniqueKey1))
	require.NoError(t, err)

	// Put some data into Sync1SatelliteTable that will be a unique key violation
	_, err = testEngine.Insert(&AlterTableUniqueKey1{Id: 1, Text: "repeat"})
	require.NoError(t, err)
	_, err = testEngine.Insert(&AlterTableUniqueKey1{Id: 2, Text: "repeat"})
	require.NoError(t, err)

	err = testEngine.Sync(new(AlterTableUniqueKey2))
	assert.ErrorIs(t, err, xorm.ErrUniqueConstraintViolation)
}

// Two SQLite errors have been encountered that this attempts to test: a) AlterTable was inspecting the database schema
// during the alter from another engine connection, causing "database schema is locked: main" errors, and b) can't
func TestAlterTableInTxWithSchemaModifications(t *testing.T) {
	assert.NoError(t, PrepareEngine())

	sess := testEngine.NewSession()
	defer sess.Close()
	sess = sess.StoreEngine("InnoDB")

	err := sess.Begin()
	require.NoError(t, err)

	err = sess.CreateTable(new(Sync1BaseTable))
	require.NoError(t, err)

	err = sess.CreateTable(new(Sync1SatelliteTable))
	require.NoError(t, err)

	err = sess.AlterTable(new(Sync1SatelliteTable), schemas.AddForeignKey{
		ForeignKey: schemas.ForeignKey{
			Name:            "test_fk",
			SourceFieldName: "base_table_id",
			TargetTableName: "sync_base_table",
			TargetFieldName: "id",
		},
	})
	assert.NoError(t, err)

	err = sess.Commit()
	require.NoError(t, err)
}

func TestAlterTableWithSessionTableName(t *testing.T) {
	assert.NoError(t, PrepareEngine())

	sess := testEngine.NewSession()
	defer sess.Close()
	sess = sess.StoreEngine("InnoDB")

	err := sess.Begin()
	require.NoError(t, err)

	err = sess.Table("real_base_table").CreateTable(new(Sync1BaseTable))
	require.NoError(t, err)

	err = sess.Table("real_satellite_table").CreateTable(new(Sync1SatelliteTable))
	require.NoError(t, err)

	err = sess.Table("real_satellite_table").AlterTable(new(Sync1SatelliteTable), schemas.AddForeignKey{
		ForeignKey: schemas.ForeignKey{
			Name:            "test_fk",
			SourceFieldName: "base_table_id",
			TargetTableName: "real_base_table",
			TargetFieldName: "id",
		},
	})
	assert.NoError(t, err)

	err = sess.Commit()
	require.NoError(t, err)
}

type Sync3SatelliteTable struct {
	Id            int16 `xorm:"pk autoincr"`
	BaseTableId   int
	AnotherField1 int64
	AnotherField2 string `xorm:"NOT NULL"`
}

func (*Sync3SatelliteTable) TableName() string {
	return "sync_satellite_table_2"
}

type Sync4SatelliteTable struct {
	BaseTableId int `xorm:"references(sync_base_table, id)"`
}

func (*Sync4SatelliteTable) TableName() string {
	return "sync_satellite_table_2"
}

// Similar to TestSyncForeignKeyCreate, but we're going to do a Sync with the struct Sync4SatelliteTable which only has
// the new foreign key field.  As Sync is intended to be additive, this should not remove the fields in
// Sync3SatelliteTable from the DB table.
func TestSyncPartialStructAddingForeignKey(t *testing.T) {
	assert.NoError(t, PrepareEngine())

	{
		err := testEngine.Sync(new(Sync1BaseTable))
		require.NoError(t, err)
		err = testEngine.Sync(new(Sync3SatelliteTable))
		require.NoError(t, err)

		tables, err := testEngine.DBMetas()
		require.NoError(t, err)

		satelliteTableIdx := slices.IndexFunc(tables, func(t *schemas.Table) bool {
			return t.Name == "sync_satellite_table_2"
		})
		require.NotEqual(t, -1, satelliteTableIdx)
		satelliteTableMeta := tables[satelliteTableIdx]

		satelliteTableInfo, err := testEngine.TableInfo(new(Sync1SatelliteTable))
		require.NoError(t, err)

		assert.Len(t, satelliteTableInfo.ForeignKeys, 0, "TableInfo(SatelliteTable) must have 0 FK")
		assert.Len(t, satelliteTableMeta.ForeignKeys, 0, "DBMetas()[SatelliteTable] must have 0 FK")
		assert.Len(t, satelliteTableInfo.Indexes, 0, "TableInfo(SatelliteTable) must have 0 Indexes")
		assert.Len(t, satelliteTableMeta.Indexes, 0, "DBMetas()[SatelliteTable] must have 0 Indexes")
	}

	// Put some data into the table so that we can ensure it isn't lost during the sync, because: (a) it's easy to
	// accidentally change these tests to use the test sync which drops tables and therefore doesn't test the table alter
	// as-expected, and (b) for SQLite we'll be doing this whole recreate table thing.
	_, err := testEngine.Insert(&Sync1BaseTable{Id: 123})
	require.NoError(t, err)
	_, err = testEngine.Insert(&Sync3SatelliteTable{
		Id:            1,
		BaseTableId:   123,
		AnotherField1: 567,
		AnotherField2: "890",
	})
	require.NoError(t, err)

	{
		err = testEngine.Sync(new(Sync4SatelliteTable))
		require.NoError(t, err)

		tables, err := testEngine.DBMetas()
		require.NoError(t, err)

		satelliteTableIdx := slices.IndexFunc(tables, func(t *schemas.Table) bool {
			return t.Name == "sync_satellite_table_2"
		})
		require.NotEqual(t, -1, satelliteTableIdx)
		satelliteTableMeta := tables[satelliteTableIdx]

		satelliteTableInfo, err := testEngine.TableInfo(new(Sync4SatelliteTable))
		require.NoError(t, err)

		require.Len(t, satelliteTableInfo.ForeignKeys, 1, "TableInfo(SatelliteTable) must have 1 FK")
		require.Len(t, satelliteTableMeta.ForeignKeys, 1, "DBMetas()[SatelliteTable] must have 1 FK")
		assertFK := func(fk *schemas.ForeignKey) {
			assert.EqualValues(t, "base_table_id", fk.SourceFieldName, "SourceFieldName")
			assert.EqualValues(t, "sync_base_table", fk.TargetTableName, "TargetTableName")
			assert.EqualValues(t, "id", fk.TargetFieldName, "TargetFieldName")
		}
		assertFK(&satelliteTableInfo.ForeignKeys[0])
		assertFK(&satelliteTableMeta.ForeignKeys[0])

		// An automatically managed foreign key index will be created as well:
		require.Len(t, satelliteTableInfo.Indexes, 1, "TableInfo(SatelliteTable) must have 1 Index")
		require.Len(t, satelliteTableMeta.Indexes, 1, "DBMetas()[SatelliteTable] must have 1 Index")
		require.Contains(t, satelliteTableInfo.Indexes, "base_table_id_fkey")
		require.Contains(t, satelliteTableMeta.Indexes, "base_table_id_fkey")
		assertIndex := func(fk *schemas.Index) {
			assert.EqualValues(t, true, fk.IsRegular, "IsRegular")
			assert.EqualValues(t, "base_table_id_fkey", fk.Name, "Name")
			assert.EqualValues(t, schemas.IndexType, fk.Type, "Type")
			assert.EqualValues(t, []string{"base_table_id"}, fk.Cols, "Cols")
			assert.EqualValues(t, true, fk.IsForeignKey, "IsForeignKey")
		}
		assertIndex(satelliteTableInfo.Indexes["base_table_id_fkey"])
		assertIndex(satelliteTableMeta.Indexes["base_table_id_fkey"])
	}

	var baseRecord Sync1BaseTable
	has, err := testEngine.ID(123).Get(&baseRecord)
	require.NoError(t, err)
	assert.True(t, has, "Sync1BaseTable[Id=123] must exist")
	assert.EqualValues(t, 123, baseRecord.Id)

	var satelliteRecord Sync3SatelliteTable
	has, err = testEngine.ID(1).Get(&satelliteRecord)
	require.NoError(t, err)
	assert.True(t, has, "Sync3SatelliteTable[Id=1] must exist")
	assert.EqualValues(t, 1, satelliteRecord.Id)
	assert.EqualValues(t, 123, satelliteRecord.BaseTableId)
	assert.EqualValues(t, 567, satelliteRecord.AnotherField1)
	assert.EqualValues(t, "890", satelliteRecord.AnotherField2)
}

type Sync5SatelliteTable struct {
	Id            int16 `xorm:"pk autoincr"`
	BaseTableId   int
	AnotherField1 int64
	AnotherField2 string `xorm:"NOT NULL"`
}

type Sync6SatelliteTable struct {
	BaseTableId          int `xorm:"references(sync_base_table, id)"`
	NewColumnBaseTableId int `xorm:"references(sync_base_table, id)"`
}

func (*Sync5SatelliteTable) TableName() string {
	return "sync_satellite_table_3"
}

func (*Sync6SatelliteTable) TableName() string {
	return "sync_satellite_table_3"
}

// Test adding a column with a foreign key on it via a partial-bean `Sync`.  Other tests added foreign keys to existing
// columns, but this has to both create the column and the FK.
func TestSyncPartialStructAddNewColumnWithForeignKey(t *testing.T) {
	assert.NoError(t, PrepareEngine())

	{
		err := testEngine.Sync(new(Sync1BaseTable))
		require.NoError(t, err)
		err = testEngine.Sync(new(Sync5SatelliteTable))
		require.NoError(t, err)

		tables, err := testEngine.DBMetas()
		require.NoError(t, err)

		satelliteTableIdx := slices.IndexFunc(tables, func(t *schemas.Table) bool {
			return t.Name == "sync_satellite_table_3"
		})
		require.NotEqual(t, -1, satelliteTableIdx)
		satelliteTableMeta := tables[satelliteTableIdx]
		println(fmt.Sprintf("satelliteTableMeta = %#v", satelliteTableMeta))

		satelliteTableInfo, err := testEngine.TableInfo(new(Sync1SatelliteTable))
		require.NoError(t, err)

		assert.Len(t, satelliteTableInfo.ForeignKeys, 0, "TableInfo(SatelliteTable) must have 0 FK")
		assert.Len(t, satelliteTableMeta.ForeignKeys, 0, "DBMetas()[SatelliteTable] must have 0 FK")
		assert.Len(t, satelliteTableInfo.Indexes, 0, "TableInfo(SatelliteTable) must have 0 Indexes")
		assert.Len(t, satelliteTableMeta.Indexes, 0, "DBMetas()[SatelliteTable] must have 0 Indexes")
	}

	// Put some data into the table so that we can ensure it isn't lost during the sync, because: (a) it's easy to
	// accidentally change these tests to use the test sync which drops tables and therefore doesn't test the table alter
	// as-expected, and (b) for SQLite we'll be doing this whole recreate table thing.
	_, err := testEngine.Insert(&Sync1BaseTable{Id: 123})
	require.NoError(t, err)
	_, err = testEngine.Insert(&Sync5SatelliteTable{
		Id:            1,
		BaseTableId:   123,
		AnotherField1: 567,
		AnotherField2: "890",
	})
	require.NoError(t, err)

	{
		err = testEngine.Sync(new(Sync6SatelliteTable))
		require.NoError(t, err)

		tables, err := testEngine.DBMetas()
		require.NoError(t, err)

		satelliteTableIdx := slices.IndexFunc(tables, func(t *schemas.Table) bool {
			return t.Name == "sync_satellite_table_3"
		})
		require.NotEqual(t, -1, satelliteTableIdx)
		satelliteTableMeta := tables[satelliteTableIdx]

		satelliteTableInfo, err := testEngine.TableInfo(new(Sync6SatelliteTable))
		require.NoError(t, err)

		require.Len(t, satelliteTableInfo.ForeignKeys, 2, "TableInfo(SatelliteTable) must have 1 FK")
		require.Len(t, satelliteTableMeta.ForeignKeys, 2, "DBMetas()[SatelliteTable] must have 1 FK")
		assertFK := func(fk *schemas.ForeignKey) {
			if fk.SourceFieldName != "base_table_id" && fk.SourceFieldName != "new_column_base_table_id" {
				assert.Failf(t, "invalid FK source field name", "source field name %q was not expected", fk.SourceFieldName)
			}
			assert.EqualValues(t, "sync_base_table", fk.TargetTableName, "TargetTableName")
			assert.EqualValues(t, "id", fk.TargetFieldName, "TargetFieldName")
		}
		assertFK(&satelliteTableInfo.ForeignKeys[0])
		assertFK(&satelliteTableInfo.ForeignKeys[1])
		assertFK(&satelliteTableMeta.ForeignKeys[0])
		assertFK(&satelliteTableMeta.ForeignKeys[1])

		// An automatically managed foreign key index will be created as well:
		require.Len(t, satelliteTableInfo.Indexes, 2, "TableInfo(SatelliteTable) must have 1 Index")
		require.Len(t, satelliteTableMeta.Indexes, 2, "DBMetas()[SatelliteTable] must have 1 Index")
		require.Contains(t, satelliteTableInfo.Indexes, "base_table_id_fkey")
		require.Contains(t, satelliteTableInfo.Indexes, "new_column_base_table_id_fkey")
		require.Contains(t, satelliteTableMeta.Indexes, "base_table_id_fkey")
		require.Contains(t, satelliteTableMeta.Indexes, "new_column_base_table_id_fkey")
		assertIndex := func(fk *schemas.Index) {
			assert.EqualValues(t, true, fk.IsRegular, "IsRegular")
			if fk.Name != "base_table_id_fkey" && fk.Name != "new_column_base_table_id_fkey" {
				assert.Failf(t, "unexpected index name", "name %q was not expected", fk.Name)
			}
			assert.EqualValues(t, schemas.IndexType, fk.Type, "Type")
			if !assert.ObjectsAreEqualValues([]string{"base_table_id"}, fk.Cols) &&
				!assert.ObjectsAreEqualValues([]string{"new_column_base_table_id"}, fk.Cols) {
				assert.Failf(t, "index cols is unexpected", "cols was %#v", fk.Cols)
			}
			assert.EqualValues(t, true, fk.IsForeignKey, "IsForeignKey")
		}
		assertIndex(satelliteTableInfo.Indexes["base_table_id_fkey"])
		assertIndex(satelliteTableMeta.Indexes["new_column_base_table_id_fkey"])
		assertIndex(satelliteTableInfo.Indexes["base_table_id_fkey"])
		assertIndex(satelliteTableMeta.Indexes["new_column_base_table_id_fkey"])
	}

	var baseRecord Sync1BaseTable
	has, err := testEngine.ID(123).Get(&baseRecord)
	require.NoError(t, err)
	assert.True(t, has, "Sync1BaseTable[Id=123] must exist")
	assert.EqualValues(t, 123, baseRecord.Id)

	var satelliteRecord Sync5SatelliteTable
	has, err = testEngine.ID(1).Get(&satelliteRecord)
	require.NoError(t, err)
	assert.True(t, has, "Sync5SatelliteTable[Id=1] must exist")
	assert.EqualValues(t, 1, satelliteRecord.Id)
	assert.EqualValues(t, 123, satelliteRecord.BaseTableId)
	assert.EqualValues(t, 567, satelliteRecord.AnotherField1)
	assert.EqualValues(t, "890", satelliteRecord.AnotherField2)
}

// Regression test for https://codeberg.org/forgejo/forgejo/issues/11608 -- adding a foreign key to a SQLite table
// requires a table rewrite, which requires that reading the column & index metadata is accurate to define the new
// replacement table.  The specific format of the table schema identified in this bug couldn't be read by xorm, leading
// to a rewrite to use native SQLite pragma statements to read table definitions.
func TestWeirdSQLiteDatabaseTable(t *testing.T) {
	assert.NoError(t, PrepareEngine())

	master := testEngine.(*xorm.Engine)
	if master.Dialect().URI().DBType != schemas.SQLITE {
		t.Skip()
		return
	}

	type ForeignKeyTarget struct {
		Id int64 `xorm:"pk autoincr"`
	}
	err := testEngine.Sync(&ForeignKeyTarget{})
	require.NoError(t, err)

	// Specific schema exactly from https://codeberg.org/forgejo/forgejo/issues/11608...
	_, err = master.Exec(
		"CREATE TABLE `access` (\n" +
			"  `id` integer NOT NULL PRIMARY KEY AUTOINCREMENT\n" +
			",  `user_id` integer DEFAULT NULL\n" +
			",  `repo_id` integer DEFAULT NULL\n" +
			",  `mode` integer DEFAULT NULL\n" +
			",  UNIQUE (`user_id`,`repo_id`)\n" +
			");\n",
	)
	require.NoError(t, err)

	type Access struct {
		UserId int64 `xorm:"UNIQUE(s) REFERENCES(foreign_key_target, id)"`
		RepoId int64 `xorm:"UNIQUE(s)"`
	}
	_, err = testEngine.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, &Access{})
	require.NoError(t, err)
}

// TestTableRecreateWithForeignKeys is a regression test to verify that syncing a table that
// is referenced by another table's foreign key succeeds when the change requires a full table
// recreate on sqlite.
func TestTableRecreateWithForeignKeys(t *testing.T) {
	require.NoError(t, PrepareEngine())

	if testEngine.Dialect().URI().DBType != schemas.SQLITE {
		t.Skip()
		return
	}

	type FkTarget struct {
		Id int64 `xorm:"pk autoincr"`
	}

	type FkParent struct {
		Id      int64 `xorm:"pk autoincr"`
		OtherId int64 `xorm:"NOT NULL references(fk_target, id)"`
	}

	// Not all new columns necessitate a recreate+copy. Adding a new FK does.
	type FkTargetV2 struct {
		Id     int64         `xorm:"pk autoincr"`
		SelfId sql.NullInt64 `xorm:"DEFAULT NULL references(fk_target, id)"`
	}

	require.NoError(t, testEngine.Sync(&FkTarget{}, &FkParent{}))

	_, err := testEngine.Insert(&FkTarget{Id: 1})
	require.NoError(t, err)
	_, err = testEngine.Insert(&FkParent{OtherId: 1})
	require.NoError(t, err)

	_, err = testEngine.Table("fk_target").SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, &FkTargetV2{})
	require.NoError(t, err)

	// Making sure the foreign key constraint actually works afterwards.
	_, err = testEngine.Table("fk_target").Delete(&FkTarget{Id: 1})
	require.Error(t, err)

	count, err := testEngine.Count(&FkParent{})
	require.NoError(t, err)
	assert.EqualValues(t, 1, count)

	count, err = testEngine.Table("fk_target").Count(&FkTargetV2{})
	require.NoError(t, err)
	assert.EqualValues(t, 1, count)
}
