Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
317 changes: 0 additions & 317 deletions usage-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,321 +10,4 @@ SPDX-License-Identifier: MIT

AshPostgres is the PostgreSQL data layer for Ash Framework. It's the most fully-featured Ash data layer and should be your default choice unless you have specific requirements for another data layer. Any PostgreSQL version higher than 13 is fully supported.

## Basic Configuration

To use AshPostgres, add the data layer to your resource:

```elixir
defmodule MyApp.Tweet do
use Ash.Resource,
data_layer: AshPostgres.DataLayer

attributes do
integer_primary_key :id
attribute :text, :string
end

relationships do
belongs_to :author, MyApp.User
end

postgres do
table "tweets"
repo MyApp.Repo
end
end
```

## PostgreSQL Configuration

### Table & Schema Configuration

```elixir
postgres do
# Required: Define the table name for this resource
table "users"

# Optional: Define the PostgreSQL schema
schema "public"

# Required: Define the Ecto repo to use
repo MyApp.Repo

# Optional: Control whether migrations are generated for this resource
migrate? true
end
```

## Foreign Key References

Use the `references` section to configure foreign key behavior:

```elixir
postgres do
table "comments"
repo MyApp.Repo

references do
# Simple reference with defaults
reference :post

# Fully configured reference
reference :user,
on_delete: :delete, # What happens when referenced row is deleted
on_update: :update, # What happens when referenced row is updated
name: "comments_to_users_fkey", # Custom constraint name
deferrable: true, # Make constraint deferrable
initially_deferred: false # Defer constraint check to end of transaction
end
end
```

### Foreign Key Actions

For `on_delete` and `on_update` options:

- `:nothing` or `:restrict` - Prevent the change to the referenced row
- `:delete` - Delete the row when the referenced row is deleted (for `on_delete` only)
- `:update` - Update the row according to changes in the referenced row (for `on_update` only)
- `:nilify` - Set all foreign key columns to NULL
- `{:nilify, columns}` - Set specific columns to NULL (Postgres 15.0+ only)

> **Warning**: These operations happen directly at the database level. No resource logic, authorization rules, validations, or notifications are triggered.

## Check Constraints

Define database check constraints:

```elixir
postgres do
check_constraints do
check_constraint :positive_amount,
check: "amount > 0",
name: "positive_amount_check",
message: "Amount must be positive"

check_constraint :status_valid,
check: "status IN ('pending', 'active', 'completed')"
end
end
```

## Custom Indexes

Define custom indexes beyond those automatically created for identities and relationships:

```elixir
postgres do
custom_indexes do
index [:first_name, :last_name]

index :email,
unique: true,
name: "users_email_index",
where: "email IS NOT NULL",
using: :gin

index [:status, :created_at],
concurrently: true,
include: [:user_id]
end
end
```

## Custom SQL Statements

Include custom SQL in migrations:

```elixir
postgres do
custom_statements do
statement "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\""

statement """
CREATE TRIGGER update_updated_at
BEFORE UPDATE ON posts
FOR EACH ROW
EXECUTE FUNCTION trigger_set_timestamp();
"""

statement "DROP INDEX IF EXISTS posts_title_index",
on_destroy: true # Only run when resource is destroyed/dropped
end
end
```

## Migrations and Codegen

### Development Migration Workflow (Recommended)

For development iterations, use the dev workflow to avoid naming migrations prematurely:

1. Make resource changes
2. Run `mix ash.codegen --dev` to generate and run dev migrations
3. Review the migrations and run `mix ash.migrate` to run them
4. Continue making changes and running `mix ash.codegen --dev` as needed
5. When your feature is complete, run `mix ash.codegen add_feature_name` to generate final named migrations (this will rollback dev migrations and squash them)
3. Review the migrations and run `mix ash.migrate` to run them

### Traditional Migration Generation

For single-step changes or when you know the final feature name:

1. Run `mix ash.codegen add_feature_name` to generate migrations
2. Review the generated migrations in `priv/repo/migrations`
3. Run `mix ash.migrate` to apply the migrations

> **Tip**: The dev workflow (`--dev` flag) is preferred during development as it allows you to iterate without thinking of migration names and provides better development ergonomics.

> **Warning**: Always review migrations before applying them to ensure they are correct and safe.

## Multitenancy

AshPostgres supports schema-based multitenancy:

```elixir
defmodule MyApp.Tenant do
use Ash.Resource,
data_layer: AshPostgres.DataLayer

# Resource definition...

postgres do
table "tenants"
repo MyApp.Repo

# Automatically create/manage tenant schemas
manage_tenant do
template ["tenant_", :id]
end
end
end
```

### Setting Up Multitenancy

1. Configure your repo to support multitenancy:

```elixir
defmodule MyApp.Repo do
use AshPostgres.Repo, otp_app: :my_app

# Return all tenant schemas for migrations
def all_tenants do
import Ecto.Query, only: [from: 2]
all(from(t in "tenants", select: fragment("? || ?", "tenant_", t.id)))
end
end
```

2. Mark resources that should be multi-tenant:

```elixir
defmodule MyApp.Post do
use Ash.Resource,
data_layer: AshPostgres.DataLayer

multitenancy do
strategy :context
attribute :tenant
end

# Resource definition...
end
```

3. When tenant migrations are generated, they'll be in `priv/repo/tenant_migrations`

4. Run tenant migrations in addition to regular migrations:

```bash
# Run regular migrations
mix ash.migrate

# Run tenant migrations
mix ash_postgres.migrate --tenants
```

## Advanced Features

### Manual Relationships

For complex relationships that can't be expressed with standard relationship types:

```elixir
defmodule MyApp.Post.Relationships.HighlyRatedComments do
use Ash.Resource.ManualRelationship
use AshPostgres.ManualRelationship

def load(posts, _opts, context) do
post_ids = Enum.map(posts, & &1.id)

{:ok,
MyApp.Comment
|> Ash.Query.filter(post_id in ^post_ids)
|> Ash.Query.filter(rating > 4)
|> MyApp.read!()
|> Enum.group_by(& &1.post_id)}
end

def ash_postgres_join(query, _opts, current_binding, as_binding, :inner, destination_query) do
{:ok,
Ecto.Query.from(_ in query,
join: dest in ^destination_query,
as: ^as_binding,
on: dest.post_id == as(^current_binding).id,
on: dest.rating > 4
)}
end

# Other required callbacks...
end

# In your resource:
relationships do
has_many :highly_rated_comments, MyApp.Comment do
manual MyApp.Post.Relationships.HighlyRatedComments
end
end
```

### Using Multiple Repos (Read Replicas)

Configure different repos for reads vs mutations:

```elixir
postgres do
repo fn resource, type ->
case type do
:read -> MyApp.ReadReplicaRepo
:mutate -> MyApp.WriteRepo
end
end
end
```

## Best Practices

1. **Organize migrations**: Run `mix ash.codegen` after each meaningful set of resource changes with a descriptive name:
```bash
mix ash.codegen --name add_user_roles
mix ash.codegen --name implement_post_tagging
```

2. **Use check constraints for domain invariants**: Enforce data integrity at the database level:
```elixir
check_constraints do
check_constraint :valid_status, check: "status IN ('pending', 'active', 'completed')"
check_constraint :positive_balance, check: "balance >= 0"
end
```

3. **Use custom statements for schema-only changes**: If you need to add database objects not directly tied to resources:
```elixir
custom_statements do
statement "CREATE EXTENSION IF NOT EXISTS \"pgcrypto\""
statement "CREATE INDEX users_search_idx ON users USING gin(search_vector)"
end
```

Remember that using AshPostgres provides a full-featured PostgreSQL data layer for your Ash application, giving you both the structure and declarative approach of Ash along with the power and flexibility of PostgreSQL.
63 changes: 63 additions & 0 deletions usage-rules/advanced_features.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<!--
SPDX-FileCopyrightText: 2020 Zach Daniel
SPDX-License-Identifier: MIT
-->

# Advanced Features

## Manual Relationships

For complex relationships that can't be expressed with standard relationship types:

```elixir
defmodule MyApp.Post.Relationships.HighlyRatedComments do
use Ash.Resource.ManualRelationship
use AshPostgres.ManualRelationship

def load(posts, _opts, context) do
post_ids = Enum.map(posts, & &1.id)

{:ok,
MyApp.Comment
|> Ash.Query.filter(post_id in ^post_ids)
|> Ash.Query.filter(rating > 4)
|> MyApp.read!()
|> Enum.group_by(& &1.post_id)}
end

def ash_postgres_join(query, _opts, current_binding, as_binding, :inner, destination_query) do
{:ok,
Ecto.Query.from(_ in query,
join: dest in ^destination_query,
as: ^as_binding,
on: dest.post_id == as(^current_binding).id,
on: dest.rating > 4
)}
end

# Other required callbacks...
end

# In your resource:
relationships do
has_many :highly_rated_comments, MyApp.Comment do
manual MyApp.Post.Relationships.HighlyRatedComments
end
end
```

## Using Multiple Repos (Read Replicas)

Configure different repos for reads vs mutations:

```elixir
postgres do
repo fn resource, type ->
case type do
:read -> MyApp.ReadReplicaRepo
:mutate -> MyApp.WriteRepo
end
end
end
```
Loading
Loading