So I'm writing some proceedures for tsql to get a group project back off the ground, and am trying to write something to create user profiles with salted passwords. However, when I run this code, the values for #seedno and #saltval never get set, so my hashed password ends up with a null value. Is there anyone who can help?
CREATE PROCEDURE [dbo].[CreateCustomerAccount]
#companyName nvarchar (50),
#emailAddress nvarchar(50),
#password nvarchar (100),
#Addr1 nvarchar (50),
#Addr2 nvarchar (50),
#Addr3 nvarchar (50),
#postCode nvarchar (11),
#Telephone NVARCHAR (20),
#vatNo NVARCHAR (20)
AS
SET NOCOUNT ON
--Declare some variables here
DECLARE
#pwdSalt NVARCHAR (25),
#Seed INT, /* this is going to store the randomly generated seed we will use to make the salt */
#CurrentChar CHAR,
#SaltedPwd NVARCHAR (125),
#LoopCountVar TINYINT, /*because counting is fun, kids :). */
#Seedno integer,
#saltval CHAR
--Generate a seed using the customers email address and a psuedo random number generator.
SET #LoopCountVar = 0;
WHILE #LoopCountVar <= LEN(#emailAddress)
BEGIN
SET #CurrentChar = SUBSTRING(#EmailAddress, #LoopCountVar, 1)
SET #Seedno = (ASCII(#currentChar)*CRYPT_GEN_RANDOM(10000))
SET #Seed = #Seed + #Seedno --Add the integer value of the current character multiplied by a random number between 0 and 100,000 to the seed
SET #LoopCountVar = #LoopCountVar+1
END;
--Now, populate our salt string by inserting random characters using the seed we just created.
SET #LoopCountVar = 0;
while #loopCountVar <= 25
BEGIN
SET #saltval = CHAR (ROUND((RAND()*94)+32,3))
SET #pwdSalt = #pwdSalt + #saltval
SET #LoopCountVar = #LoopCountVar+1
END;
--Time for delicious delicious salted hash
Set #SaltedPwd = #pwdSalt+#password;
-- Insert data into table
Insert into Customer (orgName, Email, AccountPwd, PwdSalt, AddressLine1, AddressLine2, AddressLine3, PostCode, TelephoneNo, VATNo)
VALUES (#companyName, #emailAddress, HASHBYTES('MD5', #SaltedPwd), #pwdSalt, #Addr1, #Addr2, #Addr3, #postCode, #Telephone, #vatNo);
RETURN 0
It seems a very roundabout way to generate the pwd.
Anyway in your second loop you are using an unassigned variable, this making the result null
Try this:
SET #LoopCountVar = 0;
SET #pwdSalt=''
while #loopCountVar <= 25
BEGIN
SET #saltval = CHAR (ROUND((RAND()*94)+32,3))
SET #pwdSalt = #pwdSalt + #saltval
SET #LoopCountVar = #LoopCountVar+1
END;
You have several things to fix.
First, you are trying to multiply the ASCII() function return value, which is an INT, to a VARBINARY(80) which is the return type of the CRYPT_GEN_RANDOM() function.
Second, you are calling the CRYPT_GEN_RANDOM(10000) with a parameter length bigger than 8000, which is the maximum of length you can use with this function.
Take a look at this and this links.
Generally, you can debug your stored procedure body step by step selecting the values of the variables you have inside it prior to doing any calculation.
Related
I'm converting a BDE query (Paradox) to a Firebird (2.5, not 3.x) and I have a very convenient conversion in it:
select TRIM(' 1') as order1, CAST(' 1' AS INTEGER) AS order2 --> 1
select TRIM(' 1 bis') as order1, CAST(' 1 bis' AS INTEGER) AS order2 --> 1
Then ordering by the cast value then the trimmed value (ORDER order2, order1) provide me the result I need:
1
1 bis
2 ter
100
101 bis
However, in Firebird casting an incorrect integer will raise an exception and I did not find any way around to provide same result. I think I can tell if a number is present with something like below, but I couldn't find a way to extract it.
TRIM(' 1 bis') similar to '[ [:ALPHA:]]*[[:DIGIT:]]+[ [:ALPHA:]]*'
[EDIT]
I had to handle cases where text were before the number, so using #Arioch'The's trigger, I got this running great:
SET TERM ^ ;
CREATE TRIGGER SET_MYTABLE_INTVALUE FOR MYTABLE ACTIVE
BEFORE UPDATE OR INSERT POSITION 0
AS
DECLARE I INTEGER;
DECLARE S VARCHAR(13);
DECLARE C VARCHAR(1);
DECLARE R VARCHAR(13);
BEGIN
IF (NEW.INTVALUE is not null) THEN EXIT;
S = TRIM( NEW.VALUE );
R = NULL;
I = 1;
WHILE (I <= CHAR_LENGTH(S)) DO
BEGIN
C = SUBSTRING( S FROM I FOR 1 );
IF ((C >= '0') AND (C <= '9')) THEN LEAVE;
I = I + 1;
END
WHILE (I <= CHAR_LENGTH(S)) DO
BEGIN
C = SUBSTRING( S FROM I FOR 1 );
IF (C < '0') THEN LEAVE;
IF (C > '9') THEN LEAVE;
IF (C IS NULL) THEN LEAVE;
IF (R IS NULL) THEN R=C; ELSE R = R || C;
I = I + 1;
END
NEW.INTVALUE = CAST(R AS INTEGER);
END^
SET TERM ; ^
Converting such a table, you have to add a special indexed integer column for keeping the extracted integer data.
Note, this query while using "very convenient conversion" is actually rather bad: you should use indexed columns to sort (order) large amounts of data, otherwise you are going into slow execution and waste a lot of memory/disk for temporary sorting tables.
So you have to add an extra integer indexed column and to use it in the query.
Next question is how to populate that column.
Better would be to do it once, when you move your entire database and application from BDE to Firebird. And from that point make your application when entering new data rows fill BOTH varchar and integer columns properly.
One time conversion can be done by your convertor application, then.
Or you can use selectable Stored Procedure that would repeat the table with such and added column. Or you can make Execute Block that would iterate through the table and update its rows calculating the said integer value.
How to SELECT a PROCEDURE in Firebird 2.5
If you would need to keep legacy applications, that only insert text column but not integer column, then I think you would have to use BEFORE UPDATE OR INSERT triggers in Firebird, that would parse the text column value letter by letter and extract integer from it. And then make sure your application never changes that integer column directly.
See a trigger example at Trigger on Update Firebird
PSQL language documentation: https://www.firebirdsql.org/file/documentation/reference_manuals/fblangref25-en/html/fblangref25-psql.html
Whether you would write procedure or trigger to populate the said added integer indexed column, you would have to make simple loop over characters, copying string from first digit until first non-digit.
https://www.firebirdsql.org/file/documentation/reference_manuals/fblangref25-en/html/fblangref25-functions-scalarfuncs.html#fblangref25-functions-string
https://www.firebirdsql.org/file/documentation/reference_manuals/fblangref25-en/html/fblangref25-psql-coding.html#fblangref25-psql-declare-variable
Something like that
CREATE TRIGGER my_trigger FOR my_table
BEFORE UPDATE OR INSERT
AS
DECLARE I integer;
DECLARE S VARCHAR(100);
DECLARE C VARCHAR(100);
DECLARE R VARCHAR(100);
BEGIN
S = TRIM( NEW.MY_TXT_COLUMN );
R = NULL;
I = 1;
WHILE (i <= CHAR_LENGTH(S)) DO
BEGIN
C = SUBSTRING( s FROM i FOR 1 );
IF (C < '0') THEN LEAVE;
IF (C > '9') THEN LEAVE;
IF (C IS NULL) THEN LEAVE;
IF (R IS NULL) THEN R=C; ELSE R = R || C;
I = I + 1;
END
NEW.MY_INT_COLUMN = CAST(R AS INTEGER);
END;
In this example your ORDER order2, order1 would become
SELECT ..... FROM my_table ORDER BY MY_INT_COLUMN, MY_TXT_COLUMN
Additionally, it seems your column actually contains a compound data: an integer index and an optional textual postfix. If so, then the data you have is not normalized and the table better be restructured.
CREATE TABLE my_table (
ORDER_Int INTEGER NOT NULL,
ORDER_PostFix VARCHAR(24) CHECK( ORDER_PostFix = TRIM(ORDER_PostFix) ),
......
ORDER_TXT COMPUTED BY (ORDER_INT || COALESCE( ' ' || ORDER_PostFix, '' )),
PRIMARY KEY (ORDER_Int, ORDER_PostFix )
);
When you would move your data from Paradox to Firebird - make your convertor application check and split those values like "1 bis" into two new columns.
And your query then would be like
SELECT ORDER_TXT, ... FROM my_table ORDER BY ORDER_Int, ORDER_PostFix
if you're using fb2.5 you can use the following:
execute block (txt varchar(100) = :txt )
returns (res integer)
as
declare i integer;
begin
i=1;
while (i<=char_length(:txt)) do begin
if (substring(:txt from i for 1) not similar to '[[:DIGIT:]]')
then txt =replace(:txt,substring(:txt from i for 1),'');
else i=i+1;
end
res = :txt;
suspend;
end
in fb3.0 you have more convenient way to do the same
select
cast(substring(:txt||'#' similar '%#"[[:DIGIT:]]+#"%' escape '#') as integer)
from rdb$database
--assuming that the field is varchar(15))
select cast(field as integer) from table;
Worked in firebird version 2.5.
I have a huge Firebird database with a table that counts 41 millions of rows. Recently I have added a new float column and would like to fill it with incremental data. Each next value should be a previous incremented by RAND(). The very first value is also RAND().
How to do this?
The query
SELECT ID FROM MY_TABLE WHERE MY_COLUMN IS NULL ROWS 1;
takes up to 15 seconds so I wouldn't count on this query executed in a loop.
The table has an indexed ID column which is a part of composite primary key.
something like
update MyTable set MyColumn = Gen_ID( TempGen,
round( rand() * 100000) ) / 100000.0
Create a temporary Generator - https://www.firebirdsql.org/manual/generatorguide.html
use the integer generator as your float value scaled by some coefficient, like 100 000 would stand for 1.0 and 10 000 for 0.1, etc
use the GEN_ID function to forward a generator for a specified number of integer units
drop the generator
alternatively use Stored Procedure or EXECUTE BLOCK
https://www.firebirdsql.org/refdocs/langrefupd20-execblock.html
http://firebirdsql.su/doku.php?id=execute_block
something like
execute block
as
declare f double precision = 0;
declare i int;
begin
for select ID FROM MY_TABLE WHERE MY_COLUMN IS NULL order by id into :I
do begin
f = f + rand();
update MY_TABLE SET MY_COLUMN = :f where ID = :i;
end;
end
Or you may try using cursors, but I did not try so I do not know for sure how it would work.
https://www.firebirdsql.org/refdocs/langrefupd25-psql-forselect.html
execute block
as
declare f double precision = 0;
begin
for select ID FROM MY_TABLE WHERE MY_COLUMN IS NULL order by id
as cursor C do begin
f = f + rand();
update MY_TABLE SET MY_COLUMN = :f where current of C;
end;
end
I'm trying to determine whether the content of a field is an integer value.
In firebird 2.5 there is "similar to", but this wasn't available in 2.1 yet.
Thank you for your answer.
For now I'll go with:
substring(fieldname from 1 for 1) > '0' and
substring(fieldname from 1 for 1) < '9'
This procedure using error handling, returns field value as is if contents is integer, otherwise return 0
SET TERM ^ ;
create or alter procedure INT_CHECK (
IN_STR varchar(100))
returns (
ORESULT integer)
as
BEGIN
/* because WHEN works for the entire block use a separate BEGIN..END*/
begin -- START OF BLOCK
oresult = cast(:in_str as integer);
when any do
begin
oresult = 0;
end
end -- END OF BLOCK
suspend;
END^
SET TERM ; ^
In DB2 I have one result-string, e.g. like
Color|Product|Category|Price|...
Now I like to generate four (or n) columns containing each string-token split by the pipe
Col1 Col2 Col3 Col4 Col...
Color Product Category Price ...
I am looking for a general solution with arbitrarily number of cols.
The use of this result is to use it in a UNION with another SELECT-Query.
Any ideas?
Where do you want to present this output? via the DB2CLP ou another program.
If you want to retrieve the values in another program, it depends how the program manage the output, and you just put tabulation when scanning the output.
For example, for Java
System.out.println(col1+"\t"+col2+"\t"+col3);
On the other side, if you want to retrieve the values in DB2CLP or a similar shell, an your string is retrieved from the database with these pipes, you should created a scalar function to process the field before returning it to the user.
You will need functions like
Length http://publib.boulder.ibm.com/infocenter/db2luw/v10r1/topic/com.ibm.db2.luw.sql.ref.doc/doc/r0000818.html
Concat http://publib.boulder.ibm.com/infocenter/db2luw/v10r1/topic/com.ibm.db2.luw.sql.ref.doc/doc/r0000781.html or ||
Posstr http://publib.boulder.ibm.com/infocenter/db2luw/v10r1/topic/com.ibm.db2.luw.sql.ref.doc/doc/r0000835.html
The best way is to create a recursive function, that iterates for each token. The basic case is when the string is empty or the string does not finish by a pipe. The recursive case is when the string has pipe.
The process is to read the characters (into a variable) until the first pipe. If a pipe is detected, the quantity of spaces to simulate a tab are added, and then the same function is called with the trailing string.
You should pass the string to read, and the already processed string.
The Function could be like this
function.sql
CREATE OR REPLACE MODULE TEST#
ALTER MODULE TEST PUBLISH
FUNCTION PROCESS (
STRING varchar(255),
NEW_STRING varchar(255)
) RETURNS varchar(255)
#
ALTER MODULE TEST ADD
FUNCTION PROCESS (
STRING varchar(255),
NEW_STRING varchar(255)
) RETURNS varchar(255)
PROC: begin
DECLARE IND INT;
DECLARE LEN INT;
DECLARE MODU INT;
DECLARE CHARS INT;
DECLARE TAB INT DEFAULT 8;
DECLARE PRE VARCHAR(255);
DECLARE POS VARCHAR(255);
SET IND = POSSTR(STRING, '|');
IF (IND <> 0) THEN
SET PRE = SUBSTR(STRING, 1, IND - 1);
SET POS = SUBSTR(STRING, IND + 1);
SET NEW_STRING = TEST.PROCESS (POS, NEW_STRING);
SET PRE = TRIM(PRE);
SET LEN = LENGTH(PRE);
SET MODU = MOD(LEN, TAB);
IF (MODU <> 0) THEN
SET CHARS = TAB - MODU;
WHILE (CHARS <> TAB) DO
SET PRE = CONCAT (PRE, ' ');
SET CHARS = CHARS + 1;
END WHILE;
END IF;
ELSE
SET PRE = STRING;
END IF;
RETURN CONCAT (PRE, COALESCE(NEW_STRING,''));
end PROC#
Compilation and execution
db2 -td# -f function.sql ; db2 "values test.process('Color|Product|Category|Price|...','')"
1
-----------------------------------------
Color Product CategoryPrice ...
1 record(s) selected.
Sorry, lots of code coming up..
I saw another question like this that used output parameters. I'm using the RETURN statement to return the value I want to use.
I have one stored procedure InsertMessage that looks like this:
ALTER PROCEDURE dbo.InsertNewMessage
(
#messageText text,
#dateTime DATETIME,
#byEmail bit,
#bySMS bit
)
AS
DECLARE #NewId int
BEGIN
BEGIN TRANSACTION
INSERT INTO MessageSet VALUES (#byEmail, #bySMS, #dateTime, #messageText)
SET #NewId = SCOPE_IDENTITY()
COMMIT
END
RETURN #NewId
which another stored procedure uses:
ALTER PROCEDURE dbo.InsertMessageFromUserToGroup
(
#userEmail nvarchar(256),
#groupId int,
#messageText text,
#bySMS bit,
#byEmail bit
)
AS
--Inserts a new message to a group
DECLARE #messageId int
DECLARE #dateTime DATETIME = GETDATE()
--First check if user is a part of the group
IF NOT EXISTS (SELECT userEmail FROM UserToGroupSet WHERE userEmail = #userEmail AND groupId = #groupId)
RETURN 'User not part of group'
ELSE --User is a part of the group, add message
BEGIN
BEGIN TRANSACTION
SET #messageId = [dbo].[InsertNewMessage](#messageText, #dateTime, #bySMS, #byEmail)
INSERT INTO MessageToUser VALUES(#userEmail, #messageId)
INSERT INTO MessageToGroup VALUES(#messageId, #groupId)
COMMIT
END
The row that causes the trouble and of which I'm unsure how to handle is this one:
SET #messageId = [dbo].[InsertNewMessage](#messageText, #dateTime, #bySMS, #byEmail)
The syntax seems ok because I can save it. When I run it I get the error message:
Running [dbo].[InsertMessageFromUserToGroup] ( #userEmail = test#test.com, #groupId = 5, #messageText = sdfsdf, #bySMS = false, #byEmail = true ).
Cannot find either column "dbo" or the user-defined function or aggregate "dbo.InsertNewMessage", or the name is ambiguous.
Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 0, current count = 1.
No rows affected.
(0 row(s) returned)
#RETURN_VALUE =
Finished running [dbo].[InsertMessageFromUserToGroup].
It seems as if the other stored procedure can't be found. I've tried different ways of calling the procedure but everything else fails as well. Any suggestions?
Try changing
SET #messageId = [dbo].[InsertNewMessage](#messageText, #dateTime, #bySMS,
#byEmail)
to
EXEC #messageId = [dbo].[InsertNewMessage] #messageText, #dateTime, #bySMS,
#byEmail
Notice that SET has been changed to EXEC, and the parentheses have been removed from the parameters.
See the example in the MSDN documenation at the end of the article for more information.