Claude Skill

api-database-edgedb

Graph-relational database with EdgeQL query language, code-first schema, link-based relations, computed properties, and fully typed TypeScript query builder

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_api-database-edgedb_skills_api-database-edgedb-3a51ef5.zip · 20 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-database-edgedb/skills/api-database-edgedb
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git git clone https://github.com/agents-inc/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Gel (formerly EdgeDB) Patterns

Quick Guide: Gel (formerly EdgeDB) is a graph-relational database built on PostgreSQL. Define schemas in .gel files using SDL with types, links, and computed properties. Use gel migration create + gel migrate for schema changes. Query with EdgeQL (set-based, deeply nested shapes) or the TypeScript query builder (e.select, e.insert). Everything in EdgeQL is a set -- empty sets need explicit casts, and operations on sets produce Cartesian products. Use global variables with access policies for row-level security. The query builder requires a running database for code generation (npx @gel/generate edgeql-js).

Naming: EdgeDB was rebranded to Gel in February 2025. The edgedb npm package, CLI, and .esdl extension still work via compatibility shims, but new projects should use gel, @gel/generate, and .gel files.


<critical_requirements>

CRITICAL: Before Using This Skill

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST run npx @gel/generate edgeql-js after every gel migrate -- the generated query builder is based on the database schema and becomes stale after migrations)

(You MUST cast empty sets explicitly (<str>{}, <int64>{}) -- bare {} is a syntax error because EdgeQL is strongly typed and cannot infer the type of an empty set)

(You MUST understand that all EdgeQL values are sets -- operations on multi-valued expressions produce Cartesian products, not element-wise results)

(You MUST pass the transaction object tx (not client) to ALL query .run() calls inside client.transaction() -- using client inside a transaction runs queries outside the transaction)

(You MUST NOT use volatile functions like datetime_current() in schema-defined computed properties -- use datetime_of_transaction() or datetime_of_statement() instead)

</critical_requirements>


Auto-detection: Gel, gel, EdgeDB, edgedb, EdgeQL, edgeql, .gel, .esdl, dbschema, edgeql-js, createClient, e.select, e.insert, e.update, e.delete, e.params, gel migrate, gel migration, edgedb migrate, SDL schema, backlink, access policy, gel.toml, edgedb.toml

When to use:

  • Defining graph-relational schemas with types, links, and computed properties
  • Writing type-safe queries with EdgeQL or the TypeScript query builder
  • Managing schema migrations with the built-in migration system
  • Modeling complex relationships (multi links, backlinks, polymorphism)
  • Implementing row-level security with access policies and globals

Key patterns covered:

  • Client setup and connection (createClient, DSN, environment variables)
  • Schema definition in SDL (types, properties, links, constraints, computed)
  • EdgeQL query language (SELECT shapes, INSERT, UPDATE, DELETE)
  • TypeScript query builder (e.select, e.insert, e.update, e.delete)
  • Migrations workflow (gel migration create, gel migrate)

When NOT to use:

  • Simple key-value storage (use a dedicated key-value store)
  • Projects that need raw SQL as the primary interface (Gel uses EdgeQL; Gel 6+ adds native SQL support but EdgeQL is the primary interface)
  • Environments where you cannot run the Gel server (it is not an embedded database)

Detailed Resources:

  • For decision frameworks and quick reference, see reference.md

Core Patterns:

  • examples/core.md - Client setup, schema definition, EdgeQL basics, migration workflow

Query Builder:

Advanced Schema:




<red_flags>

RED FLAGS

High Priority Issues:

  • Using client instead of tx inside client.transaction() -- queries run outside the transaction and cannot be rolled back
  • Forgetting to run npx @gel/generate edgeql-js after gel migrate -- query builder types are stale and TypeScript won't catch schema mismatches
  • Using raw uuid properties instead of link -- defeats Gel's graph traversal and referential integrity
  • Using datetime_current() in schema-defined computed properties -- volatile functions are forbidden in schema computeds; use datetime_of_statement() or datetime_of_transaction()

Medium Priority Issues:

  • Bare {} for empty sets -- EdgeQL requires explicit type cast (<str>{}, <array<int64>>[]) because the type cannot be inferred from an empty literal
  • Using := when you mean += on multi links in UPDATE -- := replaces the entire set, += adds to it, -= removes from it
  • Not specifying filter on UPDATE/DELETE -- without a filter, the operation applies to ALL objects of that type
  • Editing the database with DDL directly instead of through .gel files + migrations -- causes schema drift between files and database

Common Mistakes:

  • Expecting element-wise behavior from set operations -- {1, 2} + {10, 20} produces {11, 21, 12, 22} (Cartesian product), not {11, 22}
  • Forgetting that select on a single link returns an object (not an ID) -- you do not need to JOIN; just traverse with .
  • Using select count(MyType) and expecting querySingle to work -- count() always returns exactly one value, so use queryRequiredSingle
  • Defining a computed backlink but forgetting the type filter -- .<author without [is Post] returns all types that have an author link

Gotchas & Edge Cases:

  • Computed properties are not stored -- they are re-evaluated on every query, which can be expensive for complex expressions
  • required on a multi link means "at least one" -- an empty set violates the constraint, which can be surprising
  • String concatenation uses ++ not + -- the + operator is for arithmetic only
  • LIMIT 1 does NOT make a query return a singleton for cardinality purposes -- use filter .id = <uuid>$id (exclusive constraint) for the query builder to infer singleton cardinality
  • Multi links are unordered sets -- if you need ordering, add an order by in your query or use an intermediate type with an order property
  • Backlinks (.<link_name) default to multi cardinality -- use single keyword explicitly if you know the relationship is one-to-one
  • EdgeDB branches (v5+) are separate database copies, not lightweight references -- branching a large database takes time and disk space proportional to the data size

</red_flags>


<critical_reminders>

CRITICAL REMINDERS

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST run npx @gel/generate edgeql-js after every gel migrate -- the generated query builder is based on the database schema and becomes stale after migrations)

(You MUST cast empty sets explicitly (<str>{}, <int64>{}) -- bare {} is a syntax error because EdgeQL is strongly typed and cannot infer the type of an empty set)

(You MUST understand that all EdgeQL values are sets -- operations on multi-valued expressions produce Cartesian products, not element-wise results)

(You MUST pass the transaction object tx (not client) to ALL query .run() calls inside client.transaction() -- using client inside a transaction runs queries outside the transaction)

(You MUST NOT use volatile functions like datetime_current() in schema-defined computed properties -- use datetime_of_transaction() or datetime_of_statement() instead)

Failure to follow these rules will cause stale types, silent data bugs, or transaction isolation failures.

</critical_reminders>

Files (skills)
  • examples
    • advanced-schema.md 9.8 KB
      # Gel (formerly EdgeDB) Advanced Schema Examples
      
      > Access policies, backlinks, abstract types, polymorphism, and triggers. See [SKILL.md](../SKILL.md) for core concepts.
      
      **Core patterns:** See [core.md](core.md). **Query builder:** See [query-builder.md](query-builder.md).
      
      ---
      
      ## Pattern 1: Backlinks
      
      ### Good Example -- Computed Backlinks for Bidirectional Traversal
      
      ```
      # Backlinks let you traverse relationships in reverse without storing data twice
      module default {
        type Author {
          required name: str;
          required email: str {
            constraint exclusive;
          };
      
          # Computed backlink: all posts where this Author is the author
          multi posts := .<author[is Post];
      
          # Count of published posts (computed from backlink)
          published_count := count(
            (select .posts filter .status = Status.published)
          );
        }
      
        type Post {
          required title: str;
          required author: Author;  # forward link
          required status: Status;
        }
      
        scalar type Status extending enum<draft, published, archived>;
      }
      ```
      
      **Why good:** `.<author[is Post]` traverses the `author` link backwards to find all Posts, `[is Post]` type filter ensures only Post objects are returned (other types with an `author` link are excluded), `published_count` composes on the backlink
      
      ```
      # BAD: Storing the reverse link manually (data duplication)
      type Author {
        required name: str;
        multi posts: Post;  # Forward multi link stored on Author
      }
      
      type Post {
        required title: str;
        required author: Author;
      }
      # Now posts exist in TWO places -- Author.posts and Post.author
      # They can get out of sync!
      ```
      
      **Why bad:** Forward multi link on Author duplicates the relationship data, `Author.posts` and `Post.author` can become inconsistent, use computed backlinks (`.<author[is Post]`) instead
      
      ### Backlink Syntax Reference
      
      ```
      # Syntax: .<link_name[is TargetType]
      
      # All objects linking to this object via 'author'
      multi written_items := .<author;  # Returns mixed types
      
      # Only Posts (not Comments or other types with 'author' link)
      multi posts := .<author[is Post];
      
      # Only Comments
      multi comments := .<author[is Comment];
      
      # Single backlink (one-to-one reverse)
      single profile := .<user[is Profile];
      ```
      
      ---
      
      ## Pattern 2: Abstract Types and Polymorphism
      
      ### Good Example -- Abstract Type Hierarchy
      
      ```
      module default {
        # Abstract type -- cannot be instantiated directly
        abstract type Timestamped {
          created_at: datetime {
            default := datetime_of_statement();
            readonly := true;
          };
          updated_at: datetime {
            default := datetime_of_statement();
            rewrite insert, update using (datetime_of_statement());
          };
        }
      
        abstract type Auditable extending Timestamped {
          required created_by: User;
          modified_by: User;
        }
      
        # Concrete types extend abstract types
        type Post extending Auditable {
          required title: str;
          required body: str;
          required status: Status;
        }
      
        type Comment extending Auditable {
          required body: str;
          required post: Post;
        }
      
        type User extending Timestamped {
          required name: str;
          required email: str {
            constraint exclusive;
          };
        }
      
        scalar type Status extending enum<draft, published, archived>;
      }
      ```
      
      **Why good:** `Timestamped` adds created/updated timestamps to any type, `Auditable` extends it with audit fields, `rewrite` trigger auto-updates `updated_at`, multiple inheritance via `extending`, DRY schema definition
      
      ### Good Example -- Polymorphic Queries
      
      ```edgeql
      # Select all Auditable objects (polymorphic query)
      select Auditable {
        created_at,
        created_by: { name },
      
        # Type-specific fields via type intersection
        [is Post].title,
        [is Post].status,
        [is Comment].body,
        [is Comment].post: { title },
      }
      order by .created_at desc
      limit 50;
      ```
      
      **Why good:** Querying abstract type returns all concrete subtypes, `[is Post].title` extracts type-specific fields without separate queries, single query for activity feed across types
      
      ---
      
      ## Pattern 3: Access Policies
      
      ### Good Example -- Row-Level Security with Globals
      
      ```
      module default {
        # Global variable -- set at connection time via client.withGlobals()
        global current_user_id: uuid;
      
        # Computed global for convenience
        global current_user := (
          select User filter .id = global current_user_id
        );
      
        type User {
          required name: str;
          required email: str {
            constraint exclusive;
          };
          required role: Role;
          required organization: Organization;
        }
      
        type Organization {
          required name: str;
          multi members := .<organization[is User];
        }
      
        type Document {
          required title: str;
          required body: str;
          required owner: User;
          required organization: Organization;
          is_public: bool {
            default := false;
          };
      
          # Access policies -- enforced at the database level
          access policy owner_full_access
            allow all
            using (.owner = global current_user);
      
          access policy org_members_can_read
            allow select
            using (.organization = global current_user.organization);
      
          access policy public_documents_readable
            allow select
            using (.is_public = true);
        }
      
        scalar type Role extending enum<admin, user, viewer>;
      }
      ```
      
      **Why good:** `global current_user_id` set via `client.withGlobals()`, computed global resolves the full user object, policies are declarative and enforced by the database, multiple policies combine (any matching policy grants access), policies distinguish between `select`, `insert`, `update`, `delete`
      
      ### Using Access Policies from TypeScript
      
      ```typescript
      import { createClient } from "gel";
      
      const baseClient = createClient();
      
      // Set the global for the current request
      function getClientForUser(userId: string) {
        return baseClient.withGlobals({
          current_user_id: userId,
        });
      }
      
      // All queries through this client are filtered by access policies
      async function getDocuments(userId: string) {
        const client = getClientForUser(userId);
      
        // This only returns documents the user is allowed to see
        const docs = await client.query(
          `select Document { title, body, owner: { name } }`,
        );
      
        return docs;
      }
      
      export { getClientForUser, getDocuments };
      ```
      
      **Why good:** `withGlobals` creates a derived client (immutable, does not modify base), all queries automatically filtered by policies, no application-level authorization checks needed for basic access control
      
      ---
      
      ## Pattern 4: Custom Scalar Types
      
      ### Good Example -- Constrained Scalars
      
      ```
      module default {
        # Enum scalar
        scalar type Priority extending enum<low, medium, high, critical>;
      
        # Constrained string scalars
        scalar type EmailAddress extending str {
          constraint regexp(r'^[^@]+@[^@]+\.[^@]+$');
          constraint min_len_value(5);
          constraint max_len_value(254);
        }
      
        scalar type SlugString extending str {
          constraint regexp(r'^[a-z0-9]+(-[a-z0-9]+)*$');
          constraint max_len_value(100);
        }
      
        # Constrained numeric
        scalar type PositiveInt extending int64 {
          constraint min_value(1);
        }
      
        # Use the custom scalars in types
        type User {
          required email: EmailAddress;
          required name: str;
        }
      
        type Post {
          required title: str;
          required slug: SlugString {
            constraint exclusive;
          };
          required priority: Priority {
            default := Priority.medium;
          };
        }
      }
      ```
      
      **Why good:** Custom scalars centralize validation logic, constraints enforced everywhere the scalar is used, DRY -- no need to repeat regex on every property
      
      ---
      
      ## Pattern 5: Triggers (v4+)
      
      ### Good Example -- Automatic Side Effects
      
      ```
      module default {
        type Post {
          required title: str;
          required body: str;
          required author: User;
          required status: Status;
          published_at: datetime;
      
          # Trigger: auto-set published_at when status changes to published
          trigger set_published_at after update for each
            when (__new__.status = Status.published and
                  __old__.status != Status.published)
            do (
              update Post
              filter .id = __new__.id
              set { published_at := datetime_of_statement() }
            );
        }
      
        type AuditLog {
          required action: str;
          required target_type: str;
          required target_id: uuid;
          required performed_by: User;
          created_at: datetime {
            default := datetime_of_statement();
          };
        }
      
        type User {
          required name: str;
          required email: str {
            constraint exclusive;
          };
      
          # Trigger: log deletion
          trigger log_deletion after delete for each do (
            insert AuditLog {
              action := 'deleted',
              target_type := 'User',
              target_id := __old__.id,
              performed_by := global current_user,
            }
          );
        }
      
        global current_user: User;
        scalar type Status extending enum<draft, published, archived>;
      }
      ```
      
      **Why good:** Triggers run inside the same transaction as the triggering operation, `__new__` and `__old__` reference the object before/after the change, `when` clause prevents unnecessary execution, audit logging as a database concern (not application concern)
      
      ---
      
      ## Pattern 6: Rewrite Rules
      
      ### Good Example -- Automatic Field Updates
      
      ```
      module default {
        type Post {
          required title: str;
          required body: str;
          required status: Status;
      
          created_at: datetime {
            default := datetime_of_statement();
            readonly := true;
          };
      
          # Rewrite rule: auto-update on every insert and update
          updated_at: datetime {
            rewrite insert, update using (datetime_of_statement());
          };
      
          # Rewrite rule: normalize slug on insert
          required slug: str {
            constraint exclusive;
            rewrite insert using (
              str_lower(str_replace(__subject__.title, ' ', '-'))
            );
          };
        }
      
        scalar type Status extending enum<draft, published, archived>;
      }
      ```
      
      **Why good:** `rewrite` is more concise than triggers for simple field transformations, `__subject__` references the object being inserted/updated, slug auto-generated from title on insert, `updated_at` always reflects the latest modification
      
      ---
      
      _For core patterns, see [core.md](core.md). For query builder, see [query-builder.md](query-builder.md)._
      
    • core.md 12.1 KB
      # Gel (formerly EdgeDB) Core Examples
      
      > Client setup, schema definition, EdgeQL queries, and migration workflow. See [SKILL.md](../SKILL.md) for core concepts.
      >
      > **Package names:** New projects use `gel` and `@gel/generate`. Legacy `edgedb` and `@edgedb/generate` packages still work via compatibility shims.
      
      **Query builder:** See [query-builder.md](query-builder.md). **Advanced schema:** See [advanced-schema.md](advanced-schema.md).
      
      ---
      
      ## Pattern 1: Client Setup
      
      ### Good Example -- Auto-Discovery Connection
      
      ```typescript
      import { createClient } from "gel";
      
      // Reads connection info from gel.toml (or edgedb.toml) in the project root
      // This is the standard development workflow
      const client = createClient();
      
      export { client };
      ```
      
      **Why good:** Zero-config for development, `createClient()` auto-discovers from project directory, named export
      
      ### Good Example -- Production with Environment Variable
      
      ```typescript
      import { createClient } from "gel";
      
      // In production, set GEL_DSN (or EDGEDB_DSN) environment variable:
      // gel://user:password@hostname:5656/dbname
      const client = createClient();
      
      // Or with explicit concurrency control
      const MAX_CONCURRENCY = 10;
      const clientWithPool = createClient({
        concurrency: MAX_CONCURRENCY,
      });
      
      export { client, clientWithPool };
      ```
      
      **Why good:** `createClient()` automatically reads `GEL_DSN` (or `EDGEDB_DSN`) env var, named constant for concurrency, no credentials in code
      
      ### Good Example -- Client with Globals
      
      ```typescript
      import { createClient } from "gel";
      
      const baseClient = createClient();
      
      // Derive a client with global variables set (for access policies)
      function getAuthenticatedClient(userId: string) {
        return baseClient.withGlobals({
          current_user_id: userId,
        });
      }
      
      export { baseClient, getAuthenticatedClient };
      ```
      
      **Why good:** `withGlobals` creates a derived client with global variables set, used with access policies for row-level security, does not modify the original client
      
      ### Bad Example -- Hardcoded Credentials
      
      ```typescript
      import { createClient } from "gel";
      
      // BAD: Credentials in source code, no auto-discovery
      const client = createClient({
        dsn: "gel://admin:p@ssw0rd@db.example.com:5656/production",
      });
      export default client;
      ```
      
      **Why bad:** Hardcoded credentials, DSN will be committed to version control, default export
      
      ---
      
      ## Pattern 2: Query Methods
      
      ### Good Example -- Choosing the Right Method
      
      ```typescript
      import { createClient } from "gel";
      
      const client = createClient();
      
      // .query() -- returns an array (zero or more results)
      const users = await client.query<{ name: string; email: string }>(
        `select User { name, email } filter .is_active = true`,
      );
      
      // .querySingle() -- returns one result or null
      const user = await client.querySingle<{ name: string; email: string }>(
        `select User { name, email } filter .id = <uuid>$id`,
        { id: "a1b2c3d4-..." },
      );
      
      // .queryRequiredSingle() -- returns exactly one or throws NoDataError
      const totalUsers =
        await client.queryRequiredSingle<number>(`select count(User)`);
      
      // .execute() -- runs a statement, returns nothing
      await client.execute(
        `delete User filter .is_deactivated = true and .deactivated_at < <datetime>$cutoff`,
        { cutoff: cutoffDate },
      );
      
      export { users, user, totalUsers };
      ```
      
      **Why good:** Method matches expected cardinality, type parameter for result shape, parameterized queries prevent injection
      
      ### Bad Example -- Wrong Cardinality Method
      
      ```typescript
      // BAD: Using query() when you expect exactly one result
      const allUsers = await client.query(`select count(User)`);
      const count = allUsers[0]; // Awkward -- count() always returns exactly one value
      
      // BAD: Using queryRequiredSingle() when result might not exist
      const maybeUser = await client.queryRequiredSingle(
        `select User filter .email = <str>$email`,
        { email: "unknown@example.com" },
      );
      // Throws NoDataError if user doesn't exist!
      ```
      
      **Why bad:** `query()` for a guaranteed-single result adds unnecessary array unwrapping, `queryRequiredSingle()` throws when result is absent -- use `querySingle()` for optional results
      
      ---
      
      ## Pattern 3: Parameterized Queries
      
      ### Good Example -- Type-Safe Parameters
      
      ```edgeql
      # Parameters are declared with <type>$name syntax
      select User {
        name,
        email,
        posts: {
          title,
          status,
        } filter .status = <Status>$status,
      } filter .email = <str>$email;
      ```
      
      ```typescript
      const result = await client.querySingle(
        `select User {
          name,
          email,
          posts: {
            title,
            status,
          } filter .status = <Status>$status,
        } filter .email = <str>$email`,
        { email: "alice@example.com", status: "published" },
      );
      ```
      
      **Why good:** Parameters are type-annotated (`<str>`, `<uuid>`, `<int64>`), preventing injection and ensuring type safety at the database level
      
      ### Bad Example -- String Interpolation
      
      ```typescript
      // BAD: SQL injection vulnerability
      const email = userInput;
      const result = await client.query(
        `select User filter .email = '${email}'`, // NEVER DO THIS
      );
      ```
      
      **Why bad:** String interpolation allows injection attacks, bypasses type checking, no parameterization
      
      ---
      
      ## Pattern 4: Schema Definition
      
      ### Good Example -- Complete Type with Constraints
      
      ```
      # dbschema/default.gel
      module default {
        scalar type Role extending enum<admin, moderator, user>;
      
        type Organization {
          required name: str {
            constraint exclusive;
            constraint min_len_value(1);
            constraint max_len_value(100);
          };
          description: str;
          created_at: datetime {
            default := datetime_of_statement();
            readonly := true;
          };
      
          multi members := .<organization[is User];  # computed backlink
      
          index on (.name);
        }
      
        type User {
          required name: str {
            constraint min_len_value(1);
          };
          required email: str {
            constraint exclusive;
            constraint regexp(r'^[^@]+@[^@]+\.[^@]+$');
          };
          required role: Role {
            default := Role.user;
          };
          required organization: Organization;
          is_active: bool {
            default := true;
          };
          last_login_at: datetime;
      
          # Computed properties
          display_name := .name ++ ' (' ++ <str>.role ++ ')';
      
          index on (.email);
        }
      }
      ```
      
      **Why good:** `required` enforces non-null, `constraint exclusive` for uniqueness, enum as scalar type, `readonly` for immutable timestamps, computed backlinks for bidirectional traversal, computed property for derived data, indexes on frequently filtered fields, `default` values for optional fields
      
      ### Good Example -- Multi Links and Constraints
      
      ```
      type Course {
        required title: str {
          constraint exclusive;
        };
        required instructor: User;
        multi students: User;
        multi prerequisites: Course;
        max_enrollment: int32 {
          constraint min_value(1);
        };
      
        # Compound constraint -- same student can't enroll twice
        # (handled automatically by multi link set semantics)
      
        # Cross-field constraint
        constraint expression on (
          count(.students) <= .max_enrollment ?? 999
        );
      }
      ```
      
      **Why good:** Multi links for many-to-many (students) and self-referential (prerequisites), cross-field constraint using expression, coalesce operator `??` handles null max_enrollment
      
      ### Bad Example -- No Constraints or Links
      
      ```
      # BAD: No constraints, no links, storing IDs manually
      type User {
        name: str;
        email: str;
        org_id: uuid;  # Should be a link!
      }
      ```
      
      **Why bad:** No `required` makes everything nullable, no uniqueness on email, raw `uuid` instead of `link` -- no referential integrity, no traversal, no cascading deletes
      
      ---
      
      ## Pattern 5: EdgeQL SELECT Patterns
      
      ### Good Example -- Nested Shapes with Filtering
      
      ```edgeql
      select Post {
        title,
        status,
        excerpt := .body[0:200],  # inline computed
        author: {
          name,
          email,
        },
        tags: {
          name,
        } order by .name,
        comment_count := count(.comments),
      }
      filter .status = Status.published
      order by .published_at desc
      limit 20
      offset 40;
      ```
      
      **Why good:** Nested shapes for related objects, inline computed field (excerpt), aggregation in shape (comment_count), filtering/ordering/pagination at top level, ordering on nested link
      
      ### Good Example -- Conditional Selection
      
      ```edgeql
      # Select with conditional computed field
      select User {
        name,
        email,
        status := 'admin' if .role = Role.admin else 'regular',
        post_count := count(.posts),
        recent_post := (
          select .posts
          order by .created_at desc
          limit 1
        ) { title, created_at },
      }
      filter .is_active = true;
      ```
      
      **Why good:** Inline conditional with `if..else`, subquery for latest post with its own shape, aggregation alongside scalar fields
      
      ---
      
      ## Pattern 6: EdgeQL INSERT Patterns
      
      ### Good Example -- Insert with Link Resolution
      
      ```edgeql
      # Insert resolving links via subqueries
      insert Post {
        title := 'Getting Started with EdgeDB',
        body := 'EdgeDB is a graph-relational database...',
        status := Status.draft,
        author := (
          select User filter .email = 'alice@example.com'
        ),
        tags := {
          (select Tag filter .name = 'edgedb'),
          (select Tag filter .name = 'tutorial'),
        },
      };
      ```
      
      **Why good:** Links assigned via subqueries (not raw IDs), set literal `{ ... }` for multi link, author resolved by business key (email)
      
      ### Good Example -- Insert or Update (Upsert)
      
      ```edgeql
      # Insert unless conflict, then update
      insert User {
        name := 'Alice',
        email := 'alice@example.com',
        role := Role.admin,
        organization := (select Organization filter .name = 'Acme'),
      }
      unless conflict on .email
      else (
        update User set {
          name := 'Alice',
          role := Role.admin,
        }
      );
      ```
      
      **Why good:** `unless conflict on` targets the exclusive constraint, `else` clause performs update on conflict, avoids race conditions between check-then-insert
      
      ---
      
      ## Pattern 7: EdgeQL UPDATE Patterns
      
      ### Good Example -- Multi Link Manipulation
      
      ```edgeql
      # Add tags without removing existing ones
      update Post
      filter .id = <uuid>$post_id
      set {
        tags += (select Tag filter .name in {'featured', 'trending'}),
      };
      
      # Remove specific tags
      update Post
      filter .id = <uuid>$post_id
      set {
        tags -= (select Tag filter .name = 'draft'),
      };
      
      # Replace all tags entirely
      update Post
      filter .id = <uuid>$post_id
      set {
        tags := (select Tag filter .name in {'final', 'reviewed'}),
      };
      ```
      
      **Why good:** `+=` adds to set, `-=` removes from set, `:=` replaces entire set -- three distinct operations for multi link manipulation
      
      ### Good Example -- Self-Referential Update
      
      ```edgeql
      # Increment a counter using current value
      update Post
      filter .id = <uuid>$post_id
      set {
        view_count := .view_count + 1,
      };
      ```
      
      **Why good:** `.view_count` refers to the current value, update is atomic
      
      ---
      
      ## Pattern 8: Migration Workflow
      
      ### Good Example -- Standard Development Workflow
      
      ```bash
      # 1. Start with a clean state
      gel migration status
      # Shows: Database is up to date. No migrations pending.
      
      # 2. Edit schema files
      # vim dbschema/default.gel
      # Add: required phone: str; to User type
      
      # 3. Generate migration (interactive)
      gel migration create
      # Gel will prompt:
      # "did you add property 'phone' to object type 'default::User'?"
      # Type 'y' to confirm
      
      # 4. Review generated migration file (optional but recommended)
      # cat dbschema/migrations/00002-m1abc123.edgeql
      
      # 5. Apply migration
      gel migrate
      
      # 6. Regenerate TypeScript types
      npx @gel/generate edgeql-js
      
      # 7. Update your TypeScript code to use the new field
      ```
      
      **Why good:** Schema-first workflow, interactive migration creation with human review, separate create and apply steps, TypeScript code generation after migration
      
      ### Prototyping with Watch Mode
      
      ```bash
      # Auto-create and apply migrations on file change (prototyping only)
      gel watch --migrate
      ```
      
      **Why good:** Rapid iteration during development -- Gel monitors `.gel` files and automatically creates and applies migrations in the background. Not for production use.
      
      ### Handling Migration Conflicts
      
      ```bash
      # If migration create fails due to schema issues:
      # 1. Fix the .gel files
      # 2. Re-run migration create
      gel migration create
      
      # If you need to see what changed:
      gel migration status
      gel describe schema  # current database schema
      
      # If you need to start over (development only):
      gel migration create --allow-empty  # create empty migration to reset state
      ```
      
      ---
      
      _For query builder patterns, see [query-builder.md](query-builder.md). For advanced schema, see [advanced-schema.md](advanced-schema.md)._
      
    • query-builder.md 12.8 KB
      # Gel (formerly EdgeDB) Query Builder Examples
      
      > TypeScript query builder patterns using `e.select`, `e.insert`, `e.update`, `e.delete`, and `e.params`. See [SKILL.md](../SKILL.md) for core concepts.
      >
      > **Package names:** New projects use `gel` and `@gel/generate`. Legacy `edgedb` and `@edgedb/generate` packages still work via compatibility shims.
      
      **Core patterns:** See [core.md](core.md). **Advanced schema:** See [advanced-schema.md](advanced-schema.md).
      
      ---
      
      ## Pattern 1: Query Builder Setup
      
      ### Good Example -- Standard Setup
      
      ```typescript
      import { createClient } from "gel";
      import e, { type $infer } from "./dbschema/edgeql-js";
      
      const client = createClient();
      
      // Extract inferred result types from query expressions
      const usersQuery = e.select(e.User, () => ({ id: true, name: true }));
      type UsersResult = $infer<typeof usersQuery>;
      // => Array<{ id: string; name: string }>
      
      export { client, e };
      ```
      
      **Why good:** Query builder imported from generated directory, `e` is the conventional name, `$infer` extracts compile-time result types from any query expression, named exports
      
      ### Generating the Query Builder
      
      ```bash
      # Requires a running Gel instance (it introspects the live schema)
      npx @gel/generate edgeql-js
      
      # Output: generates files in ./dbschema/edgeql-js/
      # Re-run after every `gel migrate`
      ```
      
      **Why good:** Generator introspects the actual database schema (not just .gel files), ensuring generated types exactly match the database
      
      ---
      
      ## Pattern 2: e.select -- Querying Data
      
      ### Good Example -- Basic Select with Shape
      
      ```typescript
      import { createClient } from "gel";
      import e from "./dbschema/edgeql-js";
      
      const client = createClient();
      
      // Select all users with specific fields
      const allUsersQuery = e.select(e.User, () => ({
        id: true,
        name: true,
        email: true,
      }));
      
      // Result type is automatically inferred:
      // Array<{ id: string; name: string; email: string }>
      const users = await allUsersQuery.run(client);
      
      export { users };
      ```
      
      **Why good:** Shape expression specifies exactly which fields to return, result type is automatically inferred by TypeScript, `.run(client)` executes the query
      
      ### Good Example -- Nested Shapes and Filtering
      
      ```typescript
      import e from "./dbschema/edgeql-js";
      
      const PAGE_SIZE = 20;
      
      const publishedPostsQuery = e.select(e.Post, (post) => ({
        id: true,
        title: true,
        excerpt: true,
        author: {
          name: true,
          email: true,
        },
        tags: {
          name: true,
        },
        comment_count: e.count(post.comments),
        filter: e.op(post.status, "=", e.Status.published),
        order_by: {
          expression: post.published_at,
          direction: e.DESC,
        },
        limit: PAGE_SIZE,
      }));
      
      export { publishedPostsQuery };
      ```
      
      **Why good:** Nested shapes for links, computed field (comment_count) using `e.count`, filter/order_by/limit as shape properties, named constant for limit, status compared with generated enum `e.Status.published`
      
      ### Good Example -- Filter on Exclusive Constraint (Singleton)
      
      ```typescript
      import e from "./dbschema/edgeql-js";
      
      // Object shorthand -- preferred when filtering on exclusive properties
      const userByIdQuery = e.select(e.User, () => ({
        id: true,
        name: true,
        email: true,
        role: true,
        posts: {
          title: true,
          status: true,
        },
        filter_single: { id: "a1b2c3d4-..." },
      }));
      
      // Expression form -- use when you need e.op for complex filters
      const userByEmailQuery = e.select(e.User, (user) => ({
        id: true,
        name: true,
        filter_single: e.op(user.email, "=", "alice@example.com"),
      }));
      
      // Result type: { id: string; name: string; ... } | null
      // (not an array -- filter_single changes cardinality)
      ```
      
      **Why good:** Object shorthand `{ id: "..." }` is concise for exclusive properties, `e.op` form provides full expression flexibility, `filter_single` tells the query builder this returns zero or one result (not an array)
      
      ### Bad Example -- Missing Filter on Delete-All
      
      ```typescript
      // BAD: No filter -- selects ALL users
      const allQuery = e.select(e.User, () => ({
        id: true,
        name: true,
      }));
      // This returns every user in the database -- intentional?
      // If you want all users, be explicit about it.
      ```
      
      **Why bad:** No filter returns the entire table -- make sure this is intentional, especially in production with large datasets
      
      ---
      
      ## Pattern 3: e.insert -- Creating Data
      
      ### Good Example -- Insert with Link Resolution
      
      ```typescript
      import e from "./dbschema/edgeql-js";
      
      const insertPostQuery = e.insert(e.Post, {
        title: "Getting Started with EdgeDB",
        body: "EdgeDB is a graph-relational database...",
        status: e.Status.draft,
        author: e.select(e.User, (user) => ({
          filter_single: e.op(user.email, "=", "alice@example.com"),
        })),
        tags: e.select(e.Tag, (tag) => ({
          filter: e.op(tag.name, "in", e.set("edgedb", "tutorial")),
        })),
      });
      
      // Select the inserted post to get its fields back
      const insertAndReturnQuery = e.select(insertPostQuery, () => ({
        id: true,
        title: true,
        author: { name: true },
      }));
      
      export { insertAndReturnQuery };
      ```
      
      **Why good:** Links resolved via subqueries (not raw IDs), multi link assigned with `e.set()` for tag names, `e.select()` wrapping insert to return specific fields, enum value via `e.Status.draft`
      
      ### Good Example -- Upsert (Insert Unless Conflict)
      
      ```typescript
      import e from "./dbschema/edgeql-js";
      
      const upsertUserQuery = e
        .insert(e.User, {
          name: "Alice",
          email: "alice@example.com",
          role: e.Role.admin,
          organization: e.select(e.Organization, (org) => ({
            filter_single: e.op(org.name, "=", "Acme"),
          })),
        })
        .unlessConflict((user) => ({
          on: user.email,
          else: e.update(user, () => ({
            set: {
              name: "Alice",
              role: e.Role.admin,
            },
          })),
        }));
      
      export { upsertUserQuery };
      ```
      
      **Why good:** `.unlessConflict()` handles duplicate key gracefully, `on: user.email` targets the exclusive constraint, `else` clause updates on conflict
      
      ---
      
      ## Pattern 4: e.update -- Modifying Data
      
      ### Good Example -- Update with Multi Link Manipulation
      
      ```typescript
      import e from "./dbschema/edgeql-js";
      
      // Add tags (+=)
      const addTagsQuery = e.update(e.Post, (post) => ({
        filter_single: e.op(post.id, "=", e.uuid(postId)),
        set: {
          tags: {
            "+=": e.select(e.Tag, (tag) => ({
              filter: e.op(tag.name, "in", e.set("featured", "trending")),
            })),
          },
        },
      }));
      
      // Remove tags (-=)
      const removeTagQuery = e.update(e.Post, (post) => ({
        filter_single: e.op(post.id, "=", e.uuid(postId)),
        set: {
          tags: {
            "-=": e.select(e.Tag, (tag) => ({
              filter_single: e.op(tag.name, "=", "draft"),
            })),
          },
        },
      }));
      
      // Replace all tags (:=)
      const replaceTagsQuery = e.update(e.Post, (post) => ({
        filter_single: e.op(post.id, "=", e.uuid(postId)),
        set: {
          tags: e.select(e.Tag, (tag) => ({
            filter: e.op(tag.name, "in", e.set("final", "reviewed")),
          })),
        },
      }));
      
      export { addTagsQuery, removeTagQuery, replaceTagsQuery };
      ```
      
      **Why good:** Three distinct operations -- `+=` adds, `-=` removes, plain assignment replaces, each corresponds to EdgeQL's set manipulation operators
      
      ### Good Example -- Self-Referential Update
      
      ```typescript
      import e from "./dbschema/edgeql-js";
      
      const incrementViewsQuery = e.update(e.Post, (post) => ({
        filter_single: e.op(post.id, "=", e.uuid(postId)),
        set: {
          view_count: e.op(post.view_count, "+", 1),
        },
      }));
      
      export { incrementViewsQuery };
      ```
      
      **Why good:** `e.op(post.view_count, '+', 1)` references the current value, update is atomic
      
      ---
      
      ## Pattern 5: e.delete -- Removing Data
      
      ### Good Example -- Delete with Filter
      
      ```typescript
      import e from "./dbschema/edgeql-js";
      
      // Delete specific post
      const deletePostQuery = e.delete(e.Post, (post) => ({
        filter_single: e.op(post.id, "=", e.uuid(postId)),
      }));
      
      // Delete returns the deleted objects -- select to get fields
      const deleteAndReturnQuery = e.select(deletePostQuery, () => ({
        id: true,
        title: true,
      }));
      
      // Bulk delete with condition
      const DAYS_CUTOFF = 30;
      const cleanupQuery = e.delete(e.Post, (post) => ({
        filter: e.op(
          e.op(post.status, "=", e.Status.draft),
          "and",
          e.op(
            post.created_at,
            "<",
            e.op(e.datetime_of_statement(), "-", e.duration(`P${DAYS_CUTOFF}D`)),
          ),
        ),
      }));
      
      export { deleteAndReturnQuery, cleanupQuery };
      ```
      
      **Why good:** `filter_single` for deleting one object, `e.select` wrapping delete to return deleted fields, compound filter for bulk cleanup, named constant for cutoff
      
      ---
      
      ## Pattern 6: e.params -- Parameterized Queries
      
      ### Good Example -- Reusable Parameterized Query
      
      ```typescript
      import e from "./dbschema/edgeql-js";
      
      // Define a reusable parameterized query
      const getUserByEmailQuery = e.params({ email: e.str }, ($) =>
        e.select(e.User, (user) => ({
          id: true,
          name: true,
          email: true,
          role: true,
          organization: { name: true },
          filter_single: e.op(user.email, "=", $.email),
        })),
      );
      
      // Usage -- pass params to .run()
      const user = await getUserByEmailQuery.run(client, {
        email: "alice@example.com",
      });
      
      export { getUserByEmailQuery };
      ```
      
      **Why good:** `e.params` declares parameter types, query is reusable with different values, parameters are validated at runtime and compile time
      
      ### Good Example -- Multiple Parameters with Optional
      
      ```typescript
      import e from "./dbschema/edgeql-js";
      
      const PAGE_SIZE = 20;
      
      const searchPostsQuery = e.params(
        {
          search_term: e.str,
          status: e.optional(e.str),
          page: e.int64,
        },
        ($) =>
          e.select(e.Post, (post) => ({
            id: true,
            title: true,
            excerpt: true,
            author: { name: true },
            filter: e.op(
              e.op(post.title, "ilike", e.op("%", "++", $.search_term, "++", "%")),
              "and",
              e.op(
                e.op($.status, "is", e.set()), // null check
                "or",
                e.op(post.status, "=", $.status),
              ),
            ),
            order_by: {
              expression: post.published_at,
              direction: e.DESC,
            },
            limit: PAGE_SIZE,
            offset: e.op($.page, "*", PAGE_SIZE),
          })),
      );
      
      export { searchPostsQuery };
      ```
      
      **Why good:** `e.optional()` for nullable parameters, pagination with offset calculation, ilike for case-insensitive search, null check pattern for optional filter
      
      ---
      
      ## Pattern 7: Query Composition
      
      ### Good Example -- Composing Queries
      
      ```typescript
      import e from "./dbschema/edgeql-js";
      
      // Create a tag, then use it in an insert
      const newTag = e.insert(e.Tag, { name: "edgedb-v6" });
      
      const newPost = e.insert(e.Post, {
        title: "What's New in EdgeDB 6",
        body: "EdgeDB 6 adds SQL support...",
        status: e.Status.published,
        published_at: e.datetime_of_statement(),
        author: e.select(e.User, (user) => ({
          filter_single: e.op(user.email, "=", "alice@example.com"),
        })),
        tags: newTag, // Reference the insert expression directly
      });
      
      // Select the final result
      const query = e.select(newPost, () => ({
        id: true,
        title: true,
        tags: { name: true },
      }));
      
      // The query builder automatically hoists composed expressions into WITH blocks
      const result = await query.run(client);
      
      export { result };
      ```
      
      **Why good:** Query expressions are composable -- reference one expression inside another, query builder auto-generates `WITH` blocks, no intermediate variables needed for the actual EdgeQL
      
      ---
      
      ## Pattern 8: Transactions with Query Builder
      
      ### Good Example -- Transaction with Query Builder
      
      ```typescript
      import { createClient } from "gel";
      import e from "./dbschema/edgeql-js";
      
      const client = createClient();
      const TRANSFER_AMOUNT = 100;
      
      async function transferCredits(fromUserId: string, toUserId: string) {
        await client.transaction(async (tx) => {
          // CRITICAL: pass tx (not client) to .run()
          const debitQuery = e.update(e.Account, (account) => ({
            filter_single: e.op(account.user.id, "=", e.uuid(fromUserId)),
            set: {
              balance: e.op(account.balance, "-", TRANSFER_AMOUNT),
            },
          }));
      
          const result = await e
            .select(debitQuery, () => ({ balance: true }))
            .run(tx); // tx, not client!
      
          if (result && result.balance < 0) {
            throw new Error("Insufficient balance");
          }
      
          await e
            .update(e.Account, (account) => ({
              filter_single: e.op(account.user.id, "=", e.uuid(toUserId)),
              set: {
                balance: e.op(account.balance, "+", TRANSFER_AMOUNT),
              },
            }))
            .run(tx); // tx, not client!
        });
      }
      
      export { transferCredits };
      ```
      
      **Why good:** `client.transaction()` handles commit/abort/retry, `tx` passed to every `.run()` call, error inside callback triggers rollback, named constant for amount
      
      ### Bad Example -- Using Client Inside Transaction
      
      ```typescript
      // BAD: Using client instead of tx
      await client.transaction(async (tx) => {
        await someQuery.run(client); // WRONG -- runs outside the transaction!
        await anotherQuery.run(tx); // Only this one is transactional
      });
      ```
      
      **Why bad:** `client` inside transaction runs outside it -- will not roll back on error, data inconsistency
      
      ---
      
      _For core patterns, see [core.md](core.md). For advanced schema, see [advanced-schema.md](advanced-schema.md)._
      
  • reference.md 10.4 KB
    # Gel (formerly EdgeDB) Reference
    
    > Decision frameworks, quick reference, CLI commands, and scalar types. See [SKILL.md](SKILL.md) for core concepts and [examples/](examples/) for code examples.
    >
    > **CLI naming:** New projects use `gel` CLI commands. Legacy `edgedb` CLI commands still work via compatibility symlinks.
    
    ---
    
    ## Decision Framework
    
    ### Link vs Property
    
    ```
    Does the field reference another object type?
    |-- YES --> Use a link
    |   |-- Is it exactly one related object?
    |   |   |-- YES --> required link (or optional link if nullable)
    |   |   '-- NO --> multi link
    |   '-- Do you need to traverse it in reverse?
    |       |-- YES --> Add a computed backlink on the target type
    |       '-- NO --> Just the forward link is enough
    '-- NO --> Use a property
        |-- Is it one of the built-in scalar types?
        |   |-- YES --> Use the scalar type directly (str, int64, float64, bool, etc.)
        |   '-- NO --> Define a custom scalar (enum, constrained str, etc.)
        '-- Is it a nested structure?
            |-- YES --> Consider a separate type with a link (EdgeDB has no embedded documents)
            '-- NO --> Property with appropriate scalar type
    ```
    
    ### EdgeQL vs Query Builder
    
    ```
    Are you writing TypeScript?
    |-- YES --> Do you want compile-time type safety?
    |   |-- YES --> Use the query builder (e.select, e.insert, etc.)
    |   '-- NO --> Raw EdgeQL strings with type parameters
    '-- NO --> Raw EdgeQL strings (only option for non-TS)
    
    Is the query dynamic (runtime-determined shape)?
    |-- YES --> Query builder composes better for dynamic shapes
    '-- NO --> Either approach works; query builder preferred for consistency
    ```
    
    ### required vs optional
    
    ```
    Must this field always have a value?
    |-- YES --> required (INSERT will fail without it)
    '-- NO --> optional (default -- allows empty set)
    
    For multi links:
    |-- Must there be at least one? --> required multi link
    '-- Can it be empty? --> multi link (no required)
    ```
    
    ---
    
    ## CLI Commands
    
    ```bash
    # Project initialization
    gel project init                   # Initialize project with gel.toml
    
    # Instance management
    gel instance create my_project     # Create a new instance
    gel instance list                  # List instances
    gel instance destroy my_project    # Destroy an instance
    
    # Migration workflow
    gel migration create               # Generate migration from .gel diff (interactive)
    gel migrate                        # Apply pending migrations (idempotent)
    gel migration status               # Show migration status
    gel watch --migrate                # Auto-migrate on file change (prototyping)
    
    # Interactive query shell
    gel                                # Open EdgeQL REPL
    gel query "select 1 + 1"          # Run a single query
    
    # Schema introspection
    gel describe type User            # Show type details
    gel describe schema               # Dump full schema
    
    # Branching (v5+)
    gel branch create dev             # Create a branch
    gel branch switch dev             # Switch to a branch
    gel branch list                   # List branches
    
    # Code generation
    npx @gel/generate edgeql-js       # Generate query builder
    npx @gel/generate queries         # Generate typed functions from .edgeql files
    npx @gel/generate interfaces      # Generate TypeScript interfaces from schema
    
    # UI
    gel ui                            # Open the built-in web UI
    ```
    
    ---
    
    ## Scalar Types
    
    | EdgeQL Type           | TypeScript Type | Description                         |
    | --------------------- | --------------- | ----------------------------------- |
    | `str`                 | `string`        | Unicode string                      |
    | `bool`                | `boolean`       | Boolean                             |
    | `int16`               | `number`        | 16-bit integer                      |
    | `int32`               | `number`        | 32-bit integer                      |
    | `int64`               | `number`        | 64-bit integer                      |
    | `float32`             | `number`        | 32-bit float                        |
    | `float64`             | `number`        | 64-bit float                        |
    | `bigint`              | `bigint`        | Arbitrary precision integer         |
    | `decimal`             | N/A (string)    | Arbitrary precision decimal         |
    | `uuid`                | `string`        | UUID (auto-generated `id` property) |
    | `datetime`            | `Date`          | Timezone-aware datetime             |
    | `cal::local_datetime` | `LocalDateTime` | Timezone-naive datetime             |
    | `cal::local_date`     | `LocalDate`     | Date without time                   |
    | `cal::local_time`     | `LocalTime`     | Time without date                   |
    | `duration`            | `Duration`      | Time span                           |
    | `json`                | `unknown`       | Arbitrary JSON                      |
    | `bytes`               | `Buffer`        | Binary data                         |
    | `sequence`            | `number`        | Auto-incrementing integer           |
    
    ---
    
    ## EdgeQL Operators Quick Reference
    
    | Operator         | Description                | Example                            |
    | ---------------- | -------------------------- | ---------------------------------- |
    | `:=`             | Assignment                 | `set { name := 'Alice' }`          |
    | `=` / `!=`       | Equality / inequality      | `filter .name = 'Alice'`           |
    | `?=` / `?!=`     | Optional equality          | `filter .name ?= 'Alice'`          |
    | `++`             | String/array concatenation | `'Hello' ++ ' ' ++ 'World'`        |
    | `+` `-` `*` `/`  | Arithmetic                 | `.price * .quantity`               |
    | `and` `or` `not` | Logical                    | `.age > 18 and .active = true`     |
    | `in`             | Set membership             | `.status in {'active', 'pending'}` |
    | `like` / `ilike` | Pattern matching           | `.name ilike '%alice%'`            |
    | `exists`         | Non-empty set test         | `filter exists .email`             |
    | `??`             | Coalesce (first non-empty) | `.nickname ?? .name`               |
    | `[is Type]`      | Type filter                | `.<author[is Post]`                |
    | `if..else`       | Conditional                | `'Yes' if .active else 'No'`       |
    | `distinct`       | Deduplicate set            | `select distinct .tags`            |
    | `detached`       | Escape current scope       | `detached User` in subqueries      |
    
    ---
    
    ## Set Operations
    
    | Operation         | Description          | Example                              |
    | ----------------- | -------------------- | ------------------------------------ |
    | `union` / `UNION` | Set union            | `{1, 2} union {2, 3}` => `{1, 2, 3}` |
    | `intersect`       | Set intersection     | Not built-in; use filter + in        |
    | `except`          | Set difference       | `{1, 2, 3} except {2}` => `{1, 3}`   |
    | `count()`         | Count elements       | `select count(User)`                 |
    | `exists`          | Test non-empty       | `select exists (select User)`        |
    | `array_agg()`     | Convert set to array | `select array_agg(User.name)`        |
    | `array_unpack()`  | Convert array to set | `select array_unpack([1, 2, 3])`     |
    
    ---
    
    ## Common Constraints
    
    | Constraint             | Description               | Example Usage                              |
    | ---------------------- | ------------------------- | ------------------------------------------ |
    | `exclusive`            | Unique value              | `constraint exclusive;`                    |
    | `exclusive on (expr)`  | Compound unique           | `constraint exclusive on ((.email, .org))` |
    | `min_value(n)`         | Minimum numeric value     | `constraint min_value(0);`                 |
    | `max_value(n)`         | Maximum numeric value     | `constraint max_value(1000);`              |
    | `min_len_value(n)`     | Minimum string length     | `constraint min_len_value(1);`             |
    | `max_len_value(n)`     | Maximum string length     | `constraint max_len_value(200);`           |
    | `regexp(r'pattern')`   | Regex validation          | `constraint regexp(r'^[a-z]+$');`          |
    | `expression on (expr)` | Custom boolean expression | `constraint expression on (.start < .end)` |
    
    ---
    
    ## Link Modification Operators (UPDATE)
    
    | Operator | Description            | Example                            |
    | -------- | ---------------------- | ---------------------------------- |
    | `:=`     | Replace entire set     | `set { tags := ... }`              |
    | `+=`     | Add to multi link      | `set { tags += (select Tag ...) }` |
    | `-=`     | Remove from multi link | `set { tags -= (select Tag ...) }` |
    
    ---
    
    ## Environment Variables
    
    Both `GEL_*` and `EDGEDB_*` prefixes are supported. New projects should prefer `GEL_*`.
    
    | Variable (new / legacy)                          | Description                     |
    | ------------------------------------------------ | ------------------------------- |
    | `GEL_DSN` / `EDGEDB_DSN`                         | Full connection string          |
    | `GEL_INSTANCE` / `EDGEDB_INSTANCE`               | Instance name                   |
    | `GEL_HOST` / `EDGEDB_HOST`                       | Server hostname                 |
    | `GEL_PORT` / `EDGEDB_PORT`                       | Server port (default: 5656)     |
    | `GEL_DATABASE` / `EDGEDB_DATABASE`               | Database name                   |
    | `GEL_BRANCH` / `EDGEDB_BRANCH`                   | Branch name (v5+)               |
    | `GEL_USER` / `EDGEDB_USER`                       | Username                        |
    | `GEL_PASSWORD` / `EDGEDB_PASSWORD`               | Password                        |
    | `GEL_SECRET_KEY` / `EDGEDB_SECRET_KEY`           | Secret key for Gel Cloud        |
    | `GEL_CLIENT_SECURITY` / `EDGEDB_CLIENT_SECURITY` | `insecure_dev_mode` or `strict` |
    
    ---
    
    ## Naming Migration (Feb 2025)
    
    EdgeDB was rebranded to **Gel** in February 2025. All old names continue to work via compatibility shims.
    
    | Old Name            | New Name         | Status                    |
    | ------------------- | ---------------- | ------------------------- |
    | `edgedb` (npm)      | `gel`            | Shim available, both work |
    | `@edgedb/generate`  | `@gel/generate`  | Shim available, both work |
    | `edgedb` CLI        | `gel` CLI        | Symlinks available        |
    | `edgedb.toml`       | `gel.toml`       | Both supported            |
    | `.esdl` files       | `.gel` files     | Both supported            |
    | `EDGEDB_*` env vars | `GEL_*` env vars | Both supported            |
    
    An automated codemod tool is available for migration: see the [official blog post](https://www.geldata.com/blog/edgedb-is-now-gel-and-postgres-is-the-future) for details.
    
  • SKILL.md 12.6 KB
    ---
    name: api-database-edgedb
    description: Graph-relational database with EdgeQL query language, code-first schema, link-based relations, computed properties, and fully typed TypeScript query builder
    ---
    
    # Gel (formerly EdgeDB) Patterns
    
    > **Quick Guide:** Gel (formerly EdgeDB) is a graph-relational database built on PostgreSQL. Define schemas in `.gel` files using SDL with types, links, and computed properties. Use `gel migration create` + `gel migrate` for schema changes. Query with EdgeQL (set-based, deeply nested shapes) or the TypeScript query builder (`e.select`, `e.insert`). Everything in EdgeQL is a set -- empty sets need explicit casts, and operations on sets produce Cartesian products. Use `global` variables with access policies for row-level security. The query builder requires a running database for code generation (`npx @gel/generate edgeql-js`).
    >
    > **Naming:** EdgeDB was rebranded to **Gel** in February 2025. The `edgedb` npm package, CLI, and `.esdl` extension still work via compatibility shims, but new projects should use `gel`, `@gel/generate`, and `.gel` files.
    
    ---
    
    <critical_requirements>
    
    ## CRITICAL: Before Using This Skill
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST run `npx @gel/generate edgeql-js` after every `gel migrate` -- the generated query builder is based on the database schema and becomes stale after migrations)**
    
    **(You MUST cast empty sets explicitly (`<str>{}`, `<int64>{}`) -- bare `{}` is a syntax error because EdgeQL is strongly typed and cannot infer the type of an empty set)**
    
    **(You MUST understand that all EdgeQL values are sets -- operations on multi-valued expressions produce Cartesian products, not element-wise results)**
    
    **(You MUST pass the transaction object `tx` (not `client`) to ALL query `.run()` calls inside `client.transaction()` -- using `client` inside a transaction runs queries outside the transaction)**
    
    **(You MUST NOT use volatile functions like `datetime_current()` in schema-defined computed properties -- use `datetime_of_transaction()` or `datetime_of_statement()` instead)**
    
    </critical_requirements>
    
    ---
    
    **Auto-detection:** Gel, gel, EdgeDB, edgedb, EdgeQL, edgeql, .gel, .esdl, dbschema, edgeql-js, createClient, e.select, e.insert, e.update, e.delete, e.params, gel migrate, gel migration, edgedb migrate, SDL schema, backlink, access policy, gel.toml, edgedb.toml
    
    **When to use:**
    
    - Defining graph-relational schemas with types, links, and computed properties
    - Writing type-safe queries with EdgeQL or the TypeScript query builder
    - Managing schema migrations with the built-in migration system
    - Modeling complex relationships (multi links, backlinks, polymorphism)
    - Implementing row-level security with access policies and globals
    
    **Key patterns covered:**
    
    - Client setup and connection (`createClient`, DSN, environment variables)
    - Schema definition in SDL (types, properties, links, constraints, computed)
    - EdgeQL query language (SELECT shapes, INSERT, UPDATE, DELETE)
    - TypeScript query builder (`e.select`, `e.insert`, `e.update`, `e.delete`)
    - Migrations workflow (`gel migration create`, `gel migrate`)
    
    **When NOT to use:**
    
    - Simple key-value storage (use a dedicated key-value store)
    - Projects that need raw SQL as the primary interface (Gel uses EdgeQL; Gel 6+ adds native SQL support but EdgeQL is the primary interface)
    - Environments where you cannot run the Gel server (it is not an embedded database)
    
    **Detailed Resources:**
    
    - For decision frameworks and quick reference, see [reference.md](reference.md)
    
    **Core Patterns:**
    
    - [examples/core.md](examples/core.md) - Client setup, schema definition, EdgeQL basics, migration workflow
    
    **Query Builder:**
    
    - [examples/query-builder.md](examples/query-builder.md) - TypeScript query builder (e.select, e.insert, e.update, e.delete, e.params)
    
    **Advanced Schema:**
    
    - [examples/advanced-schema.md](examples/advanced-schema.md) - Access policies, backlinks, abstract types, polymorphism, triggers
    
    ---
    
    <philosophy>
    
    ## Philosophy
    
    Gel is a graph-relational database. It combines the relational model (tables, constraints, ACID) with a graph model (links between objects, deep traversal). The core idea: **relationships are first-class citizens, not join tables.**
    
    **Core principles:**
    
    1. **Schema is the source of truth** -- Define everything in `.gel` files (or `.esdl` for legacy projects). Migrations are auto-generated by comparing your schema files against the database state.
    2. **Links over foreign keys** -- Use `link` to connect types. Gel handles the underlying foreign keys. You never write JOIN -- you traverse links with dot notation.
    3. **Sets everywhere** -- Every value in EdgeQL is a set. A single string is a set of one element. This is the most important mental model shift from SQL.
    4. **Shapes for projection** -- SELECT returns structured, nested objects (like GraphQL responses), not flat rows. You specify the "shape" of what you want.
    5. **Computed properties are views** -- Computed properties and links are not stored; they are evaluated on every query. Use them for derived data.
    6. **Query builder for TypeScript** -- The generated query builder provides compile-time type safety. Prefer it over raw EdgeQL strings in TypeScript projects.
    
    **When to use Gel:**
    
    - Applications with complex, deeply nested relationships (social graphs, content systems, e-commerce)
    - Projects that benefit from graph-style traversals without sacrificing relational integrity
    - TypeScript projects that want compile-time type-safe database queries
    - Teams that want automatic migration generation from schema changes
    
    **When NOT to use:**
    
    - Existing projects locked into raw PostgreSQL with extensive stored procedures
    - Applications where SQL compatibility is the only acceptable query language (Gel 6+ has native SQL support, but EdgeQL is the primary interface)
    - Environments that cannot run the Gel server process
    
    </philosophy>
    
    ---
    
    <patterns>
    
    ## Core Patterns
    
    ### Pattern 1: Client Setup
    
    Create a client with `createClient()`. Connection details are auto-discovered from `gel.toml` (or `edgedb.toml`) or environment variables.
    
    ```typescript
    import { createClient } from "gel";
    
    const client = createClient(); // auto-discovers from project config
    export { client };
    ```
    
    Choose the right query method by expected cardinality: `query()` for sets, `querySingle()` for optional single, `queryRequiredSingle()` when result is guaranteed, `execute()` for side-effect-only statements. See [examples/core.md](examples/core.md) for full client setup patterns.
    
    ---
    
    ### Pattern 2: Schema Definition (SDL)
    
    Schemas live in `dbschema/*.gel` files (or `*.esdl` for legacy projects). Use `required` for non-null, `link` for relationships, `constraint exclusive` for uniqueness, computed backlinks (`.<author[is Post]`) for reverse traversal.
    
    ```
    # dbschema/default.gel
    module default {
      type User {
        required name: str;
        required email: str { constraint exclusive; };
        multi posts := .<author[is Post];  # computed backlink
      }
      type Post {
        required title: str;
        required author: User;             # link, not uuid!
      }
    }
    ```
    
    **Key rule:** Always use `link` for relationships -- never raw `uuid` properties. See [examples/core.md](examples/core.md) for complete schema patterns with constraints, indexes, and enums.
    
    ---
    
    ### Pattern 3: EdgeQL Queries
    
    SELECT uses shapes for projection (like GraphQL), INSERT assigns links via subqueries, UPDATE uses `+=`/`-=`/`:=` for multi link manipulation.
    
    ```edgeql
    select User {
      name, email,
      posts: { title, status } filter .status = Status.published,
    } filter .email = 'alice@example.com';
    ```
    
    See [examples/core.md](examples/core.md) for SELECT/INSERT/UPDATE/DELETE patterns and parameterized queries.
    
    ---
    
    ### Pattern 4: Migrations
    
    Gel compares your `.gel` files against the database and auto-generates migrations.
    
    ```bash
    gel migration create    # generate migration from schema diff (interactive)
    gel migrate             # apply pending migrations (idempotent)
    npx @gel/generate edgeql-js  # regenerate query builder
    ```
    
    Never edit the database with DDL directly -- always modify `.gel` files and use the migration workflow. See [examples/core.md](examples/core.md) for the full workflow including `gel watch --migrate` for prototyping.
    
    ---
    
    ### Pattern 5: Transactions
    
    Pass `tx` (not `client`) to ALL operations inside `client.transaction()`. Using `client` inside the callback runs queries outside the transaction.
    
    ```typescript
    await client.transaction(async (tx) => {
      await tx.execute(`update Account ...`); // tx, not client!
    });
    ```
    
    See [examples/core.md](examples/core.md) for transaction patterns and [examples/query-builder.md](examples/query-builder.md) for query builder transactions.
    
    </patterns>
    
    ---
    
    <red_flags>
    
    ## RED FLAGS
    
    **High Priority Issues:**
    
    - Using `client` instead of `tx` inside `client.transaction()` -- queries run outside the transaction and cannot be rolled back
    - Forgetting to run `npx @gel/generate edgeql-js` after `gel migrate` -- query builder types are stale and TypeScript won't catch schema mismatches
    - Using raw `uuid` properties instead of `link` -- defeats Gel's graph traversal and referential integrity
    - Using `datetime_current()` in schema-defined computed properties -- volatile functions are forbidden in schema computeds; use `datetime_of_statement()` or `datetime_of_transaction()`
    
    **Medium Priority Issues:**
    
    - Bare `{}` for empty sets -- EdgeQL requires explicit type cast (`<str>{}`, `<array<int64>>[]`) because the type cannot be inferred from an empty literal
    - Using `:=` when you mean `+=` on multi links in UPDATE -- `:=` replaces the entire set, `+=` adds to it, `-=` removes from it
    - Not specifying `filter` on UPDATE/DELETE -- without a filter, the operation applies to ALL objects of that type
    - Editing the database with DDL directly instead of through `.gel` files + migrations -- causes schema drift between files and database
    
    **Common Mistakes:**
    
    - Expecting element-wise behavior from set operations -- `{1, 2} + {10, 20}` produces `{11, 21, 12, 22}` (Cartesian product), not `{11, 22}`
    - Forgetting that `select` on a single link returns an object (not an ID) -- you do not need to JOIN; just traverse with `.`
    - Using `select count(MyType)` and expecting `querySingle` to work -- `count()` always returns exactly one value, so use `queryRequiredSingle`
    - Defining a computed backlink but forgetting the type filter -- `.<author` without `[is Post]` returns all types that have an `author` link
    
    **Gotchas & Edge Cases:**
    
    - Computed properties are not stored -- they are re-evaluated on every query, which can be expensive for complex expressions
    - `required` on a multi link means "at least one" -- an empty set violates the constraint, which can be surprising
    - String concatenation uses `++` not `+` -- the `+` operator is for arithmetic only
    - `LIMIT 1` does NOT make a query return a singleton for cardinality purposes -- use `filter .id = <uuid>$id` (exclusive constraint) for the query builder to infer singleton cardinality
    - Multi links are unordered sets -- if you need ordering, add an `order by` in your query or use an intermediate type with an `order` property
    - Backlinks (`.<link_name`) default to `multi` cardinality -- use `single` keyword explicitly if you know the relationship is one-to-one
    - EdgeDB branches (v5+) are separate database copies, not lightweight references -- branching a large database takes time and disk space proportional to the data size
    
    </red_flags>
    
    ---
    
    <critical_reminders>
    
    ## CRITICAL REMINDERS
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST run `npx @gel/generate edgeql-js` after every `gel migrate` -- the generated query builder is based on the database schema and becomes stale after migrations)**
    
    **(You MUST cast empty sets explicitly (`<str>{}`, `<int64>{}`) -- bare `{}` is a syntax error because EdgeQL is strongly typed and cannot infer the type of an empty set)**
    
    **(You MUST understand that all EdgeQL values are sets -- operations on multi-valued expressions produce Cartesian products, not element-wise results)**
    
    **(You MUST pass the transaction object `tx` (not `client`) to ALL query `.run()` calls inside `client.transaction()` -- using `client` inside a transaction runs queries outside the transaction)**
    
    **(You MUST NOT use volatile functions like `datetime_current()` in schema-defined computed properties -- use `datetime_of_transaction()` or `datetime_of_statement()` instead)**
    
    **Failure to follow these rules will cause stale types, silent data bugs, or transaction isolation failures.**
    
    </critical_reminders>
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related