Stored Procedure/SQL script that allows me to add a record to a table - sql

I have 2 tables, one with all the email data, and another with all the specific member email data that one creates a row if an email has been read.
Once an email has been read by a member its added to the Member_email_read table (which is created and populated based on all read emails).
I am trying to set (on mass) all of the messages to read (this would populate the Member_email_read table) but whilst I can add them one at a time
(see the stored procedue below), I am unable to add them on mass.
The two tables are Email, which holds a record for every email into the system. The other table is a table of all email that the member has read. Each time an email
is read a record is added to the Member_email_read table. They are assiocited on the message_id (and both should use the same user_id). The two tables are as follows -
SELECT [member_email_id]
,[member_email_FK_message_id]
,[member_email_FK_user_id]
,[member_email_status]
,[member_email_read_datetime]
,[member_email_delete_datetime]
FROM [MemberData].[dbo].[Member_email_read]
SELECT[message_id]
,[email_catlogue_num]
,[email_FK_user_id]
,[Email_time]
,[email_content]
,[Email_created_date]
FROM [MemberData].[dbo].[Email]
To set all the messages (for a certain user) to unread all I would have to do is delete every record from that table for that user, which can be done with the following:
DELETE FROM [MemberData].[dbo].[Member_email_read]
WHERE [member_email_FK_message_id_FK_user_id] ='2';
I am basically looking for the reverse of this delete.
I have created a Stored procedure that allows for the setting of ONE specific email to be set to read, however this stored procedure (when executed) requires the member to enter a
email_id, message_id, user_id, status, read_datetime & delete_datetime.
CREATE PROCEDURE [dbo].[set_member_email_to_read]
#member_email_id int,
#member_email_FK_message_id int,
#member_email_FK_user_id int
#member_email_status varchar(1),
#member_email_read_datetime dateTime,
#member_email_delete_datetime dateTime
as
if not exists (Select * from [dbo].[Member_email_read] where [member_email_FK_message_id] = #member_email_FK_message_id) begin
insert into [dbo].[Member_email_read]
(
[member_email_FK_message_id]
,[member_email_FK_user_id]
,[member_email_status]
,[member_email_read_datetime]
,[member_email_delete_datetime]
)
values
(
#member_email_FK_message_id,
#member_email_FK_user_id
#member_email_status,
#member_email_read_datetime,
#member_email_delete_datetime
)
SELECT Convert(int,SCOPE_IDENTITY()) As InsertedID
end else begin
update [dbo].[Member_email_read] set
[member_email_FK_message_id] = #member_email_FK_message_id
,[member_email_FK_user_id] = #member_email_FK_user_id
,[member_email_status] = #member_email_status
,[member_email_read_datetime] = #member_email_read_datetime
,[member_email_delete_datetime] = #member_email_delete_datetime
where [member_email_FK_user_id] = #member_email_FK_user_id
if (##ERROR = 0) begin
SELECT Convert(int,#member_email_FK_user_id) As InsertedID
end
end
GO
I was hoping to create a stored procedure (or general SQL script) that would allow me to enter in
a user_id and then allow for all emails for that user to change from unread to read (populate the Member_email_read table).

You can try using MERGE to perform a bulk insert/update from your Email table into your Member_email_read table:
MERGE [MemberData].[dbo].[Member_email_read] AS tgt
USING (
SELECT message_id, user_id, 'R', null, CURRENT_TIMESTAMP
FROM [MemberData].[dbo].[Email]
WHERE user_id = #UserId
) AS src (MessageId, UserId, Status, ReadDate, DeleteDate)
ON (
tgt.member_email_FK_message_id = src.message_id
AND tgt.member_email_FK_user_id = src.user_id
)
WHEN MATCHED THEN
UPDATE SET tgt.member_email_status = src.Status,
tgt.member_email_read_datetime = src.ReadDate,
tgt.member_email_delete_datetime = src.DeleteDate
WHEN NOT MATCHED THEN
INSERT (member_email_FK_message_id, member_email_FK_user_id,
member_email_status, member_email_read_datetime, member_email_delete_datetime)
VALUES (src.MessageId, src.UserId, src.Status, src.ReadDate, src.DeleteDate)
;
This should work, though as I mentioned in my comment above, you should rethink your table design and simply add a read_datetime and delete_datetime (possibly a status column if you really need that as well) to your Email table, rather than having a whole separate table simply to hold records identifying a delete status.

Related

Check if field exists in DB using SQL trigger

I have a database Users that has four fields: Name, Client, ID, Time. Client is an integer (0-99). How to write a trigger that will find latest user from Users (latest according to Time) during Insert and if the Client of this user equals Client of inserted user then I'd like to Rollback
I tried like this:
CREATE TRIGGER DoubledData ON Users
FOR INSERT
AS
DECLARE #client DECIMAL(2)
DECLARE #client_old DECIMAL(2)
DECLARE #name Varchar(50)
SELECT #name = Name from inserted
SELECT #client = Client from inserted
//This doesn't work, "Syntax error near Select":
SELECT #client_old = Select top(1) Client from Users where Name like #name order by Time desc;
IF #client = #client_old
BEGIN
ROLLBACK
END
The problem is that I can assign same values to Client for one user but they can't be one after another (eg for client this order is correct 1-2-3-1-3 -> order is important, but this isn't correct: 1-2-3-3 -> after 2nd occurrence of '3' in a row it needs to be rollbacked)
I'm using MS SQL
[EDIT]
I have found that I can execute it without Select top(1) like:
SELECT #client_old = Client from Users where Name like #name order by Time desc;
But the trigger doesn't execute afer insert
First, you clearly don't understand triggers in SQL Server and the inserted pseudo-tables. These can have more than one row, so your code will fail when multiple rows are inserted. Sadly, there is no compile check for this situation. And code can unexpectedly fail (even in production, alas).
Second, the right way to do this is probably with a unique constraint. That would be:
alter table users
add constraint unq_users_name_client unique (name, client);
This would ensure no duplication, so it is a stronger condition than your trigger.

3 tables, 2 DBs, 1 Stored Procedure

I'm a novice when it comes to Stored Procedures in SQL Server Management Studio. I have an application that I was told to make the following changes to using a stored procedure:
Step 1. User types in an item number.
Step 2. Customer name, address, etc. displays in the other fields on the same form.
There are 3 tables: Bulk orders, Small orders, and Customer information.
Bulk orders and small orders are in Database_1 and Customer information is in Database_2.
The primary key for small orders is the order number. A column in small orders contains the customer number for each order. That customer number is the primary key in the customer table.
The bulk orders table is similar.
I want to include a conditional statement that says: if order number is found in small orders table, show data from customer table that coorelates with that order number. I've attempted this multiple ways, but keep getting a "The multi-part identifier.... could not be bound" error.
I.E:
SELECT DB1.db.Customer_Table.Customer_Column AS CustomerNumber;
IF(CustomerNumber NOT LIKE '%[a-z]%')
BEGIN
SELECT * FROM db.small_orders_table;
END
ELSE
BEGIN
SELECT * FROM db.buld_orders_table;
END
Please help.
Sounds like it's 2 databases on the same server...in that case, you'll need to specify the fully qualified table name (database.schema.table) when referencing a table on the other database from where your stored procedure is found.
Database_1.db.small_orders_tables
first of all, you cannot use aliases as variables. If you want to assign a value to a variable in order to test it, you have to do a SELECT statement like SELECT #var = DB1.db.Customer_Table.Customer_Column FROM <YourTableFullName> WHERE <condition>. Then you can use the #var (which must be declared before) for your test.
About the error you're experiencing, youre using fully qualified names in a wrong way. If you're on the same server (different databases), you need to specify just the database name on the top and then the schema of your objects. Suppose to have the following database objects on the Database1:
USE Database1;
GO
CREATE TABLE dbo.Table1
(
id int IDENTITY(1, 1) NOT NULL PRIMARY KEY CLUSTERED
, val varchar(30)
);
GO
INSERT INTO dbo.Table1 (val) VALUES ('test1');
GO
INSERT INTO dbo.Table1 (val) VALUES ('test2');
GO
INSERT INTO dbo.Table1 (val) VALUES ('test3');
GO
And the following ones on Database2:
USE Database2;
GO
CREATE TABLE dbo.Table2
(
id int IDENTITY(1, 1) NOT NULL PRIMARY KEY CLUSTERED
, val varchar(30)
);
GO
Now, suppose that you want to read from the first table the value with id = 2, and then to apply your IF. Let's declare a variable and test it:
USE Database1;
GO
DECLARE #var varchar(30);
-- since you're on Database1, you don't need to specify full name
SELECT #var = val FROM dbo.Table1 WHERE id = 2;
IF #var = 'test2'
BEGIN
SELECT id, val FROM dbo.Table1;
END
ELSE
BEGIN
-- in this case the database name is needed
SELECT id, val FROM Database2.dbo.Table2;
END
GO
Does it help?

Automatic Deletion of Duplicate SQL Records based on date or count

After importing Excel data from a LightSwitch application into a holding table in SQL Server I end up with duplicate records. I need a way to remove the duplicates that can either be executed from LightSwitch or something that will automatically run in SQL after/during insert. I thought about a trigger, but I'm not sure it's the best solution.
The duplicates will be something like this
DocName|DocUser|DocType|DocDate|
test user1 word 10/12/2012
test user1 word 10/12/2012
test2 user2 word 10/11/2012
test2 user2 word 10/12/2012
In the case of the first set of duplicates either record can be deleted so I have one record.
However in the second case the record with the date of 10/11/2012 would need to be deleted.
I'm not apposed to a Stored Procedure if it can be executed by from LightSwitch. I know it can be done with a series of queries but I'm not sure how that could be executed from LightSwitch?
I have no experience with Lightswitch so apologies if any of this is not relevant, but speaking from the SQL side a stored procedure you could use to delete the duplicates is:
CREATE PROCEDURE dbo.DeleteDuplicatesFromT
AS
BEGIN
WITH CTE AS
( SELECT DocName,
DocUser,
DocType,
DocDate,
[RowNumber] = ROW_NUMBER() OVER(PARTITION BY DocName, DocUser, DocType ORDER BY DocDate DESC)
FROM T
)
DELETE CTE
WHERE RowNumber > 1;
END
Example on SQLFiddle
HOWEVER
I'd advise managing this before/during the insert stage, you can either do this in the application code, and ensure only unique records are passed to the table to begin with, or use a procedure again to perform the insert. To do the latter you will first need to create a TYPE to handle your new records:
CREATE TYPE dbo.TableTypeParameter AS TABLE
( DocName VARCHAR(5),
DocUser VARCHAR(5),
DocType VARCHAR(4),
DocDate DATETIME
);
You can fill this in your client code (using System.Data.DataTable) and pass this as a parameter to your stored procedure:
CREATE PROCEDURE dbo.BulkInsert #NewRecords dbo.TableTypeParameter READONLY
AS
BEGIN
WITH NewRecords AS
( SELECT DocName, DocType, DocUser, DocDate = MAX(DocDate)
FROM #NewRecords
GROUP BY DocName, DocType, DocUser
)
MERGE INTO T
USING NewRecords nr
ON T.DocName = nr.DocName
AND T.DocType = nr.DocType
AND T.DocUser = nr.DocUser
WHEN MATCHED AND nr.DocDate > T.DocDate THEN UPDATE
SET DocDate = nr.DocDate
WHEN NOT MATCHED THEN INSERT (DocName, DocUser, DocType, DocDate)
VALUES (nr.DocName, nr.DocUser, nr.DocType, nr.DocDate);
END;
EDIT
The procedure to insert can fairly easily be turned into a trigger if this is what is required:
CREATE TRIGGER dbo.T_InsteadOfInsert
ON T
INSTEAD OF INSERT
AS
BEGIN
WITH NewRecords AS
( SELECT DocName, DocType, DocUser, DocDate = MAX(DocDate)
FROM inserted
GROUP BY DocName, DocType, DocUser
)
MERGE INTO T
USING NewRecords nr
ON T.DocName = nr.DocName
AND T.DocType = nr.DocType
AND T.DocUser = nr.DocUser
WHEN MATCHED AND nr.DocDate > T.DocDate THEN UPDATE
SET DocDate = nr.DocDate
WHEN NOT MATCHED THEN INSERT (DocName, DocUser, DocType, DocDate)
VALUES (nr.DocName, nr.DocUser, nr.DocType, nr.DocDate);
END
A user on the MSDN forums had a similar problem using the Excel Importer. He wrote a few functions to check and validate his tables to prevent entry of and remove existing duplicates. Take a look: Automatic Validation Error Handling.

Trigger that creates a row in another table that is referenced by current table

I have 2 tables User and Account. I'd like to have a trigger that creates an account automatically when a user is created. Here is my code:
alter trigger Add_user on [user] for insert as
begin
insert into [account] (name) values ('Main')
declare #newAccountId int, #insertedId int
set #newAccountId = (select scope_identity())
set #insertedId = (select id from inserted)
update [user]
set accountId = #newAccountId
where id = #insertedId
end
I want to have AccountId in the User table be not null however when I try and create a new user it won't let me and I get an error complaining about the not null AccountId column :(
If you make [user].AccountId nullable, it should work.
Also consider following things:
does [account] table contain only column "name"? I.e. is it global
for all users? Then why create new account for each user? If it's
user-specific then add [account].[userId] column.
I would recommend to write stored procedure instead of trigger (first create
account record then user record), it's more explicit and safe. Be
careful with triggers, it's likely to be a surprise for other
developers that inserting user also creates account.

SQL Table Locking

I have an SQL Server locking question regarding an application we have in house. The application takes submissions of data and persists them into an SQL Server table. Each submission is also assigned a special catalog number (unrelated to the identity field in the table) which is a sequential alpha numeric number. These numbers are pulled from another table and are not generated at run time. So the steps are
Insert Data into Submission Table
Grab next Unassigned Catalog
Number from Catalog Table
Assign the Catalog Number to the
Submission in the Submission table
All these steps happen sequentially in the same stored procedure.
Its, rate but sometimes we manage to get two submission at the same second and they both get assigned the same Catalog Number which causes a localized version of the Apocalypse in our company for a small while.
What can we do to limit the over assignment of the catalog numbers?
When getting your next catalog number, use row locking to protect the time between you finding it and marking it as in use, e.g.:
set transaction isolation level REPEATABLE READ
begin transaction
select top 1 #catalog_number = catalog_number
from catalog_numbers with (updlock,rowlock)
where assigned = 0
update catalog_numbers set assigned = 1 where catalog_number = :catalog_number
commit transaction
You could use an identity field to produce the catalog numbers, that way you can safely create and get the number:
insert into Catalog () values ()
set #CatalogNumber = scope_identity()
The scope_identity function will return the id of the last record created in the same session, so separate sessions can create records at the same time and still end up with the correct id.
If you can't use an identity field to create the catalog numbers, you have to use a transaction to make sure that you can determine the next number and create it without another session accessing the table.
I like araqnid's response. You could also use an insert trigger on the submission table to accomplish this. The trigger would be in the scope of the insert, and you would effectively embed the logic to assign the catalog_number in the trigger. Just wanted to put your options up here.
Here's the easy solution. No race condition. No blocking from a restrictive transaction isolation level. Probably won't work in SQL dialects other than T-SQL, though.
I assume their is some outside force at work to keep your catalog number table populated with unassigned catalog numbers.
This technique should work for you: just do the same sort of "interlocked update" that retrieves a value, something like:
update top 1 CatalogNumber
set in_use = 1 ,
#newCatalogNumber = catalog_number
from CatalogNumber
where in_use = 0
Anyway, the following stored procedure just just ticks up a number on each execution and hands back the previous one. If you want fancier value, add a computed column that applies the transform of choice to the incrementing value to get the desired value.
drop table dbo.PrimaryKeyGenerator
go
create table dbo.PrimaryKeyGenerator
(
id varchar(100) not null ,
current_value int not null default(1) ,
constraint PrimaryKeyGenerator_PK primary key clustered ( id ) ,
)
go
drop procedure dbo.GetNewPrimaryKey
go
create procedure dbo.GetNewPrimaryKey
#name varchar(100)
as
set nocount on
set ansi_nulls on
set concat_null_yields_null on
set xact_abort on
declare
#uniqueValue int
--
-- put the supplied key in canonical form
--
set #name = ltrim(rtrim(lower(#name)))
--
-- if the name isn't already defined in the table, define it.
--
insert dbo.PrimaryKeyGenerator ( id )
select id = #name
where not exists ( select *
from dbo.PrimaryKeyGenerator pkg
where pkg.id = #name
)
--
-- now, an interlocked update to get the current value and increment the table
--
update PrimaryKeyGenerator
set #uniqueValue = current_value ,
current_value = current_value + 1
where id = #name
--
-- return the new unique value to the caller
--
return #uniqueValue
go
To use it:
declare #pk int
exec #pk = dbo.GetNewPrimaryKey 'foobar'
select #pk
Trivial to mod it to return a result set or return the value via an OUTPUT parameter.