Skip to content

Commit 6ddc29e

Browse files
committed
chore: add usage-rules.md
1 parent 0910f17 commit 6ddc29e

File tree

1 file changed

+309
-0
lines changed

1 file changed

+309
-0
lines changed

usage-rules.md

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

0 commit comments

Comments
 (0)