The API doc on CampaignMember states:
Each record has a unique ID, and must contain either a ContactId or a
LeadId, but can't contain both.
I observed the following in my PROD org using this query:
select campaign.name, name, leadId, lead.firstname, lead.lastname, lead.isConverted,
contactId, contact.firstname, contact.lastname
from CampaignMember
where LeadId != null and ContactId != null and createdDate = THIS_MONTH
I had 29 hits. Everyone of them had Lead.isConverted = true
The doc gets even weirder:
Supported Calls For API version 15.0 and earlier:create(), delete(),
describeLayout(), describeSObjects(), getDeleted(), getUpdated(),
query(), retrieve(), update(), upsert()
For API version 16.0 and later: upsert()
but this can't be right - I just did the query above in V35.0
There is also this statement under Usage
Standard fields from a Contact or Lead are associated with the
CampaignMember object but you can’t query them directly. To include a
lead’s Phone in your query, for example, query the field from the Lead
object.
SELECT Id, (SELECT Phone FROM Lead) FROM CampaignMember
This syntax doesn't work at all. Standard dot notation syntax worked just fine.
I'm not a regular user of these objects so someone else may have a better answer but I can see why you are confused.
Each record has a unique ID, and must contain either a ContactId or a LeadId, but can't contain both
?LeadId
orContactId
but never both together).