To use extended properties for documentation is a striking thought, or to have a easy way to tag your tables, but to set a snippet of documentation you have to execute something like:
EXECUTE sp_addextendedproperty
@name = N'MS_Description',
@value = N'custom description text',
@level0type = N'SCHEMA', @level0name = N'SchemaName',
@level1type = N'TABLE', @level1name = N'TableName';
or to execute several mouse-clicks until you can enter your description.
If you want to browse your documentation you can use some selects like
select
SysTbls.name as [Table Name],
ExtProp.name as [Extended Property Name],
ExtProp.value as [Extended Property],
SysObj.create_date as [Table Create Date],
SysObj.modify_date as [Table Modify Date],
ExtProp.minor_id
from
sys.tables SysTbls
left join
sys.extended_properties ExtProp on ExtProp.major_id = SysTbls.object_id
left join
sys.objects SysObj on SysTbls.object_id = SysObj.object_id
where
ExtProp.class = 1 --Object or column
and SysTbls.name is not null;
Or you can use tools like Red-Gate Documentation, but this tool seemed to be limited to the extended property "MS_Description".
So when you have to document several databases with several dozen tables with hundred of columns and several dozen of procedures/views etc. and if you like to look up some extended properties like "Ticket-Number" or "needs dataprotection", you need a tool to speed up the work. To repeat the question in the title: Is there an easy and convenient way to use SQL Server extended properties for automatic documentation? No, unless you write it yourself
Below you find the second part of my answer. I had to split the answer, because one post is limited to 30.000 characters. Seems, there is a rule, that the answer has to be in one post. So someone put the second part of the answer in the question. So, please, if you interested, read the answer first, then return here for the second part. Sorry for the inconvenience
here is the procedure-code from Step 3
create procedure [dbo].[usp_make_extended_properties_statements] @source_table nvarchar(100) , @refresh_documentation_table bit = 1
as
begin
set nocount on
--
-- =============================================
-- Name : usp_make_extended_properties_statements
-- Description:
-- it compares dbo.documentation with the values from the source_table and creates insert/update/delete-Statements for extended properties
-- Step 1) create the source_table : e.g.: select * into table_1 from dbo.documentation
-- you can restrict on lines from interest e.g. select * into only_clients_tables from dbo.documentation where Object_Name like '%client%'
-- Step 2) edit your source_table
-- e.g. update only_clients_tables
-- set Extended_Property = 'dataprotect' and Extended_Property_Value = 'true'
-- where Column_or_Index_Name in ('name', 'surname' ,'street', 'zip' , 'town')
-- or use a tool, e.g. Aquadata-DataEdit, where you can handle it like a excel-Sheet
-- or any other tool you favorize
-- it is possible to insert, update und delete extended properties with the source-Table
-- to delete extended properties empty the columns Extended_Property and Extended_Property_Value (set to value <null> )
-- Step 3) exec usp_make_extended_properties_statements 'yourDatabase.yourSchema.yourSourceTableName'
-- this will result in bunch of statements, copy them to a query-window and execute them
-- Step 4) exec dbo.usp_fill_doc_table
-- =============================================
declare @sql nvarchar(4000)
,@dbname nvarchar(100)
,@schemaname nvarchar(100)
,@tablename nvarchar(255)
SET @dbname = PARSENAME(@source_table, 3);
SET @schemaname = PARSENAME(@source_table, 2);
SET @tablename = PARSENAME(@source_table, 1);
select @dbname ,@schemaname,@tablename
if ( @tablename is null or @schemaname is null or @dbname is null )
begin
print 'table '+@source_table+' doesnt exist or Parameter not given in format DBname.Schemaname.Tablenname ' + CHAR(13) + 'which was created by: select * into source_table from dbo.documentation' + CHAR(13) + ' for editing (insert/update/delete)' + CHAR(13) + ' after editing: exec usp_make_extended_properties_statements ''source_table''
'
return 0
end
set @sql = '
select count(1) from '+@dbname+'.sys.columns c join '+@dbname+'.sys.tables t on t.object_id = c.object_id where t.name = '''+@tablename+'''
and c.name in (N''Object_Type'', N''Database_Name'', N''Schema_Name'', N''Object_Name'', N''Column_or_Index_Name'', N''Extended_Property'', N''Extended_Property_Value'')
'
create table #soll (
Object_Type varchar(60) NULL,
Database_Name varchar(50) NULL,
Schema_Name varchar(30) NOT NULL,
Object_Name varchar(128) NOT NULL,
Column_or_Index_Name varchar(100) NULL,
Extended_Property varchar(100) NULL,
Extended_Property_Value varchar(5000) NULL
)
--declare @sql nvarchar(4000), @source_table nvarchar(100) = 'worker.cjonas.ep'
set @sql = '
select Object_Type, Database_Name, Schema_Name, Object_Name, Column_or_Index_Name, Extended_Property, Extended_Property_Value
from '+@source_table+' where Object_type is not null'
begin try
insert into #soll
(
Object_Type,
Database_Name,
Schema_Name,
Object_Name,
Column_or_Index_Name,
Extended_Property,
Extended_Property_Value
)
EXEC SP_EXECUTESQL @sql
end try
begin catch
select @source_table+' must have columns Object_Type, Database_Name, Schema_Name, Object_Name, Column_or_Index_Name, Extended_Property, Extended_Property_Value '+error_message()
end catch
--replace empty String with <null>
update #soll set Column_or_Index_Name = null where Column_or_Index_Name = ''
-- retrieve actual state:
if ( @refresh_documentation_table = 1) exec dbo.usp_fill_doc_table;
select distinct d.Object_Type,
d.Database_Name,
d.Schema_Name,
d.Object_Name,
d.Column_or_Index_Name,
d.Extended_Property,
d.Extended_Property_Value
into #ist
from dbo.documentation d join #soll s on d.Database_Name = s.Database_Name and d.Schema_name = s.Schema_name and d.Object_name = s.Object_name
where d.Object_Type is not null
--------------------------------------
-- create insert or update-Statements
declare
@Object_Type sysname, @Database_Name sysname, @Schema_Name sysname, @Object_Name sysname,@level1type sysname,
@Extended_Property nvarchar(1000),@Extended_Property_Value nvarchar(1000),
@Column_or_Index_Name sysname = null
--select * from #ist; select * from #soll
--
----------------------------------------------------------------
---- add or update extended_properties
----------------------------------------------------------------
select Object_Type, Database_Name, Schema_Name, Object_Name, Column_or_Index_Name, Extended_Property, Extended_Property_Value
from #soll where Extended_Property is not null
except
select Object_Type, Database_Name, Schema_Name, Object_Name, Column_or_Index_Name, Extended_Property, Extended_Property_Value
from #ist where Extended_Property is not null;
declare db_cursor cursor local fast_forward for
select Object_Type, Database_Name, Schema_Name, Object_Name, Column_or_Index_Name, Extended_Property, Extended_Property_Value
from #soll where Extended_Property is not null and Extended_Property <> 'no extended property'
except
select Object_Type, Database_Name, Schema_Name, Object_Name, Column_or_Index_Name, Extended_Property, Extended_Property_Value
from #ist where Extended_Property is not null and Extended_Property <> 'no extended property';
open db_cursor;
fetch next from db_cursor into @Object_Type , @Database_Name , @Schema_Name , @Object_Name, @Column_or_Index_Name , @Extended_Property ,@Extended_Property_Value
while @@FETCH_STATUS = 0
begin
IF DB_ID(@Database_Name) IS NULL
BEGIN
RAISERROR ('Unknown Database Name %s', 11, 1, @Database_Name);
return -1;
end
-- Trim @Object_Type only allowed values
-- for @level1type : Aggregate, Default, Function, Logical File Name, PROCEDURE, Queue, Rule, Sequence, Synonym, Table, TABLE_TYPE, Type, View, XML Schema Collection and NULL
-- for @level2type : COLUMN, CONSTRAINT, EVENT NOTIFICATION, INDEX, PARAMETER, TRIGGER und NULL
select @Object_Type =
case
when @Object_Type like '%procedure' then 'PROCEDURE'
when @Object_Type like '%index' then 'INDEX'
when @Object_Type like '%function' then 'FUNCTION'
when @Object_Type in ('foreign_key','check','primary_key','unique') then 'CONSTRAINT'
when @Object_Type = 'sequence_object' then 'SEQUENCE'
else @Object_Type
end
---------
IF @Column_or_Index_Name IS null
begin
SET @sql = CONCAT('use ', @Database_Name, CHAR(13) + CHAR(10)
, 'go', CHAR(13) + CHAR(10)
, 'if not exists (select 1 from sys.extended_properties AS ep WHERE ep.major_id = OBJECT_ID(''',@Schema_Name ,'.', @Object_Name,''') AND ep.minor_id = 0 AND ep.name = ''', @Extended_Property,''')'
, char(13) + char(10)
, 'begin'
, char(13) + char(10)
, 'exec ', @Database_Name,'.sys.sp_addextendedproperty @name = ''', @Extended_Property,''', @value =''', @Extended_Property_value,''', @level0type = N''SCHEMA'', @level0name = ''', @Schema_Name,''', @level1type = ''', @Object_Type,''', @level1name = ''', @Object_Name,''''
, char(13) + char(10)
, 'end'
, char(13) + char(10)
, 'else begin'
, char(13) + char(10)
, 'exec ', @Database_Name,'.sys.sp_updateextendedproperty @name = ''', @Extended_Property,''', @value = ''', @Extended_Property_value,''', @level0type = N''SCHEMA'', @level0name = ''', @Schema_Name,''', @level1type = ''', @Object_Type,''', @level1name = ''', @Object_Name,''''
, char(13) + char(10)
,'end;'
, char(13) + char(10)
);
end -- if
else
begin
--- table or view:
set @level1type = (
select distinct Object_Type as level1type
from dbo.documentation d
where Object_Type in ( 'table','view') and Object_Name= @Object_Name and Schema_Name = @Schema_Name and Database_Name = @Database_Name
)
select @level1type as level1type
select *
from sys.objects o
join sys.schemas s on s.schema_id = o.schema_id
where o.name = @Object_Name and s.name = @Schema_Name
--- Spalte oder Index:
set @sql = concat('use ', @Database_Name, char(13) + char(10)
, 'go'
, char(13) + char(10)
, 'if not exists '
, char(13) + char(10)
, '(select 1 from SYS.columns AS c INNER JOIN sys.extended_properties as ep ON ep.major_id = c.object_id AND ep.minor_id = c.column_id and ep.name = ''',@Extended_Property,'''',char(13) + char(10)
, 'where c.object_id = OBJECT_ID(''',@Schema_Name ,'.', @Object_Name,''') AND c.name = ''',@Column_or_Index_Name, ''')'
, char(13) + char(10)
, 'and not exists ',char(13) + char(10)
, '(SELECT 1 FROM sys.extended_properties ep join sys.indexes i ON ep.major_id = i.object_id AND ep.name = ''',@Extended_Property,'''',char(13) + char(10)
, 'where ep.major_id = OBJECT_ID(''',@Schema_Name ,'.', @Object_Name,''') AND i.name = ''',@Column_or_Index_Name, ''')'
, char(13) + char(10)
, 'begin '
, char(13) + char(10)
, 'exec ', @Database_Name,'.sys.sp_addextendedproperty @name = ''', @Extended_Property,''', @value = ''', @Extended_Property_value,''', @level0type = N''SCHEMA'', @level0name = ''', @Schema_Name,''', @level1type = ''',@level1type,''', @level1name = ''', @Object_Name,''', @level2type = ''', @Object_Type,''', @level2name = ''',@Column_or_Index_Name,''''
, char(13) + char(10)
, 'end'
, char(13) + char(10)
, 'else begin '
, char(13) + char(10)
, 'exec ', @Database_Name,'.sys.sp_updateextendedproperty @name = ''', @Extended_Property,''', @value = ''', @Extended_Property_value,''', @level0type = N''SCHEMA'', @level0name = ''', @Schema_Name,''', @level1type = ''',@level1type,''', @level1name = ''', @Object_Name,''', @level2type = ''', @Object_Type,''', @level2name = ''', @Column_or_Index_Name,''''
, char(13) + char(10)
, 'end ;'
, char(13) + char(10)
)
;
end ---ifelse
print '-- Insert or update extended properties:'
print @sql
--EXEC SP_EXECUTESQL @sql
fetch next from db_cursor into @Object_Type , @Database_Name , @Schema_Name , @Object_Name, @Column_or_Index_Name , @Extended_Property ,@Extended_Property_Value ;
end;
close db_cursor;
deallocate db_cursor;
-- --------------------
select
i.Object_Type,
i.Database_Name,
i.Schema_Name,
i.Object_Name,
i.Column_or_Index_Name,
i.Extended_Property
from #soll s
right join #ist i
on i.Object_Type = s.Object_Type
and i.Database_Name = s.Database_Name
and i.Schema_Name = s.Schema_Name
and i.Object_Name = s.Object_Name
and isnull(i.Extended_Property, 'null') = isnull(s.Extended_Property, 'null')
and isnull(i.Column_or_Index_Name,'null') = isnull(s.Column_or_Index_Name,'null')
where s.extended_property is null
and i.Object_type is not null and i.extended_property <> 'no extended property'
----------------------------------------------------------------
---- deleted extended_properties
----------------------------------------------------------------
declare db_cursor2 cursor local fast_forward for
select
i.Object_Type,
i.Database_Name,
i.Schema_Name,
i.Object_Name,
i.Column_or_Index_Name,
i.Extended_Property
from #soll s
right join #ist i
on i.Object_Type = s.Object_Type
and i.Database_Name = s.Database_Name
and i.Schema_Name = s.Schema_Name
and i.Object_Name = s.Object_Name
and isnull(i.Extended_Property, 'null') = isnull(s.Extended_Property, 'null')
and isnull(i.Column_or_Index_Name,'null') = isnull(s.Column_or_Index_Name,'null')
where s.extended_property is null
and i.Object_type is not null and i.extended_property <> 'no extended property'
open db_cursor2;
fetch next from db_cursor2 into @Object_Type , @Database_Name , @Schema_Name , @Object_Name, @Column_or_Index_Name , @Extended_Property
while @@FETCH_STATUS = 0
begin
IF DB_ID(@Database_Name) IS NULL
BEGIN
RAISERROR ('Unknow Databasename %s', 11, 1, @Database_Name);
return -1;
end
-- Trim @Object_Type only allowed values
-- for @level1type : Aggregate, Default, Function, Logical File Name, PROCEDURE, Queue, Rule, Sequence, Synonym, Table, TABLE_TYPE, Type, View, XML Schema Collection und NULL
-- for @level2type : COLUMN, CONSTRAINT, EVENT NOTIFICATION, INDEX, PARAMETER, TRIGGER und NULL
select @Object_Type =
case
when @Object_Type like '%procedure' then 'PROCEDURE'
when @Object_Type like '%index' then 'INDEX'
when @Object_Type like '%function' then 'FUNCTION'
when @Object_Type in ('foreign_key','check','primary_key','unique') then 'CONSTRAINT'
when @Object_Type = 'sequence_object' then 'SEQUENCE'
else @Object_Type
end
if @Column_or_Index_Name is null
begin
set @sql = concat('use ', @Database_Name
, char(13) + char(10)
, 'go'
, char(13) + char(10)
, 'if exists (select 1 from sys.extended_properties AS ep '
, 'WHERE ep.major_id = OBJECT_ID(''',@Schema_Name ,'.', @Object_Name,''') AND ep.minor_id = 0 AND ep.name = ''',@Extended_Property,''')'
, char(13) + char(10)
, 'EXEC ', @Database_Name,'.sys.sp_dropextendedproperty @name = ''', @Extended_Property,''', @level0type = N''SCHEMA'', @level0name = ''', @Schema_Name,''', @level1type = ''', @Object_Type,''', @level1name = ''', @Object_Name,''''
,';'
, char(13) + char(10)
)
end
else
begin
--- table or view:
set @level1type = (
select distinct Object_Type as level1type
from dbo.documentation d
where Object_Type in ( 'table','view') and Object_Name= @Object_Name and Schema_Name = @Schema_Name and Database_Name = @Database_Name
)
--- column or Index:
set @sql = concat('USE ', @Database_Name, char(13) + char(10)
, 'go', char(13) + char(10)
, 'IF EXISTS'
, char(13) + char(10)
,'(SELECT 1 FROM SYS.columns AS c INNER JOIN sys.extended_properties AS ep ON ep.major_id = c.object_id AND ep.minor_id = c.column_id AND ep.name = ''',@Extended_Property,''''
,char(13) + char(10)
, 'WHERE c.object_id = OBJECT_ID(''',@Schema_Name ,'.', @Object_Name,''') AND c.name = ''',@Column_or_Index_Name, ''')'
,char(13) + char(10)
, 'or exists '
,char(13) + char(10)
,'(SELECT 1 FROM sys.extended_properties ep join sys.indexes i ON ep.major_id = i.object_id AND ep.name = ''',@Extended_Property,''''
,char(13) + char(10)
, 'WHERE ep.major_id = OBJECT_ID(''',@Schema_Name ,'.', @Object_Name,''') AND i.name = ''',@Column_or_Index_Name, ''')'
, char(13) + char(10)
, 'EXEC ', @Database_Name,'.sys.sp_dropextendedproperty @name = ''', @Extended_Property,''', @level0type = N''SCHEMA'', @level0name = ''', @Schema_Name,''', @level1type = ''',@level1type,''', @level1name = ''', @Object_Name,''', @level2type = ''', @Object_Type,''', @level2name = ''', @Column_or_Index_Name,''''
,';'
, char(13) + char(10)
)
end
print '-- Delete extended properties:'
print @sql
--EXEC SP_EXECUTESQL @sql
fetch next from db_cursor2 into @Object_Type , @Database_Name , @Schema_Name , @Object_Name, @Column_or_Index_Name , @Extended_Property ;
end;
close db_cursor2;
deallocate db_cursor2;
--- actualize Documentation-table:
--- only when you have uncommented every: "--EXEC SP_EXECUTESQL @sql"
--- then uncomment following line, too:
-- if ( @refresh_documentation_table = 1) exec dbo.usp_fill_doc_table;
end