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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 0d27e7d5a72541d63d026ed24e418d0acc195bf33d724f6086afcfece355998b
|
|
4
|
+
data.tar.gz: eadb86a3f0a4aae782971785f3c180a532d30561b82fe23f43d8fe236d0724af
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4a636d87228fb6e1ad6d8b83d7c101bec962423647cf096650b5026e9242776005c64287ad19817ea9a0474b64227a5282d290ff15aa802ea555c5784ffdafd4
|
|
7
|
+
data.tar.gz: a2cbe7c1417947fdc97876875de4aafba02a6cf326ff3a429f11feb7ba48db3908a33208a12c763160fe7d2c61334e22a63aaaf67bdfd85d1b1afe52682b245b
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.0.0] - 2026-09-12 (pre-publish review pass)
|
|
4
|
+
|
|
5
|
+
Fixes applied after a full review of the 1.0.0 working tree, before the first
|
|
6
|
+
rubygems release. The suite is 276 tests / 840 assertions, green.
|
|
7
|
+
|
|
8
|
+
### ๐ Correctness
|
|
9
|
+
|
|
10
|
+
* **`Dami.run` now rolls back on a `Failure` halt.** Previously a flow that wrote rows and then halted with a `Failure` (e.g. `prepare` rejecting its input) committed those rows. After-commit hooks now run after the transaction has actually committed, not inside it.
|
|
11
|
+
* **Nested flows use savepoints.** `run(:other_flow)` inside a flow rolls back only the inner flow's writes on failure; the outer flow keeps going with the returned `Failure`. Any nested `transaction` call behaves the same way.
|
|
12
|
+
* **Foreign keys are enforced.** Every pooled connection now runs `PRAGMA foreign_keys = ON`, so `references:` columns and `Dami::ForeignKeyViolation` mean something. (If your test helper drops tables in arbitrary order, drop them inside one transaction with `PRAGMA defer_foreign_keys = ON` โ see `test/database_helper.rb`.)
|
|
13
|
+
* **Two `NoMethodError`s fixed:** lazy `has_many :through` (`post.tags` without `preload`) and nested attributes without an explicit `foreign_key:` called `String#singularize`, which does not exist without ActiveSupport. Both use `Dami::Inflector` now. 17 previously-red tests pass.
|
|
14
|
+
* **`:memory:` databases use one connection.** An in-memory SQLite database exists per connection, so a pool of five was five unrelated databases. `pool_size` is forced to 1 for `path: ':memory:'`.
|
|
15
|
+
* **Inserts read `last_insert_row_id` on the connection that inserted.** `insert_record` and `insert_many` run inside a transaction so both statements share one pooled connection.
|
|
16
|
+
* **One parameter conversion path.** `execute`, `get_first_row` and prepared statements all convert `Time`/`DateTime`/`Date`/booleans/JSON the same way; `create_many` with a `Time` or `true` no longer raises inside the driver.
|
|
17
|
+
* **`min:` / `max:` validations compare numbers, not `to_i`.** `rule :amount, min: 0.01` now accepts `0.5` and `"9.99"`, and rejects `"abc"`.
|
|
18
|
+
* **Locale is thread-safe.** `Dami.locale=` remains the process-wide default; `Dami.with_locale` and the new `Dami.thread_locale=` are thread-local, so a web request can no longer leak its locale into other threads.
|
|
19
|
+
* **`NOT NULL` violations raise `Dami::NotNullViolation`** (with `.column`) instead of a raw driver error.
|
|
20
|
+
|
|
21
|
+
### โจ Schema
|
|
22
|
+
|
|
23
|
+
* `create_table` / `add_column` keep their constraints: `null: false`, `unique: true`, `default:` (incl. `:current_timestamp`), `references:` (+ `on_delete:`). `t.references :user` is shorthand for `user_id INTEGER REFERENCES users(id)`.
|
|
24
|
+
* `add_index(..., unique: true)` creates a `UNIQUE` index; multi-column indexes accepted.
|
|
25
|
+
* One type map for `create_table` and `add_column`: `:float`/`:decimal` โ `REAL`, `:datetime`/`:date`/`:time` โ `DATETIME` (previously `create_table` used `TEXT` for all of these while `add_column` did not).
|
|
26
|
+
* `:json` fields are serialized on write and parsed on read, as the docs already claimed.
|
|
27
|
+
* `created_at` / `updated_at` are filled automatically when the model declares them.
|
|
28
|
+
* `dami db rollback [STEPS]` added to the CLI; rollback refreshes `db/schema.rb` like migrate does. CLI commands are `dami db migrate`, `dami db rollback`, `dami db schema_dump` (docs previously showed `dami db:migrate`, which Thor does not accept).
|
|
29
|
+
|
|
30
|
+
### ๐ Hardening
|
|
31
|
+
|
|
32
|
+
* Column and table names passed to `where`, `join`, `insert` are validated as plain identifiers (`Dami::InvalidIdentifier` otherwise). Values were always bound; names were interpolated.
|
|
33
|
+
|
|
34
|
+
### ๐งน Packaging
|
|
35
|
+
|
|
36
|
+
* `sqlite3` dependency corrected to `>= 2.0` (was `~> 1.6`, which could not be bundled with any 2.x app). `required_ruby_version >= 3.0`, rubygems metadata (MFA required, source/changelog URIs).
|
|
37
|
+
* `spec.files` is an explicit `Dir[]` list instead of `git ls-files`.
|
|
38
|
+
* Added `README.md`, `LICENSE` (MIT), `.gitignore`; `rake test` points at the real runner.
|
|
39
|
+
* Removed dead files: `lib/dami/untitled.rb`, `lib/dami/draft.rb`, `lib/dami/flow.rb` (commented out), `lib/dami/locale_manager.rb`, `lib/dami/plugins/{scopes,timestamps,presenter}.rb`, `lib/dami/adapters/sqlite/associations.rb`; duplicate `docs/1.Getting_Started.md`.
|
|
40
|
+
* Runtime dependencies are `sqlite3`, `connection_pool` and `thor` (the previous "zero dependencies" note was wrong).
|
|
41
|
+
|
|
42
|
+
## [1.0.0] - 2025-10-18
|
|
43
|
+
|
|
44
|
+
๐ **Dami v1.0 - Production Ready**
|
|
45
|
+
|
|
46
|
+
This is the first production-ready release of Dami. The 1.0 milestone represents a mature, battle-tested ORM with a clean architecture, comprehensive test coverage (228 tests, 737 assertions), and production-grade error handling.
|
|
47
|
+
|
|
48
|
+
### ๐๏ธ **Major Architecture Changes**
|
|
49
|
+
|
|
50
|
+
* **Four Pillars DSL:** Complete separation of concerns across four distinct DSL blocks:
|
|
51
|
+
* `Dami.model` - Structure (fields, relationships, virtual fields)
|
|
52
|
+
* `Dami.behavior` - Write-side logic (validations, protection rules)
|
|
53
|
+
* `Dami.scopes` - Collection-level queries
|
|
54
|
+
* Each pillar has a focused, single responsibility, making code clearer and more maintainable
|
|
55
|
+
|
|
56
|
+
* **Architectural Guardrails:** The framework now enforces clean separation at the DSL level
|
|
57
|
+
* Attempting to define validations in `Dami.model` raises `Dami::InvalidDSLError` with helpful guidance
|
|
58
|
+
* Attempting to define fields in `Dami.behavior` raises an error pointing to the correct location
|
|
59
|
+
* Makes architectural decay impossible and provides immediate, clear feedback to developers
|
|
60
|
+
* Zero magic - explicit method definitions with no `method_missing` tricks
|
|
61
|
+
|
|
62
|
+
### ๐ **Critical Bug Fixes**
|
|
63
|
+
|
|
64
|
+
* **`create_many` Security Fix:** Fixed a critical bug where `create_many` bypassed protection rules
|
|
65
|
+
* Now enforces field protection on every record in batch operations
|
|
66
|
+
* Validates all records before any database writes (atomic validation)
|
|
67
|
+
* Raises `Dami::ProtectionError` when protected fields are present
|
|
68
|
+
* **Impact:** High - prevents bulk operations from bypassing security rules
|
|
69
|
+
|
|
70
|
+
### โจ **Enhanced Error Handling**
|
|
71
|
+
|
|
72
|
+
* **Database-Specific Exceptions:** Replaced generic SQLite errors with semantic Dami exceptions
|
|
73
|
+
* `Dami::UniqueConstraintViolation` - raised on unique constraint failures, includes column name
|
|
74
|
+
* `Dami::ForeignKeyViolation` - raised on foreign key constraint failures
|
|
75
|
+
* Improves error handling in application code with meaningful, catchable exceptions
|
|
76
|
+
|
|
77
|
+
### ๐ง **Configuration Enhancements**
|
|
78
|
+
|
|
79
|
+
* **Configurable Connection Pool:** Added `pool_size` option to connection configuration
|
|
80
|
+
* Default: 5 connections per pool
|
|
81
|
+
* Usage: `Dami.connect(:default, adapter: :sqlite, path: 'db.sqlite', pool_size: 10)`
|
|
82
|
+
* Allows tuning for high-concurrency scenarios
|
|
83
|
+
|
|
84
|
+
### ๐ **Test Coverage**
|
|
85
|
+
|
|
86
|
+
* **228 Comprehensive Tests** covering:
|
|
87
|
+
* Core CRUD operations
|
|
88
|
+
* All relationship types (belongs_to, has_many, has_one, polymorphic, has_many_through)
|
|
89
|
+
* Nested attributes (3+ levels deep)
|
|
90
|
+
* Validations (all rule types, contexts, conditionals)
|
|
91
|
+
* Protection & permissions
|
|
92
|
+
* Scopes & query building
|
|
93
|
+
* Transactions & concurrency
|
|
94
|
+
* Edge cases (connection pooling, unicode, NULL handling, large datasets)
|
|
95
|
+
* DSL guardrails enforcement
|
|
96
|
+
|
|
97
|
+
### ๐ฏ **Developer Experience**
|
|
98
|
+
|
|
99
|
+
* **Clear Error Messages:** All DSL violations now provide helpful guidance
|
|
100
|
+
* Example: `'validate' is not allowed here. Please define it in a Dami.behavior block.`
|
|
101
|
+
* **Minimal dependencies:** `sqlite3`, `connection_pool`, `thor`
|
|
102
|
+
* **Production Tested:** Stress-tested with concurrent operations, large batches, and edge cases
|
|
103
|
+
|
|
104
|
+
### ๐ **Breaking Changes**
|
|
105
|
+
|
|
106
|
+
* **DSL Reorganization:** Models must now separate concerns across pillar blocks
|
|
107
|
+
* **Before (0.9.x):**
|
|
108
|
+
```ruby
|
|
109
|
+
Dami.model :users do
|
|
110
|
+
fields { field :name, :string }
|
|
111
|
+
validate { rule :name, :required }
|
|
112
|
+
protection { protect :role }
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
* **After (1.0.0):**
|
|
116
|
+
```ruby
|
|
117
|
+
Dami.model :users do
|
|
118
|
+
fields { field :name, :string }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
Dami.behavior :users do
|
|
122
|
+
validate { rule :name, :required }
|
|
123
|
+
protection { protect :role }
|
|
124
|
+
end
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### ๐ **Upgrade Guide**
|
|
128
|
+
|
|
129
|
+
1. Separate `validate` and `protection` blocks from `Dami.model` into `Dami.behavior`
|
|
130
|
+
2. Move `scope` definitions to `Dami.scopes` blocks
|
|
131
|
+
3. Update exception handling to catch `Dami::UniqueConstraintViolation` instead of `SQLite3::ConstraintException`
|
|
132
|
+
4. Optionally configure connection pool size if needed for your workload
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
Dami 1.0 represents a stable, production-ready foundation for Ruby applications. The architecture is clean, the test coverage is comprehensive, and the framework is ready for real-world use.
|
|
137
|
+
|
|
138
|
+
## [0.9.0] - 2025-10-17
|
|
139
|
+
|
|
140
|
+
This is the official first major release of the Dami ORM. It includes a complete feature set for building robust applications, significant performance optimizations, and a hardened codebase.
|
|
141
|
+
|
|
142
|
+
### โจ New Features
|
|
143
|
+
|
|
144
|
+
* **Nested Attributes:** Implemented a powerful and secure way to manage associated records through a parent record.
|
|
145
|
+
* New `nests` DSL in models to explicitly allow nested operations (e.g., `nests :comments, allow_destroy: true`).
|
|
146
|
+
* Supports deep, recursive nesting for grandchildren and beyond.
|
|
147
|
+
* Handles creating, updating, and destroying nested records via `_attributes` keys (`comments_attributes`).
|
|
148
|
+
* All operations are wrapped in a single, atomic database transaction.
|
|
149
|
+
* Fully integrated with Dami's validation and protection layers, ensuring child records respect their own rules.
|
|
150
|
+
|
|
151
|
+
* **Resilient Flows:** The `perform` step in the Flow DSL is now robust for handling unreliable external actions.
|
|
152
|
+
* Added a `:retry_options` parameter to `perform` (e.g., `retry_options: { on: [ApiError], times: 2 }`).
|
|
153
|
+
* Added a `:timeout` parameter to automatically fail a step that takes too long.
|
|
154
|
+
|
|
155
|
+
* **Custom Inflector:** Introduced a dependency-free internal inflector (`Dami::Inflector`) to handle pluralization and singularization, removing the need for external gems like Active Support.
|
|
156
|
+
|
|
157
|
+
### ๐ Performance
|
|
158
|
+
|
|
159
|
+
A major focus of this release was optimizing the performance of association loading to be competitive with leading ORMs.
|
|
160
|
+
|
|
161
|
+
* **Optimized `.count` Method:** The `.count` method now executes a highly efficient `SELECT COUNT(*)` database query instead of loading all records into memory, resulting in a **~1.13x speed improvement** for counting operations.
|
|
162
|
+
|
|
163
|
+
* **Preload Optimization (~85% Improvement):** The performance of eager loading (`.preload`) has been massively improved through two key architectural changes:
|
|
164
|
+
1. **Flyweight Pattern:** Replaced per-instance method definition on `RecordProxy` objects with cached, shared modules. This eliminated the primary object-creation bottleneck.
|
|
165
|
+
2. **Inflector Memoization:** Implemented caching for inflection results, dramatically reducing redundant string and regex operations in hot loops.
|
|
166
|
+
* **Result:** `Dami (preload)` is now **~1.6x faster than ActiveRecord (`includes`)** in competitive benchmarks for common scenarios.
|
|
167
|
+
|
|
168
|
+
### ๐ง Fixes & Hardening
|
|
169
|
+
|
|
170
|
+
* **CLI Hardening:** The `dami` command-line interface is now more robust and provides user-friendly error messages for common misconfigurations, such as:
|
|
171
|
+
* Missing `config/initializers/dami.rb` file.
|
|
172
|
+
* Database not connected when running `db:*` tasks.
|
|
173
|
+
* Missing `db/schema.rb` file when generating migrations.
|
|
174
|
+
|
|
175
|
+
* **Test Suite Reliability:** Fixed flaky tests by implementing a central reset mechanism (`Dami.clear_all!`). This ensures that all global state (model definitions, plugin caches) is cleared before every test, guaranteeing 100% test isolation and reliability.
|
|
176
|
+
|
|
177
|
+
* **Bug Fixes:**
|
|
178
|
+
* Corrected the `perform` helper in the Flow DSL (was mistakenly documented as `call`).
|
|
179
|
+
* Fixed a bug in the associations plugin where polymorphic and `has_many :through` definitions were not being parsed correctly, leading to `NoMethodError`s in certain load orders.
|
|
180
|
+
|
|
181
|
+
## [0.8.0] - 2025-10-07
|
|
182
|
+
|
|
183
|
+
### โจ Added
|
|
184
|
+
|
|
185
|
+
* **Flows for Orchestration**: Introduced `Dami.run` and `Dami.flow`, the "Tier 3" system for orchestrating complex, multi-step business processes. This provides a clean, readable, and imperative DSL for defining application workflows.
|
|
186
|
+
* **Automatic Transactional Guarantee**: `Dami.run` automatically wraps the entire flow in a database transaction, ensuring all database operations are atomic. If any step raises an exception, the entire transaction is rolled back.
|
|
187
|
+
* **Safe Side Effects (`succeed with:`)**: Flows now have a `succeed with: ..., and_then: [...]` method. This provides a transaction-aware "holding area" for irreversible actions (like enqueuing jobs), guaranteeing they only execute *after* the database transaction has successfully committed.
|
|
188
|
+
* **Flow DSL**: The block inside a `Dami.flow` now has a rich set of helpers for building robust workflows:
|
|
189
|
+
* `prepare`: Runs a `Dami::Command` to validate and transform data, halting automatically on failure.
|
|
190
|
+
* `perform`: Executes an arbitrary block of code (e.g., an external API call) with resilience options.
|
|
191
|
+
* `db`: Provides access to the query builder.
|
|
192
|
+
* `run`: Calls other Dami flows as sub-routines.
|
|
193
|
+
* **Error Handling (`rescue_from`)**: Flows can now define custom handlers for specific exceptions, allowing for graceful error recovery.
|
|
194
|
+
|
|
195
|
+
## [0.7.0] - 2025-10-01
|
|
196
|
+
|
|
197
|
+
### โจ Added
|
|
198
|
+
|
|
199
|
+
* **Commands (`Dami::Command`)**: Introduced `Dami::Command`, a powerful class for encapsulating complex, reusable business logic. Commands provide a formal structure for contextual validation, data transformation, and dependency injection.
|
|
200
|
+
* **Declarative Command DSL**: `Dami::Command` includes a rich DSL for building business rules:
|
|
201
|
+
* `requires_context`: For declaring dependencies.
|
|
202
|
+
* `validate`: For defining validation rules with `if:` and `unless:` conditionals.
|
|
203
|
+
* `transform`: For defining data transformation steps.
|
|
204
|
+
* `apply` & `compose`: For building complex commands from smaller, reusable components.
|
|
205
|
+
|
|
206
|
+
## [0.6.0] - 2025-10-01
|
|
207
|
+
|
|
208
|
+
### โจ Added
|
|
209
|
+
|
|
210
|
+
* **Inline Drafts**: The `.create` and `.update` methods now accept an optional block that yields a `draft` object. This provides a powerful, inline DSL for adding contextual validation and data transformation for simple-to-medium complexity operations. The `draft` DSL (`verify`, `transform`, `prevent_changes`) mirrors the `Command` API for a consistent developer experience.
|
|
211
|
+
|
|
212
|
+
## [0.5.8] - 2025-10-01
|
|
213
|
+
|
|
214
|
+
### โจ Added
|
|
215
|
+
|
|
216
|
+
* **Commands**: Introduced `Dami::Command`, the "Tier 3" tool for encapsulating complex, reusable business logic. Commands provide a formal structure for contextual validation (`validate`), data transformation (`transform`), and dependency injection (`requires_context`).
|
|
217
|
+
* **Command Composition**: Commands can now be built from smaller, reusable components using `compose` and `apply`, promoting a DRY and modular architecture for business rules.
|
|
218
|
+
* **Conditional Logic**: The `validate` DSL now supports powerful conditional execution with `if:` and `unless:` options.
|
|
219
|
+
|
|
220
|
+
## [0.5.3] - 2025-10-01
|
|
221
|
+
|
|
222
|
+
### โจ Added
|
|
223
|
+
|
|
224
|
+
* **Inline Drafts**: The `.create` and `.update` methods now accept an optional block that yields a `draft` object. This provides a powerful, inline DSL for adding contextual validation and data transformation for simple-to-medium complexity operations. It serves as the "Tier 2" of the business logic spectrum and uses an API (`verify`, `transform`, `prevent_changes`) designed to mirror the full `Dami::Command` for a consistent developer experience.
|
|
225
|
+
|
|
226
|
+
## [0.5.0] - 2025-09-27
|
|
227
|
+
|
|
228
|
+
This is a major release introducing a powerful new migration workflow and a professional command-line interface, significantly improving developer productivity.
|
|
229
|
+
|
|
230
|
+
### โจ Added
|
|
231
|
+
|
|
232
|
+
* **Automatic Migration Generator**: Introduced a new `dami generate migration NAME` command. Dami now introspects your model definitions, compares them to the canonical schema (`db/schema.rb`), and automatically generates migration files for adding/removing columns and indexes. This nearly eliminates the need to write migrations by hand.
|
|
233
|
+
* **Full Command-Line Interface (CLI)**: Replaced all Rake tasks with a professional, self-documenting CLI built with Thor. The new `bin/dami` executable provides a standard interface with subcommands like `db:migrate` and `generate migration`.
|
|
234
|
+
* **Centralized Configuration**: Added a `Dami.configure` block. Users can now easily configure essential paths like `models_path`, `migrations_path`, and `schema_path` to integrate Dami into any project structure.
|
|
235
|
+
* **Schema Dumper (`db/schema.rb`)**: The migrator now automatically creates and updates a `db/schema.rb` file after successful migrations. This file serves as the reliable source of truth for the migration generator.
|
|
236
|
+
|
|
237
|
+
### โป๏ธ Changed
|
|
238
|
+
|
|
239
|
+
* **Rails-Style Migration DSL**: The migration DSL has been updated to use the more conventional `up` and `down` methods, replacing the previous `run` and `reverse`.
|
|
240
|
+
* **Index Support in Migrations**: The migration DSL now includes `add_index` and `remove_index` methods for managing database indexes.
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
## [0.4.0] - 2025-09-21
|
|
244
|
+
|
|
245
|
+
### โจ Added
|
|
246
|
+
|
|
247
|
+
* **OR Conditions**: The query builder now supports chainable `.or` conditions for building complex queries (e.g., `.where(name: 'A').or(status: 'active')`).
|
|
248
|
+
* **LEFT Joins**: Added a `.left_join` method to the query builder for performing `LEFT OUTER JOIN`s, allowing you to find records that may not have an associated record.
|
|
249
|
+
* **Advanced String Matching**: `where` clauses now support `starts_with`, `contains`, and `ends_with` for generating `LIKE` queries (e.g., `where(name: { starts_with: 'A' })`).
|
|
250
|
+
* **NOT IN Queries**: Added a `{ not_in: [...] }` operator to `where` clauses for `NOT IN` conditions.
|
|
251
|
+
* **Existence Checks**: Added performant `.any?` and `.exists?` methods to the query builder, which use `LIMIT 1` for efficient database checks.
|
|
252
|
+
* **First/Last Methods**: Added efficient `.first` and `.last` methods to the query builder. `.last` intelligently reverses the query's sort order.
|
|
253
|
+
|
|
254
|
+
### โป๏ธ Changed
|
|
255
|
+
|
|
256
|
+
* **Major Internal Refactor**: The entire test suite has been refactored to be database-agnostic. It can now run against SQLite, PostgreSQL, and MySQL via the `DB` environment variable.
|
|
257
|
+
* **Transactional Tests**: The test suite now uses a transactional cleanup strategy for significantly faster and more reliable test isolation, eliminating file I/O issues.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## [0.3.0] - 2025-09-14
|
|
262
|
+
|
|
263
|
+
### โจ Added
|
|
264
|
+
|
|
265
|
+
* **Polymorphic Associations**: `belongs_to` associations now support a `{ polymorphic: true }` option, allowing a model to belong to more than one other model on a single association. The corresponding `has_many` and `has_one` now support the `{ as: :... }` syntax.
|
|
266
|
+
* **`has_many :through` Associations**: You can now define `has_many :through` relationships to create many-to-many connections through a join model.
|
|
267
|
+
* **Performance Optimizations**: Implemented `find_each` and `find_in_batches` for efficiently processing large numbers of records without loading them all into memory at once.
|
|
268
|
+
* **Bulk Inserts**: Added a `.create_many` method for inserting a large number of records in a single, highly performant SQL statement.
|
|
269
|
+
|
|
270
|
+
### ๐ Fixed
|
|
271
|
+
|
|
272
|
+
* Corrected an issue where preloading multiple associations could fail under certain edge cases.
|
|
273
|
+
* Ensured that the query builder methods (`where`, `limit`, etc.) are fully immutable and always return a new builder instance.
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## [0.2.0] - 2025-09-08
|
|
278
|
+
|
|
279
|
+
### โจ Added
|
|
280
|
+
|
|
281
|
+
* **Comprehensive Validation System**: Introduced a powerful `validate` block for model definitions.
|
|
282
|
+
* Supports contextual validations with `on :create` and `on :update`.
|
|
283
|
+
* Supports conditional validations using `if:`, `unless:`, `when:`.
|
|
284
|
+
* Added bulk-application helpers like `all except:` and `all only:`.
|
|
285
|
+
* **Virtual Fields**: Models now support a `virtual` block to define attributes that are validated but not persisted to the database (e.g., `password_confirmation`).
|
|
286
|
+
* **Attribute Protection**: Implemented a `protection` block with `.protect` and `.permit` to prevent mass-assignment vulnerabilities. Operations can bypass this using the `permit: [...]` or `protect: false` options.
|
|
287
|
+
* **Scope DSL**: Added a `scopes` block to define reusable query scopes on models (e.g., `scope :active, -> { where(status: 'active') }`).
|
|
288
|
+
|
|
289
|
+
### ๐ Fixed
|
|
290
|
+
|
|
291
|
+
* Ensured that unknown fields in `create` or `update` calls raise an `UnknownFieldsError` *before* validations are run, providing clearer error messages.
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## [0.1.0] - 2025-09-01
|
|
296
|
+
|
|
297
|
+
### โจ Added
|
|
298
|
+
|
|
299
|
+
* **Database Migrations**: Introduced `Dami::Migration` and `Dami::Migrator` for managing database schema changes over time. Supports `migrate` and `rollback` commands.
|
|
300
|
+
* **Basic Associations**: Implemented `has_many`, `has_one`, and `belongs_to` relationships, including support for both lazy-loading and eager-loading via `.preload`.
|
|
301
|
+
* **Connection Pooling**: The SQLite adapter now uses a connection pool for improved concurrency and performance under threaded environments.
|
|
302
|
+
|
|
303
|
+
### โป๏ธ Changed
|
|
304
|
+
|
|
305
|
+
* **Hardened Query Builder**: The `.order` method is now hardened against SQL injection attacks.
|
|
306
|
+
* The core `create` method now returns a `RecordProxy` instance instead of a plain hash.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Steven Garcia
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Dami
|
|
2
|
+
|
|
3
|
+
A lightweight, policy-oriented ORM for Ruby on SQLite. One gem, no framework required.
|
|
4
|
+
|
|
5
|
+
- **Four-pillar DSL** โ structure (`Dami.model`), write rules (`Dami.behavior`), queries (`Dami.scopes`), presentation (`Dami.present`). Put something in the wrong block and Dami tells you where it goes.
|
|
6
|
+
- **Hardened writes by default** โ unknown fields raise, protected fields need an explicit `permit:`, every value is bound (never interpolated).
|
|
7
|
+
- **Real constraints** โ `null:`, `unique:`, `default:`, `references:` in migrations; foreign keys are enforced; violations come back as `Dami::NotNullViolation`, `Dami::UniqueConstraintViolation`, `Dami::ForeignKeyViolation`.
|
|
8
|
+
- **Flows** โ multi-step business operations that run in one transaction, roll back on failure, and fire side effects only after the commit.
|
|
9
|
+
- **Small and fast** โ a connection pool, WAL, prepared-statement cache, and nothing you didn't ask for.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
# Gemfile
|
|
15
|
+
gem "dami"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Five-minute tour
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
require "dami"
|
|
22
|
+
|
|
23
|
+
Dami.connect(:default, adapter: :sqlite, path: "app.db")
|
|
24
|
+
|
|
25
|
+
# Schema (or write a migration โ see docs/09.Migrations.md)
|
|
26
|
+
Dami::Migration.new(Dami.database).create_table(:users) do |t|
|
|
27
|
+
t.field :id, :primary_key
|
|
28
|
+
t.field :email, :string, null: false, unique: true
|
|
29
|
+
t.field :name, :string, null: false
|
|
30
|
+
t.field :role, :string, default: "member"
|
|
31
|
+
t.timestamps
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Structure
|
|
35
|
+
Dami.model :users do
|
|
36
|
+
fields do
|
|
37
|
+
field :email, :string
|
|
38
|
+
field :name, :string
|
|
39
|
+
field :role, :string
|
|
40
|
+
field :created_at, :datetime
|
|
41
|
+
field :updated_at, :datetime
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Write-side rules
|
|
46
|
+
Dami.behavior :users do
|
|
47
|
+
validate do
|
|
48
|
+
rule :email, :required, :email
|
|
49
|
+
rule :name, :required
|
|
50
|
+
end
|
|
51
|
+
protection { protect :role }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Collection queries
|
|
55
|
+
Dami.scopes :users do
|
|
56
|
+
scope :admins, -> { where(role: "admin") }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
user = Dami.db(:users).create(email: "ana@example.com", name: "Ana")
|
|
60
|
+
user[:id] # => 1
|
|
61
|
+
user.name # => "Ana"
|
|
62
|
+
user[:created_at] # => "2026-09-12 18:40:00" (UTC)
|
|
63
|
+
|
|
64
|
+
Dami.db(:users).where(id: user[:id]).update(name: "Ana G.")
|
|
65
|
+
Dami.db(:users).admins.count # => 0
|
|
66
|
+
Dami.db(:users).where(role: "admin").to_a # => []
|
|
67
|
+
|
|
68
|
+
Dami.db(:users).create(email: "not-an-email", name: "")
|
|
69
|
+
# => Dami::ValidationError, errors: { email: ["must be valid email"], name: ["is required"] }
|
|
70
|
+
|
|
71
|
+
Dami.db(:users).where(id: 1).update(role: "admin")
|
|
72
|
+
# => Dami::ProtectionError (protected field) โ pass permit: [:role] where you mean it
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Documentation
|
|
76
|
+
|
|
77
|
+
Everything is in [`docs/`](docs/), in reading order:
|
|
78
|
+
|
|
79
|
+
1. [Getting Started](docs/01.Getting_Started.md)
|
|
80
|
+
2. [Models and Fields](docs/02.Models_and_Fields.md)
|
|
81
|
+
3. [Querying](docs/03.Querying.md)
|
|
82
|
+
4. [Creating, Updating, Deleting](docs/04.Creating_Updating_Deleting.md)
|
|
83
|
+
5. [Validation](docs/05.Validation.md)
|
|
84
|
+
6. [Protection](docs/06.Protection.md)
|
|
85
|
+
7. [Associations](docs/07.Associations.md)
|
|
86
|
+
8. [Scopes](docs/08.Scopes.md)
|
|
87
|
+
9. [Migrations](docs/09.Migrations.md)
|
|
88
|
+
10. [Flows and Commands](docs/10.Flows_and_Commands.md)
|
|
89
|
+
11. [Localization](docs/11.Localization.md)
|
|
90
|
+
12. [The Dami Way](docs/12.TheDamiWay.md)
|
|
91
|
+
|
|
92
|
+
## Scope
|
|
93
|
+
|
|
94
|
+
SQLite only, on purpose. If you need PostgreSQL or MySQL today, Dami is not the right tool yet.
|
|
95
|
+
|
|
96
|
+
## Development
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
bundle install
|
|
100
|
+
rake test # ruby test/run_all.rb
|
|
101
|
+
rake build # gem build dami.gemspec
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT โ see [LICENSE](LICENSE).
|
data/bin/dami
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# This makes sure the local `lib` directory is on the load path, which is
|
|
4
|
+
# essential for development before the gem is installed.
|
|
5
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
6
|
+
|
|
7
|
+
require 'dami/cli'
|
|
8
|
+
|
|
9
|
+
# Start the Thor command-line interface, passing it the user's arguments.
|
|
10
|
+
Dami::CLI.start(ARGV)
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Chapter 1: Getting Started
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
Add Dami to your project:
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# Gemfile
|
|
9
|
+
gem 'dami'
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Or install directly:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
gem install dami
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Your First Database
|
|
19
|
+
|
|
20
|
+
Dami connects to SQLite by default. Create a database connection:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
require 'dami'
|
|
24
|
+
# Connect to a SQLite database
|
|
25
|
+
Dami.connect(:default, adapter: :sqlite, path: 'app.db', pool_size: 5)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Configuration options:**
|
|
29
|
+
|
|
30
|
+
`:pool_size` - Number of connections in the pool (default: 5). Increase for high-concurrency scenarios. Ignored for `path: ':memory:'` โ an in-memory database exists per connection, so Dami always uses a single connection there.
|
|
31
|
+
`:default` - optional but recommended if you're using multiple databases later.
|
|
32
|
+
|
|
33
|
+
## Define Your First Model
|
|
34
|
+
|
|
35
|
+
Models define your data structure and behavior:
|
|
36
|
+
|
|
37
|
+
### The Four Pillars Architecture
|
|
38
|
+
|
|
39
|
+
Dami organizes your code into four distinct concerns:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
# 1. STRUCTURE (Dami.model) - What your data looks like
|
|
43
|
+
Dami.model :users do
|
|
44
|
+
fields do
|
|
45
|
+
field :name, :string
|
|
46
|
+
field :email, :string
|
|
47
|
+
end
|
|
48
|
+
relationships do
|
|
49
|
+
has_many :posts
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# 2. BEHAVIOR (Dami.behavior) - Write-side rules
|
|
54
|
+
Dami.behavior :users do
|
|
55
|
+
validate do
|
|
56
|
+
rule :name, :required
|
|
57
|
+
rule :email, :email
|
|
58
|
+
end
|
|
59
|
+
protection do
|
|
60
|
+
protect :admin # Can't be mass-assigned
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# 3. SCOPES (Dami.scopes) - Collection-level queries
|
|
65
|
+
Dami.scopes :users do
|
|
66
|
+
scope :active, -> { where(status: 'active') }
|
|
67
|
+
scope :admins, -> { where(role: 'admin') }
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
This separation makes code clearer and prevents mixing concerns. Dami enforces this at the DSL level - attempting to define validations inside `Dami.model` raises a helpful error pointing you to the right place.
|
|
72
|
+
|
|
73
|
+
## Create Your Table
|
|
74
|
+
|
|
75
|
+
Dami can generate migrations directly from your model definition:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
# 1. Define your model
|
|
79
|
+
Dami.model :users do
|
|
80
|
+
fields do
|
|
81
|
+
field :name, :string
|
|
82
|
+
field :email, :string
|
|
83
|
+
field :age, :integer
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# 2. Generate and run migration
|
|
88
|
+
migrator = Dami::Migrator.new(Dami.database(:default))
|
|
89
|
+
migrator.generate_from_schema(:users)
|
|
90
|
+
migrator.migrate
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Dami automatically creates the migration file and runs it. The generated migration is reversible - you can rollback if needed.
|
|
94
|
+
|
|
95
|
+
**Advanced: Manual migrations**
|
|
96
|
+
|
|
97
|
+
For complex schema changes, write migrations by hand:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
# db/migrations/20250101000000_create_users.rb
|
|
101
|
+
def up
|
|
102
|
+
create_table(:users) do |t|
|
|
103
|
+
t.field(:name, :string)
|
|
104
|
+
t.field(:email, :string)
|
|
105
|
+
t.field(:age, :integer)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def down
|
|
110
|
+
drop_table(:users)
|
|
111
|
+
end
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Your First Record
|
|
115
|
+
|
|
116
|
+
Create, read, update, delete:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
# Create
|
|
120
|
+
user = Dami.db(:users).create(
|
|
121
|
+
name: 'Alice',
|
|
122
|
+
email: 'alice@example.com',
|
|
123
|
+
age: 30
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
puts user[:id] # => 1
|
|
127
|
+
puts user[:name] # => "Alice"
|
|
128
|
+
|
|
129
|
+
# Read
|
|
130
|
+
user = Dami.db(:users).find(1)
|
|
131
|
+
all_users = Dami.db(:users).where(age: 30).to_a
|
|
132
|
+
|
|
133
|
+
# Update
|
|
134
|
+
Dami.db(:users).where(id: 1).update(age: 31)
|
|
135
|
+
|
|
136
|
+
# Delete
|
|
137
|
+
Dami.db(:users).where(id: 1).delete
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Complete Example
|
|
141
|
+
|
|
142
|
+
Here's a full working example with automatic migrations:
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
require 'dami'
|
|
146
|
+
require 'fileutils'
|
|
147
|
+
|
|
148
|
+
# 1. Connect
|
|
149
|
+
FileUtils.mkdir_p('db/migrations')
|
|
150
|
+
Dami.connect(:default, adapter: :sqlite, path: 'blog.db')
|
|
151
|
+
|
|
152
|
+
# 2. Define model
|
|
153
|
+
Dami.model :posts do
|
|
154
|
+
fields do
|
|
155
|
+
field :title, :string
|
|
156
|
+
field :body, :text
|
|
157
|
+
field :published, :boolean
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# 3. Generate and run migrations
|
|
162
|
+
migrator = Dami::Migrator.new(Dami.database(:default))
|
|
163
|
+
migrator.generate_from_schema(:posts)
|
|
164
|
+
migrator.migrate
|
|
165
|
+
|
|
166
|
+
# 4. Use it
|
|
167
|
+
post = Dami.db(:posts).create(
|
|
168
|
+
title: 'Hello World',
|
|
169
|
+
body: 'This is my first post!',
|
|
170
|
+
published: true
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
puts "Created post ##{post[:id]}: #{post[:title]}"
|
|
174
|
+
|
|
175
|
+
# Find published posts
|
|
176
|
+
published = Dami.db(:posts).where(published: true).to_a
|
|
177
|
+
puts "Found #{published.length} published posts"
|
|
178
|
+
|
|
179
|
+
# Need to change the schema? Just update the model and regenerate
|
|
180
|
+
Dami.model :posts do
|
|
181
|
+
fields do
|
|
182
|
+
field :title, :string
|
|
183
|
+
field :body, :text
|
|
184
|
+
field :published, :boolean
|
|
185
|
+
field :view_count, :integer # New field!
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
migrator.generate_from_schema(:posts)
|
|
190
|
+
migrator.migrate
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## What's Next?
|
|
194
|
+
|
|
195
|
+
You now have a working Dami application. Here's what to explore next:
|
|
196
|
+
|
|
197
|
+
- **Chapter 2: Models & Fields** - Virtual fields, field types, defaults
|
|
198
|
+
- **Chapter 3: Querying** - Advanced queries, joins, ordering
|
|
199
|
+
- **Chapter 5: Validation** - Keep your data clean and consistent
|
|
200
|
+
|
|
201
|
+
## Quick Reference
|
|
202
|
+
|
|
203
|
+
```ruby
|
|
204
|
+
# Connect
|
|
205
|
+
Dami.connect(:default, adapter: :sqlite, path: 'db.sqlite')
|
|
206
|
+
|
|
207
|
+
# Define model
|
|
208
|
+
Dami.model :table_name do
|
|
209
|
+
fields do
|
|
210
|
+
field :column_name, :type
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# CRUD
|
|
215
|
+
Dami.db(:table_name).create(column: value)
|
|
216
|
+
Dami.db(:table_name).find(id)
|
|
217
|
+
Dami.db(:table_name).where(column: value).to_a
|
|
218
|
+
Dami.db(:table_name).where(id: id).update(column: new_value)
|
|
219
|
+
Dami.db(:table_name).where(id: id).delete
|
|
220
|
+
```
|