defmodule AshAge.Migration do @moduledoc """ 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 """ @doc """ Creates an AGE graph with the given name. """ @spec create_age_graph(String.t()) :: :ok def create_age_graph(graph_name) do validate_identifier!(graph_name) execute("SELECT ag_catalog.create_graph('#{graph_name}')") :ok end @doc """ Drops an AGE graph and all its data. """ @spec drop_age_graph(String.t()) :: :ok def drop_age_graph(graph_name) do validate_identifier!(graph_name) execute("SELECT ag_catalog.drop_graph('#{graph_name}', true)") :ok end @doc """ Creates a vertex label in the given graph. """ @spec create_vertex_label(String.t(), String.t()) :: :ok def create_vertex_label(graph_name, label) do validate_identifier!(graph_name) validate_identifier!(label) execute("SELECT ag_catalog.create_vlabel('#{graph_name}', '#{label}')") :ok end @doc """ Creates an edge label in the given graph. """ @spec create_edge_label(String.t(), String.t()) :: :ok def create_edge_label(graph_name, label) do validate_identifier!(graph_name) validate_identifier!(label) execute("SELECT ag_catalog.create_elabel('#{graph_name}', '#{label}')") :ok end @doc """ 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. """ @spec create_vertex_index(String.t(), String.t(), String.t()) :: :ok def create_vertex_index(graph_name, label, property) do validate_identifier!(graph_name) validate_identifier!(label) validate_identifier!(property) execute(""" CREATE INDEX IF NOT EXISTS idx_#{graph_name}_#{label}_#{property} ON #{graph_name}.#{label} USING btree (ag_catalog.agtype_access_operator(properties, '#{property}')) """) :ok end @doc """ 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. """ @spec create_edge_index(String.t(), String.t(), String.t()) :: :ok def create_edge_index(graph_name, label, property) do validate_identifier!(graph_name) validate_identifier!(label) validate_identifier!(property) execute(""" CREATE INDEX IF NOT EXISTS idx_#{graph_name}_#{label}_#{property} ON #{graph_name}.#{label} USING btree (ag_catalog.agtype_access_operator(properties, '#{property}')) """) :ok end @doc false def validate_identifier!(name) when is_binary(name) do unless Regex.match?(~r/\A[a-zA-Z_][a-zA-Z0-9_]*\z/, name) do raise ArgumentError, "invalid AGE identifier: #{inspect(name)}. " <> "Must start with a letter or underscore and contain only alphanumeric characters and underscores." end name end def validate_identifier!(name) when is_atom(name) do validate_identifier!(Atom.to_string(name)) end defp execute(sql) do Ecto.Migration.execute(sql) end end