mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-07 19:11:39 +08:00
✨ feat(data-grid): 支持无主键表安全编辑
- 定位策略:新增主键、唯一索引和 Oracle ROWID 三类安全行定位能力 - 查询编辑器:简单单表 SELECT 自动补充隐藏定位列,复杂结果保持只读 - 表预览:无主键表可通过唯一索引或 Oracle ROWID 安全编辑 - 提交流程:移除无主键整行 WHERE fallback,隐藏定位列不参与展示和写入 - 后端保护:Oracle、MySQL、PostgreSQL 更新删除必须恰好影响 1 行 - 测试覆盖:补充 QueryEditor、DataViewer、DataGrid 和 ApplyChanges 相关用例 Refs #419
This commit is contained in:
@@ -24,8 +24,16 @@ var (
|
||||
)
|
||||
|
||||
type oracleRecordingState struct {
|
||||
mu sync.Mutex
|
||||
execArgs [][]driver.NamedValue
|
||||
mu sync.Mutex
|
||||
execQueries []string
|
||||
execArgs [][]driver.NamedValue
|
||||
rowsAffected int64
|
||||
}
|
||||
|
||||
func (s *oracleRecordingState) snapshotExecQueries() []string {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return append([]string(nil), s.execQueries...)
|
||||
}
|
||||
|
||||
func (s *oracleRecordingState) snapshotExecArgs() [][]driver.NamedValue {
|
||||
@@ -63,11 +71,12 @@ func (c *oracleRecordingConn) Close() error { return nil }
|
||||
|
||||
func (c *oracleRecordingConn) Begin() (driver.Tx, error) { return oracleRecordingTx{}, nil }
|
||||
|
||||
func (c *oracleRecordingConn) ExecContext(_ context.Context, _ string, args []driver.NamedValue) (driver.Result, error) {
|
||||
func (c *oracleRecordingConn) ExecContext(_ context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
|
||||
c.state.mu.Lock()
|
||||
defer c.state.mu.Unlock()
|
||||
c.state.execQueries = append(c.state.execQueries, query)
|
||||
c.state.execArgs = append(c.state.execArgs, append([]driver.NamedValue(nil), args...))
|
||||
return driver.RowsAffected(1), nil
|
||||
return driver.RowsAffected(c.state.rowsAffected), nil
|
||||
}
|
||||
|
||||
func (c *oracleRecordingConn) QueryContext(_ context.Context, query string, _ []driver.NamedValue) (driver.Rows, error) {
|
||||
@@ -126,7 +135,7 @@ func openOracleRecordingDB(t *testing.T) (*sql.DB, *oracleRecordingState) {
|
||||
oracleRecordingDriverMu.Lock()
|
||||
oracleRecordingDriverSeq++
|
||||
dsn := fmt.Sprintf("oracle-recording-%d", oracleRecordingDriverSeq)
|
||||
state := &oracleRecordingState{}
|
||||
state := &oracleRecordingState{rowsAffected: 1}
|
||||
oracleRecordingDriverStates[dsn] = state
|
||||
oracleRecordingDriverMu.Unlock()
|
||||
|
||||
@@ -145,6 +154,82 @@ func openOracleRecordingDB(t *testing.T) (*sql.DB, *oracleRecordingState) {
|
||||
return dbConn, state
|
||||
}
|
||||
|
||||
func TestOracleApplyChangesReturnsErrorWhenUpdateMatchesNoRows(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbConn, state := openOracleRecordingDB(t)
|
||||
state.rowsAffected = 0
|
||||
oracleDB := &OracleDB{conn: dbConn}
|
||||
|
||||
changes := connection.ChangeSet{
|
||||
Updates: []connection.UpdateRow{{
|
||||
Keys: map[string]interface{}{
|
||||
"ID": 7,
|
||||
},
|
||||
Values: map[string]interface{}{
|
||||
"NAME": "new-name",
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
err := oracleDB.ApplyChanges("MYCIMLED.EDC_LOG", changes)
|
||||
if err == nil {
|
||||
t.Fatal("期望更新未匹配到行时返回错误,实际为 nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "更新未生效") {
|
||||
t.Fatalf("错误信息应提示更新未生效,实际=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOracleApplyChangesReturnsErrorWhenUpdateAffectsMultipleRows(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbConn, state := openOracleRecordingDB(t)
|
||||
state.rowsAffected = 2
|
||||
oracleDB := &OracleDB{conn: dbConn}
|
||||
|
||||
changes := connection.ChangeSet{
|
||||
Updates: []connection.UpdateRow{{
|
||||
Keys: map[string]interface{}{
|
||||
"ID": 7,
|
||||
},
|
||||
Values: map[string]interface{}{
|
||||
"NAME": "new-name",
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
err := oracleDB.ApplyChanges("MYCIMLED.EDC_LOG", changes)
|
||||
if err == nil {
|
||||
t.Fatal("期望更新影响多行时返回错误,实际为 nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "影响了 2 行") {
|
||||
t.Fatalf("错误信息应提示影响多行,实际=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOracleApplyChangesReturnsErrorWhenDeleteAffectsMultipleRows(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbConn, state := openOracleRecordingDB(t)
|
||||
state.rowsAffected = 2
|
||||
oracleDB := &OracleDB{conn: dbConn}
|
||||
|
||||
changes := connection.ChangeSet{
|
||||
Deletes: []map[string]interface{}{{
|
||||
"STATUS": "stale",
|
||||
}},
|
||||
}
|
||||
|
||||
err := oracleDB.ApplyChanges("MYCIMLED.EDC_LOG", changes)
|
||||
if err == nil {
|
||||
t.Fatal("期望删除影响多行时返回错误,实际为 nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "影响了 2 行") {
|
||||
t.Fatalf("错误信息应提示影响多行,实际=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOracleApplyChangesNormalizesTemporalStringsForUpdate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -181,3 +266,87 @@ func TestOracleApplyChangesNormalizesTemporalStringsForUpdate(t *testing.T) {
|
||||
t.Fatalf("日期主键字段应绑定为 time.Time,实际=%#v(%T)", args[1].Value, args[1].Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOracleApplyChangesUsesUnquotedRowIDLocator(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbConn, state := openOracleRecordingDB(t)
|
||||
oracleDB := &OracleDB{conn: dbConn}
|
||||
|
||||
changes := connection.ChangeSet{
|
||||
LocatorStrategy: "oracle-rowid",
|
||||
Updates: []connection.UpdateRow{{
|
||||
Keys: map[string]interface{}{
|
||||
"ROWID": "AAAA",
|
||||
},
|
||||
Values: map[string]interface{}{
|
||||
"NAME": "new-name",
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
if err := oracleDB.ApplyChanges("MYCIMLED.EDC_LOG", changes); err != nil {
|
||||
t.Fatalf("ApplyChanges 返回错误: %v", err)
|
||||
}
|
||||
|
||||
executions := state.snapshotExecQueries()
|
||||
if len(executions) != 1 {
|
||||
t.Fatalf("期望执行 1 条更新,实际 %d 条", len(executions))
|
||||
}
|
||||
query := executions[0]
|
||||
if !strings.Contains(query, "ROWID = :2") {
|
||||
t.Fatalf("ROWID 定位条件不正确: %s", query)
|
||||
}
|
||||
if strings.Contains(query, "\"ROWID\" =") {
|
||||
t.Fatalf("ROWID 不应被当作普通列引用: %s", query)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMySQLApplyChangesReturnsErrorWhenUpdateAffectsMultipleRows(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbConn, state := openOracleRecordingDB(t)
|
||||
state.rowsAffected = 2
|
||||
mysqlDB := &MySQLDB{conn: dbConn}
|
||||
|
||||
changes := connection.ChangeSet{
|
||||
Updates: []connection.UpdateRow{{
|
||||
Keys: map[string]interface{}{
|
||||
"id": 7,
|
||||
},
|
||||
Values: map[string]interface{}{
|
||||
"name": "new-name",
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
err := mysqlDB.ApplyChanges("users", changes)
|
||||
if err == nil {
|
||||
t.Fatal("期望 MySQL 更新影响多行时返回错误,实际为 nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "影响了 2 行") {
|
||||
t.Fatalf("错误信息应提示影响多行,实际=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostgresApplyChangesReturnsErrorWhenDeleteAffectsMultipleRows(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dbConn, state := openOracleRecordingDB(t)
|
||||
state.rowsAffected = 2
|
||||
postgresDB := &PostgresDB{conn: dbConn}
|
||||
|
||||
changes := connection.ChangeSet{
|
||||
Deletes: []map[string]interface{}{{
|
||||
"id": 7,
|
||||
}},
|
||||
}
|
||||
|
||||
err := postgresDB.ApplyChanges("public.users", changes)
|
||||
if err == nil {
|
||||
t.Fatal("期望 PostgreSQL 删除影响多行时返回错误,实际为 nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "影响了 2 行") {
|
||||
t.Fatalf("错误信息应提示影响多行,实际=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user