SQL script for updating SQL Extended Properties with a Cursor - sql

I have a task of updating the tables in a database which are missing Extended Properties for 'Category' and 'Description'. The script below is what I am currently trying to implement, but it doesn't function as I'd hoped it would.
The first part of the script successfully returns the tables which have NULL or missing values for 'Category' and 'Description'. These results are used by a cursor to iterate through each table so I can perform an operation on them. For each table in the cursor, I want to retrieve the correct values from TemporaryTable which contains the correct 'Category' and 'Description'. Then, pass those correct values to the EXEC statements to update or create the Extended Property. The only thing that doesn't seem to work is that the variables 'categ' and 'descr' don't get assigned the values in the second SELECT statement where I expected them to.
I've tried as many combinations as I can think of to get these values into the variables, but none seem to work for me. Hopefully somebody can point out where I am going wrong.
Thanks in advance for your suggestions.
Few variables declared for use in this script
*/
DECLARE #tablename VARCHAR(100)
DECLARE #categ VARCHAR(50)
DECLARE #descr VARCHAR(MAX)
DECLARE myCursor CURSOR FOR
/*
This SELECT/FROM returns all table names which have
a NULL value for either category or description. This statement correctly
returns the subset of tables.
*/
SELECT
t.name AS tablename
FROM sys.tables t
LEFT JOIN sys.extended_properties AS description
ON t.object_id = description.major_id
AND description.minor_id = 0
AND description.name = 'MS_Description'
LEFT JOIN sys.extended_properties AS category
ON t.object_id = category.major_id
AND category.minor_id = 0
AND category.name = 'Table_Category'
WHERE type = 'U'
AND (category.value IS NULL OR description.value IS NULL)
AND t.name NOT LIKE 'temp%'
AND t.name NOT LIKE 'tmp%'
AND t.name NOT LIKE '%_BAK'
OPEN myCursor
FETCH NEXT FROM myCursor INTO #tablename
WHILE ##FETCH_STATUS = 0
BEGIN
/*
This SELECT/FROM/WHERE takes information (correct extended properties) about all
cursor tables from a temporary table.
The purpose is to update the NULL (or not present) extended properties in the subset of tables
of the cursor.
The [Table Name], tp.Category, and tp.Description are correctly output in the results, but the assignment
to the variables categ, and descr (which I want to use in the EXEC statement for updating properties)
doesn't seem to happen.
*/
SELECT
tp.[Table Name],
tp.Category AS categ,
tp.Description AS descr
FROM TemporaryTable tp
WHERE #tablename = tp.[Table Name]
--If category ext_prop doesn't exist, add it
IF NOT EXISTS (SELECT NULL FROM SYS.EXTENDED_PROPERTIES WHERE [major_id] = OBJECT_ID(#tablename) AND [name] = N'Table_Category' AND [minor_id] = 0)
EXEC sys.sp_addextendedproperty #name=N'Table_Category', #value=N#categ , #level0type=N'SCHEMA',#level0name=N'dbo', #level1type=N'TABLE',#level1name=N#tablename
--Else, update extended property
ELSE
EXEC sys.sp_updateextendedproperty #name=N'Table_Category', #value=N#categ , #level0type=N'SCHEMA',#level0name=N'dbo', #level1type=N'TABLE',#level1name=N#tablename
--If description ext_prop doesn't exist, add extended property
IF NOT EXISTS (SELECT NULL FROM SYS.EXTENDED_PROPERTIES WHERE [major_id] = OBJECT_ID(#tablename) AND [name] = N'MS_Description' AND [minor_id] = 0)
EXEC sys.sp_updateextendedproperty #name=N'MS_Description', #value=N#desc , #level0type=N'SCHEMA',#level0name=N'dbo', #level1type=N'TABLE',#level1name=N#tablename
--If Not Exists, add extended property
ELSE
EXEC sys.sp_addextendedproperty #name=N'MS_Description', #value=N#desc , #level0type=N'SCHEMA',#level0name=N'dbo', #level1type=N'TABLE',#level1name=N#tablename
PRINT #categ
FETCH NEXT FROM myCursor INTO #tablename
END
CLOSE myCursor
DEALLOCATE myCursor

I don't see any trace of variable assignments, just aliases.
To assign a value to a variable you must either use
SET #varname = value or query
or you can do it using a select statement like
SELECT
#categ = tp.Category,
#descr = tp.Description
FROM TemporaryTable tp
WHERE #tablename = tp.[Table Name]

Related

How to filter data based on different values of a column in sql server

I am stuck at a point.
I want to select based on the column entitytype if entitytype value is Booking or JOb then it will filter on its basis but if it is null or empty string('') then i want it to return all the rows containing jobs and bookings
create proc spproc
#entityType varchar(50)
as
begin
SELECT TOP 1000 [Id]
,[EntityId]
,[EntityType]
,[TenantId]
FROM [FutureTrakProd].[dbo].[Activities]
where TenantId=1 and EntityType= case #EntityType when 'BOOKING' then 'BOOKING'
when 'JOB' then 'JOB'
END
end
Any help would be appreciable
Thankyou
create proc spproc
#entityType varchar(50)
as
begin
SELECT TOP 1000 [Id]
,[EntityId]
,[EntityType]
,[TenantId]
FROM [FutureTrakProd].[dbo].[Activities]
where TenantId=1 and (#EntityType is null OR EntityType= #EntityType)
end
You don't need to use case expression you can do :
SELECT TOP 1000 [Id], [EntityId], [EntityType], [TenantId]
from [FutureTrakProd].[dbo].[Activities]
WHERE TenantId = 1 AND
(#EntityType IS NULL OR EntityType = #EntityType)
ORDER BY id; -- whatever order you want (asc/desc)
For your query procedure you need to state explicit ORDER BY clause otherwise TOP 1000 will give random Ids.
You don't need a CASE expression for this, you just need an OR. The following should put you on the right path:
WHERE TenantId=1
AND (EntityType = #EntityType OR #EntityType IS NULL)
Also, note it would also be wise to declare your parameter as NULLable:
CREATE PROC spproc #entityType varchar(50) = NULL
This means that someone can simply exclude the paramter, value than having to pass NULL (thus EXEc spproc; would work).
Finally, if you're going to have lots of NULLable parameters, then you're looking at a "catch-all" query; the solution would be different if that is the case. "Catch-all" queries can be notoriously slow.
You can execute a dynamic sql query.
Query
create proc spproc
#entityType varchar(50)
as
begin
declare #sql as nvarchar(max);
declare #condition as nvarchar(2000);
select = case when #entityType is not null then ' and [EntityType] = #entityType;' else ';' end;
select #sql = 'SELECT TOP 1000 [Id], [EntityId], [EntityType], [TenantId] FROM [FutureTrakProd].[dbo].[Activities] where TenantId = 1 ';
exec sp_executesql #sql,
N'#entityType nvarchar(1000)',
#entityType = #entityType
end

Is there a way to find all invalid columns that are referenced in a view using SQL Server 2012?

I have inherited a large database project with thousands of views.
Many of the views are invalid. They reference columns that no longer exist. Some of the views are very complex and reference many columns.
Is there an easy way to track down all the incorrect columns references?
This answer finds the underlying columns that were originally defined in the views by looking at sys.views, sys.columns and sys.depends (to get the underlying column if the column has been aliased). It then compares this with the data held in INFORMATION_Schema.VIEW_COLUMN_USAGE which appears to have the current column usage.
SELECT SCHEMA_NAME(v.schema_id) AS SchemaName,
OBJECT_NAME(v.object_id) AS ViewName,
COALESCE(alias.name, C.name) As MissingUnderlyingColumnName
FROM sys.views v
INNER JOIN sys.columns C
ON C.object_id = v.object_id
LEFT JOIN sys.sql_dependencies d
ON d.object_id = v.object_id
LEFT JOIN sys.columns alias
ON d.referenced_major_id = alias.object_id AND c.column_id= alias.column_id
WHERE NOT EXISTS
(
SELECT * FROM Information_Schema.VIEW_COLUMN_USAGE VC
WHERE VIEW_NAME = OBJECT_NAME(v.object_id)
AND VC.COLUMN_NAME = COALESCE(alias.name, C.name)
AND VC.TABLE_SCHEMA = SCHEMA_NAME(v.schema_id)
)
For the following view:
create table test
( column1 varchar(20), column2 varchar(30))
create view vwtest as select column1, column2 as column3 from test
alter table test drop column column1
The query returns:
SchemaName ViewName MissingUnderlyingColumnName
dbo vwtest column1
This was developed with the help of this Answer
UPDATED TO RETRIEVE ERROR DETAILS
So this answer gets you what you want but it isn't the greatest code.
A cursor is used (yes I know :)) to execute a SELECT from each view in a TRY block to find ones that fail. Note I wrap each statement with a SELECT * INTO #temp FROM view X WHERE 1 = 0 this is to stop the EXEC returning any results and the 1=0 is so that SQL Server can optimize the query so that it is in effect a NO-OP.
I then return a list of any views whose sql has failed.
I haven't performed lots of testing on this, but it appears to work. I would like to get rid of the execution of each SELECT from View.
So here it is:
DECLARE curView CURSOR FOR
SELECT v.name AS ViewName
FROM sys.views v
INNER JOIN sys.sql_modules m
on v.object_id = m.object_id
OPEN curView
DECLARE #viewName SYSNAME
DECLARE #failedViews TABLE
(
FailedViewName SYSNAME,
ErrorMessage VARCHAR(MAX)
)
FETCH NEXT FROM curView
INTO #ViewName
WHILE ##FETCH_STATUS = 0
BEGIN
BEGIN TRY
exec ('SELECT * INTO #temp FROM ' + #viewName + ' WHERE 1=0' )
END TRY
BEGIN CATCH
INSERT INTO #failedViews VALUES (#viewName, ERROR_MESSAGE())
END CATCH
FETCH NEXT FROM curView
INTO #ViewName
END
CLOSE curView
DEALLOCATE curView
SELECT *
FROM #failedViews
An example of an ERROR returned is:
FailedViewName ErrorMessage
--------------- -------------
vwtest Invalid column name 'column1'.
You could use system tables get information.
SELECT v.VIEW_NAME,v.TABLE_CATALOG,v.TABLE_SCHEMA,v.TABLE_NAME,v.COLUMN_NAME
from INFORMATION_SCHEMA.VIEW_COLUMN_USAGE v
left outer join INFORMATION_SCHEMA.COLUMNS c
ON v.TABLE_CATALOG=c.TABLE_CATALOG AND v.TABLE_SCHEMA=c.TABLE_SCHEMA AND v.TABLE_NAME=c.TABLE_NAME AND v.COLUMN_NAME=c.COLUMN_NAME
WHERE c.TABLE_NAME IS NULL
ORDER BY v.VIEW_NAME

TSQL: Using a Table in a Variable in a Function

I'm trying to do a select from a table that will need to be in a variable. I'm working with tables that are dynamically created from an application. The table will be named CMDB_CI_XXX, where XXX will be an integer value based on a value in another table. The ultimate goal is to get the CI Name from the table.
I've tried passing the pieces that make up the table name to a function and string them together and then return the name value, but I'm not allowed to use an EXEC statement in a function.
This is what I want to execute to get the name value back:
Select [Name] from 'CMDB_CI_' + C.CI_TYPE_ID + Where CI_ID = c.CI_ID
This is the code in the SP that I'd like to use the function in to get the name value:
SELECT
CI_ID,
C.CI_TYPE_ID,
CI_CUSTOM_ID,
STATUS,
CI_TYPE_NAME,
--(Select [Name] from CMDB_CI_ + C.CI_TYPE_ID + Where CI_ID = c.CI_ID)
FROM [footprints].[dbo].[CMDB50_CI_COMMON] c
join [footprints].[dbo].[CMDB50_CI_TYPE] t
on c.CI_TYPE_ID = t.CI_TYPE_ID
where status <> 'retired'
order by CI_TYPE_NAME
I'm not sure what to do with this. Please help?
Thanks,
Jennifer
-- This part would be a SP parameter I expect
DECLARE #tableName varchar(100)
SET #tableName = 'CMDB_CI_508'
-- Main SP code
DECLARE #sqlStm VARCHAR(MAX)
SET #sqlStm = 'SELECT *
FROM '+ #tableName
EXEC (#sqlStm)
Fiddle http://sqlfiddle.com/#!3/436a7/7
First off, yes, I know it's a bad design. I didn't design it. It came with the problem tracking software that my company bought for our call center. So I gave up altogether on the approach I was going for and used a cursor to pull all the the names from the various tables into one temp table and then used said temp table to join to the original query.
ALTER Proc [dbo].[CI_CurrentItems]
As
Declare #CIType nvarchar(6)
Declare #Qry nvarchar(100)
/*
Create Table Temp_CI
( T_CI_ID int,
T_CI_Type_ID int,
T_Name nvarchar(400)
)
*/
Truncate Table Temp_CI
Declare CI_Cursor Cursor For
select distinct CI_TYPE_ID FROM [footprints].[dbo].[CMDB50_CI_COMMON]
where STATUS <> 'Retired'
Open CI_Cursor
Fetch Next from CI_Cursor into #CIType
While ##FETCH_STATUS = 0
BEGIN
Set #Qry = 'Select CI_ID, CI_Type_ID, Name from Footprints.dbo.CMDB50_CI_' + #CIType
Insert into Temp_CI Exec (#Qry)
Fetch Next from CI_Cursor into #CIType
END
Close CI_Cursor
Deallocate CI_Cursor
SELECT CI_ID,
C.CI_TYPE_ID,
CI_CUSTOM_ID,
STATUS,
CI_TYPE_NAME,
T_Name
FROM [footprints].[dbo].[CMDB50_CI_COMMON] c
JOIN [footprints].[dbo].[CMDB50_CI_TYPE] t
ON c.CI_TYPE_ID = t.CI_TYPE_ID
JOIN Temp_CI tc
ON c.CI_ID = tc.T_CI_ID
AND t.CI_TYPE_ID = tc.T_CI_TYPE_ID
WHERE STATUS <> 'retired'
ORDER BY CI_TYPE_NAME

How can I check if the table behind a synonym exists

I'm trying to create a simple script to dump the results of a complex view out into a table for reporting. I have used synonyms to simplify tweaking the view and table names.
The idea is that the user of the script can put the name of the view they want to use as the source, and the name of the target reporting table in at the start and away they go. If the table doesn't exist then the script should create it. If the table already exists then the script should only copy the records from the view which aren't already in the table over.
The script below covers all those requirements, but I can't find a nice way to check if the table behind the synonym already exists:
CREATE SYNONYM SourceView FOR my_view
CREATE SYNONYM TargetReportingTable FOR my_table
-- Here's where I'm having trouble, how do I check if the underlying table exists?
IF (SELECT COUNT(*) FROM information_schema.tables WHERE table_name = TargetReportingTable) = 0
BEGIN
-- Table does not exists, so insert into.
SELECT * INTO TargetReportingTable FROM SourceView
END
ELSE
BEGIN
-- Table already exists so work out the last record which was copied over
-- and insert only the newer records.
DECLARE #LastReportedRecordId INT;
SET #LastReportedRecordId = (SELECT MAX(RecordId) FROM TargetReportingTable)
INSERT INTO TargetReportingTable SELECT * FROM SourceView WHERE RecordId > #LastReportedRecordId
END
DROP SYNONYM SourceView
DROP SYNONYM TargetReportingTable
I know I could just get the user of the script to copy the table name into the 'information_schema' line as well as into the synonym at the top, but that leaves scope for error.
I also know I could do something filthy like put the table name into a variable and blat the SQL out as a string, but that makes me feel a bit sick!
Is there a nice elegant SQL way for me to check if the table behind the synonym exists? Or a totally different way to solve to problem?
Not the most elegant of solutions, but you could join the sys.synonyms table to the sys.tables table to check whether the table exists.
If the table does not exist, the join will fail and you will get 0 rows (hence IF EXISTS will be false). If the table does exist, the join will success and you will get 1 row (and true):
IF EXISTS( SELECT *
FROM sys.synonyms s
INNER JOIN sys.tables t ON REPLACE(REPLACE(s.base_object_name, '[', ''), ']', '') = t.name
WHERE s.name = 'TargetReportingTable')
BEGIN
-- Does exist
END
ELSE
BEGIN
-- Does not exist
END
Replace 'TargetReportingTable' with whichever synonym you wish to check.
The above solutions did not work for me if the synonym referenced another database. I recently discovered the function [fn_my_permissions] which is useful for showing permissions for a specific database object, so I figure this could be used as follows:
IF EXISTS
(
select *
from sys.synonyms sy
cross apply fn_my_permissions(sy.base_object_name, 'OBJECT')
WHERE sy.name = 'TargetReportingTable'
)
print 'yes - I exist!'
Late to the party, I have created a query to test out the existence of Synonyms and share with you.
DECLARE #Synonyms table
(
ID int identity(1,1),
SynonymsDatabaseName sysname,
SynonymsSchemaName sysname,
SynonymsName sysname,
DatabaseName nvarchar(128),
SchemaName nvarchar(128),
ObjectName nvarchar(128),
Remark nvarchar(max),
IsExists bit default(0)
)
INSERT #Synonyms (SynonymsDatabaseName, SynonymsSchemaName, SynonymsName, DatabaseName, SchemaName, ObjectName)
SELECT
DB_NAME() AS SynonymsDatabaseName,
SCHEMA_NAME(schema_id) AS SynonymsSchemaName,
name AS SynonymsName,
PARSENAME(base_object_name,3) AS DatabaseName,
PARSENAME(base_object_name,2) AS SchemaName,
PARSENAME(base_object_name,1) AS ObjectName
FROM sys.synonyms
SET NOCOUNT ON
DECLARE #ID int = 1, #Query nvarchar(max), #Remark nvarchar(max)
WHILE EXISTS(SELECT * FROM #Synonyms WHERE ID = #ID)
BEGIN
SELECT
#Query = 'SELECT #Remark = o.type_desc FROM [' + DatabaseName + '].sys.objects o INNER JOIN sys.schemas s ON o.schema_id = s.schema_id WHERE s.name = ''' + SchemaName + ''' AND o.name = ''' + ObjectName + ''''
FROM #Synonyms WHERE ID = #ID
EXEC sp_executesql #Query, N'#Remark nvarchar(max) OUTPUT', #Remark OUTPUT;
UPDATE #Synonyms SET IsExists = CASE WHEN #Remark IS NULL THEN 0 ELSE 1 END, Remark = #Remark WHERE ID = #ID
SELECT #ID += 1, #Remark = NULL
END
SELECT * FROM #Synonyms
You can do this with dynamic SQL:
-- create synonym a for information_schema.tables
create synonym a for b
declare #exists int = 1;
begin try
exec('select top 0 * from a');
end try
begin catch
set #exists = 0;
end catch
select #exists;
This doesn't work with non-dynamic SQL, because the synonym reference is caught at compile-time. That means that the code just fails with a message and is not caught by the try/catch block. With dynamic SQL, the block catches the error.
You can test if Synonym exists in your database using the Object_Id function avaliable in SQL Server
IF OBJECT_ID('YourDatabaseName..YourSynonymName') IS NOT NULL
PRINT 'Exist SYNONYM'
ELSE
PRINT 'Not Exist SYNONYM'
Another simpler solution:
IF (EXISTS (SELECT * FROM sys.synonyms WHERE NAME ='mySynonymName'))
BEGIN
UPDATE mySynonymName
SET [Win] = 1
END
In this case, I do database setup first. I drop all Synonyms in my database (database1) first, then run a SPROC to create synonyms for all tables in the destination database(database2).
Some SPROCS in database1 call on tables in DB2. If table doesnt exist in DB2 the SPROC fails. If table doesnt exist in DB2, the synonmy is not automatically created on database setup. So I just use the above to check if the Synonym exist, and skip that part of the SPROC if the Synonym is not present.

SQL DELETE hits constraints

Let's look at the famous Nortwind database. Say I run DELETE FROM Clients.
In MSAccess, when one runs a DELETE statement against a table with referential integrity constraints, Jet will delete the records it can delete, and leave the other ones. In this case, it would delete only the Clients for which there are no Orders.
In SQL Server, doing this seems to just fail, with a message stating that The DELETE statement conflicted with the REFERENCE constraint .....
My question therefore is: is there a simple way to let SQL Server delete just those records that can be deleted ? or do I have to add a WHERE ClientId NOT IN (SELECT Id FROM Clients) ?
In other words, can I make SQL Server DELETE work like Jet's DELETE ?
For info: I am not THAT lazy, but there are MANY constraints there and I'd like to keep my code simple...
Another approach is to delete in loop (row by row) using CURSOR and TRY .. CATCH block to ignore problems with deleting referenced rows.
In this approach you don't have to model your existing and future constraints.
Example:
SET NOCOUNT ON; -- use not to have "(N row(s) affected)" for each deleted row
DECLARE del_cursor CURSOR
FOR SELECT ClientID FROM Clients
DECLARE #CurrentClientID INT -- use your proper type
DECLARE #message VARCHAR(200) -- just for building messages
OPEN del_cursor
FETCH NEXT FROM del_cursor
INTO #CurrentClientID
WHILE ##FETCH_STATUS = 0
BEGIN
BEGIN TRY
DELETE FROM Clients WHERE CURRENT OF del_cursor
END TRY
BEGIN CATCH
SET #message = 'Row ' + CAST(#CurrentClientID AS VARCHAR) + ' cannot be deleted - skipping.'
PRINT #message
END CATCH
FETCH NEXT FROM del_cursor
INTO #CurrentClientID
END
CLOSE del_cursor
DEALLOCATE del_cursor
You may wrap above example with CREATE PROCEDURE DeleteClients and use EXEC DeleteClients instead of DELETE FROM Clients
Your options are:
Cascading Delete - Will delete all records that are dependent on
those clients.
Drop constraint (I don't recommend this :))
Check ahead of time that it can be delete and will not have a conflict
If you want to leave rows that have FK references, then there are only a couple of options, and none of them are pretty:
Check constraints before you do the delete
Modify the query to have the where clauses for the FK as you mentioned in your question
Change your logic to delete rows one at a time, commit each row and rollback the delete if it fails.
The 'least lousy' option really depends on how many FKs are there, how many rows you'll be deleting and the likelihood that a row has FK dependencies. If that is a relatively rare event, then option #3 may be best, although I would tend to lean towards the first two options.
I know it is a little bit complicated solution but you need to do it only once.
How about a trigger to prevent the deletions?
create table parent(
id int not null primary key
)
create table son(
id int not null primary key,
idparent int)
alter table son add foreign key(idparent) references parent(id)
insert into parent values(1)
insert into parent values(2)
insert into parent values(3)
insert into son values(1,1)
--select * from parent
--select * from son
create trigger preventDelete
on parent
instead of delete
as
begin
delete from parent where id not in (select idparent from son) and id in (select id from deleted)
end
delete from parent
records 2 and 3 will be deleted
Try this, it will generate your statements. Excuse the format I wrote it very quickly:
DECLARE #i INT, #SQL NVARCHAR(2000), #TABLENAME NVARCHAR(100)
SET #i = 1
SET #TABLENAME = 'informer_alert'
SET #SQL = ''
DECLARE #col VARCHAR(50), #basecol VARCHAR(50), #tname VARCHAR(50)
DECLARE #TABLE TABLE ([table] VARCHAR(50), col VARCHAR(50), basecol VARCHAR(50))
INSERT INTO #TABLE
SELECT t.name, sc.name, sc2.name
FROM sys.foreign_key_columns fk
INNER JOIN sys.tables t ON fk.parent_object_id = t.OBJECT_ID
INNER JOIN syscolumns sc ON fk.parent_column_id = sc.colorder
AND sc.id = fk.parent_object_id
INNER JOIN syscolumns sc2 ON fk.referenced_object_id = sc2.id
AND fk.constraint_column_id = sc2.colorder
WHERE fk.referenced_object_id = (SELECT OBJECT_ID
FROM sys.tables
WHERE name = #TABLENAME)
WHILE (#i <= (SELECT COUNT(*) FROM #TABLE))
BEGIN
SELECT #tname = [table], #col = col, #basecol = basecol
FROM (SELECT ROW_NUMBER() OVER(ORDER BY col) [N],
[table], col, basecol
FROM #TABLE) A
WHERE A.N = #i
SET #SQL = #SQL + ' DELETE FROM ' + #TABLENAME + ' WHERE ' + #basecol + ' NOT IN (SELECT ' + #col+ ' FROM ' + #tname + ')'
SET #i = #i + 1
END
SELECT #SQL
If you want to keep code simple (you said that) let you CREATE VIEW over your table.
Define WHERE clause to get only deletable rows.
Then you may just DELETE FROM ClientsDeletable.
Remember about DELETE permission for your new view.
Script example:
CREATE VIEW ClientsDeletable
AS
SELECT *
FROM Clients
WHERE
ClientID NOT IN (SELECT CliID FROM ForeignTab1)
AND
ClientID NOT IN (SELECT CliID FROM ForeignTab2)
Notice, that FROM cannot contains JOINs - other way you'll get error:
Msg 4405, Level 16, State 1, Line 1
View or function 'ClientsDeletable' is not updatable because the modification affects multiple base tables.