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.

Before You Begin

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

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.


Step 1 — Create the Solid User

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

-- 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;

Replace {db_name} with your PDB name and {password} with a strong password.

Repeat for each PDB you want to connect to Solid.


Step 2 — 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


Step 3 — 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

Use the PDB service name, not the CDB SID. Using the CDB SID may result in connection or permission errors.


What Solid Collects

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

Metadata (always collected)

Structure of your database — no row data involved.

WhatWhy
Table and column names, types, and propertiesTo build the data catalog
Primary and foreign key relationshipsTo map data lineage
Schema and object ownershipTo organize by team and domain
Table creation and modification timestampsTo track data freshness
Table and column commentsTo surface business descriptions

Query History (always collected)

Used to understand how data is actually being used.

WhatWhy
SQL query text (SELECT only)To learn which tables and columns are popular
Executing user and roleTo understand who uses what
Execution timestamps and durationTo identify performance patterns

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.

User Information (always collected)

WhatWhy
Usernames, IDs, account statusTo attribute query history to users
Last login dateTo identify active vs. inactive users

Table Usage Statistics (always collected)

Daily counts of inserts, updates, and deletes per table — used to surface active tables and detect stale data.

Data Profiling (optional)

Only collected if SELECT ANY TABLE is granted. Solid samples a small number of rows to compute:

  • MIN / MAX / AVG for numeric columns
  • Distinct value counts for text columns
  • Date ranges for date columns
  • Null rates and basic data quality metrics

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

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 — PDB architecture required
Oracle 18cSupported
Oracle 19cSupported (recommended LTS version)
Oracle 21cSupported
Oracle 23cSupported

Non-CDB Oracle deployments are not supported. If you're running a traditional single-instance Oracle database without PDBs, contact your Solid administrator.


Appendix — Collect Queries

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;