How to join data using dynamic table and field names - sql

So I have some tables that contains a foreign key that refers to different tables and fields depending on the context. I have a key source table that contains the relevant table and field names. Then I need to be able to lookup the values in those tables/fields.
I simplified it to the following tables/data for demonstration of what I have:
CREATE TABLE [dbo].[WorkOrderStatus](
[WorkOrderStatusId] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](60) NOT NULL
) ON [PRIMARY]
CREATE TABLE [dbo].[Note](
[NoteId] [int] IDENTITY(1,1) NOT NULL,
[NoteKeySourceId] [int] NULL,
[KeyValue] [int] NULL,
[Note] [nvarchar](max) NOT NULL,
) ON [PRIMARY]
CREATE TABLE [dbo].[NoteKeySource](
[NoteKeySourceId] [int] IDENTITY(1,1) NOT NULL,
[SourceTable] [nvarchar](60) NOT NULL,
[SourceField] [nvarchar](60) NOT NULL,
[SourceTextField] [nvarchar](60) NOT NULL,
) ON [PRIMARY]
GO
INSERT INTO WorkOrderStatus VALUES('Open')
DECLARE #OpenStatusId int = SCOPE_IDENTITY();
INSERT INTO WorkOrderStatus VALUES('Closed')
DECLARE #ClosedStatusId int = SCOPE_IDENTITY();
INSERT INTO NoteKeySource VALUES('WorkOrderStatus', 'WorkOrderStatusId', 'Name')
DECLARE #WorkOrderSource int = SCOPE_IDENTITY();
INSERT INTO Note VALUES(#WorkOrderSource, #OpenStatusId, 'Opened the work order')
INSERT INTO Note VALUES(#WorkOrderSource, #ClosedStatusId, 'Closed the work order')
So I need to be able to query a list of Notes, including the SourceTextField referred to in NoteKeySource, identified by KeyValue.
Here is the SP that I have now, which works:
CREATE PROCEDURE [dbo].[spGetNotes]
AS
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
CREATE TABLE #NoteTable (
NoteId int NOT NULL,
SourceTable nvarchar(60),
SourceField nvarchar(60),
SourceTextField nvarchar(60),
KeyValue int,
Context nvarchar(MAX),
Note nvarchar(MAX),
)
INSERT INTO #NoteTable (NoteId, SourceTable, SourceField, SourceTextField, KeyValue, Note)
SELECT N.NoteId, NKS.SourceTable, NKS.SourceField, NKS.SourceTextField, N.KeyValue, N.Note
FROM Note AS N
LEFT OUTER JOIN NoteKeySource AS NKS
ON NKS.NoteKeySourceId = N.NoteKeySourceId
DECLARE #Context nvarchar(max)
DECLARE #Sql nvarchar(255);
DECLARE #SqlDecl nvarchar(255) = N'#Key int, #Ctx nvarchar(MAX) OUTPUT';
DECLARE #NoteId int, #KeyValue int, #SourceTable nvarchar(60), #SourceField nvarchar(60), #SourceTextField nvarchar(60)
DECLARE db_cursor CURSOR FOR SELECT SourceTable, SourceField, SourceTextField, KeyValue
FROM #NoteTable WHERE SourceTable IS NOT NULL FOR UPDATE OF Context
OPEN db_cursor
FETCH NEXT FROM db_cursor INTO #SourceTable, #SourceField, #SourceTextField, #KeyValue
WHILE ##FETCH_STATUS = 0
BEGIN
SET #Sql = N'SELECT #Ctx = ' + #SourceTextField + ' FROM ' + #SourceTable + ' WHERE ' + #SourceField + '= #Key';
EXECUTE sp_executesql #Sql, #SqlDecl, #Key = #KeyValue, #Ctx = #Context OUTPUT
UPDATE #NoteTable SET Context=#Context WHERE CURRENT OF db_cursor
FETCH NEXT FROM db_cursor INTO #SourceTable, #SourceField, #SourceTextField, #KeyValue
END
CLOSE db_cursor
DEALLOCATE db_cursor
SELECT NoteId, Context, Note from #NoteTable
DROP TABLE #NoteTable
RETURN 0
Calling spGetNotes returns:
1:Open:Opened the work order
2:Closed:Closed the work order
It works, but it is pretty ugly, and I would just like to know if there is a better/cleaner/more appropriate way to do this without using a cursor or a while loop. It is my understanding that dynamic SQL cannot be used in a subquery of normal SQL, so this was the best I could come up with.
I am relatively green when it comes to SQL, so if I am wrong about that, please enlighten me! Also, if there is a better overall approach to the general domain problem, that would also be welcome and appreciated :)

Create a view that provides a single EAV overlay across your source tables. Then join NoteKeySource to it.
CREATE VIEW AllSources AS (
SELECT
'SourceTable1' AS [SourceTable], [SourceField], [SourceTextField]
FROM SourceTable1
UNPIVOT([SourceTextField] FOR [SourceField] IN
('Table1Field1','Table1Field2','Table1Field3','Table1Field4')
) p
UNION ALL
SELECT
'SourceTable2' AS [SourceTable], [SourceField], [SourceTextField]
FROM SourceTable2
UNPIVOT([SourceTextField] FOR [SourceField] IN
('Table2Field1','Table2Field2')
) p
UNION ALL
SELECT
'SourceTable3' AS [SourceTable], [SourceField], [SourceTextField]
FROM SourceTable3
UNPIVOT([SourceTextField] FOR [SourceField] IN
('Table3Field1','Table3Field2','Table3Field3')
) p
--etc
)

Related

SQL Server Trigger After Insert is Repeating old valus

Can someone please explain why this trigger would start failing and insert the same record repeatedly? It seems as though there is something wrong with the variables. The purpose of the trigger is to copy the inserted record from the Employee table to the EmployeeHistory table. I set the trigger and it runs fine. But then when my coworker runs some insert scripts, it starts repeating the same old value from my last execution of an insert, instead of the new values that they are trying to insert.
I have already recoded this to not use variables, but I would still like to know why this doesn't work as expected.
CREATE TRIGGER [dbo].[triggerEmployee_AfterInsert]
ON [dbo].[Employee]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
DECLARE #EmployeeID varchar(25)
DECLARE #FirstName varchar(25)
DECLARE #LastName varchar(50)
DECLARE #FullName varchar(75)
DECLARE #EmailAddress varchar(50)
DECLARE #ManagerID varchar(15)
DECLARE #JobTitle varchar(50)
DECLARE #EmployeeStatus varchar(10)
DECLARE #Office varchar(25)
SELECT #EmployeeID = [dbo].[Employee].[EmployeeID]
,#FirstName = [dbo].[Employee].[FirstName]
,#LastName = [dbo].[Employee].[LastName]
,#FullName = [dbo].[Employee].[FullName]
,#EmailAddress = [dbo].[Employee].[EmailAddress]
,#ManagerID = [dbo].[Employee].[ManagerID]
,#JobTitle = [dbo].[Employee].[JobTitle]
,#EmployeeStatus = [dbo].[Employee].[EmployeeStatus]
,#Office = [dbo].[Employee].[Office]
FROM [dbo].[Employee]
INSERT INTO [dbo].[EmployeeHistory] (
EmployeeID
,FirstName
,LastName
,FullName
,EmailAddress
,ManagerID
,JobTitle
,EmployeeStatus
,Office
)
VALUES (
#EmployeeID
,#FirstName
,#LastName
,#FullName
,#EmailAddress
,#ManagerID
,#JobTitle
,#EmployeeStatus
,#Office
)
END
GO
ALTER TABLE [dbo].[Employee] ENABLE TRIGGER [triggerEmployee_AfterInsert]
GO
Rewriting the SELECT statement to use the INSERTED table fixed the issue.
SELECT #EmployeeID = [EmployeeID]
,#FirstName = [FirstName]
,#LastName = [LastName]
,#FullName = [FullName]
,#EmailAddress = [EmailAddress]
,#ManagerID = [ManagerID]
,#JobTitle = [JobTitle]
,#EmployeeStatus = [EmployeeStatus]
,#Office = [Office]
FROM INSERTED

Roll directories up to parent

Is there a way to roll data up that looks like this:
What I am looking for is this:
Y:\Data\FS02-V\Aetna\ETL | Data, development files
So rows two and three being children of row one should roll up to the parent and the parent should show all file types contained. I am working in sql server and this is the code that produces the source table:
SELECT
[MCL].[Category Description] As Category
, [SF].[Directory] as Directory
, CONVERT(BIGINT, [SF].[Length]) AS FileSize
, (SELECT MAX([Id]) FROM [dbo].[split](RIGHT([SF].[Directory],LEN([SF].[Directory])-1),'\')) AS [LevelsFromRoot]
FROM [dbo].[FS02V_SourceFiles] [SF]
INNER JOIN [dbo].[Extensions] [E]
ON [SF].[Extension] = [E].[Extension]
INNER JOIN [dbo].[MasterCategoryLookup] [MCL]
ON [MCL].CategoryID = [E].Category
ORDER BY [SF].[Directory]
Table def:
CREATE TABLE [dbo].[FS02V_SourceFiles](
[Length] [float] NULL,
[Directory] [nvarchar](255) NULL,
[Extension] [nvarchar](255) NULL,
[Type] [nvarchar](max) NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
CREATE TABLE [dbo].[Extensions](
[Extension] [nvarchar](255) NULL,
[Type] [nvarchar](max) NULL,
[Category] [int] NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
Split function:
ALTER FUNCTION [dbo].[Split]
(
#RowData nvarchar(max),
#SplitOn nvarchar(5)
)
RETURNS #RtnValue table
(
Id int identity(1,1),
Data nvarchar(max)
)
AS
BEGIN
Declare #Cnt int
Set #Cnt = 1
While (Charindex(#SplitOn,#RowData)>0)
Begin
Insert Into #RtnValue (data)
Select
Data = ltrim(rtrim(Substring(#RowData,1,Charindex(#SplitOn,#RowData)-1)))
Set #RowData = Substring(#RowData,Charindex(#SplitOn,#RowData)+1,len(#RowData))
Set #Cnt = #Cnt + 1
End
Insert Into #RtnValue (data)
Select Data = ltrim(rtrim(#RowData))
Return
END
The path(Directory) is stored as nvarchar and it should be the driver to roll up to the parent. I would assume the path needs to be split and my function is already doing something similar to get the path levels. I think this would be easier in SQL with my raw data but at the end of the day I am going to be visualizing this data in tableau so if anyone knows if its easier to use sql for this before I feed it into tableau or just use tableau with my source data I would try that as well.
Solution to this was simple:
Use abolve SQL to prepare the directories and group them with their respective files and then roll it up using Tableau and calculated fields.

updated record is inserting into the history table not the old record

i have two tables Test and TestHistory
CREATE TABLE [dbo].[TEST](
[ID] [int] NULL,
[Name] [varchar](10) NULL,
[Status] [char](1) NULL,
[CreatedDate] [datetime] NULL
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[Test_History](
[ID] [int] NULL,
[Name] [varchar](10) NULL,
[Status] [char](1) NULL,
[CreatedDate] [datetime] NULL
) ON [PRIMARY]
GO
INSERT INTO TEST ([ID],[Name],[Status],[CreatedDate])values (1,'Mohan','A',GETDATE())
Created Trigger :
ALTER TRIGGER [dbo].[trg_Test]
ON [dbo].[TEST]
FOR UPDATE
AS
Declare #ID INT;
Declare #Name varchar(10);
Declare #Status CHAR(2);
Declare #CreatedDate DATETIME;
Select #ID = I.ID from INSERTED I
Select #Name = I.Name from INSERTED I
Select #Status = I.Status from INSERTED I
Select #CreatedDate = I.CreatedDate from INSERTED I
INSERT INTO [dbo].[Test_history]
([ID]
,[Name]
,[Status]
,[CreatedDate]
)
SELECT #ID,
#Name,
#Status,
GETDATE()
FROM INSERTED I
WHERE #ID = [ID]
When I'm updating the record like
Update [TEST] SET Status = 'I' then the old record with Status = 'A' should inserted but the what ever i'm updating it has been inserting into Testhistory table not the old record
where i'm doing wrong and how to insert old value
like if i updating Status = 'I' and in history table Status = 'A' shoul be inserted
You need to INSERT from DELETED not from INSERTED.
See examples here Understanding SQL Server inserted and deleted tables for DML triggers.
Like Karl mentioned, you need to refer deleted table for old updated row information. Apart from that your existing code doesn't work when you update more than a single row. What you need is something like this.
ALTER TRIGGER [dbo].[trg_Test]
ON [dbo].[TEST]
FOR UPDATE
AS
INSERT INTO [dbo].[Test_history]
(
[ID],
[Name],
[Status],
[CreatedDate]
)
SELECT ID,
Name,
Status,
GETDATE()
FROM deleted d

Insert multiple records using stored procedure

I have two tables
emplyoee (first table)
id primary key auto increment
emp_name varchar
student(second table)
id foriegnkey emplyoee.id
st_name varchar
I want to insert multiple student records for a single employeeid . My code is attached here , but this use to only one student record update. How can I write stored procedure for this need.
I am new with SQL server and stored procedure.
Could you please help me?
create procedure empst_Sp
#emp_name varchar(50),
#st_name varchar(50)
as
begin
insert into emplyoee (emp_name) values (#emp_name)
insert into student(id,st_name) values(SCOPE_IDENTITY(),#st_name)
end
For your case, you can try this code above ( I'm using XML parameter type)
CREATE PROCEDURE EmployeeIns
#EmployeeName NVARCHAR(50),
#Students XML
AS
/*
#Students : <Students>
<Student Name='Studen 1'/>
<Student Name='Studen 1'/>
</Students>
*/
BEGIN
DECLARE #StudenTable TABLE(Name NVARCHAR(50))
DECLARE #EmployeeId INT
INSERT INTO #StudenTable
SELECT Tbl.Col.value('#Name', 'NVARCHAR(50)')
FROM #Students.nodes('//Student') Tbl(Col)
INSERT INTO Emplyoee VALUES(#EmployeeName)
SET #EmployeeId = SCOPE_IDENTITY()
INSERT INTO Student
SELECT #EmployeeId, Name FROM #StudenTable
END
Update 1 :
Your table design should be look like this :
CREATE TABLE [dbo].[Emplyoee](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](150) NULL,
CONSTRAINT [PK_Emplyoee] PRIMARY KEY CLUSTERED
(
[Id] ASC
))
CREATE TABLE [dbo].[Student](
[EmployeeId] [int] NULL,
[Name] [nvarchar](150) NULL,
[Id] [int] IDENTITY(1,1) NOT NULL,
CONSTRAINT [PK_Student] PRIMARY KEY CLUSTERED
(
[Id] ASC
))
The execute code :
EXEC EmployeeIns #EmployeeName='trungtin1710', #Students = '<Students><Student Name="Studen 1"/><Student Name="Studen 1"/></Students>'
All you need is a local variable in which you can set the value retrieved from Scope_Identity:-
CREATE PROCEDURE empst_Sp
#emp_name varchar(50),
#st_name varchar(50)
AS
BEGIN
DECLARE #id INT
INSERT INTO emplyoee (emp_name) VALUES (#emp_name)
set #id = SCOPE_IDENTITY()
INSERT INTO student(id,st_name) VALUES (#id,#st_name)
END
As I understand:
If emplyoee with #emp_name is already exists then insert student records with ID of the emplyoee, if there is not any emplyoee with #emp_name then need to insert new emplyoee and student with ID of the new emplyoee. Yes?
CREATE PROCEDURE empst_Sp
#emp_name varchar(50),
#st_name varchar(50)
AS
BEGIN
DECLARE #EmplyoeeId int
SET #EmplyoeeId = NULL
select #EmplyoeeId = id
from emplyoee
where emp_name = #emp_name
IF #EmplyoeeId IS NULL
BEGIN
insert into emplyoee (emp_name) values (#emp_name)
SET #EmplyoeeId = SCOPE_IDENTITY()
END
insert into student(id, st_name) values(#EmplyoeeId, #st_name)
END

Return NEWSEQUENTIALID() as an output parameter

Imagine a table that looks like this:
CREATE TABLE [dbo].[test](
[id] [uniqueidentifier] NULL,
[name] [varchar](50) NULL
)
GO
ALTER TABLE [dbo].[test] ADD CONSTRAINT [DF_test_id] DEFAULT (newsequentialid()) FOR [id]
GO
With an INSERT stored procedure that looks like this:
CREATE PROCEDURE [Insert_test]
#name as varchar(50),
#id as uniqueidentifier OUTPUT
AS
BEGIN
INSERT INTO test(
name
)
VALUES(
#name
)
END
What is the best way to get the GUID that was just inserted and return it as an output parameter?
Use the Output clause of the Insert statement.
CREATE PROCEDURE [Insert_test]
#name as varchar(50),
#id as uniqueidentifier OUTPUT
AS
BEGIN
declare #returnid table (id uniqueidentifier)
INSERT INTO test(
name
)
output inserted.id into #returnid
VALUES(
#name
)
select #id = r.id from #returnid r
END
GO
/* Test the Procedure */
declare #myid uniqueidentifier
exec insert_test 'dummy', #myid output
select #myid
Try
SELECT #ID = ID FROM Test WHERE Name = #Name
(if Name has a Unique constraint)