I am new to sql programming. This is for a homework assignment for my database class. I have made all the table I need for the assignment I am just hung up one part. The line in the homework reads as follows:
If type is 'faculty' the email address must end up with '#xxx.edu'
This is what the table looks like:
create table customer
(
CID# char(10) primary key IDENTITY (1,1) NOT NULL,
F_name varchar(20),
M_name varchar(20),
L_name varchar(20),
type varchar(20),
street varchar(20),
city varchar(20),
state varchar(20),
zip char(5),
password varchar (20) NOT NULL,
email varchar(20) NOT NULL
Constraint CK_customer_type check (type in ('student', 'faculty'))
)
Any help someone could lend would be greatly appreciated!
This constraint would check if the email column ends with #xxx.edu.
Constraint CK_email_faculty check (
type<>'faculty' OR
CHARINDEX('#xxx.edu',email)=LEN(email)-LEN('#xxx.edu')+1
)
Remark 1: Better than checking the type being appropriate in a CHECK constraint, make a table customer_type with possible types ('student', 'faculty' ...), and have a foreign key in customer pointing a the type.
CREATE TABLE customer_type(id INT NOT NULL PRIMARY KEY,desc VARCHAR(128));
INSERT INTO customer_type(id,desc)VALUES(1,'student');
INSERT INTO customer_type(id,desc)VALUES(2,'faculty');
Have a foreign key in your customer table to point to the customer_type table:
CREATE TABLE customer(
-- ...
type INT NOT NULL,
-- ...
CONSTRAINT FK_type_customer_type FOREIGN KEY(type) REFERENCES customer_type(id)
)
You would then not insert the type description but the type identifier:
INSERT INTO customer(...,type,...)VALUES(...,1,...); -- for student
INSERT INTO customer(...,type,...)VALUES(...,3,...); -- fails, type doesn't exist
That way you save disk space, and memory when these tables are cached by SQL Server.
Remark 2: The widths of your varchar fields are very small. An email address of only 20 characters?
For your particular case a constraint like this might be ok:
Constraint CK_customer_email check (
type <> 'faculty' OR
email LIKE '%_%#_%.edu'
CHARINDEX('#xxx.edu',email)=LEN(email)-LEN('#xxx.edu')+1
)
This will allow 1+ characters, followed by #, followed by 1+ characters, followed by .edu.
However, in real life (where mean people try to insert bad e-mail addresses), validation is more complex (not all characters are allowed), so a custom function can be used. One that seems to be almost complete is provided here:
CREATE FUNCTION [dbo].[fnAppEmailCheck](#email VARCHAR(255))
--Returns true if the string is a valid email address.
RETURNS bit
as
BEGIN
DECLARE #valid bit
IF #email IS NOT NULL
SET #email = LOWER(#email)
SET #valid = 0
IF #email like '[a-z,0-9,_,-]%#[a-z,0-9,_,-]%.[a-z][a-z]%'
AND LEN(#email) = LEN(dbo.fnAppStripNonEmail(#email))
AND #email NOT like '%#%#%'
AND CHARINDEX('.#',#email) = 0
AND CHARINDEX('..',#email) = 0
AND CHARINDEX(',',#email) = 0
AND RIGHT(#email,1) between 'a' AND 'z'
SET #valid=1
RETURN #valid
END
Then, your constraint would be like this:
Constraint CK_customer_email check (
type <> 'faculty' OR
[dbo].[fnAppEmailCheck] (email) = 1
)
Add Constraint
Constraint CK_email check (email like case when type in ('faculty') then '%#xxx.edu' else email end )
Related
I'm using database first approach with EF core and trying to figure out a clean solution to the below problem -
Consider a Student attendance table (irrelevant columns removed) below that stores date of class and allows the student to enter his class rating -
create table Student (
Id int Identity(1, 1) not null,
ClassDate smalldatetime not null,
ClassRatingByStudent varchar(250) not null
)
This is a webapp where school attendance system automatically populates the above table at EOD and then the student (let's say a few days later) is required to add class ratings. When the table is populated by the school attendance system, there is nothing in the ClassRatingByStudent column. Then when the student logs in, he must add the rating.
As you see, ClassRatingByStudent must be null when the school attendance system populates the table and must be not-null when the student saves his changes. One obvious solution is make ClassRatingByStudent column nullable ad handle it in the code but I'm wondering if there is a neater database (or maybe EF) level solution exists or some sort of pattern/architecture guidelines for this type of scenarios?
I don't know but maybe CHECK constraint could help you:
CREATE TABLE TestTable(
ID int NOT NULL IDENTITY,
RatingAllowed bit NOT NULL DEFAULT 0, -- switcher
RatingValue varchar(250),
CONSTRAINT PK_TestTable PRIMARY KEY(ID),
CONSTRAINT CK_TestTable_RatingValue CHECK( -- constraint
CASE
WHEN RatingAllowed=0 AND RatingValue IS NULL THEN 1
WHEN RatingAllowed=1 AND RatingValue IS NOT NULL THEN 1
ELSE 0
END=1
)
)
INSERT TestTable(RatingAllowed,RatingValue)VALUES(0,NULL)
INSERT TestTable(RatingAllowed,RatingValue)VALUES(1,'AAA')
-- The INSERT statement conflicted with the CHECK constraint "CK_TestTable_RatingValue"
INSERT TestTable(RatingAllowed,RatingValue)VALUES(0,'AAA')
INSERT TestTable(RatingAllowed,RatingValue)VALUES(1,NULL)
I found a variant how to check using another table as switcher
CREATE TABLE TableA(
ID int NOT NULL IDENTITY PRIMARY KEY,
StudentID int NOT NULL,
Grade int
)
CREATE TABLE TableB(
StudentID int NOT NULL PRIMARY KEY
)
GO
-- auxiliary function
CREATE FUNCTION GradeIsAllowed(#StudentID int)
RETURNS bit
BEGIN
DECLARE #Result bit=CASE WHEN EXISTS(SELECT * FROM TableB WHERE StudentID=#StudentID) THEN 1 ELSE 0 END
RETURN #Result
END
GO
-- constraint to check
ALTER TABLE TableA ADD CONSTRAINT CK_TableA_Grade CHECK(
CASE dbo.GradeIsAllowed(StudentID) -- then we can use the function here
WHEN 1 THEN CASE WHEN Grade IS NOT NULL THEN 1 ELSE 0 END
WHEN 0 THEN CASE WHEN Grade IS NULL THEN 1 ELSE 0 END
END=1)
GO
-- Tests
INSERT TableB(StudentID)VALUES(2) -- allowed student
INSERT TableA(StudentID,Grade)VALUES(1,NULL) -- OK
INSERT TableA(StudentID,Grade)VALUES(2,5) -- OK
INSERT TableA(StudentID,Grade)VALUES(1,4) -- Error
INSERT TableA(StudentID,Grade)VALUES(2,NULL) -- Error
INSERT TableB(StudentID)VALUES(1) -- add 1
UPDATE TableA SET Grade=4 WHERE StudentID=1 -- OK
UPDATE TableA SET Grade=NULL WHERE StudentID=1 -- Error
Basically let's say I have a "Business" that owns postal codes that it services. Let's also suppose I have another relational table that sets up fees.
CREATE TABLE [dbo].[BusinessPostalCodes]
(
[BusinessPostalCodeId] INT IDENTITY (1, 1) NOT NULL,
[BusinessId] INT NOT NULL,
[PostalCode] VARCHAR (10) NOT NULL
)
CREATE TABLE [dbo].[BusinessPostalCodeFees]
(
[BusinessId] INT NOT NULL,
[BusinessProfileFeeTypeId] INT NOT NULL,
[BusinessPostalCodeId] INT NOT NULL,
[Fee] SMALLMONEY NULL
)
I want to know if it's possible to set up a foreign key (or something) on BusinessPostalCodeFees that ensures that the related BusinessId of BusinessPostalCodes is the same as the BusinessId of BusinessPostalCodeFees.
I realize that I can remove BusinessId entirely, but I would much rather keep this column and have a way of guaranteeing they will be the same. Is there anything I can do?
It sounds like (and correct me if I'm wrong) that you're trying to make sure that any entry into BusinessPostalCodeFees' BusinessId and BusinessPostalCodeId columns match an entry in the BusinessPostalCodes table. If that's the case, then yes, you can definitely have a foreign key that references a compound primary key.
However, if you need to keep the BusinessId, I'd recommend normalizing your tables a step further than you have. You'll end up with duplicate data as-is.
On a side note, I would recommend you don't use the money data types in SQL: See here.
In the end, Jeffrey's solution didn't quite work for my particular situation. Both columns in the relation have to be unique (like a composite key). Turns out the answer here (for me) is a Checked Constraint.
Create a function that you want to have the constraint pass or fail:
CREATE FUNCTION [dbo].[MatchingBusinessIdPostalCodeAndProfileFeeType]
(
#BusinessId int,
#BusinessPostalCodeId int,
#BusinessProfileFeeTypeId int
)
RETURNS BIT
AS
BEGIN
-- This works because BusinessPostalCodeId is a unique Id.
-- If businessId doesn't match, its filtered out.
DECLARE #pcCount AS INT
SET #pcCount = (SELECT COUNT(*)
FROM BusinessPostalCodes
WHERE BusinessPostalCodeId = #BusinessPostalCodeId AND
BusinessId = #BusinessId)
-- This works because BusinessProfileFeeTypeId is a unique Id.
-- If businessId doesn't match, its filtered out.
DECLARE #ftCount AS INT
SET #ftCount = (SELECT COUNT(*)
FROM BusinessProfileFeeTypes
WHERE BusinessProfileFeeTypeId = #BusinessProfileFeeTypeId AND
BusinessId = #BusinessId)
-- Both should have only one record
BEGIN IF (#pcCount = 1 AND #ftCount = 1)
RETURN 1
END
RETURN 0
END
Then just add it to your table:
CONSTRAINT [CK_BusinessPostalCodeFees_MatchingBusinessIdPostalCodeAndProfileFeeType]
CHECK (dbo.MatchingBusinessIdPostalCodeAndProfileFeeType(
BusinessId,
BusinessPostalCodeId,
BusinessProfileFeeTypeId) = 1)
I have the following stored procedure, I have two tables here, Movie and Director. Both need to be updated when a new movie has been created or added. How do you handle FKs in stored procedures? The FK in this case is director_id. It is a primary key in Director but a FK in Movie Do I need to specify it twice like so? I am getting conflict errors
CREATE PROCEDURE Book_Book_Creation
#Book_id_arg DECIMAL(12),
#author_id_arg DECIMAL(12),
#type_id_arg DECIMAL(12),
#title_arg VARCHAR(64), -
#copyright_arg DECIMAL(4),
#dauthor_id_2_arg DECIMAL(12),
#author_fname_arg VARCHAR (64),
#author_lname_arg VARCHAR (64)
AS
BEGIN
INSERT INTO Book(Book_id, author_id,genre_id, title, copyright)
VALUES (#author_arg, #author_id_arg, #type_id_arg, #title_arg, #copyright_arg);
INSERT INTO Author(author_id, author_fname, author_lname)
VALUES (#director_id_2_arg, #director_fname_arg, #director_lname_arg)
END;
EXECUTE Book_Book_Creation 32,32,1,'Tempting',2013,32,'Taylor','Mendez';
Basically, you just need to do this:
insert into the Director table first
get the newly inserted ID from that table (assuming that the Director_Id column is your primary key and is of type INT IDENTITY)
then insert into the Movie table with that new ID
Something like this:
DECLARE #NewDirectorID INT
INSERT INTO Director (Director_id, Director_fname, director_lname)
VALUES (#director_id_2_arg, #director_fname_arg, #director_lname_arg)
SELECT #NewDirectorID = SCOPE_IDENTITY()
INSERT INTO Movie (Movie_id, director_id,genre_id, title, copyright)
VALUES (#movie_id_arg, #NewDirectorID, #genre_id_arg, #title_arg, #copyright_arg);
I don't see why you would pass in the director's ID as a parameter - twice!
Try this one -
ALTER PROCEDURE dbo.Movie_Movie_Creation12
#movie_id_arg DECIMAL(12),
#director_id_arg DECIMAL(12),
#genre_id_arg DECIMAL(12),
#title_arg VARCHAR(64),
#copyright_arg DECIMAL(4),
#director_fname_arg VARCHAR (64),
#director_lname_arg VARCHAR (64)
AS BEGIN
INSERT INTO dbo.Director (Director_id, Director_fname, director_lname)
SELECT #director_id_arg, #director_fname_arg, #director_lname_arg
INSERT INTO dbo.Movie (Movie_id, director_id,genre_id, title, copyright)
SELECT #movie_id_arg, #director_id_arg, #genre_id_arg, #title_arg, #copyright_arg
END
EXECUTE dbo.Movie_Movie_Creation12
#movie_id_arg = 32
, #director_id_arg = 32
, #genre_id_arg = 1
, #title_arg = 'Argo'
, #copyright_arg = 2012
, #director_fname_arg = 'Ben'
, #director_lname_arg = 'Affleck'
I wish to make sure that my data has a constraint the following check (constraint?) in place
This table can only have one BorderColour per hub/category. (eg. #FFAABB)
But it can have multiple nulls. (all the other rows are nulls, for this field)
Table Schema
ArticleId INT PRIMARY KEY NOT NULL IDENTITY
HubId TINYINT NOT NULL
CategoryId INT NOT NULL
Title NVARCHAR(100) NOT NULL
Content NVARCHAR(MAX) NOT NULL
BorderColour VARCHAR(7) -- Can be nullable.
I'm gussing I would have to make a check constraint? But i'm not sure how, etc.
sample data.
1, 1, 1, 'test', 'blah...', '#FFAACC'
1, 1, 1, 'test2', 'sfsd', NULL
1, 1, 2, 'Test3', 'sdfsd dsf s', NULL
1, 1, 2, 'Test4', 'sfsdsss', '#AABBCC'
now .. if i add the following line, i should get some sql error....
INSERT INTO tblArticle VALUES (1, 2, 'aaa', 'bbb', '#ABABAB')
any ideas?
CHECK constraints are ordinarily applied to a single row, however, you can cheat using a UDF:
CREATE FUNCTION dbo.CheckSingleBorderColorPerHubCategory
(
#HubID tinyint,
#CategoryID int
)
RETURNS BIT
AS BEGIN
RETURN CASE
WHEN EXISTS
(
SELECT HubID, CategoryID, COUNT(*) AS BorderColorCount
FROM Articles
WHERE HubID = #HubID
AND CategoryID = #CategoryID
AND BorderColor IS NOT NULL
GROUP BY HubID, CategoryID
HAVING COUNT(*) > 1
) THEN 1
ELSE 0
END
END
Then create the constraint and reference the UDF:
ALTER TABLE Articles
ADD CONSTRAINT CK_Articles_SingleBorderColorPerHubCategory
CHECK (dbo.CheckSingleBorderColorPerHubCategory(HubID, CategoryID) = 1)
Another option that is available is available if you are running SQL2008. This version of SQL has a feature called filtered indexes.
Using this feature you can create a unique index that includes all rows except those where BorderColour is null.
CREATE TABLE [dbo].[UniqueExceptNulls](
[HubId] [tinyint] NOT NULL,
[CategoryId] [int] NOT NULL,
[BorderColour] [varchar](7) NULL,
)
GO
CREATE UNIQUE NONCLUSTERED INDEX UI_UniqueExceptNulls
ON [UniqueExceptNulls] (HubID,CategoryID)
WHERE BorderColour IS NOT NULL
This approach is cleaner than the approach in my other answer because it doesn't require creating extra computed columns. It also doesn't require you to have a unique column in the table, although you should have that anyway.
Finally, it will also be much faster than the UDF/Check Constraint solutions.
You can also do a trigger with something like this (this is actually overkill - you can make it cleaner by assuming the database is already in a valid state - i.e. UNION instead of UNION all etc):
IF EXISTS (
SELECT COUNT(BorderColour)
FROM (
SELECT INSERTED.HubId, INSERTED.CategoryId, INSERTED.BorderColour
UNION ALL
SELECT HubId, CategoryId, BorderColour
FROM tblArticle
WHERE EXISTS (
SELECT *
FROM INSERTED
WHERE tblArticle.HubId = INSERTED.HubId
AND tblArticle.CategoryId = INSERTED.CategoryId
)
) AS X
GROUP BY HubId, CategoryId
HAVING COUNT(BorderColour) > 1
)
RAISEERROR
If you have a unique column in your table, then you can accomplish this by creating a unique constraint on a computer column.
The following sample created a table that behaved as you described in your requirements and should perform better than a UDF based check constraint. You might also be able to improve the performance further by making the computed column persisted.
CREATE TABLE [dbo].[UQTest](
[Id] INT IDENTITY(1,1) NOT NULL,
[HubId] TINYINT NOT NULL,
[CategoryId] INT NOT NULL,
[BorderColour] varchar(7) NULL,
[BorderColourUNQ] AS (CASE WHEN [BorderColour] IS NULL
THEN cast([ID] as varchar(50))
ELSE cast([HuBID] as varchar(3)) + '_' +
cast([CategoryID] as varchar(20)) END
),
CONSTRAINT [UQTest_Unique]
UNIQUE ([BorderColourUNQ])
)
The one possibly undesirable facet of the above implementation is that it allows a category/hub to have both a Null AND a color defined. If this is a problem, let me know and I'll tweak my answer to address that.
PS: Sorry about my previous (incorrect) answer. I didn't read the question closely enough.
In my sql code i'm passing around a bunch of magic numbers :-
AnimalType TINYINT
/*
AnimalType can be one of the following :-
1. Cat
2. Dog
3. Bird
....
*/
Is there anyway i could make this a custom type / enumeration.
eg.
AnimalType ANIMAL
and it's constrained to contain a number between 1 <-> whatever (eg. 3 in my example above).
or constrained to strings. eg. AnimalType = 'Cat' .. etc ?
Cheers!
Edit
I know what LookUp tables are. This is not for a lookup table, but for some data passed to a number of stored procedures. Instead of passing in Magic Numbers, I wish to pass in an enumeration OR at least some contrained number (eg. numbers 1<->5), etc...
There are no enumeration types. However, you can create user defined functions to translate back and forth between INTs that you map to enumerated values.
To generate friendly names for an AnimalType based of 'INT' you could do something like this:
UDF to generate friendly names:
CREATE FUNCTION ihAnimalTypeDesc
(
#AnimalType INT
)
RETURNS VARCHAR(20)
AS
BEGIN
IF #AnimalType IS NULL
RETURN NULL
DECLARE #Temp AS VARCHAR(20)
SET #Temp = CASE #AnimalType
WHEN 1 THEN 'Cat'
WHEN 2 THEN 'Dog'
WHEN 3 THEN 'Snake'
END
RETURN #Temp
END
A SELECT statement could uses the UDF like so:
SELECT A.AnimalName, dbo.ihAnimalTypeDesc(A.AnimalType)
FROM Animals A
Here is a UDF to return true or false if an animal is of a particular type:
CREATE FUNCTION IsCat
(
#AnimalType INT
)
RETURNS BIT
AS
BEGIN
IF #AnimalType IS NULL
RETURN NULL
IF #AnimalType = 1
RETURN 1
RETURN 0
END
Here is an example using the above UDF. NOTE: you have to be careful with performance issue when doing this in the WHERE clause.:
SELECT AnimalName
FROM Animals
WHERE dbo.IsCat(AnimalType)
Enumeration is like a FOREIGN KEY to a table, but without a table.
Create a table and make a FOREIGN KEY constraint:
CREATE TABLE AnimalType (id INT NOT NULL PRIMARY KEY, name VARCHAR(50) NOT NULL)
CREATE TABLE Animal (
id INT NOT NULL PRIMARY KEY,
type INT NOT NULL,
name VARCHAR(50) NOT NULL,
CONSTRAINT FK_animal_type FOREIGN KEY (type) REFERENCES AnimalType(id)
)
You can try to add something like this:
CREATE VIEW AnimalType AS
SELECT
10 AS Cat
,20 AS Dog
,30 AS Bird
to use it:
DECLARE #animal INT
SELECT #animal = Bird FROM AnimalType
as per your latest edit, about needing to pass in parameters. T-SQL has the basics to to simple stuff in addition to the heavy lifting SQL. There are no enumeration data-types. If that is what you want, you can handle it in the following way:
create procedure YourProcedure
( #param1 int
,#param2 varchar(5)
,#param3 varchar(5)
)
as
DECLARE #Value3 int
IF #param1 IS NULL OR #param1<1 OR #param1>5
BEGIN
return 1 --error out of range param
END
IF NOT EXISTS (SELECT Value FROM LookUpTable WHERE Value=#param2)
BEGIN
return 2 --error out of range param
END
SELECT #Value3=IntValue FROM OtherLookupTable WHERE StrValue=#param3
IF #Value3 IS NULL
BEGIN
return 3 --error out of range param
END
--do work here
go