Skip to content

Commit 96a2804

Browse files
Add primary key recommendation for tables with UNIQUE NOT NULL columns (#2985)
1 parent 76082c6 commit 96a2804

File tree

10 files changed

+547
-142
lines changed

10 files changed

+547
-142
lines changed

yb-voyager/cmd/analyzeSchema.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1152,8 +1152,12 @@ func checkerMisc(detectPerfOptimizationIssues bool) {
11521152
if detectPerfOptimizationIssues {
11531153
fkIssues := parserIssueDetector.DetectMissingForeignKeyIndexes()
11541154

1155+
// Detect PK recommendation when UNIQUE+NOT NULL exists but PK is missing
1156+
pkRecs := parserIssueDetector.DetectPrimaryKeyRecommendations()
1157+
11551158
// Convert QueryIssues to AnalyzeSchemaIssues and add to report
1156-
for _, issue := range fkIssues {
1159+
issues := append(fkIssues, pkRecs...)
1160+
for _, issue := range issues {
11571161
analyzeIssue := convertIssueInstanceToAnalyzeIssue(issue, "", false, true)
11581162
schemaAnalysisReport.Issues = append(schemaAnalysisReport.Issues, analyzeIssue)
11591163
}

yb-voyager/cmd/assessMigrationCommand.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,6 +1044,7 @@ func fetchUnsupportedPGFeaturesFromSchemaReport(schemaAnalysisReport utils.Schem
10441044
unsupportedFeatures = append(unsupportedFeatures, getUnsupportedFeaturesFromSchemaAnalysisReport(queryissue.HOTSPOTS_ON_TIMESTAMP_PK_UK_ISSUE, "", queryissue.HOTSPOTS_ON_TIMESTAMP_PK_UK, schemaAnalysisReport, false))
10451045
unsupportedFeatures = append(unsupportedFeatures, getUnsupportedFeaturesFromSchemaAnalysisReport(queryissue.FOREIGN_KEY_DATATYPE_MISMATCH_ISSUE_NAME, "", queryissue.FOREIGN_KEY_DATATYPE_MISMATCH, schemaAnalysisReport, false))
10461046
unsupportedFeatures = append(unsupportedFeatures, getUnsupportedFeaturesFromSchemaAnalysisReport(queryissue.MISSING_FOREIGN_KEY_INDEX_ISSUE_NAME, "", queryissue.MISSING_FOREIGN_KEY_INDEX, schemaAnalysisReport, false))
1047+
unsupportedFeatures = append(unsupportedFeatures, getUnsupportedFeaturesFromSchemaAnalysisReport(queryissue.MISSING_PRIMARY_KEY_WHEN_UNIQUE_NOT_NULL_ISSUE_NAME, "", queryissue.MISSING_PRIMARY_KEY_WHEN_UNIQUE_NOT_NULL, schemaAnalysisReport, false))
10471048

10481049
return lo.Filter(unsupportedFeatures, func(f UnsupportedFeature, _ int) bool {
10491050
return len(f.Objects) > 0

yb-voyager/cmd/callhome_payload_builder.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,26 @@ func anonymizeIssueDetailsForCallhome(details map[string]interface{}) map[string
258258
} else {
259259
anonymizedDetails[key] = value
260260
}
261+
} else if key == "PrimaryKeyColumnOptions" {
262+
if pkOptions, ok := value.([][]string); ok {
263+
var anonymizedOptions [][]string
264+
for _, option := range pkOptions {
265+
var anonymizedOption []string
266+
for _, columnName := range option {
267+
anonymizedColumn, err := anonymizer.AnonymizeQualifiedColumnName(columnName)
268+
if err != nil {
269+
log.Warnf("failed to anonymize PK column name %s: %v", columnName, err)
270+
anonymizedOption = append(anonymizedOption, "column_xxx")
271+
} else {
272+
anonymizedOption = append(anonymizedOption, anonymizedColumn)
273+
}
274+
}
275+
anonymizedOptions = append(anonymizedOptions, anonymizedOption)
276+
}
277+
anonymizedDetails[key] = anonymizedOptions
278+
} else {
279+
anonymizedDetails[key] = value
280+
}
261281
} else {
262282
// Non-sensitive keys are kept as-is
263283
anonymizedDetails[key] = value

yb-voyager/src/query/queryissue/constants.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,11 @@ Note: If the table is created as colocated, this hotspot concern can safely be i
473473

474474
MISSING_FOREIGN_KEY_INDEX_ISSUE_NAME = "Missing index on foreign key columns"
475475
MISSING_FOREIGN_KEY_INDEX_DESCRIPTION = "Foreign key columns do not have a proper index. The index must include all foreign key columns as leading columns (either in exact order, any permutation, or as a prefix of a composite index). This can cause performance issues during DML operations on the referenced table."
476+
477+
// Recommend PK when UNIQUE + all NOT NULL but no PK exists
478+
MISSING_PRIMARY_KEY_WHEN_UNIQUE_NOT_NULL = "MISSING_PRIMARY_KEY_WHEN_UNIQUE_NOT_NULL"
479+
MISSING_PRIMARY_KEY_WHEN_UNIQUE_NOT_NULL_ISSUE_NAME = "Missing primary key for table when unique and not null columns exist"
480+
MISSING_PRIMARY_KEY_WHEN_UNIQUE_NOT_NULL_DESCRIPTION = "YugabyteDB's table storage is index-oriented. So, a table without a primary key is not viable in YugabyteDB. If you don't specify a primary key, YugabyteDB will use the internal ybrowid column as PRIMARY KEY and the table will be sharded on ybrowid HASH. However, if there are candidate primary keys (unique + NOT NULL), it is recommended to define them as a Primary Key to avoid secondary index structures."
476481
)
477482

478483
// Object types

yb-voyager/src/query/queryissue/detectors_ddl.go

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ func (d *TableIssueDetector) DetectIssues(obj queryparser.DDLObject) ([]QueryIss
129129
obj.GetObjectType(),
130130
table.GetObjectName(),
131131
c.Columns,
132-
d.columnMetadata,
132+
d.tablesMetadata,
133133
&issues,
134134
)
135135

@@ -144,7 +144,7 @@ func (d *TableIssueDetector) DetectIssues(obj queryparser.DDLObject) ([]QueryIss
144144
))
145145
}
146146

147-
if c.ConstraintType == queryparser.FOREIGN_CONSTR_TYPE && d.partitionedTablesMap[c.ReferencedTable] {
147+
if c.ConstraintType == queryparser.FOREIGN_CONSTR_TYPE && d.getPartitionedTablesMap()[c.ReferencedTable] {
148148
issues = append(issues, NewForeignKeyReferencesPartitionedTableIssue(
149149
TABLE_OBJECT_TYPE,
150150
table.GetObjectName(),
@@ -285,9 +285,13 @@ func (d *TableIssueDetector) DetectIssues(obj queryparser.DDLObject) ([]QueryIss
285285
return issues, nil
286286
}
287287

288-
func detectForeignKeyDatatypeMismatch(objectType string, objectName string, columnList []string, columnMetadata map[string]map[string]*ColumnMetadata, issues *[]QueryIssue) {
288+
func detectForeignKeyDatatypeMismatch(objectType string, objectName string, columnList []string, tablesMetadata map[string]*TableMetadata, issues *[]QueryIssue) {
289289
for _, col := range columnList {
290-
colMetadata, ok := columnMetadata[objectName][col]
290+
tm, ok := tablesMetadata[objectName]
291+
if !ok {
292+
continue
293+
}
294+
colMetadata, ok := tm.Columns[col]
291295
if !ok || !colMetadata.IsForeignKey {
292296
continue
293297
}
@@ -1038,7 +1042,7 @@ func (aid *AlterTableIssueDetector) DetectIssues(obj queryparser.DDLObject) ([]Q
10381042
}
10391043

10401044
if alter.ConstraintType == queryparser.FOREIGN_CONSTR_TYPE &&
1041-
aid.partitionedTablesMap[alter.ConstraintReferencedTable] {
1045+
aid.getPartitionedTablesMap()[alter.ConstraintReferencedTable] {
10421046
//FK constraint references partitioned table
10431047
issues = append(issues, NewForeignKeyReferencesPartitionedTableIssue(
10441048
TABLE_OBJECT_TYPE,
@@ -1054,14 +1058,14 @@ func (aid *AlterTableIssueDetector) DetectIssues(obj queryparser.DDLObject) ([]Q
10541058
obj.GetObjectType(),
10551059
alter.GetObjectName(),
10561060
alter.ConstraintColumns,
1057-
aid.columnMetadata,
1061+
aid.tablesMetadata,
10581062
&issues,
10591063
)
10601064

10611065
}
10621066

10631067
if alter.ConstraintType == queryparser.PRIMARY_CONSTR_TYPE &&
1064-
aid.partitionedTablesMap[alter.GetObjectName()] {
1068+
aid.getPartitionedTablesMap()[alter.GetObjectName()] {
10651069
issues = append(issues, NewAlterTableAddPKOnPartiionIssue(
10661070
obj.GetObjectType(),
10671071
alter.GetObjectName(),
@@ -1180,7 +1184,7 @@ func (tid *TriggerIssueDetector) DetectIssues(obj queryparser.DDLObject) ([]Quer
11801184
))
11811185
}
11821186

1183-
if trigger.IsBeforeRowTrigger() && tid.partitionedTablesMap[trigger.GetTableName()] {
1187+
if trigger.IsBeforeRowTrigger() && tid.getPartitionedTablesMap()[trigger.GetTableName()] {
11841188
issues = append(issues, NewBeforeRowOnPartitionTableIssue(
11851189
obj.GetObjectType(),
11861190
trigger.GetObjectName(),

yb-voyager/src/query/queryissue/issues_perf.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,20 @@ func NewMissingForeignKeyIndexIssue(objectType string, objectName string, sqlSta
236236

237237
return newQueryIssue(issue, objectType, objectName, sqlStatement, details)
238238
}
239+
240+
// NewMissingPrimaryKeyWhenUniqueNotNullIssue returns a recommendation to add a PK when a UNIQUE constraint's columns are all NOT NULL
241+
var missingPrimaryKeyWhenUniqueNotNullIssue = issue.Issue{
242+
Name: MISSING_PRIMARY_KEY_WHEN_UNIQUE_NOT_NULL_ISSUE_NAME,
243+
Type: MISSING_PRIMARY_KEY_WHEN_UNIQUE_NOT_NULL,
244+
Impact: constants.IMPACT_LEVEL_1,
245+
Description: MISSING_PRIMARY_KEY_WHEN_UNIQUE_NOT_NULL_DESCRIPTION,
246+
DocsLink: "https://docs.yugabyte.com/preview/yugabyte-voyager/known-issues/postgresql/#missing-primary-key-when-unique-not-null",
247+
}
248+
249+
func NewMissingPrimaryKeyWhenUniqueNotNullIssue(objectType string, objectName string, options [][]string) QueryIssue {
250+
issue := missingPrimaryKeyWhenUniqueNotNullIssue
251+
details := map[string]interface{}{
252+
"PrimaryKeyColumnOptions": options,
253+
}
254+
return newQueryIssue(issue, objectType, objectName, "", details)
255+
}

0 commit comments

Comments
 (0)