Repository: sdv-dev/SDV

Issue:
Metadata visualization doesn't indicate which columns are sequence key or sequence index
### Environment Details

* SDV version: 1.18

### Error Description

The diagram generated using `metadata.visualize()` doesn't indicate which columns are the sequence key or the sequence index.

### Steps to reproduce

Metadata diagram in [PARSynthesizer demo notebook ](https://colab.research.google.com/drive/1YLk2uwn8yrSRPy0soEeJwu8Hdk_tGTlE) showcases this.

The sequence index is the `Date` column and the sequence key is the `Symbol` column.

![Image](https://github.com/user-attachments/assets/3eb9c0ac-2b7e-41d9-bae7-c92202b3d69b)

Discussion:

Solution patch:
diff --git a/sdv/metadata/multi_table.py b/sdv/metadata/multi_table.py
index de1412f4..5c2b40f7 100644
--- a/sdv/metadata/multi_table.py
+++ b/sdv/metadata/multi_table.py
@@ -972,6 +972,26 @@ class MultiTableMetadata:
 
         return MultiTableMetadata.load_from_dict(anonymized_metadata)
 
+    def _get_table_info(self, table_name, show_table_details):
+        node_info = {}
+        table_meta = self.tables[table_name]
+
+        if show_table_details in ['full', 'summarized']:
+            node_info['primary_key'] = f'Primary key: {table_meta.primary_key}'
+            if table_meta.sequence_key:
+                node_info['sequence_key'] = f'Sequence key: {table_meta.sequence_key}'
+            if table_meta.sequence_index:
+                node_info['sequence_index'] = f'Sequence index: {table_meta.sequence_index}'
+
+        if show_table_details == 'full':
+            node_info['columns'] = create_columns_node(table_meta.columns)
+        elif show_table_details == 'summarized':
+            node_info['columns'] = create_summarized_columns_node(table_meta.columns)
+        elif show_table_details is None:
+            return
+
+        return node_info
+
     def visualize(
         self, show_table_details='full', show_relationship_labels=True, output_filepath=None
     ):
@@ -1017,22 +1037,9 @@ class MultiTableMetadata:
 
         nodes = {}
         edges = []
-        if show_table_details == 'full':
-            for table_name, table_meta in self.tables.items():
-                nodes[table_name] = {
-                    'columns': create_columns_node(table_meta.columns),
-                    'primary_key': f'Primary key: {table_meta.primary_key}',
-                }
-
-        elif show_table_details == 'summarized':
-            for table_name, table_meta in self.tables.items():
-                nodes[table_name] = {
-                    'columns': create_summarized_columns_node(table_meta.columns),
-                    'primary_key': f'Primary key: {table_meta.primary_key}',
-                }
 
-        elif show_table_details is None:
-            nodes = {table_name: None for table_name in self.tables}
+        for table_name in self.tables.keys():
+            nodes[table_name] = self._get_table_info(table_name, show_table_details)
 
         for relationship in self.relationships:
             parent = relationship.get('parent_table_name')
@@ -1053,11 +1060,18 @@ class MultiTableMetadata:
         for table, info in nodes.items():
             if show_table_details:
                 foreign_keys = r'\l'.join(info.get('foreign_keys', []))
-                keys = r'\l'.join([info['primary_key'], foreign_keys])
-                if foreign_keys:
-                    label = rf'{{{table}|{info["columns"]}\l|{keys}\l}}'
-                else:
-                    label = rf'{{{table}|{info["columns"]}\l|{keys}}}'
+                keys = r'\l'.join(
+                    filter(
+                        bool,
+                        [
+                            info.get('primary_key'),
+                            info.get('sequence_key'),
+                            info.get('sequence_index'),
+                            foreign_keys,
+                        ],
+                    )
+                )
+                label = rf'{{{table}|{info["columns"]}\l|{keys}\l}}'
 
             else:
                 label = f'{table}'


Test patch:
diff --git a/tests/integration/metadata/test_visualization.py b/tests/integration/metadata/test_visualization.py
index 23a7aee1..ddbb720a 100644
--- a/tests/integration/metadata/test_visualization.py
+++ b/tests/integration/metadata/test_visualization.py
@@ -1,10 +1,27 @@
 import pandas as pd
 
+from sdv.datasets.demo import download_demo
 from sdv.metadata.metadata import Metadata
 from sdv.multi_table.hma import HMASynthesizer
 from sdv.single_table.copulas import GaussianCopulaSynthesizer
 
 
+def test_visualize_graph_for_sequential_data():
+    """Test visualization has sequence key and index with sequential data."""
+    # Setup
+    _, metadata = download_demo(modality='sequential', dataset_name='nasdaq100_2019')
+
+    # Run
+    graph = metadata.visualize()
+
+    # Assert
+    assert 'Sequence index' in graph.source
+    assert 'Sequence key' in graph.source
+    assert 'Primary key' in graph.source
+    assert 'nasdaq100_2019' in graph.source
+    assert 'relationships' not in graph.source
+
+
 def test_visualize_graph_for_single_table():
     """Test it runs when a column name contains symbols."""
     # Setup
diff --git a/tests/unit/metadata/test_metadata.py b/tests/unit/metadata/test_metadata.py
index 5a4398c8..73560702 100644
--- a/tests/unit/metadata/test_metadata.py
+++ b/tests/unit/metadata/test_metadata.py
@@ -826,3 +826,36 @@ class TestMetadataClass:
         # Assert
         metadata._get_anonymized_dict.assert_called_once()
         mock_load_from_dict.assert_called_once_with({})
+
+    @patch('sdv.metadata.multi_table.visualize_graph')
+    def test_visualize_with_sequence_key_and_index(self, visualize_graph_mock):
+        """Test the ``visualize`` method with sequence key and index"""
+        # Setup
+        metadata_dict = {
+            'tables': {
+                'nasdaq100_2019': {
+                    'columns': {
+                        'Symbol': {'sdtype': 'id', 'regex_format': '[A-Z]{4}'},
+                        'Date': {'sdtype': 'datetime', 'datetime_format': '%Y-%m-%d'},
+                    },
+                    'sequence_index': 'Date',
+                    'sequence_key': 'Symbol',
+                }
+            },
+            'relationships': [],
+            'METADATA_SPEC_VERSION': 'V1',
+        }
+        metadata = Metadata.load_from_dict(metadata_dict)
+
+        # Run
+        metadata.visualize('full', True)
+
+        # Assert
+        expected_label = (
+            '{nasdaq100_2019|Symbol : id\\lDate : datetime\\l|Primary key: None\\l'
+            'Sequence key: Symbol\\lSequence index: Date\\l}'
+        )
+        expected_nodes = {
+            'nasdaq100_2019': expected_label,
+        }
+        visualize_graph_mock.assert_called_once_with(expected_nodes, [], None)
diff --git a/tests/unit/metadata/test_multi_table.py b/tests/unit/metadata/test_multi_table.py
index d075c273..1d129c48 100644
--- a/tests/unit/metadata/test_multi_table.py
+++ b/tests/unit/metadata/test_multi_table.py
@@ -3337,3 +3337,35 @@ class TestMultiTableMetadata:
         # Run and Assert
         with pytest.raises(InvalidMetadataError, match=error_msg):
             metadata.validate_data(data)
+
+    @patch('sdv.metadata.multi_table.create_summarized_columns_node')
+    @patch('sdv.metadata.multi_table.create_columns_node')
+    def test__get_table_info(self, mock_columns_node, mock_summarized_columns_node):
+        """Test that the `_get_table_info` method."""
+        # Setup
+        mock_columns_node.return_value = 'column'
+        mock_summarized_columns_node.return_value = 'column'
+        metadata = MultiTableMetadata()
+        table = Mock()
+        table.primary_key = 'primary_key'
+        table.sequence_key = None
+        table.sequence_index = None
+        table_all = Mock()
+        table_all.primary_key = 'primary_key'
+        table_all.sequence_index = 'sequence_index'
+        table_all.sequence_key = 'sequence_key'
+        metadata.tables = {'table': table, 'table_all': table_all}
+
+        # Run
+        result = metadata._get_table_info('table', show_table_details='full')
+        result_all = metadata._get_table_info('table_all', show_table_details='summarized')
+        result_no_info = metadata._get_table_info('table_all', show_table_details=None)
+
+        # Assert
+        assert result['primary_key'] == 'Primary key: primary_key'
+        assert 'sequence_index' not in result
+        assert 'sequence_key' not in result
+        assert result_all['primary_key'] == 'Primary key: primary_key'
+        assert result_all['sequence_key'] == 'Sequence key: sequence_key'
+        assert result_all['sequence_index'] == 'Sequence index: sequence_index'
+        assert result_no_info is None