Query Model

Source scope

Learn why a column must belong to a source in the query, and how to refer to an enclosing query.

A column carries the identity of the source that provides it. Qubu checks that identity when you assemble a query. The source must be supplied by FROM or a join for its columns to appear in the projection, a predicate, or another clause.

Add the source that owns a column

Qubu reports a missing source when a query selects a column from a table that does not appear in the query:

import { from, integer, select, table, text } from "qubu"

const users = table("users", {
  id: integer(),
})
const posts = table("posts", {
  id: integer(),
  title: text(),
})

select({ id: users.id }, from(posts))
// Type error: users is not available in this query scope

Add the source that owns the column, or join it with a condition that refers to both sources:

import { eq, from, innerJoin, integer, select, table } from "qubu"

const users = table("users", {
  id: integer(),
})
const posts = table("posts", {
  id: integer(),
  authorId: integer(),
})

const query = select(
  { userId: users.id, postId: posts.id },
  from(users),
  innerJoin(posts, eq(users.id, posts.authorId)),
)

Use the source identity you introduced

Aliases, CTEs, derived queries, and custom sources expose new source identities. Use their columns after wrapping the original source:

import { alias, from, integer, select, table, text } from "qubu"

const users = table("users", {
  id: integer(),
  name: text(),
})

const author = alias(users, "author")
const query = select({ name: author.name }, from(author))

author.name belongs to the author source. users.name is a different source identity after aliasing, even though both columns refer to the same table.

The same rule applies to a CTE or derived query. A query's selected row becomes the set of columns exposed by its new source:

import { alias, from, lower, select } from "qubu"

const names = select({ name: lower(users.name) }, from(users))
const namesSource = alias(names, "names")

const query = select({ name: namesSource.name }, from(namesSource))

Use Compose queries for the full CTE, derived-table, scalar-subquery, and set-operation workflow.

Correlate an inner query

Use correlate() when an inner query intentionally reads a source from its enclosing query. correlate() changes type checking but emits no SQL:

import { correlate, crossJoin, eq, from, integer, lateral, select, table, where } from "qubu"

const users = table("users", { id: integer() })
const posts = table("posts", {
  id: integer(),
  authorId: integer(),
})

const recentPost = select(
  { id: posts.id },
  from(posts),
  correlate(users),
  where(eq(posts.authorId, users.id)),
)

const recent = lateral(recentPost, "recent_post")
const query = select({ userId: users.id, postId: recent.id }, from(users), crossJoin(recent))

The inner query consumes posts locally. The enclosing users source satisfies its outer requirement. The same requirement flows through scalar(), exists(), and inQuery().