SQL++ / N1QL Query

When Column Names Lie: Bringing External Catalogs to NL2SQL++

Lectura de 9 minutos

The Problem

Natural Language to SQL++ (NL2SQL++) is only as good as the schema knowledge behind it. Give a large language model (LLM) a question like “How many customers with an active previous application defaulted?” and it has to know which tables exist, what each column means, and how the tables relate. Most systems solve this by hardcoding the schema into the prompt. That breaks the moment the schema changes, and it collapses entirely when column names carry no meaning on their own – the norm in real enterprise data.

Consider the columns a model actually faces in a production schema. Some names are simply opaque: CUST_VAL_04 is a customer’s lifetime revenue tier, but the name says nothing about what it holds. Others are worse than opaque, because they mislead: CLOSE_DT sounds like an account close date, yet it stores a status code (0 = open, 1 = frozen, 2 = closed) – a model that filters it like a date will silently generate wrong SQL++. And then there are near-duplicates a name alone can’t separate: BAL_CUR, BAL_AVG_30, and BAL_AVG_90 all look like “balance,” but only one is the current balance while the others are 30- and 90-day averages. Ask “What’s the customer’s current balance?” and only the column’s description – not its name – reveals that BAL_CUR is the right one and the other two are traps. A human analyst reaches for a data dictionary to read columns like these. An LLM needs the same thing.

That dictionary already exists inside most organizations, curated in data catalogs like DataHub and OpenMetadata: column descriptions, data types, and the foreign-key relationships between tables. The trouble is that this knowledge lives in a silo, disconnected from the operational database where queries actually run.

This project connects enterprise data catalogs to Couchbase so NL2SQL++ can read what columns actually mean instead of guessing from their names. It pulls column metadata out of a catalog, embeds it into Couchbase, and retrieves the right context at query time via vector search – so the schema is never hardcoded. And because different customers use different catalogs, the integration is built around a pluggable provider pattern: switching between supported catalogs is a config change, and supporting a new one means adding a small self-contained adapter — not touching the rest of the system.

The Big Idea: One Interface, Any Catalog

At the heart of this design is a single contract that every data catalog implements. Whatever the source, it exposes the same three capabilities to the rest of the pipeline:

  • Fetch column metadata – Every column across every table in scope, each with its name, description, data type, and fully-qualified location.
  • Fetch join relationships – The foreign-key or lineage links between tables, expressed as reusable join predicates.
  • Stream live changes – An optional feed of schema edits, so you can keep the metadata current without a full reload.

A single setting selects which catalog is active. Everything downstream – the loader, the query engine, the live sync – talks to this common contract and never to a specific catalog. To support the next catalog (Atlan, an S3 data lake, or something not yet built) an adapter is written that satisfies the contract; nothing else changes.

The dataset used throughout is the public Home Credit default-risk data from Kaggle: seven related tables – loan applications, credit-bureau records, prior applications, and monthly balance histories – living together in Couchbase.
How It Works, End to End

There are two lifecycles. A one-time setup loads catalog metadata into Couchbase and makes it searchable. The query flow then answers questions against it. An optional live listener keeps the metadata current as the catalog evolves.

Setup: catalog into Couchbase

Setup runs in four steps, all driven by whichever catalog is active:

  1. Pull the column metadata and store one record per column in Couchbase – its description, data type, and location.
  2. Embed each column’s description into a 384-dimension vector using a sentence-transformer model. Only the description is embedded, not the cryptic name – that is what makes a plain-English question match the meaning of a column rather than its label.
  3. Build a vector index over those embeddings (cosine similarity) so searches are fast.
  4. Pull the join relationships and store them as a set of deduplicated join predicates, for example: bureau.SK_ID_CURR = application_train.SK_ID_CURR.

Each catalog derives those joins from its own native model – DataHub from column-level lineage, OpenMetadata from foreign-key constraints – but both produce the identical predicate strings the query engine expects. Notably, the system does not precompute a rigid join graph; it retains the raw predicates and allows the query-time model assemble the minimal valid join path on demand.

Query: question into SQL++

Answering a question runs in three stages:

  1. Understand – The question is broken into focused sub-queries so each concept can be matched independently.
  2. Retrieve – Each sub-query is embedded and run through a vector search over the column metadata in Couchbase, keeping only the closest matches by cosine distance. This is where EXT_SOURCE_2 surfaces for a question about risk scores, purely because its description matches – never its name.
  3. Refine – Vector search casts a wide net, so a language model prunes the candidates down to the columns genuinely needed to answer the question, erring toward keeping anything useful.
  4. Generate – Finally, the question, the chosen columns, and the join relationships go to a Claude model on AWS Bedrock, which is constrained to use only the supplied joins and the minimal path connecting the required tables. Out comes executable SQL++.

Because join keys travel separately as predicates, the generator can still wire up correct multi-table joins even when those identifier columns were dropped during refinement.

Staying current: live metadata sync

Catalogs change; for example, columns get renamed, descriptions get corrected, and tables come and go. An optional long-running listener keeps Couchbase in sync without re-running setup. Each catalog watches its source in its native way – DataHub consumes a change-log event stream, OpenMetadata polls its events endpoint – and both translate what they see into the same six normalized change events. A shared worker pool then applies each event to the stored metadata:

ChangeWhat Happens in CouchbaseRe-embed?
New table addedStore a record for every new columnYes
Column addedStore the new columnYes
Description updatedUpdate the description and its vectorYes
Data type changedUpdate the type onlyNo
Column removedDelete that column’s recordNo
Table removedDelete every record for that tableNo

Re-embedding runs only when the text behind the vector actually changes – a new column or an edited description. A pure data-type change skips it, so nothing is recomputed without reason.
Why It Matters

Testing on the Home Credit data confirmed the core thesis: metadata-enhanced NL2SQL++ significantly outperforms a schema-only approach. Faced with opaque column names, supplying the catalog’s descriptions produced correct SQL++; without them, the model made systematic errors in which fields it mapped and how it filtered.

The broader payoff is architectural. By separating where metadata comes from – the catalog – from how it’s used – embed, search, generate – any Couchbase customer can plug their existing catalog into AI-driven query generation and get schema-aware results without hand-maintaining a schema. The pluggable design means the next catalog is a small, self-contained addition rather than a rewrite.

Two examples, side by side

The gap is easiest to see in SQL++. Both queries below are syntactically valid and execute cleanly – the schema-only version simply returns the wrong answer, because the model guessed a column’s meaning from its name.

Example A a value the name can’t reveal: STATUS

“How many monthly bureau records show a loan that was written off or over 120 days past due?”

STATUS in bureau_balance sounds self-explanatory, but it stores single-character codes, not readable labels. The catalog description spells out the encoding: “C means closed, X means status unknown, 0 means no DPD, 1 means maximal did during month between 1-30, 2 means DPD 31-60, 5 means DPD 120+ or sold or written off.” “Written off or 120+ days past due” is the code ‘5’ – and there is no way to know that from the name.

Without the catalog, the model invents plausible-looking string values that match nothing in the data:

-- Without catalog: runs fine, returns 0 — the guessed labels don't exist
SELECT COUNT(*) AS num_records
FROM creditrisk.sampleScope.bureau_balance AS bb
WHERE bb.STATUS IN ['WRITTEN_OFF', 'DPD_120_PLUS', 'written_off'];

With the catalog, the encoding is in the description, so it maps to the real code:

-- With catalog: correct — '5' = DPD 120+ / sold / written off
SELECT COUNT(*) AS num_records
FROM creditrisk.sampleScope.bureau_balance AS bb
WHERE bb.STATUS = '5';

Example B a suffix that means nothing on its own: SK_DPD vs SK_DPD_DEF

“Which POS/cash loans were past due during a month, ignoring trivial low-amount debts?”

Two near-identical columns sit side by side in POS_CASH_balance. SK_DPD is “DPD (days past due) during the month of previous credit.” SK_DPD_DEF is “DPD during the month with tolerance (debts with low loan amounts are ignored).” The only clue is the _DEF suffix – meaningless by name. “Ignoring trivial low-amount debts” is precisely SK_DPD_DEF, but nothing in the name says so.

Without the catalog, the model picks the plainer-named column and quietly counts the debts the question asked to exclude:

-- Without catalog: runs fine, but SK_DPD does NOT ignore low-amount debts
SELECT pc.SK_ID_PREV,
       pc.MONTHS_BALANCE,
       pc.SK_DPD
FROM creditrisk.sampleScope.POS_CASH_balance AS pc
WHERE pc.SK_DPD > 0;

With the catalog, the tolerance clause in the description points to the right column:

-- With catalog: correct — SK_DPD_DEF applies the low-amount tolerance
SELECT pc.SK_ID_PREV,
       pc.MONTHS_BALANCE,
       pc.SK_DPD_DEF
FROM creditrisk.sampleScope.POS_CASH_balance AS pc
WHERE pc.SK_DPD_DEF > 0;

In both cases the failure is invisible at the SQL level – nothing produces errors, and nothing looks off. Only the column’s description separates a correct query from a confidently wrong one.

Rich metadata already lives in the enterprise, this project simply teaches the database to understand it. Curious where this is headed? Get in touch with Couchbase to talk about conversational querying on your data.

Share this article

Author

Pranav Mayuram is a N1QL Query language intern, Couchbase. Built a social network platform, Touchbase, using Couchbase Server, Node.js, Express & Angular.js.

Deja un comentario

Ready to get Started with Couchbase Capella?

Start building

Check out our developer portal to explore NoSQL, browse resources, and get started with tutorials.

Use Capella free

Get hands-on with Couchbase in just a few clicks. Capella DBaaS is the easiest and fastest way to get started.

Get in touch

Want to learn more about Couchbase offerings? Let us help.