AshAge.Migration (AshAge v1.0.0)

Copy Markdown View Source

Migration helpers for Apache AGE graph database.

Provides functions to create and drop AGE graphs, labels, and indexes within Ecto migrations.

Usage

defmodule MyApp.Repo.Migrations.CreateAgeGraph do
  use Ecto.Migration
  import AshAge.Migration

  def up do
    create_age_graph("my_graph")
    create_vertex_label("my_graph", "Entity")
    create_edge_label("my_graph", "RELATES_TO")
    create_vertex_index("my_graph", "Entity", "tenant_id")
  end

  def down do
    drop_age_graph("my_graph")
  end
end

Summary

Functions

Creates an AGE graph with the given name.

Creates an index on an edge property.

Creates an edge label in the given graph.

Creates an index on a vertex property.

Creates a vertex label in the given graph.

Drops an AGE graph and all its data.

Resource-derived enable_tenant_rls: reads graph, label, tenant property, and GUC from the resource's age/multitenancy DSL. The drift-free default — the policy it writes always matches what the data layer sets at runtime.

Idempotently enables DB-enforced RLS on a resource's label table (host-invoked, runtime SQL). Emits ENABLE + FORCE ROW LEVEL SECURITY, a functional btree index on the tenant discriminator, and an expression policy over properties (NOT a generated column — those segfault AGE cypher() writes). The policy is fail-closed: a blank/unset GUC (current_setting(guc,true) = '') matches nothing.

Idempotently provisions a tenant's AGE graph and its vertex/edge labels at runtime (host-invoked in a tenant-onboarding flow, or from a migration).

Functions

create_age_graph(graph_name)

@spec create_age_graph(String.t()) :: :ok

Creates an AGE graph with the given name.

Idempotent — checks ag_catalog.ag_graph before creating.

create_edge_index(graph_name, label, property)

@spec create_edge_index(String.t(), String.t(), String.t()) :: :ok

Creates an index on an edge property.

Uses ag_catalog.agtype_access_operator() instead of the ->> operator because public comes before ag_catalog in the search path.

create_edge_label(graph_name, label)

@spec create_edge_label(String.t(), String.t()) :: :ok

Creates an edge label in the given graph.

Idempotent — checks ag_catalog.ag_label before creating.

create_vertex_index(graph_name, label, property)

@spec create_vertex_index(String.t(), String.t(), String.t()) :: :ok

Creates an index on a vertex property.

Uses ag_catalog.agtype_access_operator() instead of the ->> operator because public comes before ag_catalog in the search path.

create_vertex_label(graph_name, label)

@spec create_vertex_label(String.t(), String.t()) :: :ok

Creates a vertex label in the given graph.

Idempotent — checks ag_catalog.ag_label before creating.

drop_age_graph(graph_name)

@spec drop_age_graph(String.t()) :: :ok

Drops an AGE graph and all its data.

enable_tenant_rls(repo, resource)

@spec enable_tenant_rls(module(), module()) :: :ok

Resource-derived enable_tenant_rls: reads graph, label, tenant property, and GUC from the resource's age/multitenancy DSL. The drift-free default — the policy it writes always matches what the data layer sets at runtime.

enable_tenant_rls(repo, graph, label, tenant_property, guc)

@spec enable_tenant_rls(module(), String.t(), String.t(), String.t(), String.t()) ::
  :ok

Idempotently enables DB-enforced RLS on a resource's label table (host-invoked, runtime SQL). Emits ENABLE + FORCE ROW LEVEL SECURITY, a functional btree index on the tenant discriminator, and an expression policy over properties (NOT a generated column — those segfault AGE cypher() writes). The policy is fail-closed: a blank/unset GUC (current_setting(guc,true) = '') matches nothing.

Read/target-side only: AGE cypher() CREATE bypasses WITH CHECK, so cross-tenant INSERT is not RLS-denied (the :attribute app-layer force-set owns that). The DB role must be a non-superuser without BYPASSRLS or RLS silently no-ops.

Prefer enable_tenant_rls/2, which derives every argument from the resource DSL.

provision_tenant(repo, graph_name, opts \\ [])

@spec provision_tenant(module(), String.t(), keyword()) :: :ok

Idempotently provisions a tenant's AGE graph and its vertex/edge labels at runtime (host-invoked in a tenant-onboarding flow, or from a migration).

Unlike the create_* helpers above (which use Ecto.Migration.execute/1 and only run inside a migration), this uses Ecto.Adapters.SQL.query!/3, so it works at runtime too. graph_name and every label are validated as AGE identifiers before interpolation — the intended caller derives graph_name from AshAge.tenant_graph/2 over adversarial tenant input.

opts:

  • :vlabels — vertex labels to create (default [])
  • :elabels — edge labels to create (default [])

Idempotent: guarded by IF NOT EXISTS against ag_catalog, so re-runs are no-ops.