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.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +306 -0
  3. data/LICENSE +21 -0
  4. data/README.md +106 -0
  5. data/bin/dami +10 -0
  6. data/docs/01.Getting_Started.md +220 -0
  7. data/docs/02.Models_and_Fields.md +244 -0
  8. data/docs/03.Querying.md +387 -0
  9. data/docs/04.Creating_Updating_Deleting.md +226 -0
  10. data/docs/05.Validation.md +259 -0
  11. data/docs/06.Protection.md +160 -0
  12. data/docs/07.Associations.md +239 -0
  13. data/docs/08.Scopes.md +99 -0
  14. data/docs/09.Migrations.md +165 -0
  15. data/docs/10.Flows_and_Commands.md +181 -0
  16. data/docs/11.Localization.md +435 -0
  17. data/docs/12.TheDamiWay.md +227 -0
  18. data/docs/Manifesto.md +305 -0
  19. data/docs/site.md +477 -0
  20. data/lib/dami/actions/command.rb +80 -0
  21. data/lib/dami/actions/context.rb +63 -0
  22. data/lib/dami/actions/draft.rb +36 -0
  23. data/lib/dami/actions/flow.rb +42 -0
  24. data/lib/dami/adapters/base.rb +40 -0
  25. data/lib/dami/adapters/sqlite/connection.rb +122 -0
  26. data/lib/dami/adapters/sqlite/core.rb +17 -0
  27. data/lib/dami/adapters/sqlite/query.rb +353 -0
  28. data/lib/dami/adapters/sqlite/schema.rb +135 -0
  29. data/lib/dami/cli.rb +117 -0
  30. data/lib/dami/configuration.rb +246 -0
  31. data/lib/dami/core.rb +98 -0
  32. data/lib/dami/dsl_guardrails.rb +45 -0
  33. data/lib/dami/errors.rb +89 -0
  34. data/lib/dami/inflector.rb +245 -0
  35. data/lib/dami/localization.rb +131 -0
  36. data/lib/dami/migration.rb +84 -0
  37. data/lib/dami/migrator.rb +105 -0
  38. data/lib/dami/plugins/associations.rb +284 -0
  39. data/lib/dami/plugins/nested_attributes.rb +180 -0
  40. data/lib/dami/plugins/protection.rb +30 -0
  41. data/lib/dami/plugins/validations.rb +90 -0
  42. data/lib/dami/query/builder.rb +132 -0
  43. data/lib/dami/query/enumerable.rb +62 -0
  44. data/lib/dami/query/persistence.rb +133 -0
  45. data/lib/dami/record_proxy.rb +32 -0
  46. data/lib/dami/result.rb +27 -0
  47. data/lib/dami/schema/diff.rb +84 -0
  48. data/lib/dami/schema/dumper.rb +60 -0
  49. data/lib/dami/schema/generator.rb +69 -0
  50. data/lib/dami/schema/introspector.rb +34 -0
  51. data/lib/dami/schema/loader.rb +58 -0
  52. data/lib/dami/schema.rb +13 -0
  53. data/lib/dami/validation_rules.rb +72 -0
  54. data/lib/dami/version.rb +3 -0
  55. data/lib/dami.rb +32 -0
  56. 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.
@@ -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