dami 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +306 -0
- data/LICENSE +21 -0
- data/README.md +106 -0
- data/bin/dami +10 -0
- data/docs/01.Getting_Started.md +220 -0
- data/docs/02.Models_and_Fields.md +244 -0
- data/docs/03.Querying.md +387 -0
- data/docs/04.Creating_Updating_Deleting.md +226 -0
- data/docs/05.Validation.md +259 -0
- data/docs/06.Protection.md +160 -0
- data/docs/07.Associations.md +239 -0
- data/docs/08.Scopes.md +99 -0
- data/docs/09.Migrations.md +165 -0
- data/docs/10.Flows_and_Commands.md +181 -0
- data/docs/11.Localization.md +435 -0
- data/docs/12.TheDamiWay.md +227 -0
- data/docs/Manifesto.md +305 -0
- data/docs/site.md +477 -0
- data/lib/dami/actions/command.rb +80 -0
- data/lib/dami/actions/context.rb +63 -0
- data/lib/dami/actions/draft.rb +36 -0
- data/lib/dami/actions/flow.rb +42 -0
- data/lib/dami/adapters/base.rb +40 -0
- data/lib/dami/adapters/sqlite/connection.rb +122 -0
- data/lib/dami/adapters/sqlite/core.rb +17 -0
- data/lib/dami/adapters/sqlite/query.rb +353 -0
- data/lib/dami/adapters/sqlite/schema.rb +135 -0
- data/lib/dami/cli.rb +117 -0
- data/lib/dami/configuration.rb +246 -0
- data/lib/dami/core.rb +98 -0
- data/lib/dami/dsl_guardrails.rb +45 -0
- data/lib/dami/errors.rb +89 -0
- data/lib/dami/inflector.rb +245 -0
- data/lib/dami/localization.rb +131 -0
- data/lib/dami/migration.rb +84 -0
- data/lib/dami/migrator.rb +105 -0
- data/lib/dami/plugins/associations.rb +284 -0
- data/lib/dami/plugins/nested_attributes.rb +180 -0
- data/lib/dami/plugins/protection.rb +30 -0
- data/lib/dami/plugins/validations.rb +90 -0
- data/lib/dami/query/builder.rb +132 -0
- data/lib/dami/query/enumerable.rb +62 -0
- data/lib/dami/query/persistence.rb +133 -0
- data/lib/dami/record_proxy.rb +32 -0
- data/lib/dami/result.rb +27 -0
- data/lib/dami/schema/diff.rb +84 -0
- data/lib/dami/schema/dumper.rb +60 -0
- data/lib/dami/schema/generator.rb +69 -0
- data/lib/dami/schema/introspector.rb +34 -0
- data/lib/dami/schema/loader.rb +58 -0
- data/lib/dami/schema.rb +13 -0
- data/lib/dami/validation_rules.rb +72 -0
- data/lib/dami/version.rb +3 -0
- data/lib/dami.rb +32 -0
- metadata +218 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Chapter 2: The Four Pillars of a Model
|
|
2
|
+
|
|
3
|
+
In Dami, a "model" is more than just a schema definition; it's a complete representation of a business entity. To keep your code clean, scalable, and easy to reason about, Dami organizes this representation into **four** distinct, purpose-built pillars.
|
|
4
|
+
|
|
5
|
+
This architectural pattern solves the "fat model" problem, where data structure, business logic, presentation logic, and querying get tangled together. By separating these concerns, Dami guides you toward a cleaner, more maintainable architecture from the start.
|
|
6
|
+
|
|
7
|
+
The four pillars are:
|
|
8
|
+
|
|
9
|
+
1. **`Dami.model`**: The **Structure** - Defines the schema, fields, and relationships.
|
|
10
|
+
2. **`Dami.behavior`**: The **Write-Side Logic** - Defines validations and protection rules.
|
|
11
|
+
3. **`Dami.present`**: The **Public Interface** - Defines read-side helpers and computed properties.
|
|
12
|
+
4. **`Dami.scopes`**: The **Collection Queries** - Defines reusable query shortcuts.
|
|
13
|
+
|
|
14
|
+
**Enforced by Design:** Dami enforces this separation at the DSL level. Attempting to define validations inside `Dami.model` raises `Dami::InvalidDSLError` with a helpful message pointing you to the correct location. No documentation to memorize—the framework guides you.
|
|
15
|
+
|
|
16
|
+
-----
|
|
17
|
+
|
|
18
|
+
## Pillar 1: `Dami.model` - The Structure
|
|
19
|
+
|
|
20
|
+
The `Dami.model` block is exclusively for defining the **structure** of your data. Its single responsibility is to describe your table's schema and its relationships to other tables.
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
Dami.model :users do
|
|
24
|
+
database :default
|
|
25
|
+
fields do
|
|
26
|
+
field :first_name, :string
|
|
27
|
+
field :last_name, :string
|
|
28
|
+
field :email, :string
|
|
29
|
+
end
|
|
30
|
+
relationships do
|
|
31
|
+
has_many :posts
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Field Types
|
|
37
|
+
|
|
38
|
+
Dami supports all common SQL types.
|
|
39
|
+
|
|
40
|
+
| Type(s) | Generated SQL Type |
|
|
41
|
+
| :--- | :--- |
|
|
42
|
+
| `:string`, `:text` | `TEXT` |
|
|
43
|
+
| `:integer`, `:boolean` | `INTEGER` |
|
|
44
|
+
| `:decimal`, `:float` | `REAL` |
|
|
45
|
+
| `:datetime`, `:date`, `:time` | `DATETIME` |
|
|
46
|
+
| `:json` | `TEXT` (auto-serialized) |
|
|
47
|
+
|
|
48
|
+
Two field names are special: if a model declares `created_at` and/or `updated_at` (`:datetime`), Dami fills them automatically — both on create, `updated_at` on every update — unless you pass a value yourself. Dates and times are stored as UTC strings and come back as strings.
|
|
49
|
+
|
|
50
|
+
### Virtual Fields
|
|
51
|
+
|
|
52
|
+
Virtual fields are defined in your `Dami.model` block but do not have a corresponding column in the database. They are perfect for form data like password confirmations or "terms accepted" checkboxes.
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
Dami.model :users do
|
|
56
|
+
fields do
|
|
57
|
+
field :email, :string
|
|
58
|
+
field :password_digest, :string
|
|
59
|
+
end
|
|
60
|
+
virtual do
|
|
61
|
+
field :password, :string
|
|
62
|
+
field :password_confirmation, :string
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Virtual fields are accepted as input and can be used in validations, but they are stripped out before data is sent to the database.
|
|
68
|
+
|
|
69
|
+
-----
|
|
70
|
+
|
|
71
|
+
## Pillar 2: `Dami.behavior` - The Write-Side Logic
|
|
72
|
+
|
|
73
|
+
The `Dami.behavior` block defines all **write-side rules**. Its job is to ensure data integrity and security whenever a record is created or updated.
|
|
74
|
+
|
|
75
|
+
### Validations
|
|
76
|
+
|
|
77
|
+
Validations guard write operations and ensure data quality:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
Dami.behavior :users do
|
|
81
|
+
validate do
|
|
82
|
+
rule :email, :required, :email
|
|
83
|
+
rule :first_name, :required
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Validations are covered in depth in Chapter 5.
|
|
89
|
+
|
|
90
|
+
### Protection Rules
|
|
91
|
+
|
|
92
|
+
Protection prevents mass-assignment vulnerabilities by blocking certain fields from being set via `create` or `update`:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
Dami.behavior :users do
|
|
96
|
+
protection do
|
|
97
|
+
protect :role # Cannot be mass-assigned
|
|
98
|
+
permit :status # Explicitly allowed
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
# This will raise Dami::ProtectionError
|
|
105
|
+
Dami.db(:users).create(
|
|
106
|
+
first_name: 'Alice',
|
|
107
|
+
role: 'admin' # ❌ Protected field
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# This works - role is explicitly permitted
|
|
111
|
+
Dami.db(:users).create(
|
|
112
|
+
first_name: 'Alice',
|
|
113
|
+
role: 'admin'
|
|
114
|
+
, permit: [:role])
|
|
115
|
+
|
|
116
|
+
# Status is always allowed (in permit list)
|
|
117
|
+
Dami.db(:users).create(
|
|
118
|
+
first_name: 'Alice',
|
|
119
|
+
status: 'active' # ✅ Permitted field
|
|
120
|
+
)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Protection Options:**
|
|
124
|
+
|
|
125
|
+
- `protect :field` - Block field from mass-assignment
|
|
126
|
+
- `permit :field` - Allow field (overrides model-level protection)
|
|
127
|
+
- `permit: [:field]` - Runtime override in `create`/`update`
|
|
128
|
+
- `protect: false` - Disable all protection for the operation
|
|
129
|
+
|
|
130
|
+
-----
|
|
131
|
+
|
|
132
|
+
## Pillar 3: `Dami.present` - The Public Interface
|
|
133
|
+
|
|
134
|
+
This is the dedicated pillar for all **read-side logic** and presentation. It's where you define computed properties, formatting methods, and any other helpers that don't belong in the database. This keeps your data structure clean while providing a rich, helpful interface for your records.
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
Dami.present :users do
|
|
138
|
+
def full_name
|
|
139
|
+
"#{self[:first_name]} #{self[:last_name]}"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def greeting
|
|
143
|
+
"Hello, #{full_name}!"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def is_admin?
|
|
147
|
+
self[:role] == 'admin'
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
These methods are now available on any `RecordProxy` instance for that model:
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
user = Dami.db(:users).find(1)
|
|
156
|
+
puts user.full_name # => "Alice Smith"
|
|
157
|
+
puts user.greeting # => "Hello, Alice Smith!"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Field Access: Hash vs Method Style**
|
|
161
|
+
|
|
162
|
+
- **Hash style** (`user[:field]`): Direct, raw access to database column data.
|
|
163
|
+
- **Method style** (`user.field`): Will first look for a defined method in your `Dami.present` block. If no method is found, it falls back to hash access (`self[:field]`).
|
|
164
|
+
|
|
165
|
+
-----
|
|
166
|
+
|
|
167
|
+
## Pillar 4: `Dami.scopes` - Collection Queries
|
|
168
|
+
|
|
169
|
+
The `Dami.scopes` block defines reusable query shortcuts for filtering collections:
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
Dami.scopes :users do
|
|
173
|
+
scope :active, -> { where(status: 'active') }
|
|
174
|
+
scope :admins, -> { where(role: 'admin') }
|
|
175
|
+
scope :recent, -> { where(created_at: { gt: Time.now - 86400 }) }
|
|
176
|
+
end
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
# Use scopes like methods
|
|
181
|
+
active_users = Dami.db(:users).active.to_a
|
|
182
|
+
recent_admins = Dami.db(:users).admins.recent.to_a
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Scopes are covered in detail in Chapter 8.
|
|
186
|
+
|
|
187
|
+
-----
|
|
188
|
+
|
|
189
|
+
## The Full Picture: A Cohesive Model File
|
|
190
|
+
|
|
191
|
+
For any non-trivial application, you can organize all four pillars for a single entity into one file to see the complete picture:
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
# models/user.rb
|
|
195
|
+
|
|
196
|
+
# 1. Structure
|
|
197
|
+
Dami.model :users do
|
|
198
|
+
fields do
|
|
199
|
+
field :first_name, :string
|
|
200
|
+
field :last_name, :string
|
|
201
|
+
field :email, :string
|
|
202
|
+
field :status, :string
|
|
203
|
+
field :role, :string
|
|
204
|
+
end
|
|
205
|
+
relationships { has_many :posts }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# 2. Behavior (Write-Side)
|
|
209
|
+
Dami.behavior :users do
|
|
210
|
+
validate do
|
|
211
|
+
rule :email, :required, :email
|
|
212
|
+
_ rule :status, inclusion: %w[active inactive]
|
|
213
|
+
end
|
|
214
|
+
protection do
|
|
215
|
+
protect :role
|
|
216
|
+
permit :status
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# 3. Presentation (Read-Side)
|
|
221
|
+
Dami.present :users do
|
|
222
|
+
def full_name
|
|
223
|
+
"#{self[:first_name]} #{self[:last_name]}"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def is_admin?
|
|
227
|
+
self[:role] == 'admin'
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# 4. Querying (Collection-Side)
|
|
232
|
+
Dami.scopes :users do
|
|
233
|
+
scope :active, -> { where(status: 'active') }
|
|
234
|
+
scope :admins, -> { where(role: 'admin') }
|
|
235
|
+
end
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## What's Next?
|
|
239
|
+
|
|
240
|
+
- **Chapter 3: Querying** - Find, filter, and retrieve data using the query builder.
|
|
241
|
+
- **Chapter 4: Creating, Updating, and Deleting** - Modify your data.
|
|
242
|
+
- **Chapter 5: Validation** - Comprehensive guide to the validation DSL.
|
|
243
|
+
- **Chapter 7: Associations** - Connect models with relationships.
|
|
244
|
+
- **Chapter 8: Scopes** - Deep dive into reusable queries.
|
data/docs/03.Querying.md
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
# Chapter 3: Querying
|
|
2
|
+
|
|
3
|
+
Dami provides a powerful, chainable DSL for building SQL queries. All query methods are **lazy**, meaning they don't execute a database query until you explicitly ask for the results.
|
|
4
|
+
|
|
5
|
+
## Basic Queries
|
|
6
|
+
|
|
7
|
+
### Finding by ID
|
|
8
|
+
|
|
9
|
+
The quickest way to retrieve a single record is by its primary key using `.find`.
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
user = Dami.db(:users).find(1)
|
|
13
|
+
user[:name] # => "Alice"
|
|
14
|
+
|
|
15
|
+
# Returns nil if not found
|
|
16
|
+
user = Dami.db(:users).find(999) # => nil
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Where Clauses
|
|
20
|
+
|
|
21
|
+
You can filter records using `.where`. Chaining multiple `.where` calls will combine them with an `AND` operator.
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
# Single condition
|
|
25
|
+
users = Dami.db(:users).where(status: 'active').to_a
|
|
26
|
+
|
|
27
|
+
# Multiple conditions (implicit AND)
|
|
28
|
+
users = Dami.db(:users).where(status: 'active', role: 'admin').to_a
|
|
29
|
+
|
|
30
|
+
# Chained conditions (implicit AND)
|
|
31
|
+
users = Dami.db(:users)
|
|
32
|
+
.where(status: 'active')
|
|
33
|
+
.where('age > ?', 30) # You can also use raw SQL fragments
|
|
34
|
+
.to_a
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### OR Conditions
|
|
38
|
+
|
|
39
|
+
To combine conditions with `OR`, you can use the chainable `.or` method or a block for more complex grouping.
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
# Simple OR
|
|
43
|
+
# Find users who are admins OR active
|
|
44
|
+
users = Dami.db(:users).where(role: 'admin').or(status: 'active')
|
|
45
|
+
# => WHERE role = 'admin' OR status = 'active'
|
|
46
|
+
|
|
47
|
+
# 💡 Grouped Conditions with Blocks
|
|
48
|
+
# Find users where (name is 'Bob') AND (age is 30 OR status is 'pending')
|
|
49
|
+
users = Dami.db(:users).where(name: 'Bob').where do |q|
|
|
50
|
+
q.where(age: 30).or(status: 'pending')
|
|
51
|
+
end
|
|
52
|
+
# => WHERE name = 'Bob' AND (age = 30 OR status = 'pending')
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Operators
|
|
56
|
+
|
|
57
|
+
For more complex comparisons, `where` accepts a hash with operators.
|
|
58
|
+
|
|
59
|
+
### Table of Operators
|
|
60
|
+
|
|
61
|
+
Here is a quick reference for all available operators:
|
|
62
|
+
|
|
63
|
+
| Operator | Example | Generated SQL |
|
|
64
|
+
|---------------|---------------------------------------|--------------------------|
|
|
65
|
+
| `gt` | `{ age: { gt: 18 } }` | `age > 18` |
|
|
66
|
+
| `lt` | `{ age: { lt: 65 } }` | `age < 65` |
|
|
67
|
+
| `gte` | `{ age: { gte: 18 } }` | `age >= 18` |
|
|
68
|
+
| `lte` | `{ age: { lte: 65 } }` | `age <= 65` |
|
|
69
|
+
| `not` | `{ status: { not: 'deleted' } }` | `status != 'deleted'` |
|
|
70
|
+
| `in` | `{ id: [1, 2] }` | `id IN (1, 2)` |
|
|
71
|
+
| `not_in` | `{ role: { not_in: ['a', 'b'] } }` | `role NOT IN ('a', 'b')` |
|
|
72
|
+
| `starts_with` | `{ name: { starts_with: 'A' } }` | `name LIKE 'A%'` |
|
|
73
|
+
| `ends_with` | `{ name: { ends_with: 'e' } }` | `name LIKE '%e'` |
|
|
74
|
+
| `contains` | `{ name: { contains: 'ice' } }` | `name LIKE '%ice%'` |
|
|
75
|
+
|
|
76
|
+
**Note:** Dami is smart enough to infer an `IN` query without specifying `in:`
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
# Find users by a list of IDs. Dami automatically uses an IN clause.
|
|
80
|
+
users = Dami.db(:users).where(id: [1, 5, 10])
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
### String Matching and `NOT IN`
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
# Find users whose names start with 'A'
|
|
88
|
+
users = Dami.db(:users).where(name: { starts_with: 'A' })
|
|
89
|
+
|
|
90
|
+
# Find users whose names contain 'ice'
|
|
91
|
+
users = Dami.db(:users).where(name: { contains: 'ice' })
|
|
92
|
+
|
|
93
|
+
# Find all users who are NOT admins or moderators
|
|
94
|
+
users = Dami.db(:users).where(role: { not_in: ['admin', 'moderator'] })
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Ordering and Limiting
|
|
98
|
+
|
|
99
|
+
### Ordering
|
|
100
|
+
|
|
101
|
+
Sort your results using `.order`.
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
# Ascending (default)
|
|
105
|
+
users = Dami.db(:users).order(:name).to_a
|
|
106
|
+
|
|
107
|
+
# Descending
|
|
108
|
+
users = Dami.db(:users).order(name: :desc).to_a
|
|
109
|
+
|
|
110
|
+
# Multiple columns
|
|
111
|
+
users = Dami.db(:users).order(status: :asc, created_at: :desc).to_a
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Limit and Offset
|
|
115
|
+
|
|
116
|
+
Use `.limit` and `.offset` for pagination.
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
# Get the first 10 users
|
|
120
|
+
users = Dami.db(:users).limit(10).to_a
|
|
121
|
+
|
|
122
|
+
# Skip the first 20 and get the next 10
|
|
123
|
+
users = Dami.db(:users).order(:id).limit(10).offset(20).to_a
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### First and Last
|
|
127
|
+
|
|
128
|
+
Quickly retrieve a single record.
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
# Get the first user (ordered by primary key by default)
|
|
132
|
+
user = Dami.db(:users).first
|
|
133
|
+
|
|
134
|
+
# Get the last active user by creation date
|
|
135
|
+
user = Dami.db(:users).where(status: 'active').order(:created_at).last
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Joins
|
|
139
|
+
|
|
140
|
+
Combine data from multiple tables using joins.
|
|
141
|
+
|
|
142
|
+
### Inner Joins
|
|
143
|
+
|
|
144
|
+
Use `.join` to get records that have matching data in another table.
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
# Get posts that have an associated user
|
|
148
|
+
posts = Dami.db(:posts).join(:users, { id: :user_id })
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
[Image of two overlapping circles]
|
|
152
|
+
|
|
153
|
+
### Left Joins
|
|
154
|
+
|
|
155
|
+
Use `.left_join` to get all records from the first table, even if they don't have a match in the second.
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
# Find all users and their post titles, including users who have not posted anything
|
|
159
|
+
users_with_posts = Dami.db(:users)
|
|
160
|
+
.left_join(:posts, { id: :user_id })
|
|
161
|
+
.select('users.name', 'posts.title AS post_title')
|
|
162
|
+
.to_a
|
|
163
|
+
|
|
164
|
+
# A user with no posts will appear in the results with `post_title: nil`
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Advanced Features
|
|
168
|
+
|
|
169
|
+
### Selecting Columns
|
|
170
|
+
|
|
171
|
+
By default, Dami selects all columns (`SELECT *`). Use `.select` to be more efficient.
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
# Only get the id and email columns
|
|
175
|
+
users = Dami.db(:users).select(:id, :email).to_a
|
|
176
|
+
|
|
177
|
+
# Use aliases with joins
|
|
178
|
+
posts = Dami.db(:posts)
|
|
179
|
+
.join(:users, { id: :user_id })
|
|
180
|
+
.select('posts.title', 'users.name AS author_name')
|
|
181
|
+
.to_a
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Counting and Existence Checks
|
|
185
|
+
|
|
186
|
+
Check the number of records or if any exist.
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
# Get the total count
|
|
190
|
+
total = Dami.db(:users).count
|
|
191
|
+
|
|
192
|
+
# Get a conditional count
|
|
193
|
+
active_admins = Dami.db(:users).where(status: 'active', role: 'admin').count
|
|
194
|
+
|
|
195
|
+
# Performant existence checks (often faster than `.count > 0`)
|
|
196
|
+
if Dami.db(:users).where(status: 'pending').any?
|
|
197
|
+
puts "There are pending users to review."
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# `exists?` is a convenient alias
|
|
201
|
+
if Dami.db(:users).exists?(status: 'pending')
|
|
202
|
+
puts "There are pending users to review."
|
|
203
|
+
end
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Batch Processing
|
|
207
|
+
|
|
208
|
+
For large datasets, process records in batches to avoid high memory usage.
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
# Process one user at a time
|
|
212
|
+
Dami.db(:users).find_each(batch_size: 1000) do |user|
|
|
213
|
+
send_email(user)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Process 1000 users at a time
|
|
217
|
+
Dami.db(:users).find_in_batches(batch_size: 1000) do |batch_of_users|
|
|
218
|
+
send_bulk_notifications(batch_of_users)
|
|
219
|
+
end
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Query Execution
|
|
223
|
+
|
|
224
|
+
Queries are **lazy**. They are only sent to the database when you ask for the data.
|
|
225
|
+
|
|
226
|
+
✅ A query will be executed when you call any of these methods:
|
|
227
|
+
|
|
228
|
+
- `.to_a` (or its alias `.all`)
|
|
229
|
+
- `.first`
|
|
230
|
+
- `.last`
|
|
231
|
+
- `.count`
|
|
232
|
+
- `.any?` / `.exists?`
|
|
233
|
+
- `.each`
|
|
234
|
+
- `.find_each`
|
|
235
|
+
- `.find_in_batches`
|
|
236
|
+
|
|
237
|
+
This allows you to build complex queries step-by-step without hitting the database until you're ready.
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
# No database query has been run yet
|
|
241
|
+
query = Dami.db(:users).where(status: 'active')
|
|
242
|
+
|
|
243
|
+
# Now the query executes
|
|
244
|
+
users = query.to_a
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Best Practices
|
|
248
|
+
|
|
249
|
+
Following these guidelines will help you write efficient, readable, and scalable queries.
|
|
250
|
+
|
|
251
|
+
### 1\. Let the Database Do the Work
|
|
252
|
+
|
|
253
|
+
Always use `where`, `or`, and `order` to filter and sort your data at the database level. Avoid loading large, unfiltered collections into memory and processing them with Ruby.
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
# ✅ Good: The database does all the filtering and sorting.
|
|
257
|
+
recent_admins = Dami.db(:users)
|
|
258
|
+
.where(status: 'active', role: 'admin')
|
|
259
|
+
.order(created_at: :desc)
|
|
260
|
+
.limit(10)
|
|
261
|
+
|
|
262
|
+
# ❌ Avoid: Loading all users into memory to filter with Ruby.
|
|
263
|
+
all_users = Dami.db(:users).to_a
|
|
264
|
+
recent_admins = all_users
|
|
265
|
+
.select { |u| u[:status] == 'active' && u[:role] == 'admin' }
|
|
266
|
+
.sort_by { |u| u[:created_at] }
|
|
267
|
+
.reverse
|
|
268
|
+
.take(10)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### 2\. Select Only What You Need
|
|
272
|
+
|
|
273
|
+
For queries that return many records, use `.select` to specify only the columns you need. This reduces the amount of data transferred from the database and lowers memory usage.
|
|
274
|
+
|
|
275
|
+
```ruby
|
|
276
|
+
# ✅ Good: Only loads id and email for a mailing list.
|
|
277
|
+
mailing_list = Dami.db(:users).select(:id, :email).where(subscribed: true)
|
|
278
|
+
|
|
279
|
+
# ❌ Avoid: Loading entire user objects when you only need one field.
|
|
280
|
+
users = Dami.db(:users).where(subscribed: true).to_a
|
|
281
|
+
emails = users.map { |u| u[:email] }
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### 3\. Use Batch Processing for Large Operations
|
|
285
|
+
|
|
286
|
+
When you need to iterate over thousands or millions of records, never load them all at once. Use `.find_each` or `.find_in_batches` to process them in manageable chunks.
|
|
287
|
+
|
|
288
|
+
```ruby
|
|
289
|
+
# ✅ Good: Processes 1000 users at a time with low memory.
|
|
290
|
+
Dami.db(:users).find_each(batch_size: 1000) do |user|
|
|
291
|
+
user.send_notification!
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# ❌ Avoid: Loading the entire table into memory. This will crash on large tables.
|
|
295
|
+
all_users = Dami.db(:users).to_a # This could be millions of records!
|
|
296
|
+
all_users.each do |user|
|
|
297
|
+
user.send_notification!
|
|
298
|
+
end
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### 4\. Build Complex Logic in SQL
|
|
302
|
+
|
|
303
|
+
Leverage the full power of the DSL—including block-based `where`, `or`, and `joins`—to express complex business logic. This is almost always more performant than writing complicated Ruby logic to combine the results of multiple simple queries.
|
|
304
|
+
|
|
305
|
+
```ruby
|
|
306
|
+
# ✅ Good: One query to find admins OR active users over 30.
|
|
307
|
+
users = Dami.db(:users).where(role: 'admin').or do |q|
|
|
308
|
+
q.where(status: 'active').where(age: { gt: 30 })
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# ❌ Avoid: Running multiple queries and combining them in Ruby.
|
|
312
|
+
admins = Dami.db(:users).where(role: 'admin').to_a
|
|
313
|
+
active_users = Dami.db(:users).where(status: 'active', age: { gt: 30 }).to_a
|
|
314
|
+
users = (admins + active_users).uniq
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
-----
|
|
318
|
+
|
|
319
|
+
## Quick Reference
|
|
320
|
+
|
|
321
|
+
A concise summary of the Dami query DSL.
|
|
322
|
+
|
|
323
|
+
### Finding Records
|
|
324
|
+
|
|
325
|
+
| Method | Description |
|
|
326
|
+
|----------------|-------------------------------------------------|
|
|
327
|
+
| `.find(id)` | Retrieves a single record by its primary key. |
|
|
328
|
+
| `.first` | Retrieves the first record matching the query. |
|
|
329
|
+
| `.last` | Retrieves the last record matching the query. |
|
|
330
|
+
| `.to_a`, `.all`| Executes the query and returns an array of records. |
|
|
331
|
+
|
|
332
|
+
### Building Conditions
|
|
333
|
+
|
|
334
|
+
| Method | Example |
|
|
335
|
+
|------------------------|---------------------------------------------|
|
|
336
|
+
| `.where(hash)` | `where(status: 'active', role: 'admin')` |
|
|
337
|
+
| `.or(hash)` | `where(status: 'active').or(status: 'new')` |
|
|
338
|
+
| `.where { \|q\| ... }` | `where(name: 'A').where { \|q\| q.where(age: 30).or(status: 'B') }` |
|
|
339
|
+
|
|
340
|
+
### Where Operators
|
|
341
|
+
|
|
342
|
+
| Operator | Example Usage |
|
|
343
|
+
|-----------------|--------------------------------------------|
|
|
344
|
+
| `gt`, `lt`, `gte`, `lte` | `where(age: { gt: 18, lte: 65 })` |
|
|
345
|
+
| `not` | `where(status: { not: 'archived' })` |
|
|
346
|
+
| `in` | `where(id: [1, 5, 10])` |
|
|
347
|
+
| `not_in` | `where(role: { not_in: ['admin', 'owner'] })` |
|
|
348
|
+
| `starts_with` | `where(name: { starts_with: 'A' })` |
|
|
349
|
+
| `contains` | `where(name: { contains: 'ice' })` |
|
|
350
|
+
| `ends_with` | `where(name: { ends_with: 'z' })` |
|
|
351
|
+
|
|
352
|
+
### Ordering & Limiting
|
|
353
|
+
|
|
354
|
+
| Method | Example |
|
|
355
|
+
|----------------|---------------------------------------|
|
|
356
|
+
| `.order()` | `order(:name)`, `order(name: :desc)` |
|
|
357
|
+
| `.limit()` | `limit(10)` |
|
|
358
|
+
| `.offset()` | `offset(20)` |
|
|
359
|
+
|
|
360
|
+
### Joins & Selection
|
|
361
|
+
|
|
362
|
+
| Method | Example |
|
|
363
|
+
|----------------|---------------------------------------------|
|
|
364
|
+
| `.join()` | `join(:posts, { id: :user_id })` |
|
|
365
|
+
| `.left_join()` | `left_join(:profiles, { id: :user_id })` |
|
|
366
|
+
| `.select()` | `select(:id, :name)`, `select('users.*')` |
|
|
367
|
+
|
|
368
|
+
### Execution & Counting
|
|
369
|
+
|
|
370
|
+
| Method | Description |
|
|
371
|
+
|-------------------------|--------------------------------------------------------|
|
|
372
|
+
| `.each { \|rec\| ... }` | Executes the query and yields each record. |
|
|
373
|
+
| `.count` | Executes a `COUNT` query for the conditions. |
|
|
374
|
+
| `.any?`, `.exists?` | Executes a `LIMIT 1` query to check for existence. |
|
|
375
|
+
|
|
376
|
+
### Batch Processing
|
|
377
|
+
|
|
378
|
+
| Method | Description |
|
|
379
|
+
|--------------------------|------------------------------------------------|
|
|
380
|
+
| `.find_each(batch_size:)` | Yields each record one-by-one, in batches. |
|
|
381
|
+
| `.find_in_batches(batch_size:)` | Yields an array of records for each batch. |
|
|
382
|
+
|
|
383
|
+
## What's Next?
|
|
384
|
+
|
|
385
|
+
- **Chapter 4: Creating & Updating** - Modify your data
|
|
386
|
+
- **Chapter 5: Validation** - Ensure data quality
|
|
387
|
+
- **Chapter 7: Associations** - Query related records easily
|