[Bug]: sys.sql_expression_dependencies definition is simply a null returning view that is needed CI/CD tools like Redgate Schema Compare
What happened?
Clients that I'm working on require the need to generate change scripts for deploying code which requires knowing object dependencies. Tools like SSMS Generate Script Wizard with Dependent Objects selected, Liquibase and RedGate Schema Compare use the sys.sql_exoression_dependencies view to understand the dependency three to script changes in the correct order to avoid schema binding issues in their CI/CD pipeline.
Problem with current sys.sql_expression_dependencies
The view definition in the current main branch shown below was created to make SSMS work for core scenarios a couple of years ago. Now more customers like the ones I work need to use tools that automate change script generation and deployment to their production environments.
CREATE OR REPLACE VIEW sys.sql_expression_dependencies
AS
SELECT
CAST(0 as INT) AS referencing_id,
CAST(0 as INT) AS referencing_minor_id,
CAST(0 as sys.TINYINT) AS referencing_class,
CAST('' as NVARCHAR(60)) AS referencing_class_desc,
CAST(0 as sys.BIT) AS is_schema_bound_reference,
CAST(0 as sys.TINYINT) AS referenced_class,
CAST('' as NVARCHAR(60)) AS referenced_class_desc,
CAST('' as SYSNAME) AS referenced_server_name,
CAST('' as SYSNAME) AS referenced_database_name,
CAST('' as SYSNAME) AS referenced_schema_name,
CAST('' as SYSNAME) AS referenced_entity_name,
CAST(0 as INT) AS referenced_id,
CAST(0 as INT) AS referenced_minor_id,
CAST(0 as sys.BIT) AS is_caller_dependent,
CAST(0 as sys.BIT) AS is_ambiguous
WHERE FALSE;
GRANT SELECT ON sys.sql_expression_dependencies TO PUBLIC;
Unfortunately, the system catalog objects in pg_catalog and INFORMATION_SCHEMA lack dependency tools for functions, triggers, and functions. The pg_depends works ok with tables, views, materialized views, and foreign tables. However, other objects require using parsing of the objects text to determine the dependencies.
Prototype solution
Here is a prototype I'm working to to make Babelfish work with RedGate's Schema Compare product.
-- sys.sql_expression_dependencies source
CREATE OR REPLACE VIEW sys.sql_expression_dependencies
AS WITH RECURSIVE source_objects AS (
SELECT p.oid AS object_id,
CASE p.prokind
WHEN 'p'::"char" THEN 'PROCEDURE'::text
WHEN 'f'::"char" THEN 'FUNCTION'::text
ELSE 'FUNCTION'::text
END AS object_type,
p.prosrc AS source_code
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
JOIN sys.babelfish_namespace_ext bne ON n.nspname = bne.nspname
WHERE sys.db_id() = bne.dbid
UNION ALL
SELECT c.oid AS object_id,
'VIEW'::text AS object_type,
m.definition::text AS source_code
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
JOIN sys.babelfish_namespace_ext bne ON n.nspname = bne.nspname
JOIN sys.all_sql_modules m ON c.oid::integer = m.object_id
WHERE c.relkind = 'v'::"char" AND sys.db_id() = bne.dbid
UNION ALL
SELECT DISTINCT t.oid AS object_id,
'TABLE'::text AS object_type,
pg_get_expr(ad_1.adbin, ad_1.adrelid) AS source_code
FROM pg_class t
JOIN pg_namespace n ON n.oid = t.relnamespace
JOIN sys.babelfish_namespace_ext bne ON n.nspname = bne.nspname
JOIN pg_attrdef ad_1 ON ad_1.adrelid = t.oid
WHERE t.relkind = 'r'::"char" AND sys.db_id() = bne.dbid
), parsed_dependencies AS (
SELECT s.object_id AS referencing_id,
0 AS referencing_minor_id,
s.object_type,
rm.matches[1] AS full_reference,
'TABLE'::text AS dependency_type
FROM source_objects s,
LATERAL ( SELECT regexp_matches(s.source_code, '(?:FROM|JOIN)\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)'::text COLLATE "C", 'gi'::text) AS matches) rm
UNION ALL
SELECT s.object_id AS referencing_id,
0 AS referencing_minor_id,
s.object_type,
rm.matches[1] AS full_reference,
'TABLE'::text AS dependency_type
FROM source_objects s,
LATERAL ( SELECT regexp_matches(s.source_code, 'INSERT\s+INTO\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)'::text COLLATE "C", 'gi'::text) AS matches) rm
UNION ALL
SELECT s.object_id AS referencing_id,
0 AS referencing_minor_id,
s.object_type,
rm.matches[1] AS full_reference,
'TABLE'::text AS dependency_type
FROM source_objects s,
LATERAL ( SELECT regexp_matches(s.source_code, 'UPDATE\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)'::text COLLATE "C", 'gi'::text) AS matches) rm
UNION ALL
SELECT s.object_id AS referencing_id,
0 AS referencing_minor_id,
s.object_type,
rm.matches[1] AS full_reference,
'TABLE'::text AS dependency_type
FROM source_objects s,
LATERAL ( SELECT regexp_matches(s.source_code, 'DELETE\s+FROM\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)'::text COLLATE "C", 'gi'::text) AS matches) rm
UNION ALL
SELECT s.object_id AS referencing_id,
0 AS referencing_minor_id,
s.object_type,
rm.matches[2] AS full_reference,
'PROCEDURE'::text AS dependency_type
FROM source_objects s,
LATERAL ( SELECT regexp_matches(s.source_code, '(EXEC|EXECUTE)\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)'::text COLLATE "C", 'gi'::text) AS matches) rm
UNION ALL
SELECT s.object_id AS referencing_id,
0 AS referencing_minor_id,
s.object_type,
rm.matches[1] AS full_reference,
'FUNCTION'::text AS dependency_type
FROM source_objects s,
LATERAL ( SELECT regexp_matches(s.source_code, '([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\s*\('::text COLLATE "C", 'gi'::text) AS matches) rm
), name_components AS (
SELECT pd.referencing_id,
pd.referencing_minor_id,
pd.object_type,
pd.full_reference,
string_to_array(pd.full_reference, '.'::text COLLATE "C") AS name_parts,
array_length(string_to_array(pd.full_reference, '.'::text COLLATE "C"), 1) AS part_count,
CASE
WHEN array_length(string_to_array(pd.full_reference, '.'::text COLLATE "C"), 1) = 4 THEN (string_to_array(pd.full_reference, '.'::text COLLATE "C"))[1]
ELSE NULL::text
END AS referenced_server_name,
CASE
WHEN array_length(string_to_array(pd.full_reference, '.'::text COLLATE "C"), 1) = 4 THEN (string_to_array(pd.full_reference, '.'::text COLLATE "C"))[2]
WHEN array_length(string_to_array(pd.full_reference, '.'::text COLLATE "C"), 1) = 3 THEN (string_to_array(pd.full_reference, '.'::text COLLATE "C"))[1]
ELSE NULL::text
END AS referenced_database_name,
CASE
WHEN array_length(string_to_array(pd.full_reference, '.'::text COLLATE "C"), 1) = 4 THEN (string_to_array(pd.full_reference, '.'::text COLLATE "C"))[3]
WHEN array_length(string_to_array(pd.full_reference, '.'::text COLLATE "C"), 1) = 3 THEN (string_to_array(pd.full_reference, '.'::text COLLATE "C"))[2]
WHEN array_length(string_to_array(pd.full_reference, '.'::text COLLATE "C"), 1) = 2 THEN (string_to_array(pd.full_reference, '.'::text COLLATE "C"))[1]
ELSE NULL::text
END AS referenced_schema_name,
lower((string_to_array(pd.full_reference, '.'::text COLLATE "C"))[array_length(string_to_array(pd.full_reference, '.'::text COLLATE "C"), 1)]) AS referenced_entity_name,
pd.dependency_type
FROM parsed_dependencies pd
), normalized_references AS (
SELECT nc.referencing_id,
nc.referencing_minor_id,
nc.object_type,
nc.full_reference,
nc.name_parts,
nc.part_count,
nc.referenced_server_name,
nc.referenced_database_name,
nc.referenced_schema_name AS referencedschemaname,
nc.referenced_entity_name,
nc.dependency_type,
CASE
WHEN (( SELECT 1
FROM sys.babelfish_namespace_ext bab_ext
WHERE (bab_ext.nspname COLLATE "C") = nc.referenced_schema_name)) = 1 THEN (( SELECT bab_ext.orig_name
FROM sys.babelfish_namespace_ext bab_ext
WHERE (bab_ext.nspname COLLATE "C") = nc.referenced_schema_name))::text
ELSE nc.referenced_schema_name
END COLLATE "C" AS normalized_schema_name,
CASE
WHEN (( SELECT 1
FROM sys.babelfish_namespace_ext bab_ext
WHERE (bab_ext.orig_name::text COLLATE "C") = nc.referenced_schema_name AND (sys.db_name(bab_ext.dbid::integer)::text COLLATE "C") = nc.referenced_database_name)) = 1 THEN ( SELECT (((sys.db_name(bab_ext.dbid::integer)::text || '.'::text) || bab_ext.orig_name::text) || '.'::text) || nc.referenced_entity_name
FROM sys.babelfish_namespace_ext bab_ext
WHERE (bab_ext.orig_name::text COLLATE "C") = nc.referenced_schema_name AND (sys.db_name(bab_ext.dbid::integer)::text COLLATE "C") = nc.referenced_database_name)
WHEN (( SELECT 1
FROM sys.babelfish_namespace_ext bab_ext
WHERE (bab_ext.nspname COLLATE "C") = nc.referenced_schema_name)) = 1 THEN ( SELECT (((sys.db_name(bab_ext.dbid::integer)::text || '.'::text) || bab_ext.orig_name::text) || '.'::text) || nc.referenced_entity_name
FROM sys.babelfish_namespace_ext bab_ext
WHERE (bab_ext.nspname COLLATE "C") = nc.referenced_schema_name)
WHEN (( SELECT 1
FROM sys.babelfish_namespace_ext bab_ext
WHERE (bab_ext.orig_name::text COLLATE "C") = nc.referenced_schema_name AND bab_ext.dbid = sys.db_id())) = 1 THEN ( SELECT (((sys.db_name(bab_ext.dbid::integer)::text || '.'::text) || bab_ext.orig_name::text) || '.'::text) || nc.referenced_entity_name
FROM sys.babelfish_namespace_ext bab_ext
WHERE (bab_ext.orig_name::text COLLATE "C") = nc.referenced_schema_name AND bab_ext.dbid = sys.db_id())
WHEN (( SELECT 1
FROM sys.babelfish_namespace_ext bab_ext
WHERE nc.referenced_schema_name IS NULL AND (bab_ext.orig_name::sys."varchar" COLLATE "C") = 'dbo'::sys."varchar" AND bab_ext.dbid = sys.db_id())) = 1 THEN ( SELECT (((sys.db_name(bab_ext.dbid::integer)::text || '.'::text) || bab_ext.orig_name::text) || '.'::text) || nc.referenced_entity_name
FROM sys.babelfish_namespace_ext bab_ext
WHERE nc.referenced_schema_name IS NULL AND (bab_ext.orig_name::sys."varchar" COLLATE "C") = 'dbo'::sys."varchar" AND bab_ext.dbid = sys.db_id())
ELSE nc.referenced_schema_name
END COLLATE "C" AS normalized_object_name,
CASE
WHEN (( SELECT 1
FROM sys.babelfish_namespace_ext bab_ext
WHERE (bab_ext.nspname COLLATE "C") = nc.referenced_schema_name)) = 1 THEN ( SELECT n.oid
FROM sys.babelfish_namespace_ext bab_ext
JOIN pg_namespace n ON n.nspname = bab_ext.nspname
WHERE (bab_ext.nspname COLLATE "C") = nc.referenced_schema_name)
WHEN (( SELECT 1
FROM sys.babelfish_namespace_ext bab_ext
WHERE (bab_ext.orig_name::text COLLATE "C") = nc.referenced_schema_name AND bab_ext.dbid = sys.db_id())) = 1 THEN ( SELECT n.oid
FROM sys.babelfish_namespace_ext bab_ext
JOIN pg_namespace n ON n.nspname = bab_ext.nspname
WHERE (bab_ext.orig_name::text COLLATE "C") = nc.referenced_schema_name AND bab_ext.dbid = sys.db_id())
WHEN (( SELECT 1
FROM sys.babelfish_namespace_ext bab_ext
WHERE nc.referenced_schema_name IS NULL AND (bab_ext.orig_name::sys."varchar" COLLATE "C") = 'dbo'::sys."varchar" AND bab_ext.dbid = sys.db_id())) = 1 THEN ( SELECT n.oid
FROM sys.babelfish_namespace_ext bab_ext
JOIN pg_namespace n ON n.nspname = bab_ext.nspname
WHERE nc.referenced_schema_name IS NULL AND (bab_ext.orig_name::sys."varchar" COLLATE "C") = 'dbo'::sys."varchar" AND bab_ext.dbid = sys.db_id())
ELSE NULL::oid
END::integer AS normalized_schema_id
FROM name_components nc
)
SELECT DISTINCT ad.referencing_id::integer AS referencing_id,
ad.referencing_minor_id,
1::sys.tinyint AS referencing_class,
'OBJECT_OR_COLUMN'::sys."varchar"::sys.nvarchar(60) AS referencing_class_desc,
0::sys."bit" AS is_schema_bound_reference,
1::sys.tinyint AS referenced_class,
'OBJECT_OR_COLUMN'::sys."varchar"::sys.nvarchar(60) AS referenced_class_desc,
ad.referenced_server_name::sys.sysname AS referenced_server_name,
ad.referenced_database_name::sys.sysname AS referenced_database_name,
ad.normalized_schema_name::sys.sysname AS referenced_schema_name,
ad.referenced_entity_name::sys.sysname AS referenced_entity_name,
CASE
WHEN ad.dependency_type = ANY (ARRAY['TABLE'::text, 'VIEW'::text]) THEN ( SELECT c.oid
FROM pg_class c
WHERE ad.normalized_schema_id::oid = c.relnamespace AND c.relname = ad.referenced_entity_name)
WHEN ad.dependency_type = ANY (ARRAY['PROCEDURE'::text, 'FUNCTION'::text]) THEN ( SELECT p.oid
FROM pg_proc p
WHERE ad.normalized_schema_id::oid = p.pronamespace AND p.proname = ad.referenced_entity_name)
ELSE NULL::oid
END::integer AS referenced_id,
0 AS referenced_minor_id,
0::sys."bit" AS is_caller_dependent,
0::sys."bit" AS is_ambiguous
FROM normalized_references ad
WHERE ad.normalized_object_name ~ '^[^.]+\.[^.]+\.[^.]+$'::text;
GRANT SELECT ON TABLE sys.sql_expression_dependencies TO public;
Known limitations
[ ] TODO - Trigger Support - Current version parses objects only in pg_proc for procedures and functions
[ ] TODO - Outputs too many rows compared to SQL Server - Need to remove references to internal function names
[ ] TODO - Performance issues with large numbers of procedures and functions - Likely needs a materialized view that in the first iteration would require users to refresh. Ideal solution could be using a similar approach that Babelfish uses for CREATE or ALTER for writing T-SQL code to the sys.babelfish_function_ext and babelfish_view_def tables.
Next steps
If no one is actively working on this, I will put together a PR
Version
BABEL_5_X_DEV (Default)
Extension
babelfishpg_tsql (Default)
Which flavor of Linux are you using when you see the bug?
Ubuntu (Default)
Relevant log output
Code of Conduct
- [x] I agree to follow this project's Code of Conduct.
Test schema for sys.sql_expression_dependencies
CREATE SCHEMA [babel_sed_test_schema1]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE FUNCTION [dbo].[babel_sed_test_func1](@min_salary money)
RETURNS int
AS
BEGIN
DECLARE @count int
SELECT @count = COUNT(*)
FROM dbo.babel_sed_test_table2
WHERE salary >= @min_salary
RETURN @count
END
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- Test Case 9: Function with JOIN operations
CREATE FUNCTION [dbo].[babel_sed_test_func2](@emp_id int)
RETURNS varchar(150)
AS
BEGIN
DECLARE @result varchar(150)
SELECT @result = t1.name + ' - ' + t2.department_name
FROM babel_sed_test_table1 t1
INNER JOIN babel_sed_test_table2 t2 ON t1.id = t2.emp_id
WHERE t1.id = @emp_id
RETURN @result
END
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE FUNCTION [dbo].[babel_sed_xdb_test_func2](@emp_id int)
RETURNS varchar(150)
AS
BEGIN
DECLARE @result varchar(150)
SELECT @result = concat( e.[Title], ' ', e.[LastName], ' reports to ', em.[Title], ' ', em.[LastName] )
FROM northwind.dbo.employees e
INNER JOIN northwind.dbo.employees em on e.ReportsTo = em.EmployeeID
WHERE e.[EmployeeID] = @emp_id
RETURN @result
END
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[babel_sed_test_table1](
[id] [int] NULL,
[name] [varchar](50) NULL,
[amount] [money] NULL
) ON [PRIMARY]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE VIEW [dbo].[babel_sed_test_view1] AS
SELECT id, name, amount
FROM babel_sed_test_table1
WHERE amount > 100
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [babel_sed_test_schema1].[babel_sed_test_table3](
[product_id] [int] NULL,
[product_name] [varchar](100) NULL,
[price] [decimal](10, 2) NULL
) ON [PRIMARY]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE VIEW [dbo].[babel_sed_test_view2] AS
SELECT product_id, product_name, price
FROM babel_sed_test_schema1.babel_sed_test_table3
WHERE price > 10.00
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- Test Case 6: Table-valued function
CREATE FUNCTION [dbo].[babel_sed_test_tvf1](@min_amount money)
RETURNS TABLE
AS
RETURN (
SELECT id, name, amount
FROM babel_sed_test_table1
WHERE amount >= @min_amount
)
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- Test Case 7: View referencing another view
CREATE VIEW [dbo].[babel_sed_test_view3] AS
SELECT id, name
FROM babel_sed_test_view1
WHERE name IS NOT NULL
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[babel_sed_test_table2](
[emp_id] [int] NULL,
[salary] [numeric](10, 2) NULL,
[department_name] [varchar](100) NULL
) ON [PRIMARY]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- Test Case 10: View with subquery
CREATE VIEW [dbo].[babel_sed_test_view4] AS
SELECT
t1.id,
t1.name,
t1.amount,
(SELECT AVG(salary) FROM babel_sed_test_table2 WHERE emp_id = t1.id) as avg_salary
FROM babel_sed_test_table1 t1
WHERE EXISTS (SELECT 1 FROM babel_sed_test_table2 t2 WHERE t2.emp_id = t1.id)
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[babel_sed_test_audit_log](
[log_id] [int] IDENTITY(1,1) NOT NULL,
[action_type] [varchar](50) NULL,
[table_name] [varchar](100) NULL,
[record_count] [int] NULL,
[log_date] [datetime] NULL
) ON [PRIMARY]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[babel_sed_test_table1_audit](
[audit_id] [int] IDENTITY(1,1) NOT NULL,
[id] [int] NULL,
[name] [varchar](50) NULL,
[amount] [money] NULL,
[create_date] [datetime2](7) NOT NULL,
[created_by] [varchar](128) NOT NULL,
[modified_date] [datetime2](7) NOT NULL,
[modified_by] [varchar](128) NOT NULL,
[modification_type] [char](1) NOT NULL,
[is_record_active] [bit] NOT NULL,
PRIMARY KEY CLUSTERED
(
[audit_id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[babel_sed_udf_calc_test_table3](
[emp_id] [int] NULL,
[salary] [numeric](10, 2) NULL,
[department_name] [varchar](100) NULL,
[emp_info] AS ([dbo].[babel_sed_xdb_test_func2]([emp_id]))
) ON [PRIMARY]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Employees](
[EmployeeID] [int] IDENTITY(1,1) NOT NULL,
[LastName] [nvarchar](20) NOT NULL,
[FirstName] [nvarchar](10) NOT NULL,
[Title] [nvarchar](30) NULL,
[TitleOfCourtesy] [nvarchar](25) NULL,
[BirthDate] [datetime] NULL,
[HireDate] [datetime] NULL,
[Address] [nvarchar](60) NULL,
[City] [nvarchar](15) NULL,
[Region] [nvarchar](15) NULL,
[PostalCode] [nvarchar](10) NULL,
[Country] [nvarchar](15) NULL,
[HomePhone] [nvarchar](24) NULL,
[Extension] [nvarchar](4) NULL,
[Photo] [image] NULL,
[Notes] [ntext] NULL,
[ReportsTo] [int] NULL,
[PhotoPath] [nvarchar](255) NULL,
CONSTRAINT [PK_Employees] PRIMARY KEY CLUSTERED
(
[EmployeeID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
SET ANSI_PADDING ON
GO
CREATE NONCLUSTERED INDEX [LastName] ON [dbo].[Employees]
(
[LastName] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
GO
SET ANSI_PADDING ON
GO
CREATE NONCLUSTERED INDEX [PostalCode] ON [dbo].[Employees]
(
[PostalCode] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
GO
CREATE NONCLUSTERED INDEX [IX_babel_sed_test_table1_audit_active] ON [dbo].[babel_sed_test_table1_audit]
(
[is_record_active] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
GO
CREATE NONCLUSTERED INDEX [IX_babel_sed_test_table1_audit_id] ON [dbo].[babel_sed_test_table1_audit]
(
[id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
GO
CREATE NONCLUSTERED INDEX [IX_babel_sed_test_table1_audit_modified_date] ON [dbo].[babel_sed_test_table1_audit]
(
[modified_date] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
GO
ALTER TABLE [dbo].[babel_sed_test_audit_log] ADD DEFAULT (getdate()) FOR [log_date]
GO
ALTER TABLE [dbo].[babel_sed_test_table1_audit] ADD DEFAULT (getdate()) FOR [create_date]
GO
ALTER TABLE [dbo].[babel_sed_test_table1_audit] ADD DEFAULT (suser_sname()) FOR [created_by]
GO
ALTER TABLE [dbo].[babel_sed_test_table1_audit] ADD DEFAULT (getdate()) FOR [modified_date]
GO
ALTER TABLE [dbo].[babel_sed_test_table1_audit] ADD DEFAULT (suser_sname()) FOR [modified_by]
GO
ALTER TABLE [dbo].[babel_sed_test_table1_audit] ADD DEFAULT ((1)) FOR [is_record_active]
GO
ALTER TABLE [dbo].[Employees] WITH NOCHECK ADD CONSTRAINT [FK_Employees_Employees] FOREIGN KEY([ReportsTo])
REFERENCES [dbo].[Employees] ([EmployeeID])
GO
ALTER TABLE [dbo].[Employees] CHECK CONSTRAINT [FK_Employees_Employees]
GO
ALTER TABLE [dbo].[Employees] WITH NOCHECK ADD CONSTRAINT [CK_Birthdate] CHECK (([BirthDate]<getdate()))
GO
ALTER TABLE [dbo].[Employees] CHECK CONSTRAINT [CK_Birthdate]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[babel_sed_test_proc1]
AS
BEGIN
SELECT emp_id, salary
FROM babel_sed_test_table2
WHERE salary > 50000
UPDATE babel_sed_test_table2
SET salary = salary * 1.1
WHERE department_name = 'IT'
END
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- Test Case 5: Nested procedure calls
CREATE PROCEDURE [dbo].[babel_sed_test_proc2](@department varchar(100))
AS
BEGIN
INSERT INTO babel_sed_test_table2 (emp_id, salary, department_name)
VALUES (9999, 75000, @department)
EXEC babel_sed_test_proc1
DELETE FROM babel_sed_test_table2
WHERE emp_id = 9999
END
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[babel_sed_test_proc3]
AS
BEGIN
DECLARE @count1 int, @count2 int
-- Reference multiple tables and functions
SELECT @count1 = COUNT(*) FROM babel_sed_test_table1
SELECT @count2 = COUNT(*) FROM babel_sed_test_table2
-- Insert audit record
INSERT INTO babel_sed_test_audit_log (action_type, table_name, record_count)
VALUES ('COUNT', 'babel_sed_test_table1', @count1)
INSERT INTO babel_sed_test_audit_log (action_type, table_name, record_count)
VALUES ('COUNT', 'babel_sed_test_table2', @count2)
-- Call function
SELECT dbo.babel_sed_test_func1(50000) as high_salary_count
-- Reference view
SELECT COUNT(*) FROM babel_sed_test_view1
END
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- Test Case 11: Procedure with dynamic SQL patterns (should not be detected)
CREATE PROCEDURE [dbo].[babel_sed_test_proc4](@table_name varchar(100))
AS
BEGIN
-- This should not create dependencies since it's dynamic
DECLARE @sql varchar(500)
SET @sql = 'SELECT COUNT(*) FROM ' + @table_name
-- But this static reference should be detected
SELECT COUNT(*) FROM babel_sed_test_table1
END
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
--Create the DELETE trigger
CREATE TRIGGER [dbo].[tr_babel_sed_test_table1_delete]
ON [dbo].[babel_sed_test_table1]
AFTER DELETE
AS
BEGIN
SET NOCOUNT ON;
-- Mark current active version as inactive
UPDATE a
SET [is_record_active] = 0,
[modified_date] = GETDATE(),
[modified_by] = SUSER_SNAME()
FROM [dbo].[babel_sed_test_table1_audit] a
INNER JOIN deleted d ON a.[id] = d.[id]
WHERE a.[is_record_active] = 1;
-- Insert delete record
INSERT INTO [dbo].[babel_sed_test_table1_audit] (
[id], [name], [amount],
[create_date], [created_by], [modified_date], [modified_by],
[modification_type], [is_record_active]
)
SELECT
d.[id], d.[name], d.[amount],
COALESCE(orig.[create_date], GETDATE()),
COALESCE(orig.[created_by], SUSER_SNAME()),
GETDATE(), SUSER_SNAME(),
'D', 0
FROM deleted d
LEFT JOIN (
SELECT [id], [create_date], [created_by]
FROM [dbo].[babel_sed_test_table1_audit]
WHERE [modification_type] = 'I'
) orig ON d.[id] = orig.[id];
END
GO
ALTER TABLE [dbo].[babel_sed_test_table1] ENABLE TRIGGER [tr_babel_sed_test_table1_delete]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- Create the INSERT trigger
CREATE TRIGGER [dbo].[tr_babel_sed_test_table1_insert]
ON [dbo].[babel_sed_test_table1]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO [dbo].[babel_sed_test_table1_audit] (
[id], [name], [amount],
[create_date], [created_by], [modified_date], [modified_by],
[modification_type], [is_record_active]
)
SELECT
i.[id], i.[name], i.[amount],
GETDATE(), SUSER_SNAME(), GETDATE(), SUSER_SNAME(),
'I', 1
FROM inserted i;
END
GO
ALTER TABLE [dbo].[babel_sed_test_table1] ENABLE TRIGGER [tr_babel_sed_test_table1_insert]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
--Create the UPDATE trigger
CREATE TRIGGER [dbo].[tr_babel_sed_test_table1_update]
ON [dbo].[babel_sed_test_table1]
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
-- Mark previous version as inactive
UPDATE a
SET [is_record_active] = 0,
[modified_date] = GETDATE(),
[modified_by] = SUSER_SNAME()
FROM [dbo].[babel_sed_test_table1_audit] a
INNER JOIN deleted d ON a.[id] = d.[id]
WHERE a.[is_record_active] = 1;
-- Insert new version
INSERT INTO [dbo].[babel_sed_test_table1_audit] (
[id], [name], [amount],
[create_date], [created_by], [modified_date], [modified_by],
[modification_type], [is_record_active]
)
SELECT
i.[id], i.[name], i.[amount],
COALESCE(orig.[create_date], GETDATE()),
COALESCE(orig.[created_by], SUSER_SNAME()),
GETDATE(), SUSER_SNAME(),
'U', 1
FROM inserted i
LEFT JOIN (
SELECT [id], [create_date], [created_by]
FROM [dbo].[babel_sed_test_table1_audit]
WHERE [modification_type] = 'I'
) orig ON i.[id] = orig.[id];
END
GO
ALTER TABLE [dbo].[babel_sed_test_table1] ENABLE TRIGGER [tr_babel_sed_test_table1_update]
GO
@bill-ramos-rmoswi , Thanks for the proposed changes. One of us will get back to you in few days (tentatively next week if not earlier).
@bill-ramos-rmoswi , Thanks for the proposed changes. I am currently reviewing your proposed changes and will share feedback soon.
@bill-ramos-rmoswi proposed changes looks good to me on high level. Can you please raise a pull request for this changes. Also can you please add a high level design summary of your proposed solution.