T-SQL crazy function result - sql-server

I have this function:
CREATE FUNCTION CheckAkvNames (#Name VARCHAR(20))
RETURNS INT
AS
BEGIN
DECLARE #NoTexist int = 1
SELECT
#NoTexist = CASE WHEN COUNT(*) > 0 THEN 0 ELSE 1 END
FROM
[dbo].[Names]
WHERE
[Name] = #Name
RETURN #NoTexist
END
GO
ALTER TABLE [dbo].[Names]
ADD CONSTRAINT chkNames CHECK(dbo.CheckAkvNames([Name]) = 1);
GO
The problem is, when I run this on empty table I can't insert ...
So this change works:
CASE WHEN (COUNT(*) - 1) > 0 THEN 0 ELSE 1 END
WHY? Any ideas?
Edit:
Aim is to insert only names that are not in the table. I know it would be better to use key, point of the question is not to find better solution but why this solution does not work.

The constraint you added to the table actually means that you can't insert any name in the table, because for any value inserted in the table the function should return 1.This is impossible because if the name was inserted then the constraint would be violated.
This is why count(*) - 1 works: if there is already a name inserted and you tried to insert the same name then the constraint would be violated.

If you want unique names in a table, do not use a check constraint, use a unique constraint (or equivalently a unique index):
ALTER TABLE [dbo].[Names]
ADD CONSTRAINT unq_names_name UNIQUE (Name);

Related

How to create a column null or not-null dependent on the value of another column?

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

How can I use a trigger to allow an incremented, user-assigned ID?

I am moving a small database from MS Access into SQL Server. Each year, the users would create a new Access database and have clean data, but this change will put data across the years into one pot. The users have relied on the autonumber value in Access as a reference for records. That is very inaccurate if, say, 238 records are removed.
So I am trying to accommodate them with an id column they can control (somewhat). They will not see the real primary key in the SQL table, but I want to give them an ID they can edit, but still be unique.
I've been working with this trigger, but it has taken much longer than I expected.
Everything SEEMS TO work fine, except I don't understand why I have the same data in my INSERTED table as the table the trigger is on. (See note in code.)
ALTER TRIGGER [dbo].[trg_tblAppData]
ON [dbo].[tblAppData]
AFTER INSERT,UPDATE
AS
BEGIN
SET NOCOUNT ON;
DECLARE #NewUserEnteredId int = 0;
DECLARE #RowIdForUpdate int = 0;
DECLARE #CurrentUserEnteredId int = 0;
DECLARE #LoopCount int = 0;
--*** Loop through all records to be updated because the values will be incremented.
WHILE (1 = 1)
BEGIN
SET #LoopCount = #LoopCount + 1;
IF (#LoopCount > (SELECT Count(*) FROM INSERTED))
BREAK;
SELECT TOP 1 #RowIdForUpdate = ID, #CurrentUserEnteredId = UserEnteredId FROM INSERTED WHERE ID > #RowIdForUpdate ORDER BY ID DESC;
IF (#RowIdForUpdate IS NULL)
BREAK;
-- WHY IS THERE A MATCH HERE? HAS THE RECORD ALREADY BEEN INSERTED?
IF EXISTS (SELECT UserEnteredId FROM tblAppData WHERE UserEnteredId = #CurrentUserEnteredId)
BEGIN
SET #NewUserEnteredId = (SELECT Max(t1.UserEnteredId) + 1 FROM tblAppData t1);
END
ELSE
SET #NewUserEnteredId = #CurrentUserEnteredId;
UPDATE tblAppData
SET UserEnteredId = #NewUserEnteredId
FROM tblAppData a
WHERE a.ID = #RowIdForUpdate
END
END
Here is what I want to accomplish:
When new record(s) are added, it should increment values from the Max existing
When a user overrides a value, it should check to see the existence of that value. If found restore the existing value, otherwise allow the change.
This trigger allows for multiple rows being added at a time.
It is great for this to be efficient for future use, but in reality, they will only add 1,000 records a year.
I wouldn't use a trigger to accomplish this.
Here is a script you can use to create a sequence (op didn't tag version), create the primary key, use the sequence as your special id, and put a constraint on the column.
create table dbo.test (
testid int identity(1,1) not null primary key clustered
, myid int null constraint UQ_ unique
, somevalue nvarchar(255) null
);
create sequence dbo.myid
as int
start with 1
increment by 1;
alter table dbo.test
add default next value for dbo.myid for myid;
insert into dbo.test (somevalue)
select 'this' union all
select 'that' union all
select 'and' union all
select 'this';
insert into dbo.test (myid, somevalue)
select 33, 'oops';
select *
from dbo.test
insert into dbo.test (somevalue)
select 'oh the fun';
select *
from dbo.test
--| This should error
insert into dbo.test (myid, somevalue)
select 3, 'This is NO fun';
Here is the result set:
testid myid somevalue
1 1 this
2 2 that
3 3 and
4 4 this
5 33 oops
6 5 oh the fun
And at the very end a test, which will error.

One Bowler can not bowl two consecutive over in cricket

I am working on Cricket Project. I have a table OverDetails. I want to insert data in this table.
ID OverNumber BowlerID InningsID
1 1 150 1
2 4 160 1
3 3 165 1
4 2 150 1
Row_1, Row_2 and Row_3 are legal. Row_4 is not legal, because one bowler can not through two consecutive overs in one innings. It is not necessary that overs are added consecutively in database.
I have added a constraint in SQL Server.
#Constraint_1
ALTER TABLE OverDetails ADD CONSTRAINT UniqueOverInInning
UNIQUE(OverNumber, BowlerID, IninngsID);
This constraint works perfectly.
I need a check like this:
#Constraint_2
ALTER TABLE OverDetails ADD CONSTRAINT UniqueConsecutiveBowlerInOneInning
CHECK (OverNumber + 1 != OverNumber and BowlerID + 1 != BowlerID
and IninngID + 1 != IninngID)
You need a function which returns a last BowlerID from a given InningID:
CREATE FUNCTION dbo.GetBowlerID
( #InningId INT, #OverNumber INT, #BowlerID INT)
RETURNS INT
AS
BEGIN
RETURN (SELECT top 1 CASE WHEN
(SELECT BowlerID
FROM OverDetails
WHERE InningsId = #InningId AND OverNumber = #OverNumber - 1 ) = #BowlerID
OR
(SELECT BowlerID
FROM OverDetails
WHERE InningsId = #InningId AND OverNumber = #OverNumber + 1 ) = #BowlerID
THEN 1 else 0 end)
END
Then you can put it into a check constraint:
ALTER TABLE OverDetails ADD CONSTRAINT UniqueConsecutiveBowlerInOneInning
CHECK (dbo.GetBowlerID(InningsId, OverNumber, BowlerID)=0)
Check constraints cannot directly reference other rows data. There are some techniques that try to use UDFs to get around this limitation but they tend to not work well. Especially in this case where I presume the insert of row 4 should also be blocked if it had a bowlerID of 165 since that would mean overs 2&3 shared a bowler.
Instead, we can implement this with a pair of views. I usually put DRI somewhere in the name of views like this to indicate that they're there for Declarative Referential Integrity reasons, not because I intend people to query them.
create table dbo.Bowling (
ID int not null,
OverNumber int not null,
BowlerID int not null,
InningsID int not null,
constraint PK_Bowling PRIMARY KEY (ID),
constraint UQ_Bowling_Overs UNIQUE (OverNumber,InningsID)
)
go
create view dbo.Bowling_DRI_SuccessiveOvers_Odd
with schemabinding
as
select
(OverNumber/2) as OddON,
BowlerID
from
dbo.Bowling
go
create unique clustered index UQ_Bowling_DRI_SuccessiveOvers_Odd on dbo.Bowling_DRI_SuccessiveOvers_Odd (OddON,BowlerID)
go
create view dbo.Bowling_DRI_SuccessiveOvers_Even
with schemabinding
as
select
((OverNumber+1)/2) as EvenON,
BowlerID
from
dbo.Bowling
go
create unique clustered index UQ_Bowling_DRI_SuccessiveOvers_Even on dbo.Bowling_DRI_SuccessiveOvers_Even (EvenON,BowlerID)
go
insert into dbo.Bowling(ID,OverNumber,BowlerID,InningsID) values
(1,1,150,1),
(2,4,160,1),
(3,3,165,1)
go
insert into dbo.Bowling(ID,OverNumber,BowlerID,InningsID) values
(4,2,150,1)
This final insert generates the error:
Msg 2601, Level 14, State 1, Line 37 Cannot insert duplicate key row
in object 'dbo.Bowling_DRI_SuccessiveOvers_Even' with unique index
'UQ_Bowling_DRI_SuccessiveOvers_Even'. The duplicate key value is (1,
150). The statement has been terminated.
Hopefully, you can see the trick I'm employing to make these views check your desired constraint - it's set up so that rows are paired with either their (logical, based on OrderNumber) successor or predecessor based on dividing the OrderNumber by two using integer maths.
We then apply unique constraints on these pairs and including the BowlerID. Only if the same bowler bowls two successive overs will we generate more than one row with the same (OddON/EvenON) and BowlerID values.
Maybe this one?
create function dbo.chk_fnk (#OverNumber int, #BowlerID int, #InningsID int)
returns int
as
begin
return
case when
exists (select *
from dbo.OverDetails
where BowlerID = #BowlerID and abs(OverNumber - #OverNumber) = 1 and InningsID = #InningsID)
then 1
else 0
end;
end;
go
ALTER TABLE dbo.OverDetails ADD CONSTRAINT UniqueConsecutiveBowlerInOneInning
CHECK (dbo.chk_fnk(OverNumber, BowlerID, InningsID) = 0);

trigger for insert or update after checking relationship

I have this 3 tables:
And i need to build a trigger that: A date ("encontro") can only works when theres a friendship ("amizade") between 2 profiles ("perfis").
I've created this trigger but i feel lost.. HELP ME
CREATE TRIGGER relaƧoes_after_insert
ON encontros
INSTEAD OF insert -
as
begin
declare #idperfilA int;
declare #idperfilB int;
declare #data datetime;
declare #count int;
declare cursor_1 cursor for select * from inserted;
open cursor_1;
fetch next from cursor_1 into #idperfilA, #idperfilB, #data;
WHILE ##FETCH_STATUS = 0
BEGIN
if exists( select * from inserted i, amizade a
where i.IDPERFILA = a.IDPERFILA and i.IDPERFILB = a.IDPERFILB and GETDATE() > DATA)
RAISERROR('there isnt friendship', 16, 10);
else
insert into ENCONTROS select * from inserted;
end;
fetch next from cursor_1 into #idperfilA, #idperfilB, #data;
END
close cursor_1;
deallocate cursor_1;
I think the better answer would be to not create use a trigger for this at all. Instead I would create and enforce a foreign key constraint between encontros and amizade.
As far as I can tell, this will result in doing what you want without having to write your own code to try and recreate behavior provided by the database. It also makes it much easier to understand from a database design point of view.
alter table dbo.encontros
add constraint fk_amizade__encontros
foreign key (idperflia, idperflib) references dbo.amizade (idperflia, idperflib)
/* optional
on delete { no action | cascade | set null | set default } -- pick one, usual defualt is: no action
on update { no action | cascade | set null | set default } -- pick one, usual defualt is: no action
--*/*
;
More about table constraints.
NO ACTION
The SQL Server Database Engine raises an error and the delete action on the row in the parent table is rolled back.
CASCADE
Corresponding rows are deleted from the referencing table if that row is deleted from the parent table.
SET NULL
All the values that make up the foreign key are set to NULL when the corresponding row in the parent table is deleted. For this constraint to execute, the foreign key columns must be nullable.
SET DEFAULT
All the values that comprise the foreign key are set to their default values when the corresponding row in the parent table is deleted. For this constraint to execute, all foreign key columns must have default definitions. If a column is nullable and there is no explicit default value set, NULL becomes the implicit default value of the column.
Based on your reply to #3N1GM4:
#3N1GM4 if exists some friendship with a date after today (for example) it is an error, so the friendship doesnt exist. But i dont know if it matters at this point. IDPERFILA and IDPERFILB will match A and B at amizade table, but i need to make sure that they were not the same
You could create a check constraint on amizade that will prevent rows with invalid dates from being inserted into the table.
alter table dbo.amizade
add constraint chk_data_lt_getdate ([data] < get_date());
More about check constraints; more examples from Gregory Larson.
original answer:
I'm still waiting on some clarification on the question, but one of the versions in this should be on the right path:
create trigger relaƧoes_after_insert
on encontros
instead of insert
as
begin
/* To abort when any row doesn't have a matching friendship */
if not exists (
select 1
from inserted i
where exists (
select 1
from amizade a
where a.idperfila = i.idperfila
and a.idperfilb = i.idperfilb
and getdate() > data /* not sure what this part does */
/* as #3N1GM4 pointed out,
if the position doesn't matter between idperflia and idperflib then:
where (i.idperfila = a.idperfila and i.idperfilb = a.idperfilb)
or (i.idperfila = a.idperfilb and i.idperfilb = a.idperfila)
*/
)
begin;
raiserror('there isnt friendship', 16, 10);
else
insert into encontros
select * from inserted;
end;
end;
/* To insert all rows that have a matching friendship, you could use this instead */
insert into encontros
select i.*
from inserted i
where exists (
select 1
from amizade a
where a.idperfila = i.idperfila
and a.idperfilb = i.idperfilb
and getdate() > data /* not sure what this part does */
/* as #3N1GM4 pointed out,
if the position doesn't matter between idperflia and idperflib then:
where (i.idperfila = a.idperfila and i.idperfilb = a.idperfilb)
or (i.idperfila = a.idperfilb and i.idperfilb = a.idperfila)
*/
)
end;
The only potential issue I see with using an inner join instead of exists for the second option (inserting rows that have a friendship and ignoring ones that don't) is if there could ever be an issue where (i.idperfila = a.idperfila and i.idperfilb = a.idperfilb) or (i.idperfila = a.idperfilb and i.idperfilb = a.idperfila) would return duplicates of the inserted rows from each condition returning a match.

Simple CHECK Constraint not so simple

2nd Edit: The source code for the involved function is as follows:
ALTER FUNCTION [Fileserver].[fn_CheckSingleFileSource] ( #fileId INT )
RETURNS INT
AS
BEGIN
-- Declare the return variable here
DECLARE #sourceCount INT ;
-- Add the T-SQL statements to compute the return value here
SELECT #sourceCount = COUNT(*)
FROM Fileserver.FileUri
WHERE FileId = #fileId
AND FileUriTypeId = Fileserver.fn_Const_SourceFileUriTypeId() ;
-- Return the result of the function
RETURN #sourceCount ;
END
Edit: The example table is a simplification. I need this to work as a Scaler Function / CHECK CONSTRAINT operation. The real-world arrangement is not so simple.
Original Question: Assume the following table named FileUri
FileUriId, FileId, FileTypeId
I need to write a check constraint such that FileId are unique for a FileTypeId of 1. You could insert the same FileId as much as you want, but only a single row where FileTypeId is 1.
The approach that DIDN'T work:
1) dbo.fn_CheckFileTypeId returns INT with following logic: SELECT Count(FileId) FROM FileUri WHERE FileTypeId = 1
2) ALTER TABLE FileUri ADD CONSTRAINT CK_FileUri_FileTypeId CHECK dbo.fn_CheckFileTypeId(FileId) <= 1
When I insert FileId 1, FileTypeId 1 twice, the second insert is allowed.
Thanks SO!
You need to create a filtered unique index (SQL Server 2008)
CREATE UNIQUE NONCLUSTERED INDEX ix ON YourTable(FileId) WHERE FileTypeId=1
or simulate this with an indexed view (2000 and 2005)
CREATE VIEW dbo.UniqueConstraintView
WITH SCHEMABINDING
AS
SELECT FileId
FROM dbo.YourTable
WHERE FileTypeId = 1
GO
CREATE UNIQUE CLUSTERED INDEX ix ON dbo.UniqueConstraintView(FileId)
Why don't you make FieldTypeID and Field both the primary key of the table?
Or at least a Unique Index on the table. That should solve your problem.

Resources