Compare commits
No commits in common. "main" and "v1.5.9" have entirely different histories.
@ -1,11 +1,5 @@
|
|||||||
# 变更记录 - @go/db
|
# 变更记录 - @go/db
|
||||||
|
|
||||||
## [1.5.11] - 2026-08-22
|
|
||||||
- SQLite schema 同步支持删除定义中已经移除的字段,并同步处理影子删除表。
|
|
||||||
|
|
||||||
## [1.5.10] - 2026-08-13
|
|
||||||
- 修复 schema 同步中的字段转义解析。
|
|
||||||
|
|
||||||
## [1.5.9] - 2026-07-18
|
## [1.5.9] - 2026-07-18
|
||||||
- **依赖校验修复**: 升级 `redis` 至 `v1.5.11`,并将 `redigo` 固定为稳定版 `v1.9.3`,移除撤回版本。
|
- **依赖校验修复**: 升级 `redis` 至 `v1.5.11`,并将 `redigo` 固定为稳定版 `v1.9.3`,移除撤回版本。
|
||||||
- **依赖对齐**: 升级 `cast` 至 `v1.5.4`、`crypto` 至 `v1.5.4`、`encoding` 至 `v1.5.5`、`id` 至 `v1.5.6`。
|
- **依赖对齐**: 升级 `cast` 至 `v1.5.4`、`crypto` 至 `v1.5.4`、`encoding` 至 `v1.5.5`、`id` 至 `v1.5.6`。
|
||||||
|
|||||||
122
Schema.go
122
Schema.go
@ -5,7 +5,6 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"apigo.cc/go/cast"
|
"apigo.cc/go/cast"
|
||||||
@ -20,39 +19,6 @@ type SchemaGroup struct {
|
|||||||
var fieldSpliter = regexp.MustCompile(`\s+`)
|
var fieldSpliter = regexp.MustCompile(`\s+`)
|
||||||
var wnMatcher = regexp.MustCompile(`^([a-zA-Z]+)([0-9]+)$`)
|
var wnMatcher = regexp.MustCompile(`^([a-zA-Z]+)([0-9]+)$`)
|
||||||
|
|
||||||
func splitSchemaField(line string) []string {
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
if line == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if line[0] != '"' {
|
|
||||||
return fieldSpliter.Split(line, 10)
|
|
||||||
}
|
|
||||||
for i, escaped := 1, false; i < len(line); i++ {
|
|
||||||
if escaped {
|
|
||||||
escaped = false
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if line[i] == '\\' {
|
|
||||||
escaped = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if line[i] == '"' {
|
|
||||||
name, err := strconv.Unquote(line[:i+1])
|
|
||||||
if err != nil {
|
|
||||||
return fieldSpliter.Split(line, 10)
|
|
||||||
}
|
|
||||||
rest := strings.TrimSpace(line[i+1:])
|
|
||||||
parts := []string{name}
|
|
||||||
if rest != "" {
|
|
||||||
parts = append(parts, fieldSpliter.Split(rest, 9)...)
|
|
||||||
}
|
|
||||||
return parts
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fieldSpliter.Split(line, 10)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseField 解析单行字段描述
|
// ParseField 解析单行字段描述
|
||||||
func ParseField(line string) TableField {
|
func ParseField(line string) TableField {
|
||||||
lc := strings.SplitN(line, "//", 2)
|
lc := strings.SplitN(line, "//", 2)
|
||||||
@ -62,7 +28,7 @@ func ParseField(line string) TableField {
|
|||||||
comment = strings.TrimSpace(lc[1])
|
comment = strings.TrimSpace(lc[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
a := splitSchemaField(line)
|
a := fieldSpliter.Split(line, 10)
|
||||||
field := TableField{
|
field := TableField{
|
||||||
Name: a[0],
|
Name: a[0],
|
||||||
Type: "",
|
Type: "",
|
||||||
@ -343,6 +309,8 @@ func (db *DB) Sync(desc string) error {
|
|||||||
return outErr
|
return outErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// CheckTable 检查并同步单个表结构
|
// CheckTable 检查并同步单个表结构
|
||||||
func (db *DB) CheckTable(table *TableStruct) error {
|
func (db *DB) CheckTable(table *TableStruct) error {
|
||||||
fieldSets := make([]string, 0)
|
fieldSets := make([]string, 0)
|
||||||
@ -581,9 +549,6 @@ func (db *DB) CheckTable(table *TableStruct) error {
|
|||||||
for keyId := range oldIndexes {
|
for keyId := range oldIndexes {
|
||||||
if keyId != "PRIMARY" && !isPostgres && strings.ToLower(keySetFields[keyId]) != strings.ToLower(oldIndexes[keyId]) {
|
if keyId != "PRIMARY" && !isPostgres && strings.ToLower(keySetFields[keyId]) != strings.ToLower(oldIndexes[keyId]) {
|
||||||
if strings.HasPrefix(db.Config.Type, "sqlite") {
|
if strings.HasPrefix(db.Config.Type, "sqlite") {
|
||||||
if table.Name == "_Table" || table.Name == "_Field" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(keyId, "sqlite_autoindex_") {
|
if strings.HasPrefix(keyId, "sqlite_autoindex_") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -649,12 +614,7 @@ func (db *DB) CheckTable(table *TableStruct) error {
|
|||||||
|
|
||||||
for oldFieldName := range oldFields {
|
for oldFieldName := range oldFields {
|
||||||
if !newFieldExists[oldFieldName] {
|
if !newFieldExists[oldFieldName] {
|
||||||
if strings.HasPrefix(db.Config.Type, "sqlite") {
|
if !strings.HasPrefix(db.Config.Type, "sqlite") && !isPostgres {
|
||||||
if table.Name == "_Table" || table.Name == "_Field" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
actions = append(actions, "ALTER TABLE "+db.Quote(table.Name)+" DROP COLUMN "+db.Quote(oldFieldName))
|
|
||||||
} else if !isPostgres {
|
|
||||||
actions = append(actions, "DROP COLUMN "+db.Quote(oldFieldName))
|
actions = append(actions, "DROP COLUMN "+db.Quote(oldFieldName))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -770,44 +730,44 @@ func (db *DB) CheckTable(table *TableStruct) error {
|
|||||||
return res.Error
|
return res.Error
|
||||||
}
|
}
|
||||||
_ = tx.Commit()
|
_ = tx.Commit()
|
||||||
}
|
|
||||||
|
|
||||||
if len(ftsFields) > 0 && strings.HasPrefix(db.Config.Type, "sqlite") {
|
|
||||||
ftsTableName := table.Name + "_fts"
|
|
||||||
ftsInfo := db.Query("SELECT \"name\" FROM \"sqlite_master\" WHERE \"type\"='table' AND \"name\"='" + ftsTableName + "'").MapOnR1()
|
|
||||||
if ftsInfo["name"] == nil {
|
|
||||||
// 创建 FTS 虚拟表
|
|
||||||
db.Exec(fmt.Sprintf("CREATE VIRTUAL TABLE \"%s\" USING fts5(%s, tokenize='unicode61')", ftsTableName, strings.Join(ftsFields, ", ")))
|
|
||||||
|
|
||||||
idField := "id"
|
|
||||||
if len(pks) > 0 {
|
|
||||||
idField = pks[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// AI Trigger
|
|
||||||
newFtsFields := make([]string, 0, len(ftsFields))
|
|
||||||
for _, f := range ftsFields {
|
|
||||||
newFtsFields = append(newFtsFields, "new."+f)
|
|
||||||
}
|
|
||||||
aiSql := fmt.Sprintf("CREATE TRIGGER IF NOT EXISTS \"%s_ai\" AFTER INSERT ON \"%s\" BEGIN INSERT INTO \"%s\"(rowid, %s) VALUES (new.%s, %s); END;",
|
|
||||||
ftsTableName, table.Name, ftsTableName, strings.Join(ftsFields, ", "), idField, strings.Join(newFtsFields, ", "))
|
|
||||||
db.Exec(aiSql)
|
|
||||||
|
|
||||||
// AD Trigger
|
|
||||||
adSql := fmt.Sprintf("CREATE TRIGGER IF NOT EXISTS \"%s_ad\" AFTER DELETE ON \"%s\" BEGIN DELETE FROM \"%s\" WHERE rowid = old.%s; END;",
|
|
||||||
ftsTableName, table.Name, ftsTableName, idField)
|
|
||||||
db.Exec(adSql)
|
|
||||||
|
|
||||||
// AU Trigger
|
|
||||||
updateSets := make([]string, 0, len(ftsFields))
|
|
||||||
for _, f := range ftsFields {
|
|
||||||
updateSets = append(updateSets, fmt.Sprintf("%s = new.%s", f, f))
|
|
||||||
}
|
|
||||||
auSql := fmt.Sprintf("CREATE TRIGGER IF NOT EXISTS \"%s_au\" AFTER UPDATE ON \"%s\" BEGIN UPDATE \"%s\" SET %s WHERE rowid = old.%s; END;",
|
|
||||||
ftsTableName, table.Name, ftsTableName, strings.Join(updateSets, ", "), idField)
|
|
||||||
db.Exec(auSql)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if len(ftsFields) > 0 && strings.HasPrefix(db.Config.Type, "sqlite") {
|
||||||
|
ftsTableName := table.Name + "_fts"
|
||||||
|
ftsInfo := db.Query("SELECT \"name\" FROM \"sqlite_master\" WHERE \"type\"='table' AND \"name\"='" + ftsTableName + "'").MapOnR1()
|
||||||
|
if ftsInfo["name"] == nil {
|
||||||
|
// 创建 FTS 虚拟表
|
||||||
|
db.Exec(fmt.Sprintf("CREATE VIRTUAL TABLE \"%s\" USING fts5(%s, tokenize='unicode61')", ftsTableName, strings.Join(ftsFields, ", ")))
|
||||||
|
|
||||||
|
idField := "id"
|
||||||
|
if len(pks) > 0 {
|
||||||
|
idField = pks[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI Trigger
|
||||||
|
newFtsFields := make([]string, 0, len(ftsFields))
|
||||||
|
for _, f := range ftsFields {
|
||||||
|
newFtsFields = append(newFtsFields, "new."+f)
|
||||||
|
}
|
||||||
|
aiSql := fmt.Sprintf("CREATE TRIGGER IF NOT EXISTS \"%s_ai\" AFTER INSERT ON \"%s\" BEGIN INSERT INTO \"%s\"(rowid, %s) VALUES (new.%s, %s); END;",
|
||||||
|
ftsTableName, table.Name, ftsTableName, strings.Join(ftsFields, ", "), idField, strings.Join(newFtsFields, ", "))
|
||||||
|
db.Exec(aiSql)
|
||||||
|
|
||||||
|
// AD Trigger
|
||||||
|
adSql := fmt.Sprintf("CREATE TRIGGER IF NOT EXISTS \"%s_ad\" AFTER DELETE ON \"%s\" BEGIN DELETE FROM \"%s\" WHERE rowid = old.%s; END;",
|
||||||
|
ftsTableName, table.Name, ftsTableName, idField)
|
||||||
|
db.Exec(adSql)
|
||||||
|
|
||||||
|
// AU Trigger
|
||||||
|
updateSets := make([]string, 0, len(ftsFields))
|
||||||
|
for _, f := range ftsFields {
|
||||||
|
updateSets = append(updateSets, fmt.Sprintf("%s = new.%s", f, f))
|
||||||
|
}
|
||||||
|
auSql := fmt.Sprintf("CREATE TRIGGER IF NOT EXISTS \"%s_au\" AFTER UPDATE ON \"%s\" BEGIN UPDATE \"%s\" SET %s WHERE rowid = old.%s; END;",
|
||||||
|
ftsTableName, table.Name, ftsTableName, strings.Join(updateSets, ", "), idField)
|
||||||
|
db.Exec(auSql)
|
||||||
|
}
|
||||||
|
}
|
||||||
SYNC_SHADOW:
|
SYNC_SHADOW:
|
||||||
if table.ShadowDelete && !strings.HasSuffix(table.Name, "_deleted") {
|
if table.ShadowDelete && !strings.HasSuffix(table.Name, "_deleted") {
|
||||||
table.HasShadowTable = true
|
table.HasShadowTable = true
|
||||||
|
|||||||
@ -8,13 +8,6 @@ import (
|
|||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseFieldQuotedName(t *testing.T) {
|
|
||||||
field := db.ParseField(`"Display Name 中文" v255 I // label`)
|
|
||||||
if field.Name != "Display Name 中文" || field.Type != "varchar(255)" || field.Index != "index" || field.Comment != "label" {
|
|
||||||
t.Fatalf("unexpected quoted field: %#v", field)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSchemaSync(t *testing.T) {
|
func TestSchemaSync(t *testing.T) {
|
||||||
dbPath := "test_schema.db"
|
dbPath := "test_schema.db"
|
||||||
dbInst := db.GetDB("sqlite://"+dbPath, nil)
|
dbInst := db.GetDB("sqlite://"+dbPath, nil)
|
||||||
@ -42,24 +35,6 @@ test_table SD // Test table with shadow delete
|
|||||||
if res.IntOnR1C1() != 1 {
|
if res.IntOnR1C1() != 1 {
|
||||||
t.Fatal("Shadow delete failed")
|
t.Fatal("Shadow delete failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
withoutStatus := `== Default ==
|
|
||||||
test_table SD // Test table with shadow delete
|
|
||||||
id AI // ID
|
|
||||||
name v50 U
|
|
||||||
autoVersion ubi // Version
|
|
||||||
`
|
|
||||||
if err := dbInst.Sync(withoutStatus); err != nil {
|
|
||||||
t.Fatal("Sync removed field:", err)
|
|
||||||
}
|
|
||||||
for _, tableName := range []string{"test_table", "test_table_deleted"} {
|
|
||||||
fields := dbInst.Query("PRAGMA table_info(" + dbInst.Quote(tableName) + ")").MapResults()
|
|
||||||
for _, field := range fields {
|
|
||||||
if field["name"] == "status" {
|
|
||||||
t.Fatalf("removed field remains in physical table %s", tableName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAutoDetectShadow(t *testing.T) {
|
func TestAutoDetectShadow(t *testing.T) {
|
||||||
@ -82,3 +57,4 @@ func TestAutoDetectShadow(t *testing.T) {
|
|||||||
t.Fatal("Auto-detect shadow delete failed")
|
t.Fatal("Auto-detect shadow delete failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4
TEST.md
4
TEST.md
@ -1,9 +1,5 @@
|
|||||||
# @go/db 测试报告
|
# @go/db 测试报告
|
||||||
|
|
||||||
## v1.5.11 验证
|
|
||||||
- `TestSchemaSync` 覆盖 SQLite 主表与影子删除表的字段移除。
|
|
||||||
- `go test -v ./...` 与 `go test -bench=. ./...` 通过。
|
|
||||||
|
|
||||||
## v1.5.9 验证
|
## v1.5.9 验证
|
||||||
- `go list -m all`:模块图使用 `redis v1.5.11` 与稳定版 `redigo v1.9.3`。
|
- `go list -m all`:模块图使用 `redis v1.5.11` 与稳定版 `redigo v1.9.3`。
|
||||||
- `go test -v ./...`:通过完整测试。
|
- `go test -v ./...`:通过完整测试。
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user