SQL Server stored procedure with C# enum values - sql-server

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

Related

How create table inline function for security policy row filter depending on values in configuration table in SQL Server?

I would like to create a row level security policy.
My input is a user_id for users who connect to the database through a middle-tier application.
I would like to:
Query a configuration table (let's call it conf_table) to get the department name of user_id
Depending on value department, I want to filter on another table called customers on type_of_customers.
Example:
conf_table:
user_id
department
toto
sidney
Customers:
customer_no
typ_customer
0001
A
0002
B
Function:
IF conf_table.user_id = 'toto' AND conf_table.department = 'sidney'`
SELECT *
FROM customers
WHERE typ_customer = A`
ELSE
SELECT *
FROM customers
WHERE typ_customer = B`
Many thanks in advance for your help!
The simplest way is to do this :
DECLARE #type VARCHAR(1) = 'B'
IF EXISTS(SELECT * FROM conf_table WHERE user_id = 'toto' AND department = 'sidney')
SET #type = 'A'
SELECT * FROM customers WHERE typ_customer = #type
Ideally, each row in conf_table would have a typ_customer associated with it, alternatively you would join to a Departments table to get that value.
But without that, you can just use a CASE expression.
Note the usage of a function parameter to be able to pass in the typ_customer value from the Customers table that is being filtered.
CREATE OR ALTER FUNCTION Security.YourPredicateTVF (#typ_customer AS char(1))
RETURNS TABLE WITH SCHEMABINDING
AS RETURN
SELECT 1 AS tvf_securitypredicate_result
FROM Security.conf_table c
WHERE c.user_id = USER_NAME()
AND CASE c.department = 'sidney' THEN 'A' ELSE 'B' END = #typ_customer;
GO
Then simply define a security policy, passing in the typ_customer column to the function.
CREATE SECURITY POLICY Security.YourPolicyName
ADD FILTER PREDICATE
Security.YourPredicateTVF (typ_customer)
ON dbo.Customers;
You may want to change FILTER to BLOCK and/or add DML filters also, depending on your use case.
Be aware that Row-Level Security is subject to side-channel attacks, and is therefore not a completely secure feature. For example, triggering a divide-by-zero error could tell the user what is stored in a row without actually seeing it.

How to select the default values of a table?

In my app, when letting the user enter a new record, I want to preselect the database's default values.
Let's for example take this table:
CREATE TABLE pet (
ID INT NOT NULL,
name VARCHAR(255) DEFAULT 'noname',
age INT DEFAULT 1
)
I would like to do something like this:
SELECT DEFAULT VALUES FROM pet -- NOT WORKING
And it should return:
ID | name | age
--------------------
NULL | noname | 1
I would then let the user fill in the remaining fields, or let her change one of the defaults, before she clicks on "save".
How can I select the default values of a sql server table using tsql?
You don't "SELECT" the Default values, only insert them. A SELECT returns the rows from a table, you can't SELECT the DEFAULT VALUES as there's no such row inside the table.
You could do something silly, like use a TRANSACTION and roll it back, but as ID doesn't have a default value, and you don't define a value for it with DEFAULT VALUES, it'll fail in your scenario:
CREATE TABLE pet (
ID INT NOT NULL,
name VARCHAR(255) DEFAULT 'noname',
age INT DEFAULT 1
)
GO
BEGIN TRANSACTION;
INSERT INTO dbo.pet
OUTPUT inserted.*
DEFAULT VALUES;
ROLLBACK;
Msg 515, Level 16, State 2, Line 13
Cannot insert the value NULL into column 'ID', table 'Sandbox.dbo.pet'; column does not allow nulls. INSERT fails.
You can, therefore, just supply the values for your non-NULL columns:
BEGIN TRANSACTION;
INSERT INTO dbo.pet (ID)
OUTPUT inserted.*
VALUES(1);
ROLLBACK;
Which will output the "default" values:
ID|name |age
--|------|---
1|noname|1
Selecting the default values of all columns is not very straight-forward, and as Heinzi wrote in his comment - does require a level of permissions you normally don't want your users to have.
That being said, a simple workaround would be to insert a record, select it back and display to the user, let the user decide what they want to change (if anything) and then when they submit the record - update the record (or delete the previous record and insert a new one).
That would require you to have some indication if the record was actually reviewed and updated by the user, but that's easy enough to accomplish by simply adding a bit column and setting it to 1 when updating the data.
As I have commented before. There is no need for this query since you can press alt + f1 on any table in your editor in Management Studio and provide you every information you need for the table.
select sys1.name 'Name',replace(replace(
case
when object_definition(sys1.default_object_id) is null then 'No Default Value'
else object_definition(sys1.default_object_id)
end ,'(',''),')','') 'Default value',
information_schema.columns.data_type 'Data type'
from sys.columns as sys1
left join information_schema.columns on sys1.name = information_schema.columns.column_name
where
object_id = object_id('table_name')
and information_schema.columns.table_name = 'table_name'
It seems like this might be solution:
SELECT * FROM (
SELECT
sys1.name AS COLUMN_NAME,
replace(replace(object_definition(sys1.default_object_id),'(',''),')','') AS DEFAULT_VALUE
FROM sys.columns AS sys1
LEFT JOIN information_schema.columns ON sys1.name = information_schema.columns.column_name
WHERE object_id = object_id('pet')
AND information_schema.columns.table_name = 'pet'
) AS SourceTable PIVOT(MAX(DEFAULT_VALUE) FOR COLUMN_NAME IN(ID, name, age)) AS PivotTable;
It returns:
ID |name |age
----|------|---
NULL|noname|1
Probably the column types are incorrect - but maybe I can live with that.
Thanks for #Nissus to provide an intermediate step to this.

SQL use a variable as TABLE NAME in a FROM

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.

Advanced sql server key combination

I have an sql server address table.
The user can put either a StreetId or a NeighborhoodId or a CityID but can not put 2 of these three fields. I could restrict the user through the ui, but I would prefer to do this force this rule at the db level.
Is there a way to do this?
You are better of doing this in your business logic layer.
UI
^
Business logic
^
Data Access
All business rules like this can be defined in a separate business logic layer (BLL) which the UI will call and receive a message from the BLL (which can form a warning, information or an error) if the rules are not adhered to. If all the rules defined in the BLL are adhered to for the current use case, the BLL will call the data access layer (DAL) which will call the database e.g. a stored procedure with the parameters authorised by the BLL using the defined business rules.
Hope it is clear.
This is the first time I'm messing with CHECK constraint. But I think below code might be helpful.
Let's say we have a table BaseTable
CREATE TABLE dbo.BaseTable
(
StreetId VARCHAR(50),
NeighborhoodId VARCHAR(50),
CityID VARCHAR(50)
)
Let's add a CHECK constraint:
ALTER TABLE dbo.BaseTable
ADD CONSTRAINT CheckOnlyOneColumnValue
CHECK (
(
CASE WHEN StreetId IS NOT NULL THEN 1 ELSE 0 END
+ CASE WHEN NeighborhoodId IS NOT NULL THEN 1 ELSE 0 END
+ CASE WHEN CityID IS NOT NULL THEN 1 ELSE 0 END)
= 1
)
GO
Test:
These insert queries will work just fine:
INSERT INTO dbo.BaseTable(StreetId) VALUES ('StreetId')
INSERT INTO dbo.BaseTable(NeighborhoodId) VALUES ('NeighborhoodId')
INSERT INTO dbo.BaseTable(CityID) VALUES ('CityID')
But these queries will fail:
INSERT INTO dbo.BaseTable(StreetId, NeighborhoodId)
VALUES ('StreetId', 'NeighborhoodId')
INSERT INTO dbo.BaseTable(StreetId, NeighborhoodId, CityID)
VALUES ('StreetId', 'NeighborhoodId', 'CityID')
UPDATE dbo.BaseTable SET NeighborhoodId = 'NeighborhoodId'
WHERE StreetId = 'StreetId'

TSQL Stored Proc to copy records (with a twist!)

I am trying to write a Stored Procedure in SQL Server (2005) to do something that sounds simple, but is actually proving to be more difficult that I thought.
I have a table with 30 columns and 50,000 rows.
The number of records is fixed, but users can edit the fields of existing records.
To save them having to re-key repetitive data, I want to give them the ability to select a record, and specify a range of IDs to copy those details to.
The SP I'm trying to write will take 3 parameters: The source record primary key, and the lower and upper primary keys of the range of records that the data will be copied into.
Obviously the PKs of the destination records remain unchanged.
So I figured the SP needs to do a SELECT - to get all the data to be copied, and an UPDATE - to write the data into the specified destination records.
I just don't know how to store the results of the SELECT to slot them into the UPDATE.
A temp table wouldn't help - selecting from that would be just the same as selecting from the table!
What I need is a variable that is effectively a single record, so I can go something like:
#tempRECORD = SELECT * FROM SOURCETABLE WHERE ID = #sourcePK
UPDATE SOURCETABLE
SET FIELD1 = #tempRECORD.FIELD1,
FIELD2 = #tempRECORD.FIELD2,
...
FIELD30 = #tempRECORD.FIELD30
WHERE ID >= #LOWER_id AND ID <= #UPPER_id
But I don't know how, or if you even can.
I'm also open to any other clever way I haven't even thought of!
Thanks guys!
So I figured the SP needs to do a SELECT - to get all the data to be copied, and an UPDATE - to write the data into the specified destination records.
What you need is the T-SQL-specific extension to UPDATE, UPDATE ... FROM:
UPDATE T
SET
Field1 = source.Field1
, Field2 = source.Field2
, Field3 = source.Field3
FROM
(SELECT * FROM T AS source_T WHERE source_T.ID = #sourcePK) as source
WHERE
T.ID BETWEEN #LOWER_Id AND #UPPER_Id
Note that this ability to put a FROM clause in an UPDATE statement is not standard ANSI SQL, so I don't know how this would be done in other RDBMSs.
I am pretty sure this ain't the easiest way to do it, but it should work without any problems:
DECLARE #tempField1 varchar(255)
DECLARE #tempField2 varchar(255)
...
DECLARE #tempField30 varchar(255)
SELECT #tempField1 = FIELD1, #tempField2 = FIELD2, ... ,#tempField30 = FIELD30 FROM SOURCETABLE WHERE ID = #sourcePK
UPDATE SOURCETABLE
SET FIELD1 = #tempField1,
FIELD2 = #tempField2,
...
FIELD30 = #tempField30
WHERE ID >= #LOWER_id AND ID <= #UPPER_id
You would need to edit the tempField variables so that they have the right type.

Resources