Creating a function using PL/SQL - database

I have the following table:
create table movie(
movie_id integer primary key,
title varchar(500) not null,
kind varchar(30),
year integer not null
);
I want to create a function:
addMovie(title, kind, year)
The function must insert a new row into the movie table with a unique movie_id, and then return the movie_id.
This is the first time I'm using PL/SQL, so I'm kind of lost at the moment - couldn't find any (good) examples so far.
Thanks!

Your function needs to do 3 things
Generate the unique movie id
Insert into the table
Return the generated id
Let's take it one step at a time
Generate the unique movie id
The best way to do it is to use a seqeuence which will generated a id for you. Look up on sequences
Insert into the table
This is done by a straightforward insert. Since the movie id is generated by the sequence, we use sequence_name.nextval in the insert statement. Thus the insert statement looks like
INSERT INTO movie(movie_id, title, kind, year) values (movie_id_seq.nextval, title, kind, year)
Return the generated id back
You can make use of the Returning clause in a DML to return the generated id back into a variable. And then use the RETURN statement to return the value back.
So this is how your function will look like
FUNCTION addmovie(p_title,
p_kind,
p_year)
RETURN NUMBER
IS
v_id movie.id%TYPE;
BEGIN
INSERT INTO movie
(
movie_id,
title,
kind,
year
)
VALUES
(
movie_id_seq.NEXTVAL,
p_title,
p_kind,
p_year
)
returning id
INTO v_id;
RETURN v_id;
END;
Note that this is a fairly basic function, with no error checking, exception handling - I'll leave it up to you.
Note that max(movie_id)+1 isn't the best way forward, for purposes of the assignment. You'll need
SELECT max(movie_id)+1 into v_id from movies;
before the insert statement.
Also, because of the DML, you can't use this function as part of a query.

Related

How to set current value of a sequence as default value?

I know how to set a sequence as the default value:
ALTER TABLE TableA
ADD CONSTRAINT SequenDefault
DEFAULT NEXT VALUE FOR SeqA
FOR ColumnA
I also know how to get the current sequence value:
SELECT current_value
FROM sys.sequences
WHERE name = 'SeqA';
But how can I "put it together"?
Namely, set current value (not next value) of SeqA as the default value of ColumnA in TableA.
PS. This is the sequence if needed:
CREATE SEQUENCE SeqA AS int START WITH 1;
Thank you very much for your help!
Update
#marc_s said it is impossible to do such a thing, and #PanagiotisKanavos had mentioned Change Tracking, but before I figure out what it does, I still like to know if such thing is possible.
Say if I have a name table
CREATE TABLE NameTable
(
ID int PRIMARY KEY DEFAULT NEXT VALUE FOR SeqA,
Name nvarchar(50)
);
This table will "never" be updated! So a user who had already acquired the table (and saved) has "no need" to get the old record anymore whatsoever.
So I want to save the last record the user had downloaded in another table:
CREATE TABLE UserTable
(
ID int IDENTIFY(1,1) PRIMARY KEY,
Age int,
LastNameID int DEFAULT ??? --Don't know how to do it.
)
So I could let the user download the "new names only" by something like:
SELECT Name FROM NameTable LEFT JOIN UserTable ON NameTable.ID > UserTable.LastNameID WHERE UserTable.ID=19;
And a new user's LastNameID is "automatically" set to the current value of SeqA (upon downloading all names).
I know I could get it done by a stored procedure, but I still like to know if such a thing is possible, by simply setting a default value or something.
Thank you very much for your help!

Function to check value and returns result

I need to create function in SQL to check data in variables or parameters as below
#Category as varchar(50)='ABC,DEF'
#Value as varchar(50)='1,2'
And compare #Category value with Category in table then return value matching from parameter
JOB TABLE ---
JOB CATEGORY
123 ABC
234 DEF
234 SSS
Select JobNo,FUNCTION(#Category,#Value,CATEGORY) from JOB
FINAL RESULTS
JOB VALUE
123 1
234 2
234 0
If category match then return value from parameter else return 0.
If you can't use a static lookup table as mentioned in the comments (for example, perhaps the mapping needs to be dynamic based on data supplied by the application), then this looks like a job for a table valued parameter.
Right now you have two parameters, but it seems to me that the values in the parameters are related. That is to say, right now you have #category = 'ABC,DEF' and #value = '1,2', but I think you intend each "element" in the comma delimited set of "categories" to associate with the "element" in the comma delimiited set of "values" that is in the same position.
Right now the design is brittle, because what would happen if I use parameters like this: #category = 'ABC,DEF,GHI,JKL', #value = '1'?
So, you can make your code more durable, and use the sort of "join-based" lookup table solution being recommended to you in the comments, by using a function that takes a table valued parameter argument. To do this, you first have to create the table valued parameter as a type in your database. We then accept a parameter of that type, and join onto it. In the solution below I have "guessed" at datatypes for category and value that seem reasonable based on the sample data in your question.
Also, I've kept the "structure" of your solution - ie, the function is written in such a way that it can be "applied" against every row in jobs, individually. We don't have to do it this way. We could instead just do the whole query inside the function (ie, including the join to the job table), but perhaps you want to use the function against other tables that also have a cateogry column? I won't try to second guess the overall design here. But I will switch the function to an inline table valued function (which returns one row with one column) rather than a scalar function, for performance reasons.
-- schema and data to match your question
create table dbo.job (job int, category char(3));
insert dbo.job(job, category) values (123, 'ABC'), (234, 'DEF'), (234, 'SSS');
go
-- solution
create type dbo.CategoryValues as table
(
category char(3) unique,
[value] int
);
go
create or alter function dbo.MapCategory(#category char(3), #map dbo.CategoryValues readonly)
returns table as return
(
select [value] from #map where category = #category
);
go
-- to call the function we need to pass a parameter of type
-- dbo.CategoryValues with the mappings we desire
declare #map dbo.CategoryValues;
insert #map values ('ABC', 1), ('DEF', 2)
-- outer apply the function to each row in the job table.
select j.job, [value] = isnull(v.[value], 0)
from dbo.job j
outer apply dbo.MapCategory(j.category, #map) v

Stored procedure that needs to handle multiple scenarios

I have a rather complicated (at least for me) stored procedure to write that needs to handle multiple scenarios coming from the front end.
The frontend is passing 2 parameters that has values like this
#Levelmarker= (1234515-564546-65454,4654342-154658-56767,5465489-546549-65456)
These are GUIDS that are comma separated.
#`UserNameId= (5797823-65432143-65451213)
GUID of the user that entered this data on the front end
The values need to go to a table that has the following structure:
CREATE TABLE LevelTable
(
LevelId uniqueidentifier NOT NULL
LevelMarker uniqueidentiriet NOT NULL
UserName uniqueidentifier NOT NULL
);
I want the value to go into the table like this:
LevelId Levelmarker UserName
--------------------------------------------------------
NEWID() 1234515-564546-65454 5797823-654321-65451
NEWID() 4654342-154658-56767 5797823-654321-65451
NEWID() 5465489-546549-65456 5797823-654321-65451
Here are the scenarios the stored procedure should handle.
Once the levelmarkers are inserted into the table, if the same user comes back and wants to add additional Levelmarkers, the front end will pass the old values and the new ones as so: (1234515-564546-65454,4654342-154658-56767,5465489-546549-65456,1332245-9852135-7841265).
My stored procedure should recognize that I already have the first three Levelmarkers in the table and should only insert the new ones.
If the same user decides to delete values from before, lets say two values as an example, the front end will pass me the values (1234515-564546-65454,4654342-154658-56767). The stored procedure should recognize that the user has deleted two values and should delete the same values from the table and keep the non deleted ones.
If the user deletes some values and inserts a new ones, then the stored procedure should recognize the ones to delete and insert the new ones.
What is the best approach to this problem?
I think you can do this in a single query, using string_split() and a merge statement:
merge leveltable t
using (
select value levelmarker, #UserNameId username
from string_split(#LeveMarker, ',')
) s
on (s.levelmarker = t.levelmarker and s.username = t.username)
when not matched by target
then insert (leveid, levelmarker, username)
values (newid(), s.levelmarker, s.username)
when not matched by source
then delete
In the using clause, we split the #LevelMarker parameter into new rows, and associate the given #UserNameId. Then, the merge statement checks if each combination already exists in the target table, and creates or deletes rows accordingly.

select a column named by another column in another table

I am implementing a database that will back a role playing game. There are two relevant tables: character and weapon (plus a third table representing a standard many-to-many relationship; plus the level of each specific instance of a weapon). A character has multiple attributes (strength, agility, magic etc.) and each weapon has a base damage, a level (defined in the many-to-many association), and receives a bonus from the associated attribute of the character wielding said weapon (strength for clubs, agility for ranged weapons etc.). The effectiveness of a weapon must be derived from the three tables. The catch is that which column of the character table applies is dependent on the specific weapon being used.
My current paradigm is to perform two select queries, one to retrieve the name of the associated attribute (varchar) from the weapon table and then one - with the previously returned value substituted in - for the value of that attribute from the wielding character. I would like to replace this with a pure sql solution.
I have searched around the nets and found two other questions:
Pivot on Multiple Columns using Tablefunc and PostgreSQL Crosstab Query but neither does quite what I'm looking for. I also found the postgres internal datatype oid [https://www.postgresql.org/docs/9.1/static/datatype-oid.html ], and was able to locate the oid of a specific column, but could not find the syntax for querying the value of the column with that oid.
Table schemeta:
create table character (
id int primary key,
agility int,
strength int,
magic int,
...);
create table weapon (
id int primary key,
damage int,
associated_attribute varchar(32), --this can be another type if it'd help
...);
create table weapon_character_m2m (
id int primary key,
weapon int, --foreign key to weapon.id
character int, --foreign key to character.id
level int);
In my mind, this should be query-able with something like this (ideally resulting in the effective damage of each weapon currently in the player's possession.):
select m2m.level as level,
weapon.associated_attribute as base_attr_name,
character.??? as base_attr,
weapon.damage as base_damage,
base_damage * base_attr * level as effective_attr -- this is the column I care about, others are for clarity via alias
from weapon_character_m2m as m2m
join weapon on weapon.id=m2m.weapon
join character on character.id=m2m.character;
where m2m.character=$d -- stored proc parameter or the like
Most online resources I've found end up suggesting the database be redesigned. This is an option, but I really don't want to have a different table for each attribute to which a weapon might associate (in practice there are nearly 20 attributes that might be associated with weapon classes).
I have heard that this is possible in MSSQL by Foreign Key'ing into an internal system table, but I have no experience with MSSQL, let alone enough to attempt something like that (and I couldn't find a working sample on the internets). I would consider migrating to MSSQL (or any other sql engine) if anyone can provide a working example.
It sounds like you can just use the CASE statement. I know it is in MS SQL...not sure about PostgreSQL.
Something like (on my phone so just estimating the code 🙂):
Select other fields,
case weapon.associated_attribute
when 'agility' then character.agility
when 'strength' then character.strength
when ...
else 0 --unhandled associate_attribute
end as base_attr
from ...
The caveat here is that you will want your character attributes to be the same type which it looks like you do.
EDIT
I worked towards a view based on your feedback and realized that a view would use an unpivot rather than the case statement above but that you could use a function to do it using the CASE structure above. There are many flavours :-) MS SQL also has table-valued functions that you could use to return one attribute type for all characters. Here is the code I was playing with. It contains both a view and a function. You can choose which seems more appropriate.
create table character (
id int primary key,
agility int,
strength int,
magic int,
--...
);
insert character values (1,10,15,20),(2,11,12,13);
create table attribute_type (
attribute_id int primary key,
attribute_name varchar(20)
);
insert attribute_type values (1,'Agility'),(2,'Strength'),(3,'Magic');
create table weapon (
id int primary key,
damage int,
--associated_attribute varchar(32), --this can be another type if it'd help
attribute_id int
--...
);
insert weapon values (1,20,1),(2,30,2);
create table weapon_character_m2m (
id int primary key,
weapon int, --foreign key to weapon.id
character int, --foreign key to character.id
level int);
insert weapon_character_m2m values (1,1,1,4),(2,2,2,5);
go
create view vw_character_attributes
as
select c.id, a.attribute_id, c.attribute_value
from (
select id, attribute_name, attribute_value
from (select id, agility, strength, magic from character) p --pivoted data
unpivot (attribute_value for attribute_name in (agility, strength, magic)) u --unpivoted data
) c
join attribute_type a on a.attribute_name = c.attribute_name
;
go
create function fn_get_character_attribute (#character_id int, #attribute_id int)
returns int
as
begin
declare #attr int;
select #attr =
case #attribute_id
when 1 then c.agility
when 2 then c.strength
when 3 then c.magic
--when ...
else 0 --unhandled associate_attribute
end
from character c
where c.id = #character_id;
return #attr;
end
go
select * from vw_character_attributes;
select m2m.level as level,
at.attribute_name as base_attr_name,
ca.attribute_value as base_attr,
dbo.fn_get_character_attribute(m2m.id, weapon.attribute_id ) function_value,
weapon.damage as base_damage,
weapon.damage * ca.attribute_value * level as effective_attr -- this is the column I care about, others are for clarity via alias
from weapon_character_m2m as m2m
join weapon on weapon.id=m2m.weapon
join vw_character_attributes ca on ca.id=m2m.character and ca.attribute_id = weapon.attribute_id
join attribute_type at on at.attribute_id = weapon.attribute_id
--where m2m.character=$d; -- stored proc parameter or the like

INSERT+SELECT with a unique key

The following T-SQL statement does not work because [key] has to be unique and the MAX call in the SELECT statement only seems to be called once. In other words it is only incrementing the key value once and and trying to insert that value over and over. Does anyone have a solution?
INSERT INTO [searchOC].[dbo].[searchTable]
([key],
dataVaultType,
dataVaultKey,
searchTerm)
SELECT (SELECT MAX([key]) + 1 FROM [searchOC].[dbo].[searchTable]) AS [key]
,'PERSON' as dataVaultType
,[student_id] as dataVaultKey
,[email] as searchTerm
FROM [JACOB].[myoc4Data].[dbo].[users]
WHERE [email] != '' AND [active] = '1'
AND [student_id] IN (SELECT [userID] FROM [JACOB].[myoc4Data].[dbo].[userRoles]
WHERE ([role] = 'STUDENT' OR [role] = 'FACUTLY' OR [role] = 'STAFF'))
If you can make the key column an IDENTITY column that would probably be the easiest. That allows SQL Server to generate incremental values.
Alternatively, if you are definite about finding your own way to generate the key then a blog post I wrote last month may help. Although it uses a composite key, it shows you what you need to do to stop the issue with inserting multiple rows in a single INSERT statement safely generating a new value for each new row and it is also safe across many simultaneous writers (which many examples don't deal with)
http://colinmackay.co.uk/2012/12/29/composite-primary-keys-including-identity-like-column/
Incidentally, the reason that you get the same value for MAX(Key) on each row in your SELECT is that this happens at the time the table is read from. So for all the rows that the SELECT statement returns the MAX(key) will always be the same. Unless you add some sort of GROUP BY clause for any SELECT statement any MAX(columnName) function will return the same value for each row returned.
Also, all aggregate functions are deterministic, so for each equivalent set of input it will always have the same output. So if your set of keys was 1, 5, 9 then it will always return 9.

Resources