DBA Blogs
How to modify the database parameters in Data guard with RAC
Application Containers leaving behind "Fake" PDBs
Natural Language Support for Interactive Report Using OpenAI gpt-5.6 Models Fails
Compound Triggers: Are declarations in timing_point section allowed?
Best Practices for Optimizer Statistics on Large Volatile Tables in Oracle 19.31: Dynamic Sampling, Real-Time Statistics, or Manual Gathering?
How to Minimize Optimizer Regressions Without Hints or Forced Execution Plans?
Os Authnetication on CDB
How to automatically reconnect node-oracledb Thin connection pool after Amazon RDS Oracle restart?
New JOIN TO ONE clause in 26.2 SELECT
I've just published a short video on the new JOIN TO ONE clause in SELECT statements in Oracle 26ai 26.2
This clause allows you to let the database automatically determine JOIN columns based on Primary Key and Foreign Key relationships configured in the database. JOIN TO ONE defaults to doing a LEFT OUTER JOIN so I also demonstrate how to use it for INNER JOINs
Tracing a Power BI DirectQuery Refresh in Oracle
In today's video I have demonstrated how Power BI can use DirectQuery to query an Oracle database and refresh reports without actually storing the data in the Power BI file (as would be done if "Import" was used instead of DirectQuery).
I have used SQL Tracing in the Database Instance to identify the SQL statement that Power BI executes
For the first visual in Power BI which shows total salary by Department, the Power BI module and SQL statement are identified as :
MODULE NAME:(msmdsrv.exe)
CLIENT DRIVER:(ODPM.NET : 23.6.0.0.0)
sqlid='c8a9qd2dzks8h'
SELECT * FROM (
SELECT
*
FROM
(
SELECT
"t1"."DEPARTMENT_NAME" "c6", SUM ( "t4"."SALARY" )
"a0"
FROM
((
select "$Table"."EMPLOYEE_ID" as "EMPLOYEE_ID",
"$Table"."FIRST_NAME" as "FIRST_NAME",
"$Table"."LAST_NAME" as "LAST_NAME",
"$Table"."EMAIL" as "EMAIL",
"$Table"."PHONE_NUMBER" as "PHONE_NUMBER",
"$Table"."HIRE_DATE" as "HIRE_DATE",
"$Table"."JOB_ID" as "JOB_ID",
"$Table"."SALARY" as "SALARY",
"$Table"."COMMISSION_PCT" as "COMMISSION_PCT",
"$Table"."MANAGER_ID" as "MANAGER_ID",
"$Table"."DEPARTMENT_ID" as "DEPARTMENT_ID"
from "HR"."EMPLOYEES" "$Table"
) "t4"
LEFT OUTER JOIN
(
select "$Table"."DEPARTMENT_ID" as "DEPARTMENT_ID",
"$Table"."DEPARTMENT_NAME" as "DEPARTMENT_NAME",
"$Table"."MANAGER_ID" as "MANAGER_ID",
"$Table"."LOCATION_ID" as "LOCATION_ID"
from "HR"."DEPARTMENTS" "$Table"
) "t1" on
(
"t4"."DEPARTMENT_ID" = "t1"."DEPARTMENT_ID"
)
)
GROUP BY "t1"."DEPARTMENT_NAME"
)
"MainTable"
WHERE
(
NOT(
(
"a0" IS NULL
)
)
)
ORDER BY "a0"
DESC
,"c6"
ASC
) WHERE ROWNUM (lessthan) 1001
MODULE NAME:(msmdsrv.exe)
CLIENT DRIVER:(ODPM.NET : 23.6.0.0.0)
sqlid='dh7nbfqsy942q'
SELECT
"t1"."DEPARTMENT_NAME" "c6",
COUNT("t4"."EMPLOYEE_ID")
"a0"
FROM
((
select "$Table"."EMPLOYEE_ID" as "EMPLOYEE_ID",
"$Table"."FIRST_NAME" as "FIRST_NAME",
"$Table"."LAST_NAME" as "LAST_NAME",
"$Table"."EMAIL" as "EMAIL",
"$Table"."PHONE_NUMBER" as "PHONE_NUMBER",
"$Table"."HIRE_DATE" as "HIRE_DATE",
"$Table"."JOB_ID" as "JOB_ID",
"$Table"."SALARY" as "SALARY",
"$Table"."COMMISSION_PCT" as "COMMISSION_PCT",
"$Table"."MANAGER_ID" as "MANAGER_ID",
"$Table"."DEPARTMENT_ID" as "DEPARTMENT_ID"
from "HR"."EMPLOYEES" "$Table"
) "t4"
LEFT OUTER JOIN
(
select "$Table"."DEPARTMENT_ID" as "DEPARTMENT_ID",
"$Table"."DEPARTMENT_NAME" as "DEPARTMENT_NAME",
"$Table"."MANAGER_ID" as "MANAGER_ID",
"$Table"."LOCATION_ID" as "LOCATION_ID"
from "HR"."DEPARTMENTS" "$Table"
) "t1" on
(
"t4"."DEPARTMENT_ID" = "t1"."DEPARTMENT_ID"
)
)
GROUP BY "t1"."DEPARTMENT_NAME"
Thus, every refresh runs a number of queries -- some to synchronise the schema from Oracle to Power BI and others to refresh the numbers to present in the Visuals.
This proves that the actual load of computing the GROUP BY and aggregations is in the *database instance* (because that is where the data actually resides) and not in the Power BI file (because no data is copied into the Power BI file)Dealing with JSON serialization and how to convert JSON object strings back and forth
A simple reminder about how JSON PL/SQL methods deal with JSON values, it easy to get confused when you mix up JSON objects and their serialized counterparts, especially if some if these parts are coming from JSON SQL functions and you need to combine them with other parts generated by PL/SQL.
The code below should clarify the difference between a "real" JSON value and its textual representation, especially when you are assembling a JSON object with values containing other JSON objects or their serialized representation.
When you PUT a serialized JSON string into a new JSON, the method escapes all the special characters that otherwise would break the syntax (lines 10-16).
In order to reconstruct a valid JSON string, something that you can PARSE as JSON, you need to retrieve the value with GET_STRING or GET_CLOB if it is large (lines 18-20).
If you need to include a serialized JSON object into a new JSON object thus avoiding the automatic escaping, then you need to convert the serialized JSON string into a proper JSON object and then PUT it inside the new object (lines 26-29).
declare
c varchar2(255) := '{"key": 1, "value": "X"}';
d varchar2(255);
j json_object_t;
j1 json_object_t;
j2 json_object_t;
begin
if c is json then
dbms_output.put_line('c is a string containing a valid JSON');
j := json_object_t.parse(c);
j1 := new json_object_t;
j1.put('document', c);
dbms_output.put_line('c is now escaped and becomes a string literal value');
dbms_output.put_line(j1.to_string());
dbms_output.new_line;
d := j1.get_string('document');
dbms_output.put_line('c is converted back into the original JSON object string');
dbms_output.put_line(d);
dbms_output.new_line;
dbms_output.new_line;
j2 := new json_object_t;
j2.put('document', j);
dbms_output.put_line('c is still a json object value, now embedded in a new JSON object');
dbms_output.put_line(j2.to_string());
else
dbms_output.put_line('NOT JSON');
end if;
end;
/Watch out for NULL values because GET_STRING and GET_CLOB show two different behaviors in older releases of Oracle 19c.
GoldenGate FUNCTIONSTACKSIZE: The Parameter You’ve Never Heard Of
GoldenGate's FUNCTIONSTACKSIZE error isn't about functions — wide tables over 200 columns trigger it. Here's the one-line fix.
The post GoldenGate FUNCTIONSTACKSIZE: The Parameter You’ve Never Heard Of appeared first on DBASolved.
Protect Your Oracle Database from SQL Injection and Unauthorized Access with SQL Firewall
SQL injection and credential misuse remain serious threats to database security. Oracle SQL Firewall offers a practical way to defend against these risks by allowing only approved SQL statements and trusted connection paths for each database user.
Instead of relying solely on application-level controls, SQL Firewall operates directly inside the database kernel. This makes it difficult to bypass and gives you fine-grained control over what each user can do.
How SQL Firewall WorksSQL Firewall uses an allow-list approach. You first let it observe normal activity for a user, then create a policy that defines exactly which SQL statements and connection details are permitted. Anything outside that policy triggers a violation that can be logged or blocked in real time.
It evaluates both the SQL statement itself and the context in which it runs, including the client IP address, operating system user, and program name. This helps protect against stolen credentials and unexpected access paths.
Key Benefits in Practice- Inspects every SQL statement, including those generated inside PL/SQL
- Works whether connections are local or remote, encrypted or not
- Gives you the choice to log violations only or actively block them
- Applies per database user, making it easy to protect application accounts or individual users
- Integrates well with other Oracle security features like Database Vault and auditing
Here is a straightforward way to implement SQL Firewall for an application service account.
Step 1: Enable SQL FirewallEXEC DBMS_SQL_FIREWALL.ENABLE;
Step 2: Start Capturing Normal Activity
Begin recording what the target user typically does. This example captures activity for an application user named APP:
BEGIN
DBMS_SQL_FIREWALL.CREATE_CAPTURE(
username => 'APP',
top_level_only => TRUE,
start_capture => TRUE
);
END;
/
Let the application run normally for a sufficient period so the firewall learns the expected SQL patterns and connection details.
Step 3: Review What Was CapturedCheck the captured data to confirm it covers the expected workload:
SELECT SQL_TEXT
FROM DBA_SQL_FIREWALL_CAPTURE_LOGS
WHERE USERNAME = 'APP';
Step 4: Generate the Allow-List Policy
Once you are satisfied with the captured data, create the policy:
EXEC DBMS_SQL_FIREWALL.GENERATE_ALLOW_LIST('APP');
You can review the allowed SQL statements and connection contexts using the DBA_SQL_FIREWALL_ALLOWED_* views.
Step 5: Enable EnforcementActivate protection for the user. This example enforces allowed SQL statements and blocks violations:
BEGIN
DBMS_SQL_FIREWALL.ENABLE_ALLOW_LIST(
username => 'APP',
enforce => DBMS_SQL_FIREWALL.ENFORCE_SQL,
block => TRUE
);
END;
/
You can choose to enforce SQL statements, connection context, or both. You can also decide whether to block or only log violations.
Monitoring and Ongoing ManagementReview violations regularly using this query:
SELECT SQL_TEXT, FIREWALL_ACTION, IP_ADDRESS, CAUSE, OCCURRED_AT
FROM DBA_SQL_FIREWALL_VIOLATIONS
WHERE USERNAME = 'APP'
ORDER BY OCCURRED_AT DESC;
Periodically clean up old violation records and consider exporting policies using Data Pump for backup or movement between environments.
Important Considerations- SQL Firewall only captures statements that execute successfully
- It normalizes SQL by replacing literal values before storing signatures
- Connection context is checked at session creation time
- You can add new allowed entries later from either the capture log or violation log
- Existing sessions are not terminated when you first enable a policy
This feature works especially well for:
- Protecting application service accounts that run a known set of SQL statements
- Adding an extra layer of defense for sensitive users such as reporting accounts or DBAs
- Quickly restricting direct database access to known IP addresses or programs
- Detecting and responding to potential SQL injection attempts in real time
Oracle SQL Firewall provides a practical, database-native way to enforce least-privilege access at the SQL level. By combining allow-listing of both statements and connection contexts, it helps reduce the attack surface without requiring major changes to your applications.
Start with a focused rollout on your most critical application accounts, review the captured activity carefully, and gradually expand protection across your environment.
Using an LLM in a SELECT Loop
This post describes a way to use a Large Language Model (LLM) to investigate Oracle database problems. In this approach, the LLM repeatedly generates SELECT statements based on previous queries and their results. Each query provides additional context, allowing the LLM to gather information step by step before producing a final report for a DBA to review.
My previous attempts at using an LLM only performed a single inference. I had to gather the input data myself and decide what output I wanted the LLM to produce. In the SELECT Loop approach the LLM can gather its own information through each query it generates. Every iteration expands the context available to the model, allowing it to investigate a problem in several steps instead of trying to answer immediately with incomplete information.
Here is the outline of how this approach works:
- Question -> LLM -> SELECT1 -> Oracle -> Output1
- Question + SELECT1 + Output1 -> LLM -> SELECT2 -> Oracle -> Output2
- Loop N times
- Question + SELECT1 + Output1 + … + SELECTN + OutputN -> LLM -> Report
- Report -> DBA
The first step is to have the LLM generate a SQL query to try to answer some question about the database and then run it. The LLM’s prompt might look like this:
On an Oracle 11.2.0.4 database I want to investigate the performance of a sql statement with SQL_ID b4rj0h8hh0sxf.
Please generate a select statement that will give more information about b4rj0h8hh0sxf's performance.
Please only generate ASCII text.
Please keep to no more than 80 character lines.
Please output only the select statement that you want to run next.
The LLM generates some useful query against the database’s DBA or V$ views like:
SELECT
...
s.iowait_delta AS iowait_usecs,
s.clwait_delta AS clwait_usecs,
s.apwait_delta AS apwait_usecs,
s.ccwait_delta AS ccwait_usecs
FROM
dba_hist_sqlstat s
JOIN dba_hist_snapshot sn
ON sn.snap_id = s.snap_id
AND sn.dbid = s.dbid
AND sn.instance_number = s.instance_number
WHERE
s.sql_id = 'b4rj0h8hh0sxf'
AND s.dbid = (SELECT dbid FROM v$database)
ORDER BY
sn.begin_interval_time,
s.plan_hash_value;
The next step is just to run the generated query against the database and get its output like:
SNAP_ID BEGIN_TIME END_TIME PLAN_HASH_VALUE EXECS
---------- --------------- --------------- --------------- ----------
88771 15-MAY-26 01:00 15-MAY-26 02:00 3752201481 11225
88772 15-MAY-26 02:00 15-MAY-26 03:00 3752201481 22707
88773 15-MAY-26 03:00 15-MAY-26 04:00 3752201481 11381
88795 16-MAY-26 01:00 16-MAY-26 02:00 3752201481 11316
88796 16-MAY-26 02:00 16-MAY-26 03:00 3752201481 23920
88797 16-MAY-26 03:00 16-MAY-26 04:00 3752201481 10076
88819 17-MAY-26 01:00 17-MAY-26 02:00 3752201481 11315
88820 17-MAY-26 02:00 17-MAY-26 03:00 3752201481 24907
88821 17-MAY-26 03:00 17-MAY-26 04:00 3752201481 9086
88843 18-MAY-26 01:00 18-MAY-26 02:00 3752201481 11209
88844 18-MAY-26 02:00 18-MAY-26 03:00 3752201481 24009
88845 18-MAY-26 03:00 18-MAY-26 04:00 3752201481 10075
Question + SELECT1 + Output1 -> LLM -> SELECT2 -> Oracle -> Output2
Next take the original question and append the first SELECT statement and its output and request a second SELECT statement. The LLM suggested SELECT1. Now we are giving it both its SELECT1 query and its output. Then the LLM gets to take the next step in the problem investigation by generating a second SELECT statement. By showing it what it previously chose and the query output we are giving the LLM context that it didn’t have the first time we asked it the question. This prompt looks something like this:
On an Oracle 11.2.0.4 database I want to investigate the performance of a sql statement with SQL_ID b4rj0h8hh0sxf.
Following this prompt are the outputs of select statements that you recommended running. After evaluating them
please generate an additional select statement that will give more information about b4rj0h8hh0sxf's performance.
Please only generate ASCII text.
Please keep to no more than 80 character lines.
Please output only the select statement that you want to run next.
Output of your previous select statements:
This prompt would be followed by the first select statement and its output. I’m assuming that the output contains the SELECT statement and the query results. The query the LLM returns will probably be different from the first one because it already knows the result of the first query.
Once the LLM generates it, run the second query.
Loop N timesNow pass the original question plus all the queries and their outputs into the LLM requesting another SQL. Repeat this process N times. This pattern is the main point of this post. The LLM starts with the original question and generates a select statement. We run the query, append the query and its output to the prompt, and ask for another query. This process repeats until we have gathered enough information. The LLM is taking us down its own path investigating the question.
Keep generating new SELECT statements based on all the previous SELECT statements and their outputs.
Question + SELECT1 + Output1 + … + SELECTN + OutputN -> LLM -> ReportFinally stop generating new queries and take all the ones you have generated and all their outputs and ask the LLM to generate a final report. Here is part of a report:
## 4. The Plan Change Event on 26-JUN-26
On snap 89780 (26-JUN-26 02:00-03:00) plan 361274665 appeared for
35 executions alongside the normal plan. By snap 89781 (03:00-04:00)
the bad plan had taken over entirely.
Performance comparison for 26-JUN-26:
| Metric | Normal plan | Bad plan snap89781 | Bad plan snap89782 |
|------------------|----------- |---------------------|---------------------|
| Executions | 16,775 (03h) | 11 | 13 |
| Avg elapsed (s) | 0.08 | 54.79 | 59.56 |
| Avg buffer gets | 17,673 | 14,982,688 | 18,208,586 |
| Avg disk reads | 37 | 6,832 | 6,463 |
| Avg I/O wait (s) | ~0 | 3.31 | 3.78 |
| Rows returned | ~155 | ~73,765/exec | ~72,639/exec |
The bad plan is approximately **500x slower** per execution and
performs **800-1,000x more buffer gets**. Disk reads per execution
jump from ~37 to ~6,800.
What makes this interesting is that the LLM chose the sequence of queries that gathered the information used in this report.
Report -> DBAThe last step is for the DBA to review the report. No one should look at an AI generated report like this and act on it without an experienced DBA reviewing it. I don’t see this kind of report replacing DBAs. I see it as empowering them. Instead of me manually writing and running various queries to solve a problem I can give the problem to an LLM to take a first crack at it.
ConclusionI struggled to find a practical use for LLMs in my DBA work until I started experimenting with this SELECT loop approach. By repeatedly generating queries, evaluating their results, and using that information to drive the next query, the LLM can investigate a problem before producing a final report. I don’t see this replacing DBAs, but I do see it helping experienced DBAs investigate problems more quickly.
P.S. I uploaded the sample code to a Github repository if you want to run it: https://github.com/bobbydurrett/SelectLoop
Run Fully Autonomous Oracle Databases Natively Inside AWS
Organizations running Oracle workloads in AWS now have a powerful new option. Oracle Autonomous AI Database Serverless is generally available on Oracle AI Database@AWS. This allows teams to deploy a completely self-managing Oracle database directly through the AWS Console while using their existing AWS spending commitments.
The service handles patching, performance tuning, scaling, and security updates automatically, so teams spend less time on routine maintenance and more time on strategic initiatives.
The Shift Toward Hands-Off Database ManagementMany companies want the benefits of automation without moving away from AWS. This release delivers exactly that. You get Oracle’s most advanced autonomous database technology running inside AWS data centers, with the same tools and purchasing experience your teams already use.
Independent research has shown strong returns for organizations adopting this approach, including major gains in team productivity and sharp reductions in unexpected outages. Those advantages are now available without leaving the AWS environment.
Reduced Day-to-Day Database WorkThe service takes care of many tasks that traditionally require manual effort:
- Patches and upgrades are applied automatically with no downtime
- Indexing and query optimization happen continuously in the background
- Compute and storage scale independently based on actual demand
- Security updates are managed without scheduling windows
This automation helps database administrators and infrastructure teams become significantly more productive. They can shift focus toward architecture, data strategy, and innovation instead of ongoing maintenance.
Support for Different Workload Needs in One ServiceRather than running separate systems for different requirements, a single instance can handle multiple patterns:
- High-volume transactional applications and mixed workloads
- Analytics, data warehousing, and lakehouse scenarios with support for open table formats
- Document-centric applications using familiar JSON interfaces
- Rapid application development through low-code tools with built-in AI features
Mission-critical applications need strong uptime guarantees. The service includes:
- A 99.995 percent availability commitment
- Disaster recovery options across AWS regions
- Local high availability with automatic failover between availability zones
- Fully managed backups stored in Amazon S3
Because the database runs on Oracle infrastructure inside AWS data centers, connectivity to other AWS services is fast and straightforward. Key integrations include:
- Automated backups landing in Amazon S3 with options for immutability
- Encryption using customer-managed keys through AWS KMS
- Zero-ETL data movement into Amazon Redshift for analytics
- Native metrics and events flowing into CloudWatch and EventBridge
- Provisioning and management through the AWS Console, APIs, and CloudFormation templates
This offering works especially well when teams want to:
- Bring existing Oracle applications into AWS with limited changes
- Lower the operational load on database and infrastructure teams
- Build AI applications that need vector search and natural language capabilities
- Consolidate different workload types onto a single managed platform
The service is available now in the US East (N. Virginia) and US West (Oregon) regions. Both Oracle AI Database 26ai and 19c are supported, along with flexible licensing models.
Teams already comfortable with AWS tools can start exploring the service through familiar interfaces while taking advantage of Oracle’s automation and resilience features.
Build a Production-Ready RAG Pipeline with LangChain and Oracle AI Database
Creating a reliable retrieval-augmented generation (RAG) system usually involves a lot of boilerplate. With the langchain-oracledb integration, you can build a complete pipeline; document loading, chunking, vector storage, hybrid search, semantic caching, and conversation memory; in a clean and compact way.
This guide walks you through a practical, end-to-end implementation using Oracle AI Database as the single source of truth for vectors, text, cache, and history.
Why Oracle AI Database + LangChain?Instead of juggling multiple systems (vector database + cache + message store), everything lives inside Oracle AI Database. This simplifies architecture, improves consistency, and gives you enterprise-grade features like transactions and security out of the box.
High-Level ArchitectureHere’s how the pieces connect:
- Ingestion: Load → Split → Embed → Store in OracleVS
- Retrieval: Hybrid search combining semantic similarity and keyword search
- Intelligence Layer: Semantic cache + persistent chat history
- Answer Generation: Use retrieved context to generate responses
We’ll read runbook content from a database table, split it intelligently, and store the chunks with embeddings.
from langchain_oracledb.document_loaders import OracleDocLoader, OracleTextSplitter
from langchain_oracledb.vectorstores import OracleVS, DistanceStrategy
from langchain_community.embeddings import HuggingFaceEmbeddings
def ingest_documents(conn):
# Load documents with metadata
loader = OracleDocLoader(
conn=conn,
params={
"owner": conn.username,
"tablename": "RUNBOOK_SOURCE",
"colname": "CONTENT",
"mdata_cols": ["ID", "TITLE", "CATEGORY"]
}
)
docs = loader.load()
# Create vector store
embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
vs = OracleVS(
conn,
embeddings,
table_name="DOCUMENT_VECTORS",
distance_strategy=DistanceStrategy.COSINE
)
# Split and store
splitter = OracleTextSplitter(
conn=conn,
params={"by": "words", "max": 250, "split": "sentence"}
)
vs.add_documents(docs, text_splitter=splitter)
return vs
Step 2: Hybrid Retrieval
Combine semantic search (vector similarity) with keyword search for better results.
from langchain_oracledb.retrievers import OracleTextSearchRetriever
def hybrid_retrieve(vector_store, query, k=5):
# Semantic search
semantic_docs = vector_store.similarity_search(query, k=k)
# Keyword search
keyword_retriever = OracleTextSearchRetriever(
vector_store=vector_store,
k=k
)
keyword_docs = keyword_retriever.invoke(query)
# Simple fusion logic (you can improve this with RRF)
combined = semantic_docs + keyword_docs
# Deduplicate and rank (basic version shown)
seen = set()
unique_docs = []
for doc in combined:
doc_id = doc.metadata.get("ID")
if doc_id not in seen:
seen.add(doc_id)
unique_docs.append(doc)
return unique_docs[:k]
Step 3: Add Semantic Caching
Reuse answers for similar questions to reduce cost and latency.
from langchain_oracledb.cache import OracleSemanticCache
from langchain.schema import Generation
def get_cached_or_generate(conn, embeddings, query, context):
cache = OracleSemanticCache(
conn,
embeddings,
table_name="SEMANTIC_CACHE",
score_threshold=0.85
)
cached = cache.lookup(query, "answer_cache")
if cached:
return cached[0].text, True # cache hit
# Generate new answer (call your LLM here)
answer = generate_response(query, context)
cache.update(query, "answer_cache", [Generation(text=answer)])
return answer, False
Step 4: Persist Conversation History
Keep full chat context across sessions.
from langchain_oracledb.chat_message_histories import OracleChatMessageHistory
from langchain.schema import HumanMessage, AIMessage
def save_to_history(conn, session_id, question, answer):
history = OracleChatMessageHistory(
session_id,
client=conn,
table_name="CONVERSATION_HISTORY"
)
history.add_messages([
HumanMessage(content=question),
AIMessage(content=answer)
])
return len(history.messages)
Complete End-to-End Function
Here’s how everything works together in a single function:
def ask_question(conn, vector_store, question, session_id="default"):
embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
# 1. Hybrid retrieval
relevant_docs = hybrid_retrieve(vector_store, question)
context = "\n\n".join([doc.page_content for doc in relevant_docs])
# 2. Check cache or generate
answer, was_cached = get_cached_or_generate(conn, embeddings, question, context)
# 3. Save to history
history_length = save_to_history(conn, session_id, question, answer)
return {
"answer": answer,
"cached": was_cached,
"sources": len(relevant_docs),
"history_messages": history_length
}
How to Run This Locally
- Install dependencies with Poetry
- Run the sample script — it automatically spins up an Oracle AI Database Free container using Testcontainers
- Ask questions and observe hybrid retrieval, caching behavior, and growing chat history
- Single database for vectors, documents, cache, and history
- Hybrid search improves answer quality
- Semantic caching reduces expensive LLM calls
- Conversation state is durable and queryable
- Very little custom code required thanks to langchain-oracledb
Building a solid RAG system doesn’t have to be complex. By leveraging Oracle AI Database through the official LangChain integration, you get a clean, maintainable pipeline that handles ingestion, retrieval, caching, and memory in one place.
This pattern scales well and keeps your architecture simple while delivering strong retrieval performance.
Start experimenting with the sample, you’ll be surprised how quickly you can get a capable system running.
Take Full Control of Your AI Agents with Archestra
If you have been wiring local AI agents to MCP servers, you already know the uneasy feeling. You connect a model, give it some tools, send it off on a task, and then you just hope it behaves. You have no real view into what it is calling, and no easy way to stop it when it does something it should not.
I spent some hands-on time with Archestra, an open source AI platform that fixes exactly this, and ran the whole thing locally on my own hardware.
Archestra is an all-in-one, open source platform for running AI agents safely. It pulls together the pieces you would normally wire up yourself: a chat interface, a no-code agent builder, an LLM gateway, an MCP gateway, a private MCP registry, deterministic guardrails, and full observability. One Docker command brings the whole stack up. The team behind it previously worked on Grafana OnCall, so the production thinking shows.
Running It LocallyI drove everything with a local model, Qwen3.6 27B served through Ollama, on my own GPU. No cloud dependency for inference. Archestra is provider agnostic, so pointing it at a local Ollama endpoint took a single configuration step.
The Part That MattersI built a simple agent and gave it one tool: a website reader pulled from Archestra's MCP registry. What stood out is that every MCP server runs as its own isolated pod inside a Kubernetes cluster that Archestra spins up automatically. That is real isolation, not just a local process.
I ran a task and watched the agent make its tool call live. Every step it took was visible. Then came the payoff. I set a deterministic guardrail to block that tool, re-ran the task, and the agent could no longer touch it. The block is enforced at the platform level, so a prompt cannot talk its way around it.
That is the whole point. Seeing what your agents do is good. Being able to stop them, deterministically, is what makes agentic AI safe to run.
Try It YourselfArchestra is open source and self-hostable. Grab it from GitHub, run the Docker quickstart, and try the same flow on your own machine.
Watch the full hands-on walkthrough in the video below.


