Skip to content

Commit a913127

Browse files
author
James Robinson
authored
Eng 5335 sql cell describe to enumerate constraints (#68)
1 parent 24eb8dd commit a913127

File tree

4 files changed

+102
-8
lines changed

4 files changed

+102
-8
lines changed

noteable_magics/sql/meta_commands.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -455,9 +455,14 @@ def run(self, invoked_as: str, args: List[str]) -> Tuple[DataFrame, bool]:
455455

456456
display(HTML('\n'.join(html_buf)))
457457
else:
458-
# Is a table. Let's go get indices, foreign keys. If meaningful dataframe returned, transform to
458+
# Is a table. Let's go get indices, foreign keys, other table constraints.
459+
# If meaningful dataframe returned for any of these, transform to
459460
# HTML for presentation (DEX only expects at most a single DF display()ed per cell) and display it.
460-
for secondary_function in (index_dataframe, foreignkeys_dataframe):
461+
for secondary_function in (
462+
index_dataframe,
463+
foreignkeys_dataframe,
464+
constraints_dataframe,
465+
):
461466
secondary_df = secondary_function(inspector, relation_name, schema)
462467
if len(secondary_df):
463468
display(secondary_dataframe_to_html(secondary_df))
@@ -478,6 +483,32 @@ def _split_schema_table(schema_table: str) -> Tuple[Optional[str], str]:
478483
return (schema, table)
479484

480485

486+
def constraints_dataframe(
487+
inspector: SchemaStrippingInspector, table_name: str, schema: Optional[str]
488+
) -> DataFrame:
489+
"""Transform results from inspector.get_check_constraints() into a single dataframe for display() purposes"""
490+
491+
names: List[str] = []
492+
definitions: List[str] = []
493+
494+
constraint_dicts: List[dict] = inspector.get_check_constraints(table_name, schema)
495+
496+
for constraint_dict in sorted(constraint_dicts, key=lambda d: d['name']):
497+
names.append(constraint_dict['name'])
498+
definitions.append(constraint_dict['sqltext'])
499+
500+
df = DataFrame(
501+
{
502+
'Constraint': names,
503+
'Definition': definitions,
504+
}
505+
)
506+
507+
title = f'Table <code>{displayable_relation_name(schema, table_name)}</code> Check Constraints'
508+
509+
return set_dataframe_metadata(df, title=title)
510+
511+
481512
def foreignkeys_dataframe(
482513
inspector: SchemaStrippingInspector, table_name: str, schema: Optional[str]
483514
) -> DataFrame:
@@ -736,6 +767,9 @@ def get_pk_constraint(self, table_name: str, schema: Optional[str] = None) -> di
736767
def get_foreign_keys(self, table_name: str, schema: Optional[str] = None) -> List[dict]:
737768
return self.underlying_inspector.get_foreign_keys(table_name, schema=schema)
738769

770+
def get_check_constraints(self, table_name: str, schema: Optional[str] = None) -> List[dict]:
771+
return self.underlying_inspector.get_check_constraints(table_name, schema=schema)
772+
739773
def get_indexes(self, table_name: str, schema: Optional[str] = None) -> List[dict]:
740774
return self.underlying_inspector.get_indexes(table_name, schema=schema)
741775

tests/conftest.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,17 @@ def populate_database(connection: Connection, include_comments=False):
158158
db.execute('insert into int_table (a, b, c) values (1, 2, 3), (4, 5, 6)')
159159

160160
db.execute(
161-
"create table str_table(str_id text not null default 'foonly', int_col int default 22)"
161+
"""create table str_table(
162+
str_id text not null default 'f'
163+
constraint single_char_str_id check (length(str_id) = 1),
164+
int_col int default 22
165+
constraint only_even_int_col_values check (int_col % 2 = 0),
166+
constraint never_f_10 check (not (str_id = 'f' and int_col = 10))
167+
)
168+
"""
162169
)
163170
db.execute(
164-
"insert into str_table(str_id, int_col) values ('a', 1), ('b', 2), ('c', 3), ('d', null)"
171+
"insert into str_table(str_id, int_col) values ('a', 2), ('b', 2), ('c', 4), ('d', null)"
165172
)
166173

167174
db.execute(

tests/test_sql_magic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ def test_against_string(self, sql_magic, ipython_shell):
233233
)
234234

235235
# Scalar result.
236-
assert results == 1
236+
assert results == 2
237237

238238
@pytest.mark.parametrize('ret_col,expected_value', [('a', 1), ('b', 2)])
239239
def test_sqlsafe(self, sql_magic, ipython_shell, ret_col, expected_value):

tests/test_sql_magic_meta_commands.py

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -490,15 +490,68 @@ def test_against_table_without_a_primary_key(self, sql_magic, ipython_namespace,
490490
# str_table on sqlite will not have any primary key or any indices at all
491491
# (all tables in cockroach have an implicit PK, so can't test with it)
492492

493-
# So the only output should be the dataframe. Not a subsequent HTML blob describing indices.
493+
# The output should NOT include an HTML blob describing indices.
494494
sql_magic.execute(r'@sqlite \d str_table')
495495

496-
assert len(mock_display.call_args_list) == 1
496+
assert len(mock_display.call_args_list) == 2 # main df, constraints df-as-html
497497

498498
df = mock_display.call_args_list[0].args[0]
499+
assert isinstance(df, pd.DataFrame)
499500
assert df.attrs['noteable']['decoration']['title'] == 'Table "str_table" Structure'
500501

501-
assert isinstance(df, pd.DataFrame)
502+
# Test test_constraints() will exercise this further. Only mention it here
503+
# because will be returned and should not be talking about primary key / indices.
504+
constraint_html = mock_display.call_args_list[1].args[0].data
505+
assert constraint_html.startswith(
506+
'<br />\n<h2>Table <code>str_table</code> Check Constraints</h2>'
507+
)
508+
509+
# All CRDB tables have a primary key, so conditionally expect it to be described.
510+
@pytest.mark.parametrize(
511+
'handle,expected_display_callcount', [('@cockroach', 3), ('@sqlite', 2)]
512+
)
513+
def test_constraints(
514+
self, handle, expected_display_callcount, sql_magic, ipython_namespace, mock_display
515+
):
516+
517+
sql_magic.execute(rf'{handle} \d str_table')
518+
519+
assert (
520+
len(mock_display.call_args_list) == expected_display_callcount
521+
) # main df, maybe index html, constraints df-as-html
522+
523+
# The constraints HTML blob will be the final one always.
524+
constraint_html = mock_display.call_args_list[-1].args[0].data
525+
assert constraint_html.startswith(
526+
'<br />\n<h2>Table <code>str_table</code> Check Constraints</h2>'
527+
)
528+
529+
# Convert back to dataframe
530+
constraint_df = pd.read_html(constraint_html)[0]
531+
532+
assert len(constraint_df) == 3 # Three check constraints on this table
533+
534+
assert constraint_df.columns.tolist() == [
535+
'Constraint',
536+
'Definition',
537+
]
538+
539+
# Should be alpha sorted by constraint name.
540+
assert constraint_df['Constraint'].tolist() == [
541+
'never_f_10',
542+
'only_even_int_col_values',
543+
'single_char_str_id',
544+
]
545+
546+
# The SQL dialects convert the constraint expressions back to strings with slightly
547+
# varying spellings (as expected), so can't simply blindly assert all of them.
548+
constraint_definitions = constraint_df['Definition'].tolist()
549+
550+
# This one happens to be regurgitated consistently between sqlite and CRDB.
551+
assert 'length(str_id) = 1' in constraint_definitions
552+
# Little gentler substring matching for the other two.
553+
assert any("str_id = 'f'" in cd for cd in constraint_definitions)
554+
assert any("int_col % 2" in cd for cd in constraint_definitions)
502555

503556
def test_no_args_gets_table_list(self, sql_magic, ipython_namespace):
504557
sql_magic.execute(r'@sqlite \d')

0 commit comments

Comments
 (0)