Skip to content

SQL naming model

How dynamically built statements get their object names, and where values go instead. How a statement is assembled is the dequeue composition model. It is a sub-model of the system design.

Flow

┌───────────────┐  prefix, db_schema, per-object overrides,
│  DBSettings   │  from arguments or PGQUEUER_* environment
└───────┬───────┘
        │ validated once
        ▼
┌───────────────┐  bare:      pgqueuer_log
│ Name spellings│  qualified: app.pgqueuer_log
│               │  namespace: 'app' or current_schema()
└───────┬───────┘
        │ interpolated as identifiers
        ▼
┌───────────────┐  values bound as $N, never interpolated
│   Statement   │
└───────┬───────┘
        ▼
┌───────────────┐
│    Driver     │
└───────────────┘

Every object name defaults to prefix plus a base name (pgqueuer, pgqueuer_log, pgqueuer_statistics, pgqueuer_schedules, pgqueuer_status, fn_pgqueuer_changed, tg_pgqueuer_changed, ch_pgqueuer) and can be overridden on its own. Index names derive from their table. prefix and db_schema must be plain identifiers. They and the object names are lowercased so their identifier and string-literal uses agree; the channel keeps its case, since it is a string literal in pg_notify and quoted in LISTEN. qualified is cached on first use, so a DBSettings must not change after it is used.

Spellings

One name appears in three spellings, and most bugs in this layer come from using the wrong one.

The bare name is what the declaration holds and what the catalog is compared against. DDL uses it where it names something new: CREATE INDEX, RENAME TO, a trigger. The qualified name, settings.qualify(name) or settings.qualified.<name>, is used for every reference to an existing table, type, function or index. With db_schema unset it is the bare name. The namespace expression, settings.schema_expr, is '<db_schema>' or current_schema() and scopes catalog queries and DO blocks.

The status enum is the awkward one, because declared expressions mention it inside casts. schema_ddl.spelled qualifies it when rendering, and only as a whole column type or after ::, so a column that shares its name stays bare. schema_inspect.unqualify strips <db_schema>. from what the catalog reports. The connected planner therefore always compares bare text; the offline index guard is the one place that compares qualified text.

The NOTIFY channel is database-wide and never qualified. With db_schema unset, statements resolve through the whole search_path, but catalog reads use current_schema(), so an object in a later schema works at runtime and still inspects as absent.

Identifiers and values

Runtime values (ids, entrypoints, limits, payloads, intervals) are always bound, through SqlComposer.bind or a fixed placeholder list; catalog lookups bind their name lists too. What is interpolated comes from settings, from the declaration, or from a closed Literal such as the sort order. That includes object names used as string literals, in pg_get_serial_sequence('…'), hashtext('…') and the offline guards, and everything inside a DO block, which cannot take parameters.

Restated definitions

Some statements must repeat what the declaration owns. Enqueue's ON CONFLICT restates the dedupe index predicate and reads it from queue.dedupe_predicate; the statistics bucket fold reads statistics.UNIQUE_COUNT_KEY. Several places still restate by hand: the log aggregation and schedules ON CONFLICT targets, the startup checks in QueueManager.verify_structure and SchedulerManager.run, pgq verify, the slot index name in Queries.dequeue, the offline redefined_indexes list, and the id and status column names in the widening and retype blocks. Renaming an object in the declaration means updating each of them.

Lock keys

The upgrade lock is session-scoped and keyed on crc32 of the qualified queue table; log aggregation takes a transaction lock on hashtext of the qualified statistics table. Installations sharing a database contend only when those names match, which includes same-named installations separated only by search_path.

Invariants

  • No runtime value is interpolated into SQL text.
  • The declaration and the catalog meet bare; qualification happens only when rendering.

Guards

test_schema_support.py runs install, upgrade, dedupe, widening and the dashboard queries under db_schema and a prefix, and checks the channel stays unqualified. test_schema_render.py and test_schema_model.py check qualification and prefixes on rendered and declared names. test_composer.py and test_dequeue_shapes.py cover placeholder numbering, and test_cli_sql.py the offline SQL.

Known gaps

Per-object overrides are interpolated as given, apart from a dot check under db_schema that skips the channel and trigger. Names render unquoted, so PostgreSQL truncates them at 63 bytes while catalog lookups bind them as configured; a very long prefix makes inspection miss objects that exist. With db_schema unset, the enum label check scans every schema.