Indexes
Indexes are declared in the INDICES { } block inside a table’s { }. By default, DPG emits plain CREATE INDEX for additions on existing tables. Write CONCURRENTLY on an individual index to make it CREATE INDEX CONCURRENTLY instead — emitted as a non-transactional step after COMMIT.
UNIQUE and CONCURRENTLY are both prefix keywords, written before the index name — mirroring real PostgreSQL’s own CREATE UNIQUE INDEX CONCURRENTLY name ON table order exactly. CONCURRENTLY is a bare presence keyword, same as in real PostgreSQL: there is no CONCURRENTLY false and no project-wide setting that changes the default — writing the keyword is the only way an index is ever created concurrently.
Standard btree index
TABLE users ( email TEXT NOT NULL, ... )
{
INDICES { idx_users_email (email); }
}
-- emits (transactional)
CREATE INDEX IF NOT EXISTS "idx_users_email"
ON "public"."users" ("email");
Unique index
{ INDICES { UNIQUE idx_unique_slug (slug); } }
CREATE UNIQUE INDEX IF NOT EXISTS "idx_unique_slug"
ON "public"."users" ("slug");
Composite index with sort order
{ INDICES { idx_tenant_created (tenant_id ASC, created_at DESC); } }
CREATE INDEX IF NOT EXISTS "idx_tenant_created"
ON "public"."events" ("tenant_id" ASC, "created_at" DESC);
Partial index
{ INDICES { idx_active_users (email) WHERE (status = 'active'); } }
CREATE INDEX IF NOT EXISTS "idx_active_users"
ON "public"."users" ("email") WHERE (status = 'active');
Expression index
{ INDICES { idx_lower_email (lower(email)); } }
CREATE INDEX IF NOT EXISTS "idx_lower_email"
ON "public"."users" (lower("email"));
Covering index (INCLUDE)
{ INDICES { idx_covering (user_id) INCLUDE (email, created_at); } }
CREATE INDEX IF NOT EXISTS "idx_covering"
ON "public"."users" ("user_id") INCLUDE ("email", "created_at");
GIN index
{ INDICES {
idx_tags USING gin (tags);
idx_fts USING gin (search_vec);
} }
CREATE INDEX IF NOT EXISTS "idx_tags"
ON "public"."posts" USING gin ("tags");
CREATE INDEX IF NOT EXISTS "idx_fts"
ON "public"."posts" USING gin ("search_vec");
GiST index
{ INDICES { idx_location USING gist (location); } }
CREATE INDEX IF NOT EXISTS "idx_location"
ON "public"."places" USING gist ("location");
BRIN index with storage parameter
{ INDICES { idx_brin USING brin (created_at) WITH (pages_per_range = 128); } }
CREATE INDEX IF NOT EXISTS "idx_brin"
ON "public"."events" USING brin ("created_at") WITH (pages_per_range = 128);
Index with tablespace
{ INDICES { idx_archived (archived_at) TABLESPACE archive_space; } }
CREATE INDEX IF NOT EXISTS "idx_archived"
ON "public"."records" ("archived_at") TABLESPACE "archive_space";
Concurrent index creation
Write CONCURRENTLY to avoid locking the table during index creation on a large, live table:
{ INDICES { CONCURRENTLY idx_email (email); } }
-- emits (non-transactional, after COMMIT)
CREATE INDEX CONCURRENTLY IF NOT EXISTS "idx_email" ON "public"."users" ("email");
This has no effect on an index declared alongside its own brand-new table — PostgreSQL rejects CREATE INDEX CONCURRENTLY inside a transaction block, and a new table’s indexes are always emitted transactionally with it, so the compiler silently forces them non-concurrent regardless of this keyword.
Index removal
Removing an index from the INDICES block emits DROP INDEX — classified as CAUTION (acquires ACCESS EXCLUSIVE lock; no data loss, but blocks concurrent reads during the drop).
-- emits
DROP INDEX IF EXISTS "public"."idx_old_index";