SQL use a variable as TABLE NAME in a FROM - sql-server

We install our database(s) to different customers and the name can change depending on the deployment.
What I need to know is if you can use a variable as a table name.
The database we are in is ****_x and we need to access ****_m.
This code is part of a function.
I need the #metadb variable to be the table name - Maybe using dynamic SQL with
sp_executesql. I am just learning so take it easy on me.
CREATE FUNCTION [dbo].[datAddSp] (
#cal NCHAR(30) -- calendar to use to non-working days
,#bDays INT -- number of business days to add or subtract
,#d DATETIME
)
RETURNS DATETIME
AS
BEGIN
DECLARE #nDate DATETIME -- the working date
,#addsub INT -- factor for adding or subtracting
,#metadb sysname
SET #metadb = db_name()
SET #metadb = REPLACE (#metadb,'_x','_m')
SET #metadb = CONCAT (#metadb,'.dbo.md_calendar_day')
SET #ndate = #d
IF #bdays > 0
SET #addsub = 1
ELSE
SET #addsub = -1
IF #cal = ' ' OR #cal IS NULL
SET #cal = 'CA_ON'
WHILE #bdays <> 0 -- Keep adding/subtracting a day until #bdays becomes 0
BEGIN
SELECT #ndate = dateadd(day, 1 * #addsub, #ndate) -- increase or decrease #ndate
SELECT #bdays = CASE
WHEN (##datefirst + datepart(weekday, #ndate)) % 7 IN (0, 1) -- ignore if it is Sat or Sunday
THEN #bdays
WHEN ( SELECT 1
FROM #metadb -- **THIS IS WHAT I NEED** (same for below) this table holds the holidays
WHERE mast_trunkibis_m.dbo.md_calendar_day.calendar_code = #cal AND mast_trunkibis_m.dbo.md_calendar_day.calendar_date = #nDate AND mast_trunkibis_m.dbo.md_calendar_day.is_work_day = 0
) IS NOT NULL -- ignore if it is in the holiday table
THEN #bdays
ELSE #bdays - 1 * #addsub -- incr or decr #ndate
END
END
RETURN #nDate
END
GO

The best way to do this, if you aren't stuck with existing structures is to keep all of the table structures and names the same, simply create a schema for each customer and build out the tables in the schema. For example, if you have the companies: Global Trucking and Super Store you would create a schema for each of those companies: GlobalTrucking and SuperStore are now your schemas.
Supposing you have products and payments tables for a quick example. You would create those tables in each schema so you end up with something that looks like this:
GlobalTrucking.products
GlobalTrucking.payments
and
SuperStore.products
SuperStore.payments
Then in the application layer, you specify the default schema name to use in the connection string for queries using that connection. The web site or application for Global Trucking has the schema set to GlobalTrucking and any query like: SELECT * FROM products; would actually automatically be SELECT * FROM GlobalTrucking.products; when executed using that connection.
This way you always know where to look in your tables, and each customer is in their own segregated space, with the proper user permissions they will never be able to accidentally access another customers data, and everything is just easier to navigate.
Here is a sample of what your schema/user/table creation script would look like (this may not be 100% correct, I just pecked this out for a quick example, and I should mention that this is the Oracle way, but SQL Server should be similar):
CREATE USER &SCHEMA_NAME IDENTIFIED BY temppasswd1;
CREATE SCHEMA AUTHORIZATION &SCHEMA_NAME
CREATE TABLE "&SCHEMA_NAME".products
(
ProductId NUMBER,
Description VARCHAR2(50),
Price NUMBER(10, 2),
NumberInStock NUMBER,
Enabled VARCHAR2(1)
)
CREATE TABLE "&SCHEMA_NAME".payments
(
PaymentId NUMBER,
Amount NUMBER(10, 2),
CardType VARCHAR2(2),
CardNumber VARCHAR2(15),
CardExpire DATE,
PaymentTimeStamp TIMESTAMP,
ApprovalCode VARCHAR2(25)
)
GRANT SELECT ON "&SCHEMA_NAME".products TO &SCHEMA_NAME
GRANT SELECT ON "&SCHEMA_NAME".payments TO &SCHEMA_NAME
;
However, with something like the above, you only have 1 script that you need to keep updated for automation of adding new customers. When you run this, the &SCHEMA_NAME variable will be populated with whatever you choose for the new customer's username/schemaname, and an identical table structure is created every time.

Related

Creating a trigger that pulls the price from one table and prevents entry into another

So, I'm kinda an SQL beginner who got himself into a conundrum.
I have a database that's set up with the concept of a luxury goods store. The table Products includes the field Price which can't be less than $500 (through a trigger). The table ProductReceipt has ID, ProductID, ReceiptID, Amount, Installments
I want to create a trigger that pulls the price from Products and if the price is higher than $600, you can input True on Installments. If it's lower, then it forces a False or rollback into a False. This should also take into account the field Amount, so if something costs $500, but there's two of said item, the Installments option should be applicable.
I'm not sure how exactly to go about it? A Join, perhaps?
The Product table contains for example:
ID: 1,
Name: Rouge Coco
Description: Random Description
Price: $500
CompanyID: 1 (in the Company table, 1 is Chanel)
As you said you are designing the data, so you can handle this while designing the database only.
I have tested the following in SQL Server.
CREATE TABLE Products
(Price int
, Installments AS CASE WHEN Price > 600 THEN 'TRUE'
WHEN Price < 600 THEN 'FALSE'
ELSE NULL END
)
Try this
create table Products123
(
ID int,
Name varchar(10),
Description varchar(100),
Price money,
CompanyID int
)
create table ProductReceipt123
(
ID int,
ProductID int,
ReceiptID int,
Amount money,
Installments bit
)
create TRIGGER tInsert
ON Products123
AFTER INSERT,DELETE
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
Declare #insertedPrice money,#insertedID int
select #insertedPrice=Price,#insertedID=ID from inserted
if(#insertedPrice<500)
Begin
delete from Products123 where ID=#insertedID
select 'Price should be greater than 500'
End
else if(#insertedPrice>600)
Begin
insert into ProductReceipt123(ProductID,Amount,Installments) values(#insertedID,#insertedPrice,1)
End
else if(#insertedPrice<600)
Begin
insert into ProductReceipt123(ProductID,Amount,Installments) values(#insertedID,#insertedPrice,0)
End
-- Insert statements for trigger here
END
GO
Whilst this may not 100% answer the question you as described (does not use triggers) it will do the same job, more efficiently (although I stand to be corrected on that) and with less code
Your reason for using a trigger seems odd, like you have to use three triggers for the sake of using triggers?
Any way, here is the constraint:
ALTER TABLE ProductReceipt ADD CONSTRAINT CK_Installments CHECK ((Amount > 600 AND Installments IN (1,0)) OR (Amount <= 600 AND Installments = 0))

Importing Grouped Report Data to Database

My company receives data from a client that is unable to provide data in any direct format so we have to import several reports that are in a grouped layout like the one below. We have to develop in house methods to ungroup the report and then import the data to get all of the data we need. Currently a member on my team is using MS Access / VBA to generate the needed detail records but I want to move this to a server based and automated process. We are using SQL Server 2008R2 for storage and I would like to use SSIS to accomplish the task. Does anyone know of a way I can generate the detail records and import the data directly into SQL Server?
Hmm - well you will definitely have to do some programmatic adjusting of the data set to add that group date to the detail line. I'm unsure of how you will be importing the xlsx but I would recommend first off just using a SSIS package and then doing the adjustments in a script task as the "best" way to do this. See here on how to handle Excel in SSIS Script tasks.
If you don't know SSIS or especially programming though, you're next best bet (in my opinion) is to just import the data into a staging table, do the manipulations with T-SQL and then insert that table into your main table. I did a SQL Fiddle of this here.
CREATE TABLE ActivitySummary
(
id int identity(1,1),
activity_date date,
activity varchar(100),
paid_time decimal(5,2),
unpaid_time decimal(5,2),
total_time decimal(5,2)
)
CREATE TABLE ActivitySummary_STG
(
id int identity(1,1),
activity_date date,
activity varchar(100),
paid_time decimal(5,2),
unpaid_time decimal(5,2),
total_time decimal(5,2)
)
GO
-- Simulate import of Excel sheet into staging table
truncate table ActivitySummary_STG;
GO
INSERT INTO ActivitySummary_STG (activity_date, activity, paid_time, unpaid_time, total_time)
select '8/14/17',null,null,null,null
UNION ALL
select null,'001 Lunch',0,4.4,4.4
UNION ALL
select null,'002 Break',4.2,0,4.2
UNION ALL
select null,'007 System Down',7.45,0,7.45
UNION ALL
select null,'019 End of Work Day',0.02,0,0.02
UNION ALL
select '8/15/17',null,null,null,null
UNION ALL
select null,'001 Lunch',0,4.45,4.45
UNION ALL
select null,'002 Break',6.53,0,6.53
UNION ALL
select null,'007 System Down',0.51,0,0.51
UNION ALL
select null,'019 End of Work Day',0.02,0,0.02
GO
-- Code to massage data
declare #table_count int = (select COALESCE(count(id),0) from ActivitySummary_STG);
declare #counter int = 1;
declare #activity_date date,
#current_date date;
WHILE (#table_count > 0 AND #counter <= #table_count)
BEGIN
select #activity_date = activity_date
from ActivitySummary_STG
where id = #counter;
if (#activity_date is not null)
BEGIN
set #current_date = #activity_date;
delete from ActivitySummary_STG
where id = #counter;
END
else
BEGIN
update ActivitySummary_STG SET
activity_date = #current_date
where id = #counter;
END
set #counter += 1;
END
INSERT INTO ActivitySummary (activity_date, activity, paid_time, unpaid_time, total_time)
select activity_date, activity, paid_time, unpaid_time, total_time
from ActivitySummary_STG;
truncate table ActivitySummary_STG;
GO
select * from ActivitySummary;
I'd do it with a script component.
Total Data Flow:
ExcelSource --> Script Component (Tranformation) --> Conditional Split --> SQL Destination
In script component:
Check accountSummary on InputColumns
Add ActivityDate as output column.
Open Script:
outside of your row processing.
Add:
public datetime dte;
inside row processing:
if (DateTime.TryParse(Row.ActivitySummary.ToString()))
{dte=DateTime.Parse(Row.ActivitySummary.ToString());}
else
{Row.ActivityDate = dte;}
Then add a Conditional Split to remove null Activity Dates

SQL Server contraints for date ranges

I am trying to constrain a SQL Server Database by a Start Date and End Date such that I can never double book a resource (i.e. no overlapping or duplicate reservations).
Assume my resources are numbered such that the table looks like
ResourceId, StartDate, EndDate, Status
So lets say I have resource #1. I want to make sure that I cannot have have the a reservation for 1/8/2017 thru 1/16/2017 and a separate reservation for 1/10/2017 - 1/18/2017 for the same resource.
A couple of more complications, a StartDate for a resource can be the same as the EndDate for the resource. So 1/8/1027 thru 1/16/2017 and 1/16/2017 thru 1/20/2017 is ok (i.e., one person can check in on the same day another person checkouts).
Furthermore, the Status field indicates whether the booking of the resource is Active or Cancelled. So we can ignore all cancelled reservations.
We have protected against these overlapping or double booking reservations in Code (Stored Procs and C#) when saving but we are hoping to add an extra layer of protection by adding a DB Contraint.
Is this possible in SQL Server ?
Thanks in Advance
You can use a CHECK constraint to make sure startdate is on or before EndDate easily enough:
CONSTRAINT [CK_Tablename_ValidDates] CHECK ([EndDate] >= [StartDate])
A constraint won't help with preventing an overlapping date range. You can instead use a TRIGGER to enforce this by creating a FOR INSERT, UPDATE trigger that rolls back the transaction if it detects a duplicate:
CREATE TRIGGER [TR_Tablename_NoOverlappingDates] FOR INSERT, UPDATE AS
IF EXISTS(SELECT * from inserted INNER JOIN [MyTable] ON blah blah blah ...) BEGIN
ROLLBACK TRANSACTION;
RAISERROR('hey, no overlapping date ranges here, buddy', 16, 1);
RETURN;
END
Another option is to create a indexed view that finds duplicates and put a unique constraint on that view that will be violated if more than 1 record exists. This is usually accomplished with a dummy table that has 2 rows cartesian joined to an aggregate view that selects the duplicate id-- thus one record with a duplicate would return two rows in the view with the same fake id value that has a unique index.
I've done both, I like the trigger approach better.
Drawing from this answer here: Date range overlapping check constraint.
First, check to make sure there are not existing overlaps:
select *
from dbo.Reservation as r
where exists (
select 1
from dbo.Reservation i
where i.PersonId = r.PersonId
and i.ReservationId != r.ReservationId
and isnull(i.EndDate,'20990101') > r.StartDate
and isnull(r.EndDate,'20990101') > i.StartDate
);
go
If it is all clear, then create your function.
There are a couple of different ways to write the function, e.g. we could skip the StartDate and EndDate and use something based only on ReservationId like the query above, but I will use this as the example:
create function dbo.udf_chk_Overlapping_StartDate_EndDate (
#ResourceId int
, #StartDate date
, #EndDate date
) returns bit as
begin;
declare #r bit = 1;
if not exists (
select 1
from dbo.Reservation as r
where r.ResourceId = #ResourceId
and isnull(#EndDate ,'20991231') > r.StartDate
and isnull(r.EndDate,'20991231') > #StartDate
and r.[Status] = 'Active'
group by r.ResourceId
having count(*)>1
)
set #r = 0;
return #r;
end;
go
Then add your constraint:
alter table dbo.Reservation
add constraint chk_Overlapping_StartDate_EndDate
check (dbo.udf_chk_Overlapping_StartDate_EndDate(ResourceId,StartDate,EndDate)=0);
go
Last: Test it.

Create report using dynamic SQL

I have a table example as such:
State Project Build Type ACTUAL0 ACTUAL1 ACTUAL2
------- ------- ----------- ----------- ----------- ----------
Ohio 154214 Residential 1/5/2013 2/25/2013 7/12/12
Utah 214356 Commercial 7/08/13 6/9/13 7/1/12
I am trying to create a report that takes the column headers beginning with the word actual and get a count of how many dates are less than a specific date. I have a temp table that I create of the column headers beginning with the word actual. This is just and example, there are over 250 columns name actual. So the table looks like this:
MilestoneNmbr
-------------
ACTUAL1
ACTUAL2
ACTUAL3
Now what I think would work is to take the row as a variable for the column header and pass in a date into a function. Here is a function I created:
CREATE FUNCTION [dbo].[GetMSActualCount]
(
#ACTUAL nvarchar(16),
#DATE nvarchar(16)
)
RETURNS int
AS
BEGIN
DECLARE #ACTUALRETURN int
DECLARE #SQL nVarchar(255) =
'SELECT COUNT(' + #ACTUAL + ') AS Expr1
FROM [CASPR_MILESTONES_000-036]
WHERE '+ #ACTUAL +' > ' + #DATE
exec sp_executesql #SQL, N''
SET #ACTUALRETURN = #SQL
-- Return the result of the function
RETURN #ACTUALRETURN
END
If I run the following query:
DECLARE #DATE varchar(20)
SET #DATE = '''1/1/2013'''
SELECT MilestoneNmbr, dbo.getMSActualCount(milestonenmbr,#Date) from #List_CASPR_Milestones
So my error is that I can't use dynamic SQL in a function. With that being so, what can I do? My easy query here I think will turn into hundreds of lines. Is there another easy way I can do this?
EDIT:
My results I am looking for is something like this:
MilestoneNmbr CountofDate
--------------- ------------
ACTUAL1 200
ACTUAL2 344
ACTUAL3 400
You are right you can't use dynamic SQL in a function. There are two answers:
First your table with 250 columns ACTUAL plus a number is a nightmare. You can't use any of the built in stuff that SQL does well to help. You should have two tables. First a projects table that has an ID column plus columns for State, Project, and BuildType. Then a table of ProjectDates with a ProjectID column that references the first table and then a column for ActualDate. Reporting from that should be easy.
Given that you probably can't fix the structure try writing a stored procedure. That can use dynamic SQL. Event better is that your stored procedure can create temp tables like above and then use them to do statistics.
I agree 100% with Charles. If you CAN change the structure this is what I would do:
If possible have a build type table (ID/Build Type), don't have text columns unless you need them as text for something. Anything that can be coded, code it.
The two tables:
project header (Proj_ID (long_int)/State (int or char(2)) / build_type (int)), primary key either Proj_id by itself or a new ID if its not unique (as a PK Proj_id & State would not be too useful as a PK).
Project_date (Proj_ID (same as PK above) / Date_ID (int) / Actual_Date (DateTime))
So your second example would be:
Project_Header:
214356 / UT / 2 (being 1 Residential, 2 Commercial, 3 Industrial ...)
Project_Date:
214356 / 0 / '07/08/13'
214356 / 1 / '06/09/13'
214356 / 2 / '07/01/12'
Latest build date by project would be:
Select 'Actual_date'
from Project_date
where Project_id='nnn'
order by date_id DESC
Limit 1;
Your query would be something like (if the dates are in incremental order):
Select Project_id, max(Date_id)
From Project_date
Group by Project_id
having Actual_date < #date
you can see it's pretty straight forward.
If you CAN'T change the structures but you CAN make new tables I would make an SP that takes that ugly table and generates the Project_Date x times per day ( or you could even tie it to a trigger on inert/update of the first table) and the Project_header once per day (or more often if needed). This would take considerably less time and effort than what you are attempting, plus you could use it for other queries.
To solve this I created a table housing the ACTUAL dates. I then went and looped through each row in the List_ACTUAL table to get the names and select the count of the dates names greater than the variable I pass in. I will be converting this to a PROC. This is how:
DECLARE #MS nvarchar(16)
DECLARE MSLIST CURSOR LOCAL FOR SELECT MilstoneNmbr FROM List_ACTUAL
DECLARE #SQL nvarchar(max)
DECLARE #DATE nvarchar(16)
SET #DATE = '1/1/2013'
CREATE #TEMP (Milestones nvarchar(16), Frozen int)
OPEN MSLIST
FETCH NEXT FROM MSLIST INTO #MS
WHILE ##FETCH_STATUS=0
BEGIN
SELECT #SQL = 'INSERT INTO #TEMP VALUES (''' +#MS+ ''', (Select count(' +#MS+ ') FROM PROJECTDATA WHERE ' +#MS+ ' > ''' + #Date + '''))'
EXEC sp_executesql #SQL, N''
FETCH NEXT FROM MSLIST INTO #MS
END
CLOSE MSLIST
DEALLOCATE MSLIST
Hope this helps someone.

SQL Server stored procedure with C# enum values

I am writing some stored procedures for my application. Within the C# code I have lots of enums. As an example:
public enum Status
{
Active = 1
Inactive = 2
}
Now when I write my stored procedures, is there any way of replicating enums in the database? I want to avoid using a table to hold them all, but I was wondering if you can tuck them all away in an object somewhere, and reference this in other stored procedures?
So I want my stored procedures to use the style of:
SELECT *
FROM Users
WHERE status = Status.Active
Instead of using raw numerical values like this:
SELECT *
FROM Users
WHERE status = 1
Does anyone have any ideas on how this might be achieved?
It turns out the best way to do this is to use a SQL Server Scalar function for the job as follows:
ALTER FUNCTION [dbo].[fn_GetEnumValue]
(
#enumName varchar(512)
)
RETURNS INT
AS
BEGIN
DECLARE #returnVal INT = 0
SELECT #returnVal = (
CASE
WHEN #enumName = 'Active' THEN 1
WHEN #enumName = 'Inactive' THEN 2
ELSE 0
END
)
RETURN #returnVal
END
Then you can use it like this:
SELECT * FROM Users WHERE status = dbo.fn_GetEnumValue('Active');
You could create a set of single row tables, each one representing an enumeration. So for user statuses:
CREATE TABLE Enum_Status
(
Active TINYINT NOT NULL DEFAULT 1,
Inactive TINYINT NOT NULL DEFAULT 2
);
INSERT INTO Enum_Status VALUES( 1 , 2 );
Then your SELECT statement would look quite neat (if not as neat as the equivalent in Oracle):
SELECT Users.*
FROM Users, Enum_Status Status
WHERE status = Status.Active;
To keep things tidy I would be tempted to put the enumeration tables in their own schema and grant all users the relevant permissions.
As i understand, your aim is to ensure that the value passed to the stored procedure is valid.
You could create a Status table as follows
CREATE TABLE Status
(
StatusID int,
StatusName nvarchar,
)
insert the valid data here.
Then, in your query, you could do an INNER JOIN to validate.
SELECT *
FROM Users
INNER JOIN Status ON Users.status = Status.StatusID
WHERE StatusID = #StatusID

Resources