fivenines
6/25

Build Your Own PostgreSQL

Binding Names, Types, and Schemas

Time
6 min
Prerequisites
Parsing SQL Into Trees

What You Will Learn

  • Understand syntax meets the catalog in a PostgreSQL-like database
  • Understand scope comes before strategy in a PostgreSQL-like database
  • Understand binding pipeline in a PostgreSQL-like database
  • Understand types are chosen, not assumed in a PostgreSQL-like database
bindingnamestypesschemassyntaxmeetscatalogscopecomesbefore

Syntax meets the catalog

After parsing, a SQL statement has shape but not yet meaning. The tree may say that a query selects `balance` from `accounts`, compares it to a literal, and orders by `created_at`. It does not yet say which `accounts` table is meant, which `balance` column, or whether the literal is an integer, numeric, text, or some type not yet decided. It also does not say whether this user may read the table at all. Binding is the stage where the database connects syntax to the live catalog.

The catalog is the database's record of itself. It stores tables, columns, indexes, functions, operators, types, schemas, constraints, privileges, and many other facts. In a Postgre-like system, the catalog is not a side document. It is stored in relations and queried by the engine. When the analyzer resolves a name, it is reading the database about the database.

Scope comes before strategy

Name resolution starts with scope. A query can mention relations in a `from` clause, aliases for those relations, columns exposed by subqueries, common table expressions, and names from outer queries. A column reference such as `id` is valid only if exactly one visible relation provides that name. If two do, the reference is ambiguous. If none do, it is unknown. These errors are not syntax errors. They are failures to attach the parsed tree to available objects.

Schemas add another layer. The table name `accounts` may resolve through a search path, while `billing.accounts` names a specific namespace. Search paths are convenient, but they are also part of the execution context. Changing the path can change which object a query means. Binding freezes that choice for the statement by replacing loose names with object identities from the catalog.

Binding pipeline

Types are chosen, not assumed

Types turn a tree of expressions into something executable. The text `1` does not always start with a final type. It may become an integer, numeric, or another compatible type depending on context. The operator `+` is not one operation either. It is a name resolved against input types. Integer addition, numeric addition, interval addition, and user-defined additions can all share a surface symbol. The analyzer must choose the exact operator implementation or reject the expression.

A dynamically typed design pushes this work in the other direction. Values carry loose runtime tags, and operators decide what to do at execution. That can make the analyzer smaller, but it pushes errors later and makes planning weaker. If the planner does not know whether a predicate compares integers or text, it cannot reason well about indexes, selectivity, or function volatility. Early type resolution gives later stages a firmer world.

The opposite mistake is to over-resolve too soon. Some statements are prepared with parameters whose values arrive later. A placeholder may have an inferred type from context, or it may need explicit typing from the client. The analyzer must preserve enough parameter information for later binding while still rejecting statements that cannot possibly be made meaningful. This balance is one reason prepared statements are more than cached strings.

Security rides on meaning

Privileges belong in this layer because they depend on meaning. The server cannot decide whether a user may read `balance` until it knows which column named `balance` is being referenced. It cannot decide whether a function call is allowed until it resolves the function. Authorization checks ride on top of successful binding. This keeps the security boundary attached to real catalog objects rather than textual guesses.

Views and rules make binding more interesting. A view looks like a table to the user, but it expands into a stored query. The analyzer may replace a view reference with the view's underlying query tree, adjusted for aliases and permissions. This creates a layered kind of meaning. The user's query binds to the view, and the view binds to its base objects. The system must preserve both the semantic result and the security rules that came with the view.

The output of analysis is still not a physical plan. It is an enriched query tree. Names have become object identifiers. Column references point to positions in range tables. Operators and functions are selected. Types are known. Coercions may have been inserted. Permissions have been checked. The tree now says not merely what the SQL looked like, but what it means in this database at this moment.

This stage prevents a great deal of confusion from leaking downstream. The planner should not wonder whether `accounts` exists. The executor should not discover halfway through a scan that `balance > 'abc'` has no valid operator. Binding settles meaning early so later layers can focus on strategy and movement.

Into planning

There is still a major gap between meaning and execution. An analyzed query may describe joins, filters, grouping, and ordering, but it does not say which relation to scan first or whether a predicate should be applied before or after a join. To answer that, the database translates meaning into a more algebraic form where equivalent routes can be compared.

Next step

See what actually stuck.

Take the practice scenarios now.