Skip to content

Commit 5fb8072

Browse files
improvement: Split up usage rules into sub-rules (#691)
* Extract configuration section * Extract foreign keys section * Extract check constraints * Extract custom indexes * Extract custom SQL statements * Extract migrations * Extract multitenancy * Extract advanced features * Extract best practices * Fix heading level in configuration
1 parent 643a147 commit 5fb8072

File tree

10 files changed

+362
-317
lines changed

10 files changed

+362
-317
lines changed

usage-rules.md

Lines changed: 0 additions & 317 deletions
Original file line numberDiff line numberDiff line change
@@ -10,321 +10,4 @@ SPDX-License-Identifier: MIT
1010

1111
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.
1212

13-
## Basic Configuration
14-
15-
To use AshPostgres, add the data layer to your resource:
16-
17-
```elixir
18-
defmodule MyApp.Tweet do
19-
use Ash.Resource,
20-
data_layer: AshPostgres.DataLayer
21-
22-
attributes do
23-
integer_primary_key :id
24-
attribute :text, :string
25-
end
26-
27-
relationships do
28-
belongs_to :author, MyApp.User
29-
end
30-
31-
postgres do
32-
table "tweets"
33-
repo MyApp.Repo
34-
end
35-
end
36-
```
37-
38-
## PostgreSQL Configuration
39-
40-
### Table & Schema Configuration
41-
42-
```elixir
43-
postgres do
44-
# Required: Define the table name for this resource
45-
table "users"
46-
47-
# Optional: Define the PostgreSQL schema
48-
schema "public"
49-
50-
# Required: Define the Ecto repo to use
51-
repo MyApp.Repo
52-
53-
# Optional: Control whether migrations are generated for this resource
54-
migrate? true
55-
end
56-
```
57-
58-
## Foreign Key References
59-
60-
Use the `references` section to configure foreign key behavior:
61-
62-
```elixir
63-
postgres do
64-
table "comments"
65-
repo MyApp.Repo
66-
67-
references do
68-
# Simple reference with defaults
69-
reference :post
70-
71-
# Fully configured reference
72-
reference :user,
73-
on_delete: :delete, # What happens when referenced row is deleted
74-
on_update: :update, # What happens when referenced row is updated
75-
name: "comments_to_users_fkey", # Custom constraint name
76-
deferrable: true, # Make constraint deferrable
77-
initially_deferred: false # Defer constraint check to end of transaction
78-
end
79-
end
80-
```
81-
82-
### Foreign Key Actions
83-
84-
For `on_delete` and `on_update` options:
85-
86-
- `:nothing` or `:restrict` - Prevent the change to the referenced row
87-
- `:delete` - Delete the row when the referenced row is deleted (for `on_delete` only)
88-
- `:update` - Update the row according to changes in the referenced row (for `on_update` only)
89-
- `:nilify` - Set all foreign key columns to NULL
90-
- `{:nilify, columns}` - Set specific columns to NULL (Postgres 15.0+ only)
91-
92-
> **Warning**: These operations happen directly at the database level. No resource logic, authorization rules, validations, or notifications are triggered.
93-
94-
## Check Constraints
95-
96-
Define database check constraints:
97-
98-
```elixir
99-
postgres do
100-
check_constraints do
101-
check_constraint :positive_amount,
102-
check: "amount > 0",
103-
name: "positive_amount_check",
104-
message: "Amount must be positive"
105-
106-
check_constraint :status_valid,
107-
check: "status IN ('pending', 'active', 'completed')"
108-
end
109-
end
110-
```
111-
112-
## Custom Indexes
113-
114-
Define custom indexes beyond those automatically created for identities and relationships:
115-
116-
```elixir
117-
postgres do
118-
custom_indexes do
119-
index [:first_name, :last_name]
120-
121-
index :email,
122-
unique: true,
123-
name: "users_email_index",
124-
where: "email IS NOT NULL",
125-
using: :gin
126-
127-
index [:status, :created_at],
128-
concurrently: true,
129-
include: [:user_id]
130-
end
131-
end
132-
```
133-
134-
## Custom SQL Statements
135-
136-
Include custom SQL in migrations:
137-
138-
```elixir
139-
postgres do
140-
custom_statements do
141-
statement "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\""
142-
143-
statement """
144-
CREATE TRIGGER update_updated_at
145-
BEFORE UPDATE ON posts
146-
FOR EACH ROW
147-
EXECUTE FUNCTION trigger_set_timestamp();
148-
"""
149-
150-
statement "DROP INDEX IF EXISTS posts_title_index",
151-
on_destroy: true # Only run when resource is destroyed/dropped
152-
end
153-
end
154-
```
155-
156-
## Migrations and Codegen
157-
158-
### Development Migration Workflow (Recommended)
159-
160-
For development iterations, use the dev workflow to avoid naming migrations prematurely:
161-
162-
1. Make resource changes
163-
2. Run `mix ash.codegen --dev` to generate and run dev migrations
164-
3. Review the migrations and run `mix ash.migrate` to run them
165-
4. Continue making changes and running `mix ash.codegen --dev` as needed
166-
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)
167-
3. Review the migrations and run `mix ash.migrate` to run them
168-
169-
### Traditional Migration Generation
170-
171-
For single-step changes or when you know the final feature name:
172-
173-
1. Run `mix ash.codegen add_feature_name` to generate migrations
174-
2. Review the generated migrations in `priv/repo/migrations`
175-
3. Run `mix ash.migrate` to apply the migrations
176-
177-
> **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.
178-
179-
> **Warning**: Always review migrations before applying them to ensure they are correct and safe.
180-
181-
## Multitenancy
182-
183-
AshPostgres supports schema-based multitenancy:
184-
185-
```elixir
186-
defmodule MyApp.Tenant do
187-
use Ash.Resource,
188-
data_layer: AshPostgres.DataLayer
189-
190-
# Resource definition...
191-
192-
postgres do
193-
table "tenants"
194-
repo MyApp.Repo
195-
196-
# Automatically create/manage tenant schemas
197-
manage_tenant do
198-
template ["tenant_", :id]
199-
end
200-
end
201-
end
202-
```
203-
204-
### Setting Up Multitenancy
205-
206-
1. Configure your repo to support multitenancy:
207-
208-
```elixir
209-
defmodule MyApp.Repo do
210-
use AshPostgres.Repo, otp_app: :my_app
211-
212-
# Return all tenant schemas for migrations
213-
def all_tenants do
214-
import Ecto.Query, only: [from: 2]
215-
all(from(t in "tenants", select: fragment("? || ?", "tenant_", t.id)))
216-
end
217-
end
218-
```
219-
220-
2. Mark resources that should be multi-tenant:
221-
222-
```elixir
223-
defmodule MyApp.Post do
224-
use Ash.Resource,
225-
data_layer: AshPostgres.DataLayer
226-
227-
multitenancy do
228-
strategy :context
229-
attribute :tenant
230-
end
231-
232-
# Resource definition...
233-
end
234-
```
235-
236-
3. When tenant migrations are generated, they'll be in `priv/repo/tenant_migrations`
237-
238-
4. Run tenant migrations in addition to regular migrations:
239-
240-
```bash
241-
# Run regular migrations
242-
mix ash.migrate
243-
244-
# Run tenant migrations
245-
mix ash_postgres.migrate --tenants
246-
```
247-
248-
## Advanced Features
249-
250-
### Manual Relationships
251-
252-
For complex relationships that can't be expressed with standard relationship types:
253-
254-
```elixir
255-
defmodule MyApp.Post.Relationships.HighlyRatedComments do
256-
use Ash.Resource.ManualRelationship
257-
use AshPostgres.ManualRelationship
258-
259-
def load(posts, _opts, context) do
260-
post_ids = Enum.map(posts, & &1.id)
261-
262-
{:ok,
263-
MyApp.Comment
264-
|> Ash.Query.filter(post_id in ^post_ids)
265-
|> Ash.Query.filter(rating > 4)
266-
|> MyApp.read!()
267-
|> Enum.group_by(& &1.post_id)}
268-
end
269-
270-
def ash_postgres_join(query, _opts, current_binding, as_binding, :inner, destination_query) do
271-
{:ok,
272-
Ecto.Query.from(_ in query,
273-
join: dest in ^destination_query,
274-
as: ^as_binding,
275-
on: dest.post_id == as(^current_binding).id,
276-
on: dest.rating > 4
277-
)}
278-
end
279-
280-
# Other required callbacks...
281-
end
282-
283-
# In your resource:
284-
relationships do
285-
has_many :highly_rated_comments, MyApp.Comment do
286-
manual MyApp.Post.Relationships.HighlyRatedComments
287-
end
288-
end
289-
```
290-
291-
### Using Multiple Repos (Read Replicas)
292-
293-
Configure different repos for reads vs mutations:
294-
295-
```elixir
296-
postgres do
297-
repo fn resource, type ->
298-
case type do
299-
:read -> MyApp.ReadReplicaRepo
300-
:mutate -> MyApp.WriteRepo
301-
end
302-
end
303-
end
304-
```
305-
306-
## Best Practices
307-
308-
1. **Organize migrations**: Run `mix ash.codegen` after each meaningful set of resource changes with a descriptive name:
309-
```bash
310-
mix ash.codegen --name add_user_roles
311-
mix ash.codegen --name implement_post_tagging
312-
```
313-
314-
2. **Use check constraints for domain invariants**: Enforce data integrity at the database level:
315-
```elixir
316-
check_constraints do
317-
check_constraint :valid_status, check: "status IN ('pending', 'active', 'completed')"
318-
check_constraint :positive_balance, check: "balance >= 0"
319-
end
320-
```
321-
322-
3. **Use custom statements for schema-only changes**: If you need to add database objects not directly tied to resources:
323-
```elixir
324-
custom_statements do
325-
statement "CREATE EXTENSION IF NOT EXISTS \"pgcrypto\""
326-
statement "CREATE INDEX users_search_idx ON users USING gin(search_vector)"
327-
end
328-
```
329-
33013
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.

usage-rules/advanced_features.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<!--
2+
SPDX-FileCopyrightText: 2020 Zach Daniel
3+
4+
SPDX-License-Identifier: MIT
5+
-->
6+
7+
# Advanced Features
8+
9+
## Manual Relationships
10+
11+
For complex relationships that can't be expressed with standard relationship types:
12+
13+
```elixir
14+
defmodule MyApp.Post.Relationships.HighlyRatedComments do
15+
use Ash.Resource.ManualRelationship
16+
use AshPostgres.ManualRelationship
17+
18+
def load(posts, _opts, context) do
19+
post_ids = Enum.map(posts, & &1.id)
20+
21+
{:ok,
22+
MyApp.Comment
23+
|> Ash.Query.filter(post_id in ^post_ids)
24+
|> Ash.Query.filter(rating > 4)
25+
|> MyApp.read!()
26+
|> Enum.group_by(& &1.post_id)}
27+
end
28+
29+
def ash_postgres_join(query, _opts, current_binding, as_binding, :inner, destination_query) do
30+
{:ok,
31+
Ecto.Query.from(_ in query,
32+
join: dest in ^destination_query,
33+
as: ^as_binding,
34+
on: dest.post_id == as(^current_binding).id,
35+
on: dest.rating > 4
36+
)}
37+
end
38+
39+
# Other required callbacks...
40+
end
41+
42+
# In your resource:
43+
relationships do
44+
has_many :highly_rated_comments, MyApp.Comment do
45+
manual MyApp.Post.Relationships.HighlyRatedComments
46+
end
47+
end
48+
```
49+
50+
## Using Multiple Repos (Read Replicas)
51+
52+
Configure different repos for reads vs mutations:
53+
54+
```elixir
55+
postgres do
56+
repo fn resource, type ->
57+
case type do
58+
:read -> MyApp.ReadReplicaRepo
59+
:mutate -> MyApp.WriteRepo
60+
end
61+
end
62+
end
63+
```

0 commit comments

Comments
 (0)