PL SQL Oracle Store Dynamic SQL result (Dynamic in a loop) - sql

I'm new to PL/SQL on Oracle (have to do some work on this but it's clearly not my domain).
So I have to do an 'audit' of our DB which consists in giving for each column of each table of our schema its max length (how we declared it (VARCHAR2(15) for example) and the max value currently of this column (ID 15000 for example) (might evolve and want more data in my results but at the moment i just need this).
I will explain my problem with an example
to be clear :
I have a table EMPLOYEE with 3 columns :
NAME in VARCHAR2(50), the longest i have (in length) is 48
CITY in VARCHAR2(100), the longest i have (in length) is 95
AGE in NUMBER, the longest i have (in length) is 2
So for this table of my schema I would like to have as output of my script (to work on it in excel), it must be taken into account that here the employee table is only one among many others which is returned by the first request:
TABLE_NAME
COLUMN_NAME
MAX_LENGTH_DATA
MAX_LENGTH_COLUMN
EMPLOYEE
NAME
48
50
EMPLOYEE
CITY
95
100
EMPLOYEE
AGE
2
() (don't need)
So we will have 1 line per column and table, if my table have 5 columns i will have 5 lines.
I've tried many solutions with LOOP, CURSOR and now TYPE OBJECT but i'm doing something wrong i know but can't figure out what it is.
CREATE OR REPLACE TYPE t_output_allColumns FORCE AS OBJECT
(maxLengthColumn NUMBER,
COLUMN_NAME VARCHAR2(80),
TABLE_NAME VARCHAR2(80));
/
CREATE OR REPLACE TYPE output_allColumns FORCE AS TABLE OF t_output_allColumns;
DECLARE
maxlengthTab output_allColumns;
v_requete_maxLength varchar2(4000);
TYPE MyCurTyp IS REF CURSOR;
c1 MyCurTyp;
v_column_name VARCHAR2(400);
v_table_name VARCHAR2(400);
begin
maxlengthTab:= output_allColumns();
OPEN c1 FOR 'select TABLE_NAME, COLUMN_NAME from ALL_TAB_COLUMNS';
FETCH c1 INTO v_column_name , v_table_name;
v_requete_maxLength := 'SELECT MAX( LENGTH(' || v_column_name ||'), ' || v_column_name ||',' || v_table_name ||' FROM ' ||v_table_name;
EXECUTE IMMEDIATE v_requete_maxLength BULK COLLECT INTO maxlengthTab;
dbms_output.put_line(output_allColumns);
CLOSE c1;
END;
Here is a script i tried, first thing i do is to select all columns from my schema (no problem with this, i already printed them to test and it's good)
But the main probleme is when i try to use dynamic SQL on my result
I try SELECT MAX( LENGTH(' || Colum_name i get from my 1st request||'), ' || Colum_name i get from my 1st request||',' || Table_name i get from my 1st request||' FROM ' ||Table_name i get from my 1st request; and this is where I'm stuck, I can't store each result and display it.

You can use a pipelined function.
Given the types:
CREATE TYPE t_output_allColumns AS OBJECT(
OWNER VARCHAR2(80),
TABLE_NAME VARCHAR2(80),
COLUMN_NAME VARCHAR2(80),
maxLengthData NUMBER,
maxLengthColumn NUMBER
);
CREATE TYPE output_allColumns AS TABLE OF t_output_allColumns;
Then the function:
CREATE FUNCTION column_details(
i_owner IN VARCHAR2
)
RETURN output_allcolumns PIPELINED
IS
v_data_length NUMBER;
BEGIN
FOR r IN (
SELECT owner,
table_name,
column_name,
data_length
FROM all_tab_columns
WHERE owner = i_owner
)
LOOP
EXECUTE IMMEDIATE
'SELECT MAX(LENGTH("'||r.column_name||'")) FROM "'||r.owner||'"."'||r.table_name||'"'
INTO v_data_length;
PIPE ROW (
t_output_allcolumns(
r.owner,
r.table_name,
r.column_name,
v_data_length,
r.data_length
)
);
END LOOP;
END;
/
Then you can use:
SELECT * FROM TABLE(column_details('SCHEMA_NAME'));
Which outputs:
OWNER
TABLE_NAME
COLUMN_NAME
MAXLENGTHDATA
MAXLENGTHCOLUMN
SCHEMA_NAME
EMPLOYEES
NAME
48
50
SCHEMA_NAME
EMPLOYEES
AGE
2
22
SCHEMA_NAME
EMPLOYEES
CITY
95
100
db<>fiddle here

Related

Get column names of the result of a query Oracle SQL

I need a query that will get the column names of the result of another query. The other query can be anything - I can't make any assumptions about it but it will typically be some SELECT statement.
For example, if I have this table Members
Id | Name | Age
---|------|----
1 | John | 25
2 | Amir | 13
And this SELECT statement
SELECT Name, Age FROM Members
Then the result of the query I'm trying to write would be
Name
Age
In SQL Server, there is a function - sys.dm_exec_describe_first_result_set - that does this but I can't find an equivalent in Oracle.
I tried to use this answer but I can't use CREATE TYPE statements because of permissions issues and I probably can't use CREATE FUNCTION statements for the same reason.
Suppose you have a query like this:
select *
from (select deptno, job, sal from scott.emp)
pivot (avg(sal) as avg_sal for job in
('ANALYST' as analyst, 'CLERK' as clerk, 'SALESMAN' as salesman)
)
order by deptno
;
This produces the result:
DEPTNO ANALYST_AVG_SAL CLERK_AVG_SAL SALESMAN_AVG_SAL
---------- --------------- ------------- ----------------
10 1300
20 3000 950
30 950 1400
Notice the column names (like ANALYST_AVG_SAL) - they don't appear exactly in that form anywhere in the query! They are made up from two separate pieces, put together with an underscore.
Now, if you were allowed to create views (note that this does not create any data in your database - it just saves the text of a query), you could do this:
Create the view (just add the first line of code to what we already had):
create view q201028_vw as
select *
from (select deptno, job, sal from scott.emp)
pivot (avg(sal) as avg_sal for job in
('ANALYST' as analyst, 'CLERK' as clerk, 'SALESMAN' as salesman)
)
order by deptno
;
(Here I assumed you have some way to identify the query, an id like Q201028, and used that in the view name. That is not important, unless you need to do this often and for a large number of queries at the same time.)
Then you can find the column names (and also their order, and - if needed - their data type, etc.) by querying *_TAB_COLUMNS. For example:
select column_id, column_name
from user_tab_columns
where table_name = 'Q201028_VW'
order by column_id
;
COLUMN_ID COLUMN_NAME
---------- --------------------
1 DEPTNO
2 ANALYST_AVG_SAL
3 CLERK_AVG_SAL
4 SALESMAN_AVG_SAL
Now you can drop the view if you don't need it for anything else.
As an aside: The "usual" way to "save" queries in the database, in Oracle, is to create views. If they already exist as such in your DB, then all you need is the last step I showed you. Otherwise, were is the "other query" (for which you need to find the columns) coming from in the first place?
I would use the dbms_sql package and the following code example should show you how to start:
DECLARE
cursorID INTEGER;
status INTEGER;
colCount INTEGER;
rowCount INTEGER;
description dbms_sql.desc_tab;
colType INTEGER;
stringValue VARCHAR2(32676);
sqlCmd VARCHAR2(32767);
BEGIN
-- open cursor
cursorID := dbms_sql.open_cursor;
-- parse statement
dbms_sql.parse(cursorID, 'select * from user_tables', dbms_sql.native);
-- describe columns
dbms_sql.describe_columns(cursorID, colCount, description);
-- cursor close
dbms_sql.close_cursor(cursorID);
-- open cursor
cursorID := dbms_sql.open_cursor;
-- assemble a new select only using up to 5 the "text" columns
FOR i IN 1 .. description.COUNT LOOP
IF (i > 5) THEN
EXIT;
END IF;
IF (description(i).col_type IN (1, 112)) THEN
IF (sqlCmd IS NOT NULL) THEN
sqlCmd := sqlCmd || ', ';
END IF;
sqlCmd := sqlCmd || description(i).col_name;
END IF;
END LOOP;
sqlCmd := 'SELECT ' || sqlCmd || ' FROM user_tables';
dbms_output.put_line(sqlCmd);
-- parse statement
dbms_sql.parse(cursorID, sqlCmd, dbms_sql.native);
-- describe columns
dbms_sql.describe_columns(cursorID, colCount, description);
-- define columns
FOR i IN 1 .. description.COUNT LOOP
dbms_sql.define_column(cursorID, i, stringValue, 4000);
END LOOP;
-- execute
status := dbms_sql.execute(cursorID);
-- fetch up to 5 rows
rowCount := 0;
WHILE (dbms_sql.fetch_rows(cursorID) > 0) LOOP
rowCount := rowCount + 1;
IF (rowCount > 5) THEN
EXIT;
END IF;
dbms_output.put_line('row # ' || rowCount);
FOR i IN 1 .. description.COUNT LOOP
dbms_sql.column_value(cursorID, i, stringValue);
dbms_output.put_line('column "' || description(i).col_name || '" = "' || stringValue || '"');
END LOOP;
END LOOP;
-- cursor close
dbms_sql.close_cursor(cursorID);
END;
/
As astentx suggested, you can use a common table expression function to package the PL/SQL code into a SQL statement. This solution is just a single SQL statement, and requires no non-default privileges and does not create any permanent objects.
(The only downside is that not all SQL tools understand these kinds of WITH clauses, and they may throw an error expecting a different statement terminator.)
SQL> create table members(id number, name varchar2(100), age number);
Table created.
SQL> with function get_result_column_names(p_sql varchar2) return sys.odcivarchar2list is
2 v_cursor_id integer;
3 v_col_cnt integer;
4 v_columns dbms_sql.desc_tab;
5 v_column_names sys.odcivarchar2list := sys.odcivarchar2list();
6 begin
7 v_cursor_id := dbms_sql.open_cursor;
8 dbms_sql.parse(v_cursor_id, p_sql, dbms_sql.native);
9 dbms_sql.describe_columns(v_cursor_id, v_col_cnt, v_columns);
10
11 for i in 1 .. v_columns.count loop
12 v_column_names.extend;
13 v_column_names(v_column_names.count) := v_columns(i).col_name;
14 end loop;
15
16 dbms_sql.close_cursor(v_cursor_id);
17
18 return v_column_names;
19 exception when others then
20 dbms_sql.close_cursor(v_cursor_id);
21 raise;
22 end;
23 select *
24 from table(get_result_column_names(q'[select name, age from members]'));
25 /
COLUMN_VALUE
--------------------------------------------------------------------------------
NAME
AGE

Executing GROUP BY clause with Aggregate Function inside FOR LOOP in PL/SQL

I have an EMPLOYEE table with some columns. I'm interested about three columns - ENTRY_TIME (NUMBER), EXIT_TIME (NUMBER), NAME (VARCHAR2). The NAME column has no distinct entries, i.e., a value may appear multiple times.
I have taken few distinct NAME values and I want to get the maximum EXIT_TIME and the minimum ENTRY_TIME for each of these selected NAME values from the entire data.
I have written the following PL/SQL block:
DECLARE
type namearray IS VARRAY(6) OF VARCHAR2(50);
name namearray;
total INTEGER;
EntryTime NUMBER;
ExitTime NUMBER;
employeeNames VARCHAR2(100);
BEGIN
name := namearray('Peter','Job','George','Hans','Marco','Alison');
total := name.count;
FOR i in 1 .. total LOOP
SELECT min(ENTRY_TIME), max(EXIT_TIME), NAME
INTO EntryTime, ExitTime, employeeNames
from EMPLOYEE
GROUP BY NAME having NAME = name(i);
dbms_output.put_line('EntryTime: ' || EntryTime || 'ExitTime: ' || ExitTime || 'Name: ' || employeeNames);
END LOOP;
END;
/
It is providing following error:
Error report -
ORA-01403: no data found.
ORA-06512: in line 12
01403. 00000 - "no data found"
*Cause: No data was found from the objects.
*Action: There was no data from the objects which may be due to end of fetch.
But data is there. I think there is something wrong with the query or the block itself.
Can someoneone help/suggest.
At first glance I was also confused with the HAVING CLAUSE as #APC commented but I believe the problem is with the employee names you passed as there could be names which are not available in the employee table which leads to NO_DATA_FOUND exception and you need to handle such scenarios with exception and other than that even though its working I would suggest to change the having clause to where clause
Try below,
DECLARE
type namearray IS VARRAY(6) OF VARCHAR2(50);
name namearray;
total INTEGER;
EntryTime NUMBER;
ExitTime NUMBER;
employeeNames VARCHAR2(100);
BEGIN
name :=namearray('Peter','Job','George','Hans','Marco','Alison');
total := name.count;
FOR i in 1 .. total
LOOP
BEGIN
SELECT min(1), max(1), NAME
INTO EntryTime, ExitTime, employeeNames
FROM EMPLOYEE
GROUP BY NAME
HAVING NAME = name(i);
dbms_output.put_line('EntryTime: ' || EntryTime || 'ExitTime: ' || ExitTime || 'Name: ' || employeeNames);
EXCEPTION
WHEN NO_DATA_FOUND THEN
dbms_output.put_line('No data found for Name: ' || name(i));
END;
END LOOP;
END;
/
Consider the suggestion to have WHERE clause.:)
Why I am saying this is what you can find in the db<>fiddle
Another option to exception handling is to outer join the selected employee names with the table. This is accomplished by creating a collection (array) at the schema level, then using the TABLE function on that a variable of that collection type. See fiddle.
create type names_att is table of varchar2(50);
declare
name names_att := names_att('Peter','Job','George','Hans','Marco','Alison');
begin
for rec in
( select min(entrytime) entrytime
, max(exittime) exittime
, na.column_value employeenames
from table (name) na
left join employee emp on emp.employeenames = na.column_value
group by na.column_value
order by na.column_value
)
loop
dbms_output.put_line('Name: ' ||rec.employeenames) ||
' EntryTime: ' || nvl(to_char(rec.entrytime), '---') ||
' ExitTime: ' || nvl(to_char(rec.exittime), '---')
);
end loop;
end;
/
Note: I defined an Associative Array rather than a Varray for the names. I am not sure this technique works with varray. I have never seen any reason for a varray.

ORACLE SQL: Looping over table

I have a table that contains a list of table names.
I would like to search each of these tables one by one to see if they contain a particular element (the primary key, specified at the start of the script).
I would like to return a list of all of the tables that this element is present in (ideally distinct).
I'm fairly new to this PL/SQL "not just a query" stuff. so i apologise in advance for the attrocious attempt you are about to see, but hopefully it illustrates what i'm going for:
PROCEDURE CHECK_FOR_ELEMENTS
BEGIN
DECLARE
ELEMENT_KEY varchar(5):=X78ehryfk;
RNUM_MAX int :=167;
----create output table for script
create or replace table ALL_TABLES CONTAINING_&ELEMENT_KEY
(ELEMENT_KEY VARCHAR(255),
TABLE_NAME varchar(255))
/
commit;
---begin loop over rnum;
FOR rnum_counter in 1..&RNUM_MAX
LOOP
--define this statement as variable TABLE_NAME_VAR
select table_name from (select * from (select table_name, rownum as rnum
from all_tables
where owner = 'RMS'
and table_name like 'ABC%'
and table_name not like '%STG'
and table_name not like '%BKP'
and num_rows>0
order by num_rows desc)
where rnum = rnum_counter
)INTO TABLE_NAME_VAR
;
----run below to collect row, if it exists, from table being searched
SQL_STMT:='INSERT INTO ALL_TABLES CONTAINING_&ELEMENT_KEY
SELECT distinct key,'||TABLE_NAME_VAR||' as UMF from
'||TABLE_NAME_VAR||
' where key like 'ELEMENT_KEY-%'
execute immediate SQL_STMT;
commit;
---insert row into table created for output
END LOOP
---loop over all tables
END;
The main error message i get is that TABLE_NAME_VAR is not a valid table name within the dynamic SQL statement. I've googled a bit and i now understand you can't use variables to input table names in this way.
Any help is greatly appreciated!
Thankyou!
Here, I tried to clean it up for you. Let me know if you still get errors.
create or replace PROCEDURE CHECK_FOR_ELEMENTS is
ELEMENT_KEY varchar2(14):='X78ehryfk';
RNUM_MAX int :=167;
TABLE_NAME_VAR varchar2(30);
SQL_STMT varchar2(4000);
BEGIN
----create output table for script
begin
execute immediate 'drop table ALL_TABLES_WITH_' || element_key;
exception when others then null;
end;
execute immediate 'create table ALL_TABLES_WITH_' || element_key || '
(ELEMENT_KEY VARCHAR2(255), -- does this need to be 255 characters?
TABLE_NAME varchar2(30))';
--- implicit cursor loop
FOR rnum_row in (select table_name, rownum as rnum
from all_tables
where owner = 'RMS'
and table_name like 'ABC%'
and table_name not like '%STG'
and table_name not like '%BKP'
and num_rows>0
order by num_rows desc)
LOOP
if rnum_row.rnum > RNUM_MAX
then exit;
end if;
TABLE_NAME_VAR := rnum_row.table_name;
----run below to collect row, if it exists, from table being searched
SQL_STMT:='INSERT INTO ALL_TABLES_WITH_' || element_key || '
(ELEMENT_KEY, TABLE_NAME)
SELECT distinct key, :1 as UMF from
'||TABLE_NAME_VAR||
' where key like :2';
execute immediate SQL_STMT using TABLE_NAME_VAR, element_key || '-%';
---insert row into table created for output
END LOOP;
commit;
---loop over all tables
END CHECK_FOR_ELEMENTS;
/

Get max(length(column)) for all columns in an Oracle table

I need to get the maximum length of data per each column in a bunch of tables. I'm okay with doing each table individually but I'm looking for a way to loop through all the columns in a table at least.
I'm currently using the below query to get max of each column-
select max(length(exampleColumnName))
from exampleSchema.exampleTableName;
I'm basically replacing the exampleColumnName with each column in a table.
I've already went through 3-4 threads but none of them were working for me either because they weren't for Oracle or they had more details that I required (and I couldn't pick the part I needed).
I'd prefer to have it in SQL than in PLSQL as I don't have any create privileges and won't be able to create any PLSQL objects.
Got the below query to work -
DECLARE
max_length INTEGER; --Declare a variable to store max length in.
v_owner VARCHAR2(255) :='exampleSchema'; -- Type the owner of the tables you are looking at
BEGIN
-- loop through column names in all_tab_columns for a given table
FOR t IN (SELECT table_name, column_name FROM all_tab_cols where owner=v_owner and table_name = 'exampleTableName') LOOP
EXECUTE IMMEDIATE
-- store maximum length of each looped column in max_length variable
'select nvl(max(length('||t.column_name||')),0) FROM '||t.table_name
INTO max_length;
IF max_length >= 0 THEN -- this isn't really necessary but just to ignore empty columns. nvl might work as well
dbms_output.put_line( t.table_name ||' '||t.column_name||' '||max_length ); --print the tableName, columnName and max length
END IF;
END LOOP;
END;
Do let me know if the comments explain it sufficiently, else I'll try to do better. Removing table_name = 'exampleTableName' might loop for all tables as well, but this is okay for me right now.
You can try this; although it uses PL/SQL it will work from within SQL-Plus. It doesn't loop. Hopefully you don't have so many columns that the SELECT query can't fit in 32,767 characters!
SET SERVEROUTPUT ON
DECLARE
v_sql VARCHAR2(32767);
v_result NUMBER;
BEGIN
SELECT 'SELECT GREATEST(' || column_list || ') FROM ' || table_name
INTO v_sql
FROM (
SELECT table_name, LISTAGG('MAX(LENGTH(' || column_name || '))', ',') WITHIN GROUP (ORDER BY NULL) AS column_list
FROM all_tab_columns
WHERE owner = 'EXAMPLE_SCHEMA'
AND table_name = 'EXAMPLE_TABLE'
GROUP BY table_name
);
EXECUTE IMMEDIATE v_sql INTO v_result;
DBMS_OUTPUT.PUT_LINE(v_result);
END;
/

Datawarehousing Automation using stored procedure

I am trying to code a procedure where i can validate all the records are moved from source to target provided there is no transformation logic involved in between for that particular column. My approach is to take the group by count of source column and target column and match their count. If the count is 0 then all the records are matched for that particular column to the target table.
Further a minus from the 2 data group by count will provide the missing data.
Can any one help me further on this.
Stored procedure :
create or replace
PROCEDURE MOVE_CHECK
(SCHEMA_SOURCE IN VARCHAR2,SCHEMA_TARGET IN VARCHAR2, TABLE_SOURCE IN VARCHAR2, TABLE_TARGET IN VARCHAR2, COLUMN_SOURCE IN VARCHAR2,
COLUMN_TARGET IN VARCHAR2)
AS
A varchar2 (30);
B varchar2 (30);
C varchar2 (30);
D varchar2 (30);
E varchar2 (30);
F varchar2 (30);
COUNT_SOURCE number(38);
TEMP_1 VARCHAR2(500);
TEMP_2 VARCHAR2(500);
TEMP_3 VARCHAR2(500);
TEMP_4 VARCHAR2(500);
COUNT_QUERY number(38);
BEGIN
A:=SCHEMA_SOURCE;
B:=SCHEMA_TARGET;
C:=TABLE_SOURCE;
D:=TABLE_TARGET;
E:=COLUMN_SOURCE;
F:=COLUMN_TARGET;
-- checking the count of the source records
TEMP_1 :='select count ( '|| E ||' ) from ' || C;
EXECUTE IMMEDIATE TEMP_1 INTO COUNT_SOURCE;
DBMS_OUTPUT.PUT_LINE ('source_count:'||Count_source);
TEMP_2 :='CREATE GLOBAL TEMPORARY TABLE SET_SOURCE AS (SELECT COUNT(1) AS COUNT_SOURCE, '|| E ||' from ' || C || ' GROUP BY ' || E||' )';
EXECUTE IMMEDIATE TEMP_2;
TEMP_3 :='CREATE GLOBAL TEMPORARY TABLE SET_TARGET AS (SELECT COUNT(1) AS COUNT_TARGET, ' || F||'FROM '||D||' GROUP BY ' ||D ||' )';
EXECUTE IMMEDIATE TEMP_3;
TEMP_4:= 'SELECT COUNT(1) FROM SET_SOURCE INTERSECT SET_TARGET ';
EXECUTE IMMEDIATE TEMP_4 INTO COUNT_QUERY;
DBMS_OUTPUT.PUT_LINE ('OUTPUT:'||COUNT_QUERY);
IF COUNT_QUERY <> 0
THEN DBMS_OUTPUT.PUT_LINE ('PLEASE CHECK');
ELSE DBMS_OUTPUT.PUT_LINE ('DONE- NO MISMATCH');
END IF;
END MOVE_CHECK;
I am unable to run execute Temp_2,Temp_3,Temp_4
Error:
ORA-00955: name is already used by an existing object
ORA-06512: at "YDSCST.MOVE_CHECK", line 35
ORA-06512: at line 16
source_count:7
Process exited.
It generally helps if you print out the dynamic SQL and try to run it manually; it can make syntax errors much easier to spot. In this case if you called the original procedure for the DUAL table you'd see:
CREATE GLOBAL TEMPORARY TABLE SET_SOURCE AS (SELECT COUNT(1), DUMMY )
from DUAL GROUP BY DUMMY )
You'd then get the same ORA-00923 if you ran that, and it suggests the problem might be before the from. And looking at the code it's clear that there was an extra closing parenthesis after the column name. But you spotted that yourself and have now moved on to the next error.
The next error after that should be ORA-00998: must name this expression with a column alias, because you haven't specified what the temporary table's columns should be called. You can do that in the select or in the create:
TEMP_2 :='CREATE GLOBAL TEMPORARY TABLE SET_SOURCE (ROW_COUNT, COLUMN_NAME) '
|| 'AS (SELECT COUNT(1), ' || E || ' from ' || C || ' GROUP BY ' || E || ')';
But you seem to have found and fixed that yourself too as you're now getting an ORA-06502, which is because your TEMP variables are smaller than your query string now needs to be. But you've also fixed that yourself now, making them 500 characters instead of 100.
Your TEMP_3 is missing a space before its from; the value of F is currently concatenated to that directly, making that an invalid statement. And the group by is using D (table name) instead of F (column name)
TEMP_3 :='CREATE GLOBAL TEMPORARY TABLE SET_TARGET (ROW_COUNT, COLUMN_NAME) '
|| 'AS (SELECT COUNT(1), ' || F || ' FROM ' || D || ' GROUP BY ' || F ||' )';
That would be more obvious if you were using the procedure argument names instead of meaningless (and pointless) local variables.
Creating global temporary tables, or any structure, within a procedure is generally not a good idea. The second time you call this they will already exist and it will error on that. If you really need them at all, you should create the temporary tables once outside the procedure as a separate task, and just populate them inside the procedure.