package postgres import ( "context" "errors" "reflect" "strings" "testing" "time" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/layercompositions" ) func TestLayerCompositionAdapterScopesReadsAndUsesOptimisticSave(t *testing.T) { now := time.Date(2026, 9, 2, 8, 0, 0, 0, time.UTC) row := []any{"job-1", "owner", "base", true, []byte(`[{"id":"layer-1","assetId":"asset-1","name":"主体","visible":true,"box":[100,100,900,900],"rotation":0,"source":"provider"}]`), int64(2), now, now} querier := &jobQuerier{rows: &jobRows{rows: [][]any{row}}} database := NewDatabase(Config{Backend: BackendPostgres}, querier) composition, found, err := database.FindLayerComposition(context.Background(), "owner", "job-1") if err != nil || !found || composition.Version != 2 || len(composition.Layers) != 1 { t.Fatalf("composition=%#v found=%v err=%v", composition, found, err) } if querier.query != FindLayerCompositionSQL || !reflect.DeepEqual(querier.args, []any{"owner", "job-1"}) { t.Fatalf("query=%q args=%#v", querier.query, querier.args) } savedRow := append([]any(nil), row...) savedRow[5] = int64(3) querier.rows = &jobRows{rows: [][]any{savedRow}} saved, err := database.SaveLayerComposition(context.Background(), layercompositions.Composition{ JobID: "job-1", OwnerID: "owner", BaseAssetID: "base", BaseVisible: true, Layers: []layercompositions.Layer{{ID: "layer-1", AssetID: "asset-1", Name: "主体", Visible: true, Box: layercompositions.Box{100, 100, 900, 900}, Source: "provider"}}, }, 2) if err != nil || saved.Version != 3 { t.Fatalf("saved=%#v err=%v", saved, err) } if querier.query != SaveLayerCompositionSQL || len(querier.args) != 6 || querier.args[5] != int64(2) { t.Fatalf("query=%q args=%#v", querier.query, querier.args) } for _, fragment := range []string{"WITH updated AS", "WHERE $6::bigint = 0", "version = seedream_layer_compositions.version + 1", "AND version = $6::bigint", "ON CONFLICT (job_id) DO NOTHING"} { if !strings.Contains(SaveLayerCompositionSQL, fragment) { t.Fatalf("upsert omits %q", fragment) } } } func TestLayerCompositionSaveSQLKeepsExistingVersionedUpdatesReachable(t *testing.T) { // A top-level INSERT ... SELECT ... WHERE expectedVersion = 0 filters the // source row before ON CONFLICT can run. In that shape every normal update // (expectedVersion > 0) returns no row and is reported as a false conflict. if strings.Contains(SaveLayerCompositionSQL, "SELECT $1::text, $2::text, $3::text, $4::boolean, $5::jsonb, 1, now(), now()\nWHERE $6::bigint = 0\nON CONFLICT") { t.Fatal("versioned updates are unreachable because the INSERT source is filtered before ON CONFLICT") } for _, fragment := range []string{ "WITH updated AS", "version = $6::bigint", "$6::bigint > 0", "WHERE $6::bigint = 0", "UNION ALL", } { if !strings.Contains(SaveLayerCompositionSQL, fragment) { t.Fatalf("save query does not preserve both update and create paths; missing %q", fragment) } } } func TestLayerCompositionAdapterReturnsConflictWhenVersionUpdateMatchesNoRow(t *testing.T) { querier := &jobQuerier{rows: &jobRows{}} database := NewDatabase(Config{Backend: BackendPostgres}, querier) _, err := database.SaveLayerComposition(context.Background(), layercompositions.Composition{JobID: "job-1", OwnerID: "owner", BaseAssetID: "base", Layers: []layercompositions.Layer{}}, 4) if !errors.Is(err, layercompositions.ErrConflict) { t.Fatalf("err=%v", err) } }