Does Oracle have an equivalent of SQL Server's table variables? - oracle

In SQL Server, you can declare a table variable (DECLARE #table TABLE), which is produced while the script is run and then removed from memory.
Does Oracle have a similar function? Or am I stuck with CREATE/DROP statements that segment my hard drive?

Yes.
Declare TABLE TYPE variables in a
PL/SQL declare block. Table variables
are also known as index-by table or
array. The table variable contains one
column which must be a scalar or
record datatype plus a primary key of
type BINARY_INTEGER. Syntax:
DECLARE
TYPE type_name IS TABLE OF
(column_type |
variable%TYPE |
table.column%TYPE
[NOT NULL]
INDEX BY BINARY INTEGER;
-- Then to declare a TABLE variable of this type:
variable_name type_name;
-- Assigning values to a TABLE variable:
variable_name(n).field_name :=
'some text'; -- Where 'n' is the
index value
Ref: http://www.iselfschooling.com/syntax/OraclePLSQLSyntax.htm
You might want to also take a look at Global Temporary Tables

The below solution is the closest from SQL Server I can do today.
Objects:
CREATE OR REPLACE TYPE T_NUMBERS IS TABLE OF NUMBER;
CREATE OR REPLACE FUNCTION ACCUMULATE (vNumbers T_NUMBERS)
RETURN T_NUMBERS
AS
vRet T_NUMBERS;
BEGIN
SELECT SUM(COLUMN_VALUE)
BULK COLLECT INTO vRet
FROM TABLE(CAST(vNumbers AS T_NUMBERS));
RETURN vRet;
END;
Queries:
--Query 1: Fixed number list.
SELECT *
FROM TABLE(ACCUMULATE(T_NUMBERS(1, 2, 3, 4, 5)));
--Query 2: Number list from query.
WITH cteNumbers AS
(
SELECT 1 AS COLUMN_VALUE FROM DUAL UNION
SELECT 2 AS COLUMN_VALUE FROM DUAL UNION
SELECT 3 AS COLUMN_VALUE FROM DUAL UNION
SELECT 4 AS COLUMN_VALUE FROM DUAL UNION
SELECT 5 AS COLUMN_VALUE FROM DUAL
)
SELECT *
FROM TABLE(
ACCUMULATE(
(SELECT CAST(COLLECT(COLUMN_VALUE) AS T_NUMBERS)
FROM cteNumbers)
)
);

Yes it does have a type that can hold the result set of a query (if I can guess what TABLE does). From ask Tom: your procedure may look like this:
procedure p( p_state in varchar2, p_cursor in out ref_cursor_type )
is
begin
open p_cursor for select * from table where state = P_STATE;
end;
where p_cursor is like a table type. As has been already answered there are plenty of options for storing result sets in Oracle. Generally Oracle PL/SQL is far more powerful than sqlserver scripts.

the table in variable in oracle not the same as table variables in MS SQLServer.
in oracle it's like regular array in java or c#. but in MS SQLserver it is the same as any table, you can call it logical table.
but if you want something in oracle that does exactly the same as table variable of SQLserver you can use cursor.
regards

Related

Execute SQL query in another Query as result

I have a table which contains SQL query as one of the columns. Based on record id, I want to execute SQL query which is in the record.
Master_Table
------------------------------
|Rec_ID | Query |
------------------------------
|1 | SELECT * from EMP |
------------------------------
|2 | SELECT * FROM DEPT |
------------------------------
SELECT Query FROM Master_Table WHERE Rec_ID=1
I am expecting that If I select Rec_Id =1, I have to EMP records.
If I select Rec_Id=2, I need to get Dept records.
Is it possible to do it in SQL query?
There are a few ways to run dynamic SQL in SQL. In 18c we can use a polymorphic table function. If it's OK to get the results as XML we can use DBMS_XMLGEN.getXML. If we're able to create custom PL/SQL objects we can use Oracle data cartridge to build a Method4 solution.
For example, after installing Method4, we can run SQL like this:
select * from table(method4.dynamic_query(
'
select query
from master_table
where rec_id = 1
'
));
The above code will work with the below sample schema:
create table master_table as
select 1 rec_id, 'SELECT * from EMP' query from dual union all
select 2 rec_id, 'SELECT * FROM DEPT' query from dual;
create table emp(emp_name varchar2(100));
create table dept(dept_name varchar2(100));
The preceding information literally answers your question. But I agree with Mark D Powell that this design is often a bad idea and we should only create code like this after we've evaluated alternative designs.
Vsau, I agree with JNevill in that you need PL/SQL to execute the 'SQL' from your table column. Also I will add that these kind of designs are usually a bad idea. You want your application SQL to be static sql using bind variables otherwise your system will suffer from too high a percentage of hard parses and resulting contention on the dictionary objects and you may run into shared pool fragmentation issues.
You can use sys_refcursor
SQL> create or replace function get_emp_tab return sys_refcursor is
v_rc sys_refcursor;
v_sql varchar2(4000);
begin
select query into v_sql from Master_Table where Rec_ID = 1
open v_rc for v_sql;
return v_rc;
end;
/
SQL> declare
v_rc sys_refcursor;
begin
:v_rc := get_emp_tab;
end;
/
SQL> print v_rc;

How to use Collection as a table for UPDATE with an IN clause

I need this piece of code in a stored procedure so I can pass an array of ids and update the related records. I am wondering whether I have to use the loop rather than use an IN clause in the sp.
SET SERVEROUTPUT ON
DECLARE
P_IDS PKGINFO.t_ids; --type: table of NUMBER index by pls_integer;
P_RESULT NUMBER;
BEGIN
p_IDS(1) := 12345;
--this works fine:
for i in ( select * from table(p_ids))
loop
UPDATE TABLE1
SET FD1 = 'test'
WHERE P_ID = i.column_value;
end loop;
--this works fine too:
SELECT COUNT(*) INTO p_RESULT FROM TABLE1
WHERE P_ID IN (SELECT * FROM TABLE (p_ids));
--but this does not work, why????? how to make it work?
UPDATE TABLE1
SET FD1 = 'test'
WHERE P_ID IN (SELECT * FROM TABLE (p_ids));
END;
--==================PKGINFO.t_ids==================
CREATE OR REPLACE package dbname.PKGINFO as
-- package created to perform Associative array calls
type t_ids is table of NUMBER index by pls_integer;
end PKGINFO;
/
I expected the UPDATE can use IN clause, but it gives me an INVALID TYPE error.
Until fairly recently Oracle didn't allow PL/SQL types in SQL statements, including in a table collection expression. You seem to be using a version where support has been added for select but not (yet) for update. If you had one available then you could use a schema-level type. Also have a look at member of.
With the type you have now you could use FORALL, which would be more efficient than a loop with individual updates::
FORALL i IN p_ids.first..p_ids.last
UPDATE TABLE1
SET FD1 = 'test'
WHERE P_ID = p_ids(i);

PL/SQL Table type compilation error

I got table types:
CREATE OR REPLACE TYPE "TABLE_OF_VARCHAR2" AS TABLE OF VARCHAR2(4000);
CREATE OR REPLACE TYPE "TABLE_OF_NUMBER" AS TABLE OF NUMBER;
And in my package body i got a cursor:
cursor c_src_m(trans_list table_of_number, v_codes table_of_varchar2, ...) is
select ....something
where ....
--AND ta.id in (select COLUMN_VALUE from TABLE(trans_list))
--AND (tb.UNDERLYING_VALUE in (select COLUMN_VALUE from TABLE(v_codes)) OR (v_codes is null or (select count(1) from TABLE(v_codes)) = 0))
For each of last 2 lines if i uncomment them i get an error:
ORA-22905: cannot access rows from a non-nested table item
*Cause: attempt to access rows of an item whose type is not known at
parse time or that is not of a nested table type
*Action: use CAST to cast the item to a nested table type
I looked over 3 hours for a solution and still couldn't find a working one. Do i really need to cast it as error message says? Does anyone know what is the problem here?
There might be something wrong in your code which you are either not showing in your question or have missed it. Please see the below DEMO which is on the similar lines of your question. I used it the same way you did and its working fine on Oracle11g. Also, you can use MEMBER OF as well inplace of your Select statement query in where clause. See below and read my inline comments to understand more.
Table and Types Creation:
CREATE OR REPLACE TYPE TABLE_OF_VARCHAR2 AS TABLE OF VARCHAR2(4000);
/
CREATE OR REPLACE TYPE TABLE_OF_NUMBER AS TABLE OF NUMBER;
/
Create table Num_varchar(col1 number, col2 varchar2(1000));
/
INSERT ALL
INTO Num_varchar VALUES (1,'A')
INTO Num_varchar VALUES (2,'B')
INTO Num_varchar VALUES (3,'C')
INTO Num_varchar VALUES (4,'D')
INTO Num_varchar VALUES (5,'E')
SELECT * FROM dual;
/
Block:
DECLARE
CURSOR c_src_m(trans_list table_of_number, v_codes table_of_varchar2)
IS
SELECT col1,
col2
FROM Num_varchar
--Inplace of select query you can use Member of as well. However if you want to use select query, this will also work.
--WHERE col1 IN (SELECT COLUMN_VALUE FROM TABLE(trans_list) )
--AND col2 IN (SELECT COLUMN_VALUE FROM TABLE(v_codes));
WHERE col1 MEMBER OF trans_list
and col2 MEMBER OF v_codes;
--Populated the collection so that i can use it in my query above
var_varchr2 TABLE_OF_VARCHAR2:=TABLE_OF_VARCHAR2('A','B','C','D');
var_number TABLE_OF_NUMBER :=TABLE_OF_NUMBER(1,2,3,4);
var1 NUMBER;
var2 VARCHAR2(100);
BEGIN
OPEN c_src_m ( var_number , var_varchr2);
LOOP
FETCH c_src_m INTO var1,var2;
EXIT WHEN c_src_m%NOTFOUND;
--dispalying the result of the cursor
dbms_output.put_line(var1 || var2);
END LOOP;
Close c_src_m;
END;
Output:
1A
2B
3C
4D
Note: If you had create the types in Package specification, it will not work. Until Oracle11g , any types created under PLSQL scope cannot be referred in SQL statement inside the PLSQL block. If you have done so, just create types out of package scope and it should work then.

PL/SQL Creating a procedure that contains result set joins

I want to create a procedure in PL/SQL that has 5 steps. Step 1 and 2 execute first and return an ID. In step 3, we have a SELECT statement that has a condition with that returned ID. I want then to take all of the results of that SELECT statement and use them in a JOIN in another SELECT statement and use THOSE results in a 3rd SELECT statement again using JOIN. From what I've seen, I can't use CURSOR in JOIN statements. Some of my co-workers have suggested that I save the results in a CURSOR and then use a loop to iterate through each row and use that data for the next SELECT. However since I'm going to do 2 selects this will create a huge fork of inside loops and that's exactly what I'm trying to avoid.
Another suggestion was to use Temprary Tables to store the data. However this procedure could be executed at the same time by many users and the table's data would conflict with each other. Right now I'm looking at LOCAL Temporary tables that supposedly filter the data according the the session but I'm not really sure I want to create dummy tables for my procedures since I want to avoid leaving trash in the schema (this procedure is for a custom part of the application). Is there a standard way of doing this? Any ideas?
Sample:
DECLARE
USERID INT := 1000000;
TEXT1 VARCHAR(100);
TEXT_INDEX INT;
CURSOR NODES IS SELECT * FROM NODE_TABLE WHERE DESCRIPTION LIKE TEXT || '%';
CURSOR USERS IS SELECT * FROM USERGROUPS JOIN NODES ON NODES.ID = USERGROUPS.ID;
BEGIN
SELECT TEXT INTO TEXT1 FROM TABLE_1 WHERE ID = USERID;
TEXT_INDEX = INSTR(TEXT, '-');
TEXT = SUBSTR(TEXT, 0, TEXT_INDEX);
OPEN NODES;
OPEN USERS;
END;
NOTE: This does NOT work. Oracle doesn't support joins between cursors.
NOTE2: This CAN be done in a single query but for the sake of argument (and in my real use case) I want to break those steps down in a procedure. The sample code is a depiction of what I'm trying to achieve IF joins between cursors worked. But they don't and I'm looking for an alternative.
I ended up using a function (although a procedure could be used as well) along with tables. Things I've learned and one should pay attention to:
PL/SQL functions can only return types that have been declared in the schema in advance and are clear. You can't create a function that returns something like MY_TABLE%ROWTYPE, even though it seems the type information is available it is not acceptable. You have to instead create a custom type of MY_TABLE%ROWTYPE is you want to return it.
Oracle treats tables of declared types differently from tables of %ROWTYPE. This confused the hell out of me at first but from what I've gathered this is how it works.
DECLARE TYPE MY_CUSTOM_TABLE IS TABLE OF MY_TABLE%ROWTYPE;
Declares a collection of types of MY_TABLE row. In order to add to this we must use BULK COLLECT INTO from an SQL statement that queries MY_TABLE. The resulting collection CANNOT be used in JOIN statements is not queryable and CANNOT be returned by a function.
DECLARE
CREATE TYPE MY_CUSTOM_TYPE AS OBJECT (COL_A NUMBER, COL_B NUMBER);
CREATE TYPE MY_CUSTOM_TABLE AS TABLE OF MY_CUSTOM_TYPE;
my_custom_tab MY_CUSTOM_TABLE;
This create my_custom_tab which is a table (not a collection) and if populated can be queried at using TABLE(my_custmo_tab) in the FROM statement. As a table which is declared in advance in the schema this CAN be returned from a function. However it CANNOT be populated using BULK COLLECT INTO since it is not a collection. We must instead use the normal SELECT INTO statement. However, if we want to populate it with data from an existing table that has 2 number columns we cannot simply do SELECT * INTO my_custom_tab FROM DOUBLE_NUMBER_TABLE since my_custom_tab hasn't been initialized and doesn't contain enough rows to receive the data. And if we don't know how many rows a query returns we can't initialize it. The trick into populating the table is to use the CAST command and cast our select result set as a MY_CUSTOM_TABLE and THEN add it.
SELECT CAST(MULTISET(SELECT COL_A, COL_B FROM DOUBLE_NUMBER_TABLE) AS MY_CUSTOM_TABLE) INTO my_custom_tab FROM DUAL
Now we can easily use my_custom_tab in queries etc through the use of the TABLE() function.
SELECT * FROM TABLE(my_custom_tab)
is valid.
You can do such decomposition in many ways, but all of them have a significant performance penalty in comaration with single SQL statement.
Maintainability improvement are also questionable and depends on specific situation.
To review all possibilities please look through documentation.
Below is some possible variants based on simple logic:
calculate Oracle user name prefix based on given Id;
get all users whose name starts with this prefix;
find all tables owned by users from step 2;
count a total number of found tables.
1. pipelined
Prepare types to be used by functions:
create or replace type TUserRow as object (
username varchar2(30),
user_id number,
created date
)
/
create or replace type TTableRow as object (
owner varchar2(30),
table_name varchar2(30),
status varchar2(8),
logging varchar2(3)
-- some other useful fields here
)
/
create or replace type TUserList as table of TUserRow
/
create or replace type TTableList as table of TTableRow
/
Simple function to find prefix by user id:
create or replace function GetUserPrefix(piUserId in number) return varchar2
is
vUserPrefix varchar2(30);
begin
select substr(username,1,3) into vUserPrefix
from all_users
where user_id = piUserId;
return vUserPrefix;
end;
/
Function searching for users:
create or replace function GetUsersPipe(
piNameStart in varchar2
)
return TUserList pipelined
as
vUserList TUserList;
begin
for cUsers in (
select *
from
all_users
where
username like piNameStart||'%'
)
loop
pipe row( TUserRow(cUsers.username, cUsers.user_id, cUsers.created) ) ;
end loop;
return;
end;
Function searching for tables:
create or replace function GetUserTablesPipe(
piUserNameStart in varchar2
)
return TTableList pipelined
as
vTableList TTableList;
begin
for cTables in (
select *
from
all_tables tab_list,
table(GetUsersPipe(piUserNameStart)) user_list
where
tab_list.owner = user_list.username
)
loop
pipe row ( TTableRow(cTables.owner, cTables.table_name, cTables.status, cTables.logging) );
end loop;
return;
end;
Usage in code:
declare
vUserId number := 5;
vTableCount number;
begin
select count(1) into vTableCount
from table(GetUserTablesPipe(GetUserPrefix(vUserId)));
dbms_output.put_line('Users with name started with "'||GetUserPrefix(vUserId)||'" owns '||vTableCount||' tables');
end;
2. Simple table functions
This solution use same types as a variant with pipelined functions above.
Function searching for users:
create or replace function GetUsers(piNameStart in varchar2) return TUserList
as
vUserList TUserList;
begin
select TUserRow(username, user_id, created)
bulk collect into vUserList
from
all_users
where
username like piNameStart||'%'
;
return vUserList;
end;
/
Function searching for tables:
create or replace function GetUserTables(piUserNameStart in varchar2) return TTableList
as
vTableList TTableList;
begin
select TTableRow(owner, table_name, status, logging)
bulk collect into vTableList
from
all_tables tab_list,
table(GetUsers(piUserNameStart)) user_list
where
tab_list.owner = user_list.username
;
return vTableList;
end;
/
Usage in code:
declare
vUserId number := 5;
vTableCount number;
begin
select count(1) into vTableCount
from table(GetUserTables(GetUserPrefix(vUserId)));
dbms_output.put_line('Users with name started with "'||GetUserPrefix(vUserId)||'" owns '||vTableCount||' tables');
end;
3. cursor - xml - cursor
It's is a specific case, which may be implemented without user-defined types but have a big performance penalty, involves unneeded type conversion and have a low maintainability.
Function searching for users:
create or replace function GetUsersRef(
piNameStart in varchar2
)
return sys_refcursor
as
cUserList sys_refcursor;
begin
open cUserList for
select * from all_users
where username like piNameStart||'%'
;
return cUserList;
end;
Function searching for tables:
create or replace function GetUserTablesRef(
piUserNameStart in varchar2
)
return sys_refcursor
as
cTableList sys_refcursor;
begin
open cTableList for
select
tab_list.*
from
(
XMLTable('/ROWSET/ROW'
passing xmltype(GetUsersRef(piUserNameStart))
columns
username varchar2(30) path '/ROW/USERNAME'
)
) user_list,
all_tables tab_list
where
tab_list.owner = user_list.username
;
return cTableList;
end;
Usage in code:
declare
vUserId number := 5;
vTableCount number;
begin
select count(1) into vTableCount
from
XMLTable('/ROWSET/ROW'
passing xmltype(GetUserTablesRef(GetUserPrefix(vUserId)))
columns
table_name varchar2(30) path '/ROW/TABLE_NAME'
)
;
dbms_output.put_line('Users with name started with "'||GetUserPrefix(vUserId)||'" owns '||vTableCount||' tables');
end;
Of course, all variants may be mixed, but SQL looks better at least for simple cases:
declare
vUserId number := 5;
vUserPrefix varchar2(100);
vTableCount number;
begin
-- Construct prefix from Id
select max(substr(user_list.username,1,3))
into vUserPrefix
from
all_users user_list
where
user_list.user_id = vUserId
;
-- Count number of tables owned by users with name started with vUserPrefix string
select
count(1) into vTableCount
from
all_users user_list,
all_tables table_list
where
user_list.username like vUserPrefix||'%'
and
table_list.owner = user_list.username
;
dbms_output.put_line('Users with name started with "'||vUserPrefix||'" owns '||vTableCount||' tables');
end;
P.S. All code only for demonstration purposes: no optimizations and so on.

How do you select into a nested type in oracle pl/sql?

I want to be able to delete by rowid then immediately insert the data being deleted in an audit table.
There are far too many records to
INSERT INTO ... SELECT CRITERIA then DELETE ... CRITERIA.
I already know how to do everything just using rowid and INSERT INTO ... SELECT.
Inside package body:
TYPE some_type IS RECORD (
row_id ROWID,
full_row table_name%ROWTYPE
);
TYPE some_type_list IS TABLE OF some_type
INDEX BY BINARY_INTEGER;
PROCEDURE do_stuff
IS
lc_data SYS_REFCURSOR;
lt_recs some_type_list;
BEGIN
OPEN lc_date FOR
SELECT rowid, a.*
FROM table_name;
LOOP
FETCH lc_data
BULK COLLECT INTO lt_recs
LIMIT 50000;
EXIT WHEN lt_recs.COUNT = 0;
--
FORALL i IN lt_recs.FIRST..lt_recs.LAST
DELETE table_name
WHERE ROWID = lt_recs(i).row_id;
--
FORALL i IN lt_recs.FIRST..lt_recs.LAST
INSERT INTO table_name_audit VALUES lt_recs(i).full_row;
END LOOP;
END;
If I try that i get the following error:
Line: 117 Column: 25 Type: error Text: PLS-00597: expression 'LT_RECS' in the INTO list is of wrong type
Oracle versions prior to 11gR2 restrict us to use BULK COLLECT into a collection (nested table or varray) of records. Read more here on Oracle Docs.
If you want to see how it is done in 11gR2, scroll down to EDIT 2 section of this answer.
An alternative tho this can be the use of separate collections for every column- an approach that is most widely used. In this you can have:
/*
TYPE some_type IS RECORD (
row_id ROWID,
full_row table_name%ROWTYPE
);
TYPE some_type_list IS TABLE OF some_type
INDEX BY BINARY_INTEGER;
-- */
CREATE TYPE t_row_id IS TABLE OF ROWID;
CREATE TYPE t_col1 IS TABLE OF table_name.col1%TYPE;
CREATE TYPE t_col2 IS TABLE OF table_name.col2%TYPE;
CREATE TYPE t_col3 IS TABLE OF table_name.col3%TYPE;
...
...
CREATE TYPE t_colN IS TABLE OF table_name.colN%TYPE;
PROCEDURE do_stuff
IS
lc_data SYS_REFCURSOR;
-- lt_recs some_type_list;
row_id t_row_id;
col1 t_col1;
col2 t_col2;
col3 t_col3;
...
...
colN t_colN;
BEGIN
OPEN lc_date FOR
SELECT rowid, a.*
FROM table_name;
LOOP
FETCH lc_data
BULK COLLECT INTO row_id, col1, col2, col3, ..., colN
LIMIT 50000;
EXIT WHEN lt_recs.COUNT = 0;
--
FORALL i IN row_id.FIRST..row_id.LAST
DELETE table_name
WHERE ROWID = row_id(i);
--
FORALL i IN col1.FIRST..col1.LAST
INSERT INTO table_name_audit VALUES (col1(i), col2(i), col3(i), ..., colN(i));
END LOOP;
END;
I have not removed many of the rows in your program in order to let you understand the changes.
EDIT : Refer to the "Restrictions on BULK COLLECT" section of the Oracle Docs link I have given above and also here.
EDIT #2 :
You have to use CREATE TYPE ... IS OBJECT instead of RECORD. Also, You need to modify the SELECT statement the way I have done when I tried it. Please see the Oracle Docs here and a StackOverflow question here for further reference.
The code I tried on my machine (runs Oracle 11g R2) is as follows:
-- SELECT * FROM user_objects WHERE object_type = 'TYPE';
CLEAR SCREEN;
SET SERVEROUTPUT ON;
CREATE OR REPLACE TYPE temp_t_test AS OBJECT ( -- << OBJECT, not RECORD.
test_id INTEGER
, test_val VARCHAR2(50)
);
/
CREATE OR REPLACE TYPE temp_tbl_test AS TABLE OF TEMP_T_TEST;
/
DECLARE
v_test TEMP_TBL_TEST;
BEGIN
SELECT temp_t_test(t_id, t_val) -- << Notice the syntax
-- I'm selecting the columns as the defined OBJECT type.
BULK COLLECT INTO v_test
FROM (SELECT 1 AS t_id, 'ABCD' AS t_val FROM dual
UNION ALL
SELECT 2, 'WXYZ' FROM dual
UNION ALL
SELECT 3, 'PQRS' FROM dual);
dbms_output.put_line('Bulk Collect Successful!');
END;
/
** OUTPUT **:
TYPE temp_t_test compiled
TYPE temp_tbl_test compiled
anonymous block completed
Bulk Collect Successful!
I don't think that I'd take this approach at all, to be honest.
A faster method would be along the lines of performing a multitable insert:
insert the table columns into the audit table, possibly using direct path (APPEND hint) for efficiency
insert the rowid's into an on-commit-delete-rows global temporary table.
Then perform a delete against the original table using DELETE .. WHERE ROWID IN (SELECT ORIGINAL_ROWID FROM MY_GLOBAL_TEMP_TAB)
... and then commit.
Faster, and less code I think.
What you're trying to works in 11gR2 - what version are you on?.
The only wrong-looking thing in your post is this:
OPEN lc_date FOR
SELECT rowid, a.*
FROM table_name;
It ought to be this ...
OPEN lc_data FOR
SELECT a.rowid, a.*
FROM table_name a;
... but these may simply be typos you introduced when sanitizing your code to post here.

Resources