Oracle

This guide walks you through connecting your Oracle database to Solid. By the end, Solid will be able to discover your data, collect metadata, and analyze query history.

Overview

This guide walks you through connecting your Oracle database to Solid. By the end, Solid will be able to discover your data, collect metadata, and analyze query history.

Make sure you have the following:

  • Admin access to the Oracle Pluggable Database (PDB) you want to connect
  • A SQL client (e.g., SQL*Plus, SQL Developer, or similar)
  • The PDB service name — you'll need this for the Solid connection string
  • Read access to your data (SELECT ANY TABLE) — this is required, not optional, for Solid's Text2SQL engine to generate and validate SQL

What is a PDB?
Oracle databases use a "container" architecture. The top level is the CDB (Container Database), and inside it are one or more PDBs (Pluggable Databases). Solid connects at the PDB level — this is where your actual data lives.


What Is Collected

Here's a plain-English summary of what Solid reads from your database — and what it never touches.

CategoryWhatWhy
MetadataTable and column names, types, and propertiesTo build the data catalog
MetadataPrimary and foreign key relationshipsTo map data lineage
MetadataSchema and object ownershipTo organize by team and domain
MetadataTable creation and modification timestampsTo track data freshness
MetadataTable and column commentsTo surface business descriptions
Query HistorySQL query text (SELECT only)To learn which tables and columns are popular
Query HistoryExecuting user and roleTo understand who uses what
Query HistoryExecution timestamps and durationTo identify performance patterns
User InformationUsernames, IDs, account statusTo attribute query history to users
User InformationLast login dateTo identify active vs. inactive users
Table Usage StatisticsDaily counts of inserts, updates, and deletes per tableTo surface active tables and detect stale data
Data ProfilingSampled rows: MIN/MAX/AVG for numeric columns, distinct value counts for text columns, date ranges for date columns, and null rates / basic data quality metricsSELECT ANY TABLE is required for Solid's Text2SQL engine to generate and validate SQL, and is also used to sample this data

Query history requires AWR.
Solid reads from Oracle's Automatic Workload Repository (AWR), which requires the Oracle Diagnostics Pack license (included with Enterprise Edition). Check with your DBA if you're unsure whether AWR is enabled.


Automatic Pull (Recommended)

Create the Solid User

Run this script while connected as an admin user inside the target PDB.

Two options are provided: a quick setup using broad system privileges (simpler), or a scoped least-privilege setup using explicit grants (recommended for security-sensitive environments).

Option A — Quick Setup

-- Set your PDB as the active container
ALTER SESSION SET CONTAINER = {db_name};
 
-- Create the Solid user
CREATE USER solid_read_only_user IDENTIFIED BY "{password}";
 
-- Allow the user to log in
GRANT CREATE SESSION TO solid_read_only_user;
 
-- Allow access to database structure and metadata
GRANT SELECT ANY DICTIONARY TO solid_read_only_user;
 
-- Allow read-only access to table data
GRANT SELECT ANY TABLE TO solid_read_only_user;
 
-- Lock the user to this PDB only (prevents cross-container access)
ALTER USER solid_read_only_user SET CONTAINER_DATA = CURRENT;

SELECT ANY DICTIONARY and SELECT ANY TABLE are broader than Solid strictly requires, but get you to a working connection in a single script. Repeat for each PDB you want to connect to Solid.

Option B — Scoped Least-Privilege Setup

This grants access only to the specific dictionary views Solid queries and the schemas you explicitly allow. Run while connected as an admin user inside the target PDB.

-- Step 1: Create the user and allow login
ALTER SESSION SET CONTAINER = {db_name};
CREATE USER solid_read_only_user IDENTIFIED BY "{password}";
GRANT CREATE SESSION TO solid_read_only_user;
ALTER USER solid_read_only_user SET CONTAINER_DATA = CURRENT;

-- Step 2: Grant SELECT on each dictionary view Solid uses
-- (replaces SELECT ANY DICTIONARY)
GRANT SELECT ON SYS.DBA_TAB_COLUMNS     TO solid_read_only_user;
GRANT SELECT ON SYS.DBA_TABLES          TO solid_read_only_user;
GRANT SELECT ON SYS.DBA_OBJECTS         TO solid_read_only_user;
GRANT SELECT ON SYS.DBA_TAB_MODIFICATIONS TO solid_read_only_user;
GRANT SELECT ON SYS.DBA_TAB_COMMENTS    TO solid_read_only_user;
GRANT SELECT ON SYS.DBA_COL_COMMENTS    TO solid_read_only_user;
GRANT SELECT ON SYS.DBA_IND_COLUMNS     TO solid_read_only_user;
GRANT SELECT ON SYS.DBA_CONSTRAINTS     TO solid_read_only_user;
GRANT SELECT ON SYS.DBA_CONS_COLUMNS    TO solid_read_only_user;
GRANT SELECT ON SYS.DBA_USERS          TO solid_read_only_user;
GRANT SELECT ON SYS.DBA_ROLE_PRIVS      TO solid_read_only_user;
GRANT SELECT ON SYS.V_$PDBS             TO solid_read_only_user;

-- AWR views (requires Oracle Diagnostics Pack license)
GRANT SELECT ON SYS.DBA_HIST_SQLSTAT    TO solid_read_only_user;
GRANT SELECT ON SYS.DBA_HIST_SNAPSHOT   TO solid_read_only_user;
GRANT SELECT ON SYS.DBA_HIST_SQLTEXT    TO solid_read_only_user;

-- Step 3: Grant schema-scoped SELECT for data profiling
-- (replaces SELECT ANY TABLE)
-- Run this block once per schema Solid should monitor.
-- Replace {schema_name} with each schema name (uppercase).
BEGIN
  FOR t IN (
    SELECT owner, table_name
    FROM dba_tables
    WHERE owner = UPPER('{schema_name}')
      AND table_name NOT LIKE 'BIN$%'
  ) LOOP
    EXECUTE IMMEDIATE
      'GRANT SELECT ON ' || t.owner || '.' || t.table_name ||
      ' TO solid_read_only_user';
  END LOOP;
END;
/

Repeat the Step 3 block for every schema Solid should be able to profile and validate SQL against. Tables added to a schema after this script runs will need an incremental grant — the block only covers tables that exist at run time.

Repeat for each PDB you want to connect to Solid.

Verify the Setup

Log in as solid_read_only_user and run these checks to confirm everything is working.

Check 1 — User is local to the PDB

SELECT username, common, oracle_maintained
FROM dba_users
WHERE username = 'SOLID_READ_ONLY_USER';

Expected: COMMON = NO


Check 2 — User cannot access the root container

ALTER SESSION SET CONTAINER = CDB$ROOT;

Expected: ORA-01031: insufficient privileges ← this error means isolation is working correctly


Check 3 — User can read metadata

SELECT owner, table_name
FROM dba_tables
WHERE owner = '{schema}'
AND ROWNUM <= 5;

Expected: Rows returned without errors


Check 4 — User can read table data

SELECT *
FROM {schema}.{table}
WHERE ROWNUM <= 1;

Expected: A row returned without errors

Connect to Solid

In Solid, create a new Oracle data source connection using:

FieldValue
HostYour Oracle listener address
Port1521 (default)
Service nameYour PDB service name
Usernamesolid_read_only_user
PasswordThe password you set above

Permissions Needed

Permission/GrantPurpose
CREATE SESSIONAllows the user to log in
SELECT ANY DICTIONARYAllows access to database structure and metadata
SELECT ANY TABLEAllows read-only access to table data; also required for Solid's Text2SQL engine to generate and validate SQL
CONTAINER_DATA = CURRENT (via ALTER USER ... SET CONTAINER_DATA = CURRENT)Locks the user to this PDB only, preventing cross-container access

Troubleshooting

"Insufficient privileges" errors

Symptoms: Errors when Solid tries to read metadata or table structure.

  1. Confirm the grants were run inside the PDB, not at the CDB level
  2. Verify the user's privileges:
SELECT * FROM dba_sys_privs
WHERE grantee = 'SOLID_READ_ONLY_USER';

You should see rows for CREATE SESSION, SELECT ANY DICTIONARY, and SELECT ANY TABLE.


Query history is missing or empty

Symptoms: Solid shows no query history after connecting.

  1. Confirm AWR is collecting snapshots:
SELECT MIN(end_interval_time), MAX(end_interval_time)
FROM dba_hist_snapshot;

If this returns no rows, AWR is not running. Contact your DBA to enable it.

  1. Check that Solid's last_collect timestamp falls within AWR's retention window (typically 8–30 days).

Can't connect to the database

Symptoms: Solid returns a connection timeout or authentication error.

  1. Confirm you're using the PDB service name, not the CDB SID
  2. Check the account isn't locked:
SELECT username, account_status, lock_date
FROM dba_users
WHERE username = 'SOLID_READ_ONLY_USER';
  1. Verify the Oracle listener is reachable on port 1521 from Solid's network

Security Notes

The solid_read_only_user account is built on least-privilege principles:

  • Read-only — no INSERT, UPDATE, DELETE, or DDL privileges
  • PDB-scoped — cannot access the root CDB or other PDBs
  • No admin rolesDBA, SYSDBA, and similar roles are not granted

On SELECT ANY TABLE and SELECT ANY DICTIONARY: The grants in the setup script above are the simplest path to a working connection. However, both are broader than strictly required — SELECT ANY TABLE grants read access to every table in the database, and SELECT ANY DICTIONARY grants read access to all system catalog views. Organizations with strict least-privilege policies can replace these with explicit grants scoped to only the schemas Solid monitors and the specific dictionary views Solid queries. Contact your Solid team for the scoped grant script if your security posture requires it.

Additional recommendations:

  • Use a strong, unique password and rotate it per your organization's policy
  • Lock the account when not in use for extended periods
  • Restrict access to Oracle's listener port (1521) via firewall rules
  • Enable SSL/TLS or Oracle Wallet for encrypted connections where available

Multi-Database Support

Solid currently supports one PDB per connection. To monitor multiple databases, add a separate Solid data source for each PDB.

Support for discovering multiple databases through a single connection is planned for a future release.


Supported Oracle Versions

VersionNotes
Oracle 12c (12.2+)Supported
Oracle 18cSupported
Oracle 19cSupported (recommended LTS version)
Oracle 21cSupported
Oracle 23cSupported


Appendix

These are the SQL queries Solid runs internally to collect metadata and usage data. They are provided for transparency and for DBAs who want to audit exactly what Solid reads.

Columns

SELECT
    (SELECT NAME FROM V$PDBS) AS TABLE_CATALOG,
    atc.OWNER AS TABLE_SCHEMA,
    atc.TABLE_NAME,
    atc.COLUMN_NAME,
    atc.COLUMN_ID AS ORDINAL_POSITION,
    atc.DATA_TYPE,
    CASE WHEN atc.NULLABLE = 'Y' THEN 'YES' ELSE 'NO' END AS IS_NULLABLE,
    atc.DATA_DEFAULT AS COLUMN_DEFAULT,
    tc.COMMENTS AS TABLE_COMMENT,
    cc.COMMENTS AS COLUMN_COMMENT,
    ao.CREATED AS TABLE_CREATED_AT,
    ao.CREATED AS COLUMN_CREATED_AT,
    COALESCE(atm.TIMESTAMP, ao.CREATED) AS TABLE_UPDATED_AT,
    NVL(cons.IS_PK, 'FALSE') AS IS_PK,
    NVL(cons.IS_FK, 'FALSE') AS IS_FK,
    CASE
        WHEN idx.COLUMN_NAME IS NOT NULL THEN 'TRUE'
        ELSE 'FALSE'
    END AS IS_INDEX
FROM DBA_TAB_COLUMNS atc
JOIN DBA_TABLES at ON atc.OWNER = at.OWNER AND atc.TABLE_NAME = at.TABLE_NAME
JOIN DBA_OBJECTS ao ON atc.TABLE_NAME = ao.OBJECT_NAME AND atc.OWNER = ao.OWNER
LEFT JOIN DBA_TAB_MODIFICATIONS atm ON atc.OWNER = atm.TABLE_OWNER AND atc.TABLE_NAME = atm.TABLE_NAME
LEFT JOIN DBA_TAB_COMMENTS tc ON atc.OWNER = tc.OWNER AND atc.TABLE_NAME = tc.TABLE_NAME
LEFT JOIN DBA_COL_COMMENTS cc ON atc.OWNER = cc.OWNER AND atc.TABLE_NAME = cc.TABLE_NAME AND atc.COLUMN_NAME = cc.COLUMN_NAME
LEFT JOIN (
    SELECT DISTINCT TABLE_OWNER, TABLE_NAME, COLUMN_NAME
    FROM DBA_IND_COLUMNS
) idx ON atc.OWNER = idx.TABLE_OWNER
     AND atc.TABLE_NAME = idx.TABLE_NAME
     AND atc.COLUMN_NAME = idx.COLUMN_NAME
LEFT JOIN (
    SELECT
        acc.OWNER,
        acc.TABLE_NAME,
        acc.COLUMN_NAME,
        MAX(CASE WHEN ac.CONSTRAINT_TYPE = 'P' THEN 'TRUE' ELSE 'FALSE' END) AS IS_PK,
        MAX(CASE WHEN ac.CONSTRAINT_TYPE = 'R' THEN 'TRUE' ELSE 'FALSE' END) AS IS_FK
    FROM DBA_CONSTRAINTS ac
    JOIN DBA_CONS_COLUMNS acc ON ac.CONSTRAINT_NAME = acc.CONSTRAINT_NAME AND ac.OWNER = acc.OWNER
    WHERE ac.CONSTRAINT_TYPE IN ('P', 'R')
    GROUP BY acc.OWNER, acc.TABLE_NAME, acc.COLUMN_NAME
) cons
    ON atc.OWNER = cons.OWNER
    AND atc.TABLE_NAME = cons.TABLE_NAME
    AND atc.COLUMN_NAME = cons.COLUMN_NAME
WHERE (SELECT NAME FROM V$PDBS) = '{db_name}'
  AND atc.owner IN ({schema_list})
ORDER BY atc.OWNER, atc.TABLE_NAME, atc.COLUMN_ID;

Foreign Keys and Primary Keys

SELECT
    (SELECT NAME FROM V$PDBS) AS fk_database_name,
    acc.OWNER AS fk_schema_name,
    acc.TABLE_NAME AS fk_table_name,
    acc.COLUMN_NAME AS fk_column_name,
    (SELECT NAME FROM V$PDBS) AS pk_database_name,
    p_acc.OWNER AS pk_schema_name,
    p_acc.TABLE_NAME AS pk_table_name,
    p_acc.COLUMN_NAME AS pk_column_name,
    ac.R_CONSTRAINT_NAME AS pk_name,
    acc.POSITION AS key_sequence,
    'NO ACTION' AS update_rule,
    ac.delete_rule,
    ac.CONSTRAINT_NAME AS fk_name,
    ac.deferrable,
    ac.deferred,
    ac.rely,
    ao.CREATED AS created_on
FROM DBA_CONS_COLUMNS acc
JOIN DBA_CONSTRAINTS ac
    ON acc.CONSTRAINT_NAME = ac.CONSTRAINT_NAME AND acc.OWNER = ac.OWNER
JOIN DBA_OBJECTS ao
    ON acc.OWNER = ao.OWNER AND acc.TABLE_NAME = ao.OBJECT_NAME
JOIN DBA_CONS_COLUMNS p_acc
    ON ac.R_CONSTRAINT_NAME = p_acc.CONSTRAINT_NAME
    AND ac.R_OWNER = p_acc.OWNER
    AND acc.POSITION = p_acc.POSITION
WHERE (SELECT NAME FROM V$PDBS) = '{db_name}'
  AND acc.OWNER IN ({schema_list})
  AND ac.CONSTRAINT_TYPE = 'R'
  AND ao.OBJECT_TYPE = 'TABLE'
  AND acc.TABLE_NAME NOT LIKE 'BIN$%'
ORDER BY acc.OWNER, acc.TABLE_NAME, ac.CONSTRAINT_NAME, acc.POSITION;

Query History

WITH USER_ROLES_CTE AS (
    SELECT
        GRANTEE,
        LISTAGG(DISTINCT GRANTED_ROLE, ', ') WITHIN GROUP (ORDER BY GRANTED_ROLE) AS ASSIGNED_ROLES
    FROM DBA_ROLE_PRIVS
    GROUP BY GRANTEE
)
SELECT
    stat.SQL_ID AS QUERY_ID,
    q.SQL_TEXT AS QUERY_TEXT,
    p.NAME AS DATABASE_NAME,
    stat.PARSING_SCHEMA_NAME AS SCHEMA_NAME,
    CASE q.COMMAND_TYPE
        WHEN 3   THEN 'SELECT'
        WHEN 2   THEN 'INSERT'
        WHEN 6   THEN 'UPDATE'
        WHEN 7   THEN 'DELETE'
        WHEN 189 THEN 'MERGE'
        ELSE 'OTHER (' || q.COMMAND_TYPE || ')'
    END AS QUERY_TYPE,
    u.USERNAME AS USER_NAME,
    r.ASSIGNED_ROLES AS ROLE_NAME,
    CASE
        WHEN NVL(stat.ROWS_PROCESSED_DELTA, 0) > 0 THEN 'SUCCESS'
        ELSE 'FAILED'
    END AS EXECUTION_STATUS,
    snaps.END_INTERVAL_TIME - NUMTODSINTERVAL(stat.ELAPSED_TIME_DELTA / 1000000, 'SECOND') AS START_TIME,
    snaps.END_INTERVAL_TIME AS END_TIME,
    ROUND(NVL(stat.ELAPSED_TIME_DELTA, 0) / 1000, 2) AS TOTAL_ELAPSED_TIME_MS,
    u.ORACLE_MAINTAINED AS IS_ORACLE_MAINTAINED
FROM DBA_HIST_SQLSTAT stat
JOIN DBA_HIST_SNAPSHOT snaps ON stat.SNAP_ID = snaps.SNAP_ID AND stat.DBID = snaps.DBID
JOIN DBA_HIST_SQLTEXT q ON stat.SQL_ID = q.SQL_ID AND stat.DBID = q.DBID AND stat.CON_DBID = q.CON_DBID
JOIN DBA_USERS u ON stat.PARSING_USER_ID = u.USER_ID
JOIN V$PDBS p ON stat.CON_ID = p.CON_ID
LEFT JOIN USER_ROLES_CTE r ON u.USERNAME = r.GRANTEE
WHERE p.NAME = '{db_name}'
  AND (u.ORACLE_MAINTAINED = 'N' OR u.USERNAME != 'SYS')
  AND snaps.END_INTERVAL_TIME > TIMESTAMP '{last_collect}'
  AND q.COMMAND_TYPE = 3
ORDER BY snaps.END_INTERVAL_TIME DESC, stat.SQL_ID
FETCH FIRST 1000000 ROWS ONLY;

Users

SELECT
    user_id,
    username AS name,
    created AS created_on,
    '' AS deleted_on,
    CASE
        WHEN account_status LIKE '%LOCKED%' THEN 'LOCKED'
        WHEN account_status = 'OPEN' THEN 'ACTIVE'
        ELSE account_status
    END AS status,
    username AS login_name,
    username AS display_name,
    last_login,
    '' AS first_name,
    '' AS last_name,
    '' AS email,
    CASE WHEN oracle_maintained = 'N' THEN 'HUMAN' ELSE 'MACHINE' END AS user_type
FROM dba_users
ORDER BY username;

Table Usage Statistics

SELECT
    (SELECT NAME FROM V$PDBS) AS DATABASE_NAME,
    at.OWNER AS SCHEMA_NAME,
    at.TABLE_NAME AS TABLE_NAME,
    TO_CHAR(TRUNC(atm.TIMESTAMP), 'YYYY-MM-DD') AS DAY,
    SUM(atm.INSERTS) AS TOTAL_ROWS_ADDED,
    SUM(atm.DELETES) AS TOTAL_ROWS_REMOVED,
    SUM(atm.UPDATES) AS TOTAL_ROWS_UPDATED,
    (SUM(atm.INSERTS) + SUM(atm.DELETES) + SUM(atm.UPDATES)) AS TOTAL_COUNT,
    MAX(at.LAST_ANALYZED) AS LAST_ANALYZED
FROM DBA_TABLES at
JOIN DBA_TAB_MODIFICATIONS atm
    ON atm.TABLE_OWNER = at.OWNER AND atm.TABLE_NAME = at.TABLE_NAME
WHERE (SELECT NAME FROM V$PDBS) = '{db_name}'
  AND at.OWNER IN ({schema_list})
  AND atm.TIMESTAMP BETWEEN TIMESTAMP '{start_day_to_collect}' AND TIMESTAMP '{end_day_to_collect}'
GROUP BY at.OWNER, at.TABLE_NAME, TRUNC(atm.TIMESTAMP)
HAVING (SUM(atm.INSERTS) + SUM(atm.DELETES) + SUM(atm.UPDATES)) > 0
ORDER BY DAY, SCHEMA_NAME, TABLE_NAME;

Did this page help you?