OraStory

Entries categorized as ‘oracle’

Loader

November 6, 2009 · Leave a Comment

It’s been years and years since I worked regularly with SQL*Loader-based feeds.

There are loads of tips and tricks regarding formats, encodings, character sets, etc.
I had forgotten nearly everything that I could ever have claimed to know in this area, all aged out of my personal buffer cache, at best distant memories, very distant.

However, I’ve recently had to resurrect some of these distant memories to feed some data via External Tables.

I thought it would be worthwhile to do an incoherent brain dump on some of the particular issues for future reference.

This article is my no means comprehensive and will just touch on some of the considerations specific to what I was doing.

The starting place for documentation on SQL*Loader is the Oracle Database Utilies Manual and similarly for External Tables.

The thing about external tables is that they can use the ORACLE_LOADER or the ORACLE_DATAPUMP access drivers.

My circumstances involved the ORACLE_LOADER driver.

As the names suggest, ORACLE_LOADER is related to SQL*LOADER. In fact, you can use the EXTERNAL_TABLE=GENERATE_ONLY parameter with SQL*LOADER to generate the syntax for your EXTERNAL TABLE.

I think things like this are brilliant but I never use them. I suppose I’m old skool. Neat features that auto generate code mean that a) I don’t necessarily need to understand what’s going on and b) skip the line-by-line sanity checks that I still believe in.

I work in an environment where it’s not so easy to get access to servers to ftp and view files, etc.

As a result, my approach would be to work with SQL*Loader early on and then graduate to external files nearer the time. However, I’m going to skip all that, fast forward past the SQL*Loader bit and move on to the issues.

So the main considerations for loading up data?
- Encoding / Character sets
- Delimiters – field and line
- File Transfer

(I’m going to ignore Endianness for the sake of “clarity”. But it can be a factor. See Byte Ordering for futher information.)

In my experience, when you get errors whilst loading, it’s due to one or more of the reasons above.
More than one?
Sure.

For example, the combination of file transfer mechanism and delimiters.
Ever wondered why there’s confusion over whether you should transfer a file as text (or ascii), as binary? What’s the difference?

As ever, it depends.
- SFTP doesn’t have a text mode, all transfers are binary.
- FTP lets you choose between ascii (default) and binary.
- What about WinSCP and similar tools with their automatic mode?

Automatic modes usually work off the file extension and would pick a text transfer for HTML, TXT, PHP, XML, etc and binary otherwise.

And the differences in mode?

For Text mode, there are two basic methods – either the tool is responsible for doing some conversion to the format supported by the destination or, more commonly, the client converts to a canonical format and the service then does a further conversion to its own format if necessary.

With Binary mode, the raw bytes are transferred as is, i.e. the file is transferred in its original form.

So, the main significance of this related to field and line delimiters – things like tabs and line feed characters are different between platforms, different between Unix and Windows for example.

This was particularly relevant for my file export from SQL Server to Oracle on Linux.

In Windows, a new line is often represented by two characters – one carriage return and one line feed.
In Unix, a new line is normally just a line feed.

Sometimes you see “^M” characters on *nix. What’s this about?
This represents the carriage return part of the newline for Windows as described above.
If you need to, you can get rid of that using dos2unix.

So, in the ACCESS PARAMETERS subsection, if you use the “RECORDS DELIMITED BY NEWLINE” syntax in your external table definiton, what does that mean?

The NEWLINE keyword uses the newline format for your platform – so just a line feed in the case of *nix.

For my process, I decided that I would just go with the format as extracted from SQL Server – a 50:50 decision that there’s no point on expanding on.

So, in my situation, the syntax for an external table should not use the NEWLINE keyword because the format was Windows new lines, the syntax for which can be:

RECORDS DELIMITED BY '\r\n'

Here’s a situation. Things have been going fine in DEV,etc and eventually go to PREPRODUCTION and there’s a problem.

The feeds don’t work. The external table loader isn’t finding the right delimiters.

The question really is how can you tell what characters are in your file?
So, there’s no point looking at the file prior to transfer in case the technological clue (or the person in a manual process) changes the file on transfer. We need to look at it in the destination directory on the destination server.

And what’s the best way to do that?

On a Unix/Linux, a useful command is the od command which dumps files in octal and other formats. For example,

od -c <filename>

which gives ascii characters or backslash escapes.
There’s a lot of output from this command but you don’t need to do the whole file, e.g.

head -2 <filename> | od -x | more

If the file suddenly doesn’t match the expected format, what’s the sort of errors you might get?
Well, if it should have transfered in binary but it was done in text by error, you might get:

KUP-04020: found record longer than buffer size supported

i.e. the lines are running into each other
or under slightly different circumstances

KUP-04023: field start is after end of record

etc.

That’s about it on transfers and delimiters.

What’s there to say about encoding and character sets.
Using the od command above we can see the hex codes to double check the encoding is as expected.
Other that that, in our external table definition we can specify the character set in the ACCESS PARAMETERS section using the CHARACTERSET keyword, e.g.

CREATE TABLE <my_tablename>
(<my_columns>)
ORGANIZATION EXTERNAL
  (  TYPE ORACLE_LOADER
     DEFAULT DIRECTORY <my_directory>
     ACCESS PARAMETERS
       ( RECORDS DELIMITED BY '\r\n'
       CHARACTERSET AL16UTF16
       FIELDS TERMINATED BY '\t'
       MISSING FIELD VALUES ARE NULL  )
     LOCATION (<my_directory>:'<my_filename>')
  )
REJECT LIMIT 0;

and then the expected characterset conversion will take place.
Note that SQLServer tends to use USC2 as its unicode characterset and UCS2 is a subset of AL16UTF16.

It’s Friday, it’s 5 o’clock, I’ve lost focus. I’m done.

Categories: binary transfer · delimiter · external tables · newline · oracle · sql*loader

Learning

October 28, 2009 · Leave a Comment

Earlier this month I attended Tanel Poder’s course “Advanced Oracle Troubleshooting for DBAs and Performance Engineers”.

It was good – definitely worthwhile, lots of useful information and really puts into perspective those scripts of his, like LatchProf, LatchProfX, WaitProf and sample.sql. Plus a whole bunch of other stuff. So definitely worth thinking about if you’ve any training budget (that’s mine blown for forseeable future).

I’ve already mentioned the Metalink Headlines tip and I’ve had a little jump in traffic over the past couple of days from Tanel saying the same thing.

The other top tip was to read James Morle’s book Scaling Oracle 8i. James was there on the second day but that wasn’t why it was recommended. Ignore the 8i in the title, most of it remains as relevant as it ever was. I don’t know how I never read this at the time but I’m ploughing through it on the train and it’s a real, real benefit. It’s not just about Oracle. It’s as much about the stuff it interacts with – OS, IO subsystems, memory, etc.
It’s downloadable. Read it.

Categories: oracle

Metalink headlines

September 16, 2009 · 4 Comments

When I was thinking about whether to go on Tanel Poder’s course, I read the testimonials and there’s a great piece of advice from Tanel via Coskan Gundogar:

I asked him how do you learn all these interesting stuff. He gave me a perfect clue. Sign up for metalink headlines mailing list. Bugs docs they are all there after they are created. Perfect resource.

I just want to emphasise what a great resource these Metalink updates are.

I’ve only been signed up for a couple of weeks but some of the nuggets that come into your mailbox are pure gold.

I’ve learnt some stuff that I would never come across, plus I’m building up links for some great articles and explanations, some of which are invaluable as a reference for yourself or to give others (I’m rubbish at explaining stuff in my own words).

For example, just from the last couple of days, I’ve been notified of these new or updated articles:
(Note: Login to Metalink first before clicking the links)

Character set and character set conversion (comes up all the time on the forums and from other teams at work):

Metalink Scripts:

General:

Interesting behaviour:

To name but a few…

It’s just invaluable stuff.

Sign up if you don’t already.

Categories: Metalink · oracle

Data modelling in the modern world

August 6, 2009 · Leave a Comment

I first read Robyn Sands’s article on the importance of a good data model back when it was published in May.

However, the comments have since moved on and taken a life of their own – a sure sign of a great post – with some excellent points and poignant observations on the frequent neglect of the data model in the agile world.

If you’ve not read it or have not kept updated with the comments, then I urge you to.

Categories: agile · database · design · oracle

Gotcha: Application Contexts

July 14, 2009 · 7 Comments

This is one of those feature behaviours which isn’t surprising, but you probably wouldn’t think about it unless you saw it.

The flop:

I make heavy use of application contexts in flexible, variable input, dynamically-built queries (one of the options mentioned in this asktom thread for example).

(In the latest edition of Oracle Magazine, Tom explains why this is not an approach he would recommend any more. I don’t necessarily agree but more of that another time perhaps)

The Turn:

My Query Builder – a set of code which I’ve always intended to make publicly available but have yet to do so – uses a context with single namespace and typically reuses attribute names between queries – e.g. a product id populated from an on-screen product selector will typically populate a context attribute called PRODUCT_ID.

And a Query Builder implementation will typically reset any context attributes(DBMS_SESSION.CLEAR_ALL_CONTEXT) at the start to clear any parameters from a previous query.

In one of the more complex scenarios, a controller proc constructs three different queries, based on the input parameters supplied, in order to return three resultsets to populate three different parts of a GUI. A bit like this dummy code:

CREATE OR REPLACE PROCEDURE p_get_matching_data (
  i_parameters             IN  some_array_type
  o_cursor1                 OUT SYS_REFCURSOR,
  o_cursor2                 OUT SYS_REFCURSOR,
  o_cursor3                 OUT SYS_REFCURSOR
)
AS
   l_query1 LONG;
   l_query2 LONG;
   l_query3 LONG;
BEGIN
   l_query1 := my_query_builder_pkg.build_query1(i_parameters);
   OPEN o_cursor1 FOR l_query1;
   l_query2 := my_query_builder_pkg.build_query2(i_parameters);
   OPEN o_cursor2 FOR l_query2;
   l_query3 := my_query_builder_pkg.build_query3(i_parameters);
   OPEN o_cursor3 FOR l_query3;
END;

The River:

The problem with SYS_CONTEXT is that it is not bound into the query like a bind variable, nor is it subject to the same read consistency mechanism as block data.

Witness:

DOMINIC@11gR1&gt;VAR r REFCURSOR
DOMINIC@11gR1&gt;CREATE CONTEXT demo_ctx USING context_demo_pkg;

Context created.

Elapsed: 00:00:00.00
DOMINIC@11gR1&gt;CREATE OR REPLACE PACKAGE context_demo_pkg
  2  AS
  3    --
  4    PROCEDURE p_set_context (
  5      i_attribute                IN     VARCHAR2,
  6      i_value                    IN     VARCHAR2
  7    );
  8    --
  9    PROCEDURE p_unset_context (
 10      i_attribute                IN     VARCHAR2
 11    );
 12    --
 13  END;
 14  /

Package created.

Elapsed: 00:00:00.00
DOMINIC@11gR1&gt;CREATE OR REPLACE PACKAGE BODY context_demo_pkg
  2  AS
  3    --
  4    k_namespace CONSTANT VARCHAR2(24) := 'DEMO_CTX';
  5    --
  6    PROCEDURE p_set_context (
  7      i_attribute                IN     VARCHAR2,
  8      i_value                    IN     VARCHAR2
  9    )
 10    AS
 11    BEGIN
 12       --
 13       DBMS_SESSION.SET_CONTEXT
 14       (namespace =&gt; k_namespace,
 15        attribute =&gt; i_attribute,
 16        value     =&gt; i_value);
 17       --
 18    END p_set_context;
 19    --
 20    PROCEDURE p_unset_context (
 21      i_attribute                IN     VARCHAR2
 22    )
 23    AS
 24    BEGIN
 25       --
 26       DBMS_SESSION.CLEAR_CONTEXT
 27       (namespace =&gt; k_namespace,
 28        attribute =&gt; i_attribute);
 29       --
 30    END p_unset_context;
 31    --
 32  END;
 33  /

Package body created.

Elapsed: 00:00:00.00
DOMINIC@11gR1&gt;

Now to demonstrate the different behaviour.

The bind variable:

DOMINIC@11gR1&gt;DECLARE
  2   l_value  varchar2(24) := 'My Test Bind';
  3  BEGIN
  4    open :r for select l_value from dual;
  5    l_value := null;
  6  END;
  7  /

PL/SQL procedure successfully completed.

:B1
--------------------------------
My Test Bind

Elapsed: 00:00:00.00
DOMINIC@11gR1&gt;

No surprises there.
Now the illustration of read consistency:

DOMINIC@11gR1&gt;CREATE TABLE test_data
  2  (col1 VARCHAR2(1));

Table created.

Elapsed: 00:00:01.02
DOMINIC@11gR1&gt;
DOMINIC@11gR1&gt;INSERT
  2  INTO   test_data
  3  VALUES ('A');

1 row created.

Elapsed: 00:00:00.00
DOMINIC@11gR1&gt;BEGIN
  2     OPEN :r FOR SELECT * FROM test_data;
  3     DELETE FROM test_data;
  4     COMMIT;
  5  END;
  6  /

PL/SQL procedure successfully completed.

C
-
A

Elapsed: 00:00:00.00
DOMINIC@11gR1&gt;

Again, no surprises. Read consistency around cursor opening/fetching as you might expect.
Next, the application context.

Firstly, just to prove that it works:

DOMINIC@11gR1&gt;BEGIN
  2     context_demo_pkg.p_set_context('VALUE1','My Test Bind');
  3     OPEN :r FOR SELECT SYS_CONTEXT('DEMO_CTX','VALUE1') FROM dual;
  4  END;
  5  /

PL/SQL procedure successfully completed.

SYS_CONTEXT('DEMO_CTX','VALUE1')
-------------------------------------
My Test Bind

Elapsed: 00:00:00.00
DOMINIC@11gR1&gt;

But if we reset it before we fetch from the cursor:

DOMINIC@11gR1&gt;BEGIN
  2     context_demo_pkg.p_set_context('VALUE1','My Test Bind');
  3     OPEN :r FOR SELECT SYS_CONTEXT('DEMO_CTX','VALUE1') FROM dual;
  4     context_demo_pkg.p_unset_context('VALUE1');
  5  END;
  6  /

PL/SQL procedure successfully completed.

SYS_CONTEXT('DEMO_CTX','VALUE1')
---------------------------------------

Elapsed: 00:00:00.00
DOMINIC@11gR1&gt;

So, once you see it, not entirely unsurprising but maybe not something that you’d considered before ( I hadn’t ). Behaviour neither like a bind nor read consistency like block data.

Categories: 11.1.0.7 · context · oracle · sys_context

Madness

May 29, 2009 · 5 Comments

Just a quick one…

Had a meeting request today to get an idea of the different authorisation groups that will be required when in the future we move to a model that carries the authorisation ACL with the data.

This is a part of an ongoing architectural initiative to turn a logic-heavy database effectively into a bit bucket.

I know these things are going on all the time everywhere in a quasi-religious battle.
I know I’ve talked about it before as have many others elsewhere.
I’m sure I resolved to let it wash over me and not to get bothered by it or get sucked into battles I can’t win.

But this is madness this aspiration that this database, probably one of the most expensive bits of this tech stack, should be reduced to a bit bucket at some point in the future.

With the middle tier doing the data joining…

The middle tier doing what should be the set operations in the database in some sort of horrific giant nested loop operation…

And the middle tier doing the data security, which should be right there with the data in the database.

It’s a mad world.

And in today’s economic climate, this can’t be what they mean by “doing more for less”.

Categories: architecture · database · oracle

YAGNI?

May 14, 2009 · 2 Comments

YAGNI, it’s a very common term in development teams these days. Overused even.

However, in today’s more austere and economically challenging times, I have some new acronyms for you.

I’m not really up on popular acronyms so these may already exist in some form or another, but here are some more appropriate sayings that I’m saying/seeing more and more, together especially:

I Do Bloody Well Need It (IBLOWENI – ha ha) … but … NO-one’s Gonna Pay For It (NOGOPAFI).

Categories: development · oracle · yagni

Motivation

February 19, 2009 · 5 Comments

I’ve mentioned before that I’m lucky enough, given my own database-heavy focus, to be currently servicing a client whose applications tend to be database-heavy in terms of logic.

They are moving their applications gradually, yet wholesale and with no exceptions, towards an N-tier architecture throughout the company and their particular interpretation of N-tier means treating the database like a bit bucket (the only place I’ve been where they treat n-tier and database independent as synonymous although I know this is not an uncommon misunderstanding).

Today I had a conversation with one of the .Net developers along the lines of “you must be gutted given your database expertise.”

To which my response was that I was not particularly bothered which tier had the most lines of code – at the end of the day, it’s not a competition.

There are four things which primarily motivate me, or give me the most pleasure, in a professional capacity:

  1. Making slow, inefficient code fly.
  2. *Writing cool satisfying code.
  3. *Good design and good data modelling .
  4. Using databases efficiently.

*Incidentally, when designing and developing, I’m a great believer in the frequent need to not force it but wait for the right way to make itself known (at whatever time of day or night that might be).

So, moving a bunch of business logic (and in general very poorly written database logic) out of the database doesn’t bother me – the location of this logic doesn’t matter as long as it makes sense.

Remodelling and rearchitecting processes out of the database doesn’t bother me as long as it’s done accurately and properly wherever, and data is modelled appropriately.

In fact, the only thing which bothers me is that this rearchitecture is going to eventually involve Hibernate or similar, which will translate a single object save into a hundred individual insert or update statements, no bulk operations, nothing. I don’t like ORM (although I am very fond of OR views or an OR api in the database which can map one object save to as few operations in the database as is necessary).

Categories: oracle

Adventures with VPD I: Result Cache

February 19, 2009 · 1 Comment

Oh, the challenges of implementing VPD on a mature system!

Like partitioning and other major features, implementing VPD on a “mature”* database that has been “evolving”* over many years can be nowhere near as simple as designing such a solution from scratch. (* “Mature” databases have not necessarily “evolved” in a way that you might recommend. Best practice might not apply.)

Anyway, I’ve been intending to write a case study of my current VPD implementation, dealing with some detail of the buy versus build decision of VPD versus OLS and the subsequent challenges and incompatibilities with various features. As usual, that article awaits but I wanted to kick off a mini-series on the various difficulties experienced (and this series might then help the coming together of a single article on the whole experience).

Challenge I
The challenge illustrated here is VPD + Result Cache.

Spoiler: For those who don’t want to read the whole article (it’s a bit long and the formatting which doesn’t help), the two work together but beware the subtle differences in behaviour between the SQL and PL/SQL Result Cache – read the documentation for each.

Quick Background
First a bit of background information which may help put these snippets of info into some sort of context.

  • As mentioned, this is a database that has been around for quite a few years now.
  • There has been a simple requirement to restrict access such that:
    • Users in the US can only see US data.
    • Users in the Rest of the World (ROW) cannot see US data.
  • VPD, OLS and a separate database were the options considered with VPD considered the only practical solution for one reason or another.

Demo
Let’s get into it.

Step 1: First up, script to create users for this demo:

  1. User to own table – APP_DATA
  2. User to own manage VPD security – SECURITY_MGR
  3. User to simulate app user with US visibility
  4. User to simulate app user with ROW visibility


-- Schema to hold some data
CREATE USER APP_DATA
IDENTIFIED BY password
DEFAULT TABLESPACE USERS
TEMPORARY TABLESPACE TEMP
QUOTA UNLIMITED ON USERS
ACCOUNT UNLOCK;

GRANT CREATE SESSION TO app_data;
GRANT CREATE TABLE TO app_data;
GRANT CREATE PROCEDURE TO app_data; 

-- Schema to manage security policies
CREATE USER SECURITY_MGR
IDENTIFIED BY password
DEFAULT TABLESPACE USERS
TEMPORARY TABLESPACE TEMP
QUOTA UNLIMITED ON USERS
ACCOUNT UNLOCK;

GRANT EXEMPT ACCESS POLICY TO security_mgr;
GRANT CREATE SESSION TO security_mgr;
GRANT CREATE TABLE TO security_mgr;
GRANT CREATE ANY CONTEXT TO security_mgr;
GRANT CREATE TRIGGER TO security_mgr;
GRANT ADMINISTER DATABASE TRIGGER TO security_mgr;
GRANT SELECT ANY TABLE TO security_mgr;
GRANT ALTER ANY TABLE TO security_mgr;
GRANT UPDATE ANY TABLE TO security_mgr;
GRANT EXECUTE ON DBMS_RLS TO security_mgr;
GRANT CREATE ANY TRIGGER TO security_mgr;
GRANT ADMINISTER DATABASE TRIGGER TO security_mgr;

-- User to see only US data
CREATE USER APP_USER_US
IDENTIFIED BY password
DEFAULT TABLESPACE USERS
TEMPORARY TABLESPACE TEMP
QUOTA UNLIMITED ON USERS
ACCOUNT UNLOCK;

GRANT CREATE SESSION TO app_user_us;

-- User to see only non-US data
CREATE USER APP_USER_ROW
IDENTIFIED BY password
DEFAULT TABLESPACE USERS
TEMPORARY TABLESPACE TEMP
QUOTA UNLIMITED ON USERS
ACCOUNT UNLOCK;

GRANT CREATE SESSION TO app_user_row;

Step 2: Next up, script to create table and data for this demo. It’s hardly imaginative nor a real world example…



conn app_data/password@dom11g

DROP TABLE cars;

CREATE TABLE cars
(id              NUMBER       NOT NULL
,make            VARCHAR2(48) NOT NULL
,security_label  VARCHAR2(6)  NOT NULL
,CONSTRAINT pk_cars PRIMARY KEY (id));

INSERT INTO app_data.cars
VALUES (1,'Audi','ROW');

INSERT INTO app_data.cars
VALUES (2,'Buick','US');

INSERT INTO app_data.cars
VALUES (3,'BMW','ROW');

INSERT INTO app_data.cars
VALUES (4,'Chrysler','US');

INSERT INTO app_data.cars
VALUES (5,'Citroen','US');

INSERT INTO app_data.cars
VALUES (6,'Dodge','US');

COMMIT;

GRANT SELECT ON cars TO app_user_us, app_user_row;

Step 3: Create vpd policies including

  • Create application context
  • Create package to manage context
  • Create logon trigger to set contexts
  • Create policy function that will apply predicates to policied tables
  • Apply policy to table


conn security_mgr/password@dom11g

CREATE CONTEXT security_ctx USING security_pkg;

CREATE OR REPLACE PACKAGE security_pkg
AS
  --
  PROCEDURE p_initialise_user;
  --
END security_pkg;
/

CREATE OR REPLACE PACKAGE BODY security_pkg
AS
  k_namespace CONSTANT VARCHAR2(12) := 'SECURITY_CTX';
  --
  PROCEDURE p_set_context (
    i_attribute  IN     VARCHAR2,
    i_value      IN     VARCHAR2
  )
  AS
  BEGIN
     --
     dbms_session.set_context(k_namespace,i_attribute,i_value);
     --
  END p_set_context;
  --
  PROCEDURE p_set_us_context
  AS
  BEGIN
     --
     p_set_context('CAN_SEE_US','Y');
     --
  END p_set_us_context;
  --
  PROCEDURE p_set_row_context
  AS
  BEGIN
     --
     p_set_context('CAN_SEE_ROW','Y');
     --
  END p_set_row_context;
  --
  PROCEDURE p_initialise_user
  AS
  BEGIN
     --
     IF USER = 'APP_USER_US'
     THEN
         --
         p_set_us_context;
         --
     ELSIF USER = 'APP_USER_ROW'
     THEN
         --
         p_set_row_context;
         --
     ELSE
         --
         p_set_us_context;
         p_set_row_context;
         --
     END IF;
     --
  END p_initialise_user;
  --
END security_pkg;
/

CREATE OR REPLACE TRIGGER tri_logon_set_rls_security
AFTER LOGON ON DATABASE
BEGIN
   --
   security_pkg.p_initialise_user;
   --
END tri_logon_set_rls_security;
/

CREATE OR REPLACE FUNCTION f_security_policy (
  i_schema                     IN     VARCHAR2 DEFAULT NULL,
  i_object                     IN     VARCHAR2 DEFAULT NULL
)
RETURN VARCHAR2
AS
BEGIN
   --
   RETURN CASE WHEN SYS_CONTEXT('SECURITY_CTX','CAN_SEE_US')  = 'Y'
               AND  SYS_CONTEXT('SECURITY_CTX','CAN_SEE_ROW') = 'Y'
               THEN NULL
               WHEN SYS_CONTEXT('SECURITY_CTX','CAN_SEE_US')  = 'Y'
               THEN ' security_label  = ''US'' '
               WHEN SYS_CONTEXT('SECURITY_CTX','CAN_SEE_ROW') = 'Y'
               THEN ' security_label = ''ROW'' '
          END;
   --
END f_security_policy;
/

begin
  dbms_rls.add_policy
  (object_schema   => 'APP_DATA',
   object_name     => 'CARS',
   policy_name     => 'RLS_CARS',
   function_schema => 'SECURITY_MGR',
   policy_function => 'F_SECURITY_POLICY',
   policy_type     => SYS.DBMS_RLS.SHARED_CONTEXT_SENSITIVE,
   statement_types => 'SELECT');
end;
/

Step 4: Test the VPD policies.
Step 4a: Connecting as APP_USER_US, we should only be able to see data from APP_DATA.CARS which is labelled as US:



conn app_user_us/password@dom11g

app_user_us@dom11g>select * from app_data.cars;

 ID MAKE       SECUR
--- ---------- -----
  2 Buick      US
  4 Chrysler   US
  5 Citroen    US
  6 Dodge      US

Elapsed: 00:00:00.01

Step 4b: Connecting as APP_USER_ROW, we should only be able to see data from APP_DATA.CARS which is labelled as ROW:


conn app_user_row/password@dom11g

app_user_row@dom11g>select * from app_data.cars;

app_user_row@dom11g>select * from app_data.cars;

 ID MAKE       SECUR
--- ---------- -----
  1 Audi       ROW
  3 BMW        ROW

Elapsed: 00:00:00.00
app_user_row@dom11g>

Good stuff. Spotted the mistake? Last time I checked, Citroen was French – should be labelled ROW. I’ll leave it in and update it after we’ve done some result cache queries just to show that that all works.

Step 5: Let’s take a look at the SQL result cache using the result_cache hint.
Step 5a: We’ll go with APP_USER_US first.



conn app_user_us/password@dom11g

app_user_us@dom11g>set autotrace on
app_user_us@dom11g>select /*+ result_cache */ * from app_data.cars;

 ID MAKE       SECUR
--- ---------- -----
  2 Buick      US
  4 Chrysler   US
  5 Citroen    US
  6 Dodge      US

Elapsed: 00:00:00.02

Execution Plan
----------------------------------------------------------
   0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=5 Card=4 Bytes=176)
   1    0   RESULT CACHE OF '25tyax8u9ktwf2g61acmsb5apa'
   2    1     TABLE ACCESS (FULL) OF 'CARS' (TABLE) (Cost=5 Card=4 Bytes=176)

Statistics
----------------------------------------------------------
        121  recursive calls
          0  db block gets
         58  consistent gets
          0  physical reads
          0  redo size
        508  bytes sent via SQL*Net to client
        427  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
          4  rows processed

app_user_us@dom11g>select /*+ result_cache */ * from app_data.cars;

 ID MAKE       SECUR
--- ---------- -----
  2 Buick      US
  4 Chrysler   US
  5 Citroen    US
  6 Dodge      US

Elapsed: 00:00:00.02

Execution Plan
----------------------------------------------------------
   0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=5 Card=4 Bytes=176)
   1    0   RESULT CACHE OF '25tyax8u9ktwf2g61acmsb5apa'
   2    1     TABLE ACCESS (FULL) OF 'CARS' (TABLE) (Cost=5 Card=4 Bytes=176)

Statistics
----------------------------------------------------------
          0  recursive calls
          0  db block gets
          0  consistent gets
          0  physical reads
          0  redo size
        508  bytes sent via SQL*Net to client
        427  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          4  rows processed


I’m happy enough that it’s coming from the cache the second time.

Step 5a: How about APP_USER_US?



conn app_user_row/password@dom11g

app_user_row@dom11g>set autotrace on
app_user_row@dom11g>select /*+ result_cache */ * from app_data.cars;

 ID MAKE       SECUR
--- ---------- -----
  1 Audi       ROW
  3 BMW        ROW

Elapsed: 00:00:00.02

Execution Plan
----------------------------------------------------------
   0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=5 Card=2 Bytes=88)
   1    0   RESULT CACHE OF 'g0ubwn87rvz5s15cj3089zgn3h'
   2    1     TABLE ACCESS (FULL) OF 'CARS' (TABLE) (Cost=5 Card=2 Bytes=88)

Statistics
----------------------------------------------------------
         89  recursive calls
          0  db block gets
         57  consistent gets
          0  physical reads
          0  redo size
        473  bytes sent via SQL*Net to client
        427  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          2  rows processed

app_user_row@dom11g>select /*+ result_cache */ * from app_data.cars;

 ID MAKE       SECUR
--- ---------- -----
  1 Audi       ROW
  3 BMW        ROW

Elapsed: 00:00:00.02

Execution Plan
----------------------------------------------------------
   0      SELECT STATEMENT Optimizer=ALL_ROWS (Cost=5 Card=2 Bytes=88)
   1    0   RESULT CACHE OF 'g0ubwn87rvz5s15cj3089zgn3h'
   2    1     TABLE ACCESS (FULL) OF 'CARS' (TABLE) (Cost=5 Card=2 Bytes=88)

Statistics
----------------------------------------------------------
          1  recursive calls
          0  db block gets
          0  consistent gets
          0  physical reads
          0  redo size
        473  bytes sent via SQL*Net to client
        427  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          2  rows processed

Step 6: Let’s update that Citroen row – double check what happens.



conn app_data/password@dom11g

app_data@dom11g>update cars set security_label = 'ROW' where id = 5;

1 row updated.

Elapsed: 00:00:00.00
app_data@dom11g>commit;

Commit complete.

Step 7: Back to the visibility users
Step 7a: APP_USER_US



app_user_us@dom11g>select /*+ result_cache */ * from app_data.cars;

 ID MAKE       SECUR
--- ---------- -----
  2 Buick      US
  4 Chrysler   US
  6 Dodge      US

Elapsed: 00:00:00.00
app_user_us@dom11g>

Step 7b: APP_USER_ROW



app_user_row@dom11g>select /*+ result_cache */ * from app_data.cars;

 ID MAKE       SECUR
--- ---------- -----
  1 Audi       ROW
  3 BMW        ROW
  5 Citroen    ROW

Elapsed: 00:00:00.00
app_user_row@dom11g>

Looking more accurate now.

SQL Query Result Cache
So, the SQL Result Cache apparently works fine with VPD.
Let’s review what the documentation says:

Cached results are parameterized with the parameter values if any of the following constructs are used in the query:

Bind variables.

The following SQL functions: dbtimezone, sessiontimezone, userenv/sys_context (with constant variables), uid, and user.

NLS parameters.

Parameterized cache results can be reused if the query is equivalent and the parameter values are the same.

So what of the PL/SQL Function Result Cache?

Step 8: Create a result cache function to demonstrate (this is an artificial and simplified function – it demos what I want to demo, it’s derived from a real-world implementation but I would advocate using the result cache for this purpose)


conn app_data/password@dom11g

CREATE OR REPLACE PACKAGE cars_pkg
AS
--
FUNCTION f_get_make (
i_id IN cars.id%TYPE
)
RETURN cars.make%TYPE;
--
END cars_pkg;
/

CREATE OR REPLACE PACKAGE BODY cars_pkg
AS
--
TYPE t_cars IS TABLE of cars%ROWTYPE INDEX BY PLS_INTEGER;
--
FUNCTION f_get_cars
RETURN t_cars
RESULT_CACHE
RELIES_ON (cars)
AS
--
l_cars t_cars;
l_tmp_cars t_cars;
i PLS_INTEGER;
--
BEGIN
--
SELECT *
BULK COLLECT INTO l_tmp_cars
FROM cars;
--
i := l_tmp_cars.FIRST;
--
WHILE (i IS NOT NULL)
LOOP
--
l_cars(l_tmp_cars(i).id).id := l_tmp_cars(i).id;
l_cars(l_tmp_cars(i).id).make := l_tmp_cars(i).make;
l_cars(l_tmp_cars(i).id).security_label := l_tmp_cars(i).security_label;
--
i := l_tmp_cars.NEXT(i);
--
END LOOP;

--
RETURN l_cars;
--
END f_get_cars;
--
FUNCTION f_get_make (
i_id IN cars.id%TYPE
)
RETURN cars.make%TYPE
AS
--
l_cars t_cars;
--
BEGIN
--
l_cars := f_get_cars;
RETURN l_cars(i_id).make;
--
EXCEPTION
WHEN no_data_found THEN
RETURN NULL;
END f_get_make;
--
END cars_pkg;
/

GRANT EXECUTE ON cars_pkg TO app_user_us, app_user_row;

Step 9: Let’s try it using this.
Step 9a: As APP_USER_US initially


app_user_us@dom11g>var v varchar2(48)
app_user_us@dom11g>set autoprint on
app_user_us@dom11g>select * from app_data.cars;

 ID MAKE       SECUR
--- ---------- -----
  2 Buick      US
  4 Chrysler   US
  6 Dodge      US

Elapsed: 00:00:00.01
app_user_us@dom11g>exec :v := app_data.cars_pkg.f_get_make(1);

PL/SQL procedure successfully completed.

V
---------------

Elapsed: 00:00:00.01
app_user_us@dom11g>exec :v := app_data.cars_pkg.f_get_make(2);

PL/SQL procedure successfully completed.

V
---------------
Buick

Elapsed: 00:00:00.01

As expected, APP_USER_US cannot see the Audi data, can see Buick. Perfect!
Step 9b: Let’s try APP_USER_ROW.



conn app_user_row/password@dom11g

app_user_row@dom11g>var v varchar2(48)
app_user_row@dom11g>set autoprint on
app_user_row@dom11g>select * from app_data.cars;

  ID MAKE       SECUR
---- ---------- -----
   1 Audi       ROW
   3 BMW        ROW
   5 Citroen    ROW

Elapsed: 00:00:00.00
app_user_row@dom11g>exec :v := app_data.cars_pkg.f_get_make(1);

PL/SQL procedure successfully completed.

V
----------

Elapsed: 00:00:00.01
app_user_row@dom11g>exec :v := app_data.cars_pkg.f_get_make(2);

PL/SQL procedure successfully completed.

V
----------
Buick

Elapsed: 00:00:00.01
app_user_row@dom11g>

Wrong! And just what I wanted to demo. By implementing a result function like this, I have bypassed my VPD and rendered results from this function unpredictable depending on who ran the results which were cached.

So, with the PL/SQL function RESULT_CACHE directive you have to be careful, not necessarily anymore so than certain other features but just something to be aware of.

Before I knew the solution, but after I had these results, what happened when I combined VPD with such a Result Cache mis-implementation was not a massive surprise, but I had expected that either an error message would be raised, similiar to Materialized Views or the MERGE statement (forward references to future articles on the challenges of combining VPD with those two), or perhaps that the RESULT_CACHE directive would be ignored.

However the behaviour is documented and the answer is very much documented in the PL/SQL Language Reference:


Making Result-Cached Functions Handle Session-Specific Settings
The PL/SQL function result-caching feature does not automatically handle dependence on session-specific application contexts. If you must cache the results of a function that depends on session-specific application contexts, you must pass the application context to the function as a parameter. You can give the parameter a default value, so that not every user must specify it.

The same advice applies to other session specific settings, e.g. NLS_DATE_FORMAT etc.

Step 10: Make safe the result_cache implementation of APP_DATA.CARS_PKG.


conn app_data/password@dom11g

CREATE OR REPLACE PACKAGE APP_DATA.cars_pkg
AS
  --
  FUNCTION f_get_make_safe (
    i_id          IN  cars.id%TYPE,
    i_can_see_us  IN  VARCHAR2 DEFAULT SYS_CONTEXT('SECURITY_CTX','CAN_SEE_US'),
    i_can_see_row IN  VARCHAR2 DEFAULT SYS_CONTEXT('SECURITY_CTX','CAN_SEE_ROW')
  )
  RETURN cars.make%TYPE;
  --
END cars_pkg;
/

CREATE OR REPLACE PACKAGE BODY APP_DATA.cars_pkg
AS
  --
  TYPE t_cars IS TABLE of cars%ROWTYPE INDEX BY PLS_INTEGER;
  --
  FUNCTION f_get_cars_safe (
    i_can_see_us  IN  VARCHAR2 DEFAULT SYS_CONTEXT('SECURITY_CTX','CAN_SEE_US'),
    i_can_see_row IN  VARCHAR2 DEFAULT SYS_CONTEXT('SECURITY_CTX','CAN_SEE_ROW')
  )
  RETURN t_cars
  RESULT_CACHE
  RELIES_ON (cars)
  AS
    --
    l_cars     t_cars;
    l_tmp_cars t_cars;
    i          PLS_INTEGER;
    --
  BEGIN
     --
     SELECT *
     BULK COLLECT INTO l_tmp_cars
     FROM   cars;
     --
     i := l_tmp_cars.FIRST;
     --
     WHILE (i IS NOT NULL)
     LOOP
         --
         l_cars(l_tmp_cars(i).id).id             := l_tmp_cars(i).id;
         l_cars(l_tmp_cars(i).id).make           := l_tmp_cars(i).make;
         l_cars(l_tmp_cars(i).id).security_label := l_tmp_cars(i).security_label;
         --
         i := l_tmp_cars.NEXT(i);
         --
     END LOOP;

     --
     RETURN l_cars;
     --
  END f_get_cars_safe;
  --
  FUNCTION f_get_make_safe (
    i_id          IN  cars.id%TYPE,
    i_can_see_us  IN  VARCHAR2 DEFAULT SYS_CONTEXT('SECURITY_CTX','CAN_SEE_US'),
    i_can_see_row IN  VARCHAR2 DEFAULT SYS_CONTEXT('SECURITY_CTX','CAN_SEE_ROW')
  )
  RETURN cars.make%TYPE
  AS
    --
    l_cars t_cars;
    --
  BEGIN
     --
     l_cars := f_get_cars_safe;
     RETURN l_cars(i_id).make;
     --
  EXCEPTION
       WHEN no_data_found THEN
            RETURN NULL;
  END f_get_make_safe;
  --
END cars_pkg;
/

Step 11: With this our previous function lookup should work as expected:
Step 11a: As APP_USER_US



conn app_user_us/password@dom11g

app_user_us@dom11g>var v varchar2(48)
app_user_us@dom11g>set autoprint on
app_user_us@dom11g>select * from app_data.cars;

  ID MAKE       SECUR
---- ---------- -----
   2 Buick      US
   4 Chrysler   US
   6 Dodge      US

Elapsed: 00:00:00.01
app_user_us@dom11g>exec :v := app_data.cars_pkg.f_get_make_safe(1);

PL/SQL procedure successfully completed.

V
----------

Elapsed: 00:00:00.01
app_user_us@dom11g>exec :v := app_data.cars_pkg.f_get_make_safe(2);

PL/SQL procedure successfully completed.

V
----------
Buick

Elapsed: 00:00:00.01



Step 11a: As APP_USER_ROW



conn app_user_row/password@dom11g

app_user_row@dom11g>var v varchar2(48)
app_user_row@dom11g>set autoprint on
app_user_row@dom11g>select * from app_data.cars;

  ID MAKE       SECUR
---- ---------- -----
   1 Audi       ROW
   3 BMW        ROW
   5 Citroen    ROW

Elapsed: 00:00:00.01
app_user_row@dom11g>exec :v := app_data.cars_pkg.f_get_make_safe(1);

PL/SQL procedure successfully completed.

V
----------
Audi

Elapsed: 00:00:00.01
app_user_row@dom11g>exec :v := app_data.cars_pkg.f_get_make_safe(2);

PL/SQL procedure successfully completed.

V
----------

Elapsed: 00:00:00.01
app_user_row@dom11g>

Problem fixed.

Moral
The moral of the story is simple in this case – RTFM.
This time everything I needed was there in the docs. I hadn’t picked up on it fully when I implemented the result cache procs when VPD wasn’t in the picture and I didn’t review the information up front when looking at the VPD solution.
Caveat Developer.

Documentation

Categories: 11.1.0.7 · oracle · result cache · vpd

11.1.0.7 Follow up

January 12, 2009 · 3 Comments

As a follow-up to Friday’s post about some local issues with the 11.1.0.7 patch, I’ve distilled an isolated piece of SQL to demonstrate the ORA-03113.

Demo test case looks like this:


DOM@11g>l
    WITH subq_goes_bang_data AS
         (SELECT 'CST1' database_name,'-6' ro_id,'\\xxxxx\uts_dev\SplitterToolSystemTestRO1' ros_path
          FROM   dual)
    ,    subq_db_name   AS
         (SELECT DECODE (INSTR (global_name, '.WORLD'),
                         0,global_name,
                         SUBSTR (global_name,1,INSTR (global_name,'.WORLD') - 1)
                        ) database_name
          FROM   global_name)
   ,    subq_goes_bang AS
       (
        SELECT rrc.ro_id,
               rrc.ros_path
        FROM   subq_goes_bang_data rrc
        WHERE rrc.database_name = (SELECT database_name FROM subq_db_name)
        UNION ALL
        SELECT rrc.ro_id,
               rrc.ros_path
        FROM   subq_goes_bang_data rrc
        WHERE  rrc.database_name = 'CST1'
        AND    NOT EXISTS(SELECT NULL
                          FROM subq_goes_bang_data rrc1
                          WHERE rrc1.database_name =(SELECT database_name FROM subq_db_name)
                          AND rrc1.ro_id = rrc.ro_id)
        AND    'CSP1' <>(SELECT database_name FROM subq_db_name)
   )
   SELECT  '<?xml version="1.0" encoding="UTF-16"?>'
           || XMLELEMENT ("LocalConfiguration",XMLFOREST (a.ros_path AS "RootFilePath"))
   FROM    subq_goes_bang a
  WHERE   a.ro_id = -6

DOM@11g>/
WITH subq_goes_bang_data AS
*
ERROR at line 1:
ORA-03113: end-of-file on communication channel

Elapsed: 00:00:00.05
DOM@11g>

Comment out the XML bits and bobs and just select the column – no problem.
Comment out the UNION ALL in subq_goes_bang – no problem.

Explain plan looks like this:


DOM@11g>explain plan for
  2  WITH subq_goes_bang_data AS
  3       (SELECT 'CST1' database_name,'-6' ro_id,'\\xxxxx\uts_dev\SplitterToolSystemTestRO1' ros_path
  4        FROM   dual)
  5  ,    subq_db_name   AS
  6       (SELECT DECODE (INSTR (global_name, '.WORLD'),
  7                       0,global_name,
  8                       SUBSTR (global_name,1,INSTR (global_name,'.WORLD') - 1)
  9                      ) database_name
 10        FROM   global_name)
 11  ,    subq_goes_bang AS
 12      (
 13       SELECT rrc.ro_id,
 14              rrc.ros_path
 15       FROM   subq_goes_bang_data rrc
 16       WHERE rrc.database_name = (SELECT database_name FROM subq_db_name)
 17       UNION ALL
 18       SELECT rrc.ro_id,
 19              rrc.ros_path
 20       FROM   subq_goes_bang_data rrc
 21       WHERE  rrc.database_name = 'CST1'
 22       AND    NOT EXISTS(SELECT NULL
 23                         FROM subq_goes_bang_data rrc1
 24                         WHERE rrc1.database_name =(SELECT database_name FROM subq_db_name)
 25                         AND rrc1.ro_id = rrc.ro_id)
 26       AND    'CSP1' <> (SELECT database_name FROM subq_db_name)
 27  )
 28  SELECT  '<?xml version="1.0" encoding="UTF-16"?>'
 29          || XMLELEMENT ("LocalConfiguration",XMLFOREST (a.ros_path AS "RootFilePath"))
 30  FROM    subq_goes_bang a
 31  WHERE   a.ro_id = -6;

Explained.

Elapsed: 00:00:00.00
DOM@11g>
DOM@11g>select * from table(dbms_xplan.display);

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------
Plan hash value: 4143783385

----------------------------------------------------------------------------------------------------
| Id  | Operation                  | Name                        | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT           |                             |     2 |    86 |    16   (7)| 00:00:01 |
|   1 |  TEMP TABLE TRANSFORMATION |                             |       |       |            |          |
|   2 |   LOAD AS SELECT           |                             |       |       |            |          |
|   3 |    INDEX FULL SCAN         | SYS_IOT_TOP_57625           |     1 |       |     1   (0)| 00:00:01 |
|   4 |   LOAD AS SELECT           |                             |       |       |            |          |
|*  5 |    TABLE ACCESS FULL       | PROPS$                      |     1 |    27 |     2   (0)| 00:00:01 |
|   6 |   VIEW                     |                             |     2 |    86 |    13   (8)| 00:00:01 |
|   7 |    UNION-ALL               |                             |       |       |            |          |
|*  8 |     VIEW                   |                             |     1 |    53 |     2   (0)| 00:00:01 |
|   9 |      TABLE ACCESS FULL     | SYS_TEMP_0FD9D6643_B0B39986 |     1 |    53 |     2   (0)| 00:00
|  10 |      VIEW                  |                             |     1 |  2002 |     2   (0)| 00:00:01 |
|  11 |       TABLE ACCESS FULL    | SYS_TEMP_0FD9D6644_B0B39986 |     1 |    27 |     2   (0)| 00:0
|* 12 |     FILTER                 |                             |       |       |            |          |
|* 13 |      HASH JOIN ANTI        |                             |     1 |    57 |     7  (15)| 00:00:01 |
|* 14 |       VIEW                 |                             |     1 |    53 |     2   (0)| 00:00:01 |
|  15 |        TABLE ACCESS FULL   | SYS_TEMP_0FD9D6643_B0B39986 |     1 |    53 |     2   (0)| 00:0
|  16 |       VIEW                 | VW_SQ_1                     |     1 |     4 |     4   (0)| 00:00:01 |
|* 17 |        VIEW                |                             |     1 |    10 |     2   (0)| 00:00:01 |
|  18 |         TABLE ACCESS FULL  | SYS_TEMP_0FD9D6643_B0B39986 |     1 |    53 |     2   (0)| 00:00:01 |
|  19 |         VIEW               |                             |     1 |  2002 |     2   (0)| 00:00:01 |
|  20 |          TABLE ACCESS FULL | SYS_TEMP_0FD9D6644_B0B39986 |     1 |    27 |     2   (0)| 00:00:01 |
|  21 |      VIEW                  |                             |     1 |  2002 |     2   (0)| 00:00:01 |
|  22 |       TABLE ACCESS FULL    | SYS_TEMP_0FD9D6644_B0B39986 |     1 |    27 |     2   (0)| 00:0
----------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   5 - filter("NAME"='GLOBAL_DB_NAME')
   8 - filter(TO_NUMBER("RRC"."RO_ID")=(-6) AND "RRC"."DATABASE_NAME"= (SELECT "DATABASE_NAME"
              FROM  (SELECT /*+ CACHE_TEMP_TABLE ("T1") */ "C0" "DATABASE_NAME" FROM
              "SYS"."SYS_TEMP_0FD9D6644_B0B39986" "T1") "SUBQ_DB_NAME"))
  12 - filter( (SELECT "DATABASE_NAME" FROM  (SELECT /*+ CACHE_TEMP_TABLE ("T1") */ "C0"
              "DATABASE_NAME" FROM "SYS"."SYS_TEMP_0FD9D6644_B0B39986" "T1") "SUBQ_DB_NAME")<>'CSP1')
  13 - access("ITEM_0"="RRC"."RO_ID")
  14 - filter("RRC"."DATABASE_NAME"='CST1' AND TO_NUMBER("RRC"."RO_ID")=(-6))
  17 - filter("RRC1"."DATABASE_NAME"= (SELECT "DATABASE_NAME" FROM  (SELECT /*+ CACHE_TEMP_TABLE
              ("T1") */ "C0" "DATABASE_NAME" FROM "SYS"."SYS_TEMP_0FD9D6644_B0B39986" "T1") "SUBQ_DB_NAME")

43 rows selected.

Elapsed: 00:00:00.01
DOM@11g>

So, to Oracle support with some trace files.

But I’d be interested if anyone else with an 11.1.0.7 environment can run the above statement.

Categories: 11.1.0.7 · 11g · ora-03113 · oracle