Skip to content

Commit f3113eb

Browse files
committed
add docs
1 parent 011f4e6 commit f3113eb

10 files changed

Lines changed: 1139 additions & 1 deletion

File tree

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
db/
66
/spec/repo.cr
77

8+
/docs/.vitepress/cache/
9+
/docs/.vitepress/dist/
10+
811
# SQLite3 testing database
912
crecto_test.db
1013
spec/integration/integration_test.db
@@ -48,4 +51,3 @@ node_modules/
4851
/.cursor/
4952
/.specify/
5053
/bmad/
51-
/docs/

docs/.vitepress/config.mts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { defineConfig } from 'vitepress'
2+
3+
export default defineConfig({
4+
title: 'Crecto Documentation',
5+
description: 'A Crystal ORM',
6+
base: '/crecto/',
7+
ignoreDeadLinks: true,
8+
themeConfig: {
9+
nav: [
10+
{ text: 'Home', link: '/' },
11+
{ text: 'Getting Started', link: '/guide' },
12+
{ text: 'Core Concepts', link: '/core-concepts' },
13+
{ text: 'Features', link: '/features-guide' },
14+
{ text: 'Advanced', link: '/advanced-patterns' },
15+
{ text: 'Examples', link: '/examples' },
16+
{ text: 'API Reference', link: '/api-reference' }
17+
],
18+
19+
sidebar: [
20+
{
21+
text: 'Getting Started',
22+
items: [
23+
{ text: 'Introduction', link: '/guide' },
24+
{ text: 'Installation', link: '/guide#installation' },
25+
{ text: 'Configuration', link: '/configuration' },
26+
{ text: 'Quick Start', link: '/guide#quick-start-tutorial' }
27+
]
28+
},
29+
{
30+
text: 'Core Concepts',
31+
items: [
32+
{ text: 'Repository Pattern', link: '/core-concepts#repository-pattern' },
33+
{ text: 'Model System', link: '/core-concepts#model-system' },
34+
{ text: 'Changeset Pattern', link: '/core-concepts#changeset-pattern' },
35+
{ text: 'Query System', link: '/core-concepts#query-system' },
36+
{ text: 'Adapter System', link: '/core-concepts#adapter-system' }
37+
]
38+
},
39+
{
40+
text: 'Features Guide',
41+
items: [
42+
{ text: 'Schema Definition', link: '/features-guide#schema-definition' },
43+
{ text: 'CRUD Operations', link: '/features-guide#crud-operations' },
44+
{ text: 'Data Validation', link: '/features-guide#data-validation' },
45+
{ text: 'Associations', link: '/features-guide#associations' },
46+
{ text: 'Query Building', link: '/features-guide#query-building' }
47+
]
48+
},
49+
{
50+
text: 'Advanced Usage',
51+
items: [
52+
{ text: 'Transaction Management', link: '/advanced-patterns#transaction-management' },
53+
{ text: 'Bulk Operations', link: '/advanced-patterns#bulk-operations' },
54+
{ text: 'Performance Optimization', link: '/advanced-patterns#performance-optimization' },
55+
{ text: 'Error Handling', link: '/advanced-patterns#error-handling-and-resilience' },
56+
{ text: 'Testing Strategies', link: '/advanced-patterns#testing-strategies' }
57+
]
58+
},
59+
{
60+
text: 'Examples & Tutorials',
61+
items: [
62+
{ text: 'Complete Blog Application', link: '/examples#complete-application-example' },
63+
{ text: 'User Management', link: '/examples#user-management' },
64+
{ text: 'Blog Post Management', link: '/examples#blog-post-management' },
65+
{ text: 'Advanced Queries', link: '/examples#advanced-query-examples' },
66+
{ text: 'Bulk Operations', link: '/examples#bulk-operations' },
67+
{ text: 'Transaction Examples', link: '/examples#transaction-examples' },
68+
{ text: 'Performance Optimization', link: '/examples#performance-optimization-examples' },
69+
{ text: 'Testing Examples', link: '/examples#testing-examples' }
70+
]
71+
},
72+
{
73+
text: 'API Reference',
74+
items: [
75+
{ text: 'Crecto::Repo', link: '/api-reference#crectorepo' },
76+
{ text: 'Crecto::Model', link: '/api-reference#crectomodel' },
77+
{ text: 'Crecto::Changeset', link: '/api-reference#crectochangeset' },
78+
{ text: 'Crecto::Query', link: '/api-reference#crectoquery' },
79+
{ text: 'Associations', link: '/api-reference#associations' },
80+
{ text: 'Adapters', link: '/api-reference#adapters' },
81+
{ text: 'Error Types', link: '/api-reference#error-types' }
82+
]
83+
}
84+
],
85+
86+
socialLinks: [
87+
{ icon: 'github', link: 'https://github.com/Crecto/Crecto' }
88+
]
89+
}
90+
})

docs/advanced-patterns.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Advanced Patterns
2+
3+
The existing codebase already covers a few patterns that help when applications grow beyond basic CRUD. This document focuses on what is implemented today.
4+
5+
## Coordinating Work with `Multi`
6+
7+
`Crecto::Multi` batches operations that should be executed inside a single transaction. It is best suited for independent changes where you only need to know whether the batch succeeded.
8+
9+
```crystal
10+
multi = Crecto::Multi.new
11+
multi.insert(User.changeset(user))
12+
multi.insert(Profile.changeset(profile))
13+
multi.update(Post.changeset(post))
14+
15+
result = Repo.transaction(multi)
16+
17+
if result.errors.empty?
18+
puts "Batch committed"
19+
else
20+
pp result.errors
21+
end
22+
```
23+
24+
`Multi` validates all queued changesets before opening a transaction. If any are invalid, the transaction will be skipped and `multi.errors` will contain the validation messages.
25+
26+
## Immediate Feedback with `transaction!`
27+
28+
`Repo.transaction!` yields a `Crecto::LiveTransaction`, which forwards calls to the repository while reusing the same low-level `DB::Transaction`.
29+
30+
```crystal
31+
Repo.transaction! do |tx|
32+
user_result = tx.insert(User.changeset(user))
33+
raise "user invalid" unless user_result.valid?
34+
35+
profile = Profile.new.tap { |p| p.user_id = user_result.instance.id }
36+
profile_result = tx.insert(Profile.changeset(profile))
37+
raise "profile invalid" unless profile_result.valid?
38+
39+
# raise will rollback automatically
40+
end
41+
```
42+
43+
Every helper on `LiveTransaction` returns either a changeset or a bulk result—there is no special casing beyond sharing the connection.
44+
45+
## Pagination and Batching
46+
47+
Large result sets should be processed in slices. `Repo.all` accepts `limit` and `offset`, so you can build a loop that paginates manually.
48+
49+
```crystal
50+
Query = Crecto::Repo::Query
51+
52+
offset = 0
53+
batch_size = 500
54+
55+
loop do
56+
batch = Repo.all(User, Query.limit(batch_size).offset(offset))
57+
break if batch.empty?
58+
59+
batch.each { |user| process(user) }
60+
offset += batch_size
61+
end
62+
```
63+
64+
This approach keeps memory usage predictable even without a streaming cursor API.
65+
66+
## Association Preloading
67+
68+
Preloads fetch related rows in follow-up queries and attach them to existing models. Only `has_many`, `has_one`, `belongs_to`, and `has_many ... through:` associations are supported.
69+
70+
```crystal
71+
users = Repo.all(User,
72+
Query.preload(:posts, Query.where(published: true))
73+
.preload(:profile)
74+
)
75+
76+
users.each do |user|
77+
puts user.profile?.try(&.bio) if user.profile?
78+
user.posts?.try(&.each) { |post| puts post.title }
79+
end
80+
```
81+
82+
Attempting to access an association that was not preloaded raises `Crecto::AssociationNotLoaded`.
83+
84+
## Bulk Inserts with Error Reporting
85+
86+
`Repo.insert_all` aggregates validation failures and database errors so you can handle partial success gracefully.
87+
88+
```crystal
89+
records = [
90+
{name: "Alice", email: "alice@example.com"},
91+
{name: "Bob", email: "bob@example.com"},
92+
{name: "Invalid", email: "broken"} # fails validation
93+
]
94+
95+
result = Repo.insert_all(User, records)
96+
97+
puts "Inserted #{result.successful_count} / #{result.total_count}"
98+
99+
result.errors.each do |error|
100+
puts "Index #{error.index} failed: #{error.error_message}"
101+
pp error.validation_errors
102+
end
103+
```
104+
105+
The adapters insert valid records in bulk and fall back to per-record inserts when a database error occurs, mirroring the behaviour in `src/crecto/adapters/*_adapter.cr`.
106+
107+
These are the patterns supported by the current implementation. Features such as optimistic locking, cursor streaming, or explicit savepoint management are on the roadmap but not yet shipped, so they are intentionally excluded here.

docs/api-reference.md

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# Crecto API Reference
2+
3+
This reference captures the public APIs that exist in the repository today. All snippets are derived from the current code under `src/crecto`.
4+
5+
## `Crecto::Repo`
6+
7+
Repositories are singletons that wrap a database connection.
8+
9+
### Configuration
10+
11+
```crystal
12+
class Repo < Crecto::Repo
13+
config do |conf|
14+
conf.adapter = Crecto::Adapters::Postgres
15+
conf.database = "app_dev"
16+
conf.username = "postgres"
17+
conf.password = "postgres"
18+
conf.hostname = "localhost"
19+
end
20+
end
21+
```
22+
23+
`config` returns the same `Config` instance every time; use it to tweak settings at boot.
24+
25+
### Reading
26+
27+
- `Repo.all(queryable, query = Crecto::Repo::Query.new, **opts)` – returns an `Array(queryable)`. Optional keyword `preload:` merges extra preloads.
28+
- `Repo.get(queryable, id, tx = nil)` / `Repo.get!(...)`.
29+
- `Repo.get_by(queryable, **opts)` / `Repo.get_by!(...)`.
30+
- `Repo.get_association(model, :name, query = Query.new)` – loads associations on demand.
31+
- `Repo.aggregate(queryable, function, field, query = Query.new)` – executes `AVG/COUNT/MAX/MIN/SUM`.
32+
- `Repo.query(queryable, sql, params = [] of DbValue)` – executes raw SQL, casting rows into models.
33+
- `Repo.query(sql, params = [] of DbValue)` – returns `DB::ResultSet`. Callers must close the result set.
34+
- `Repo.raw_exec(*args)`, `Repo.raw_query(sql, *args)`, `Repo.raw_scalar(*args)` – thin wrappers around the underlying `DB::Database`.
35+
36+
### Writing
37+
38+
All mutating methods expect a valid changeset and return a `Crecto::Changeset::Changeset`.
39+
40+
- `Repo.insert(changeset)` / `Repo.insert!(changeset)`
41+
- `Repo.update(changeset)` / `Repo.update!(changeset)`
42+
- `Repo.delete(changeset)` / `Repo.delete!(changeset)`
43+
- `Repo.insert_all(queryable, Array(Model | Hash | NamedTuple))` – returns `Crecto::BulkResult`. Hashes and named tuples are cast into models before validation.
44+
- `Repo.insert_all!(...)` – raises if any record fails.
45+
- `Repo.update_all(queryable, query, Hash | NamedTuple, tx = nil)` – returns `Nil`.
46+
- `Repo.delete_all(queryable, query = Query.new, tx = nil)` – returns `Nil`.
47+
48+
### Transactions
49+
50+
- `Repo.transaction(multi : Crecto::Multi)` – runs the operations described by the multi and returns the same multi. If any operation fails the transaction is rolled back and `multi.errors` contains the failure details.
51+
- `Repo.transaction! { |tx| ... }` – yields a `Crecto::LiveTransaction`. Any raised exception rolls back.
52+
- `Repo.transaction_with_savepoint!(name = nil) { |tx| ... }` – creates a savepoint when already inside a transaction, otherwise behaves like `transaction!`.
53+
54+
`Crecto::LiveTransaction` exposes the same methods as the repository (`insert`, `update`, `delete`, `insert_all`, `update_all`, `delete_all`, `get`, `get_by`). Each forwards to the repo while reusing the `DB::Transaction`.
55+
56+
## `Crecto::Repo::Query`
57+
58+
The query builder composes SQL fragments.
59+
60+
Common helpers:
61+
62+
- `Query.where(...)`, `Query.or_where(...)` – accept keyword arguments, `(String, Array(DbValue))`, `(Symbol, DbValue)`, or plain SQL.
63+
- `Query.join(:association)` / `Query.join("JOIN ...")`
64+
- `Query.preload(:association, query = Query.new)`
65+
- `Query.order_by("field DESC")`
66+
- `Query.limit(Int32 | Int64)` and `Query.offset(Int32 | Int64)`
67+
- `Query.group_by("expression")`
68+
- `Query.distinct("expression")`
69+
- `Query.combine(other_query)` – merges select lists and where expressions.
70+
- `Query.and { |expr| ... }` and `Query.or { |expr| ... }` – scope combinators.
71+
72+
`Query#stream` and `Query#each_cursor` exist as placeholders and currently raise `NotImplementedError`.
73+
74+
## `Crecto::Model`
75+
76+
Subclassing `Crecto::Model` adds:
77+
78+
- `schema` macro for columns.
79+
- Association macros: `has_many`, `has_one`, `belongs_to`. `has_many` accepts `through:` to model join tables.
80+
- Field introspection: `self.fields`, `self.primary_key_field`, `self.table_name`.
81+
- Initializers: `new`, `new(**attrs)` to apply attributes via the casting pipeline, and `new(named_tuple)` for runtime data.
82+
- Changeset helpers: `self.changeset(instance, validation_context = nil)`, `instance.get_changeset`.
83+
- Casting helpers: `self.cast(hash)`, `instance.cast(hash)`, `cast!(...)`.
84+
- Timestamp helpers: `instance.created_at_to_now`, `instance.updated_at_to_now`.
85+
86+
Associations store metadata in `CRECTO_ASSOCIATIONS` and raise `Crecto::AssociationNotLoaded` until preloaded.
87+
88+
## `Crecto::Changeset`
89+
90+
`Crecto::Changeset::Changeset` instances expose:
91+
92+
- `#valid?` – boolean result.
93+
- `#errors``Array(Tuple(String, String))`.
94+
- `#changes` / `#source` – change tracking.
95+
- `#instance` – the wrapped model.
96+
- `#action` – set by the repository (`:insert`, `:update`, `:delete`).
97+
- `#validate_required`, `#validate_format`, etc. – imperative validators run inside custom logic.
98+
- `#unique_constraint(field)` – converts database uniqueness violations into changeset errors.
99+
100+
Validations declared on the model class (e.g. `validate_required`) accumulate in class-level caches and run automatically when a changeset is instantiated.
101+
102+
## `Crecto::Multi`
103+
104+
`Crecto::Multi` collects operations to run inside a transaction.
105+
106+
- `multi.insert(model_or_changeset)`
107+
- `multi.update(model_or_changeset)`
108+
- `multi.delete(model_or_changeset)`
109+
- `multi.delete_all(queryable, query = Crecto::Repo::Query.new)`
110+
- `multi.update_all(queryable, query, update_hash)`
111+
- `multi.operations` – the ordered list of pending operations.
112+
- `multi.errors` – populated when `Repo.transaction(multi)` encounters a failure.
113+
- `multi.changesets_valid?` – returns `false` and sets `errors` if any queued changeset is invalid.
114+
115+
## `Crecto::BulkResult`
116+
117+
Returned by `Repo.insert_all`.
118+
119+
- `#total_count`, `#successful_count`, `#failed_count`
120+
- `#success_rate`
121+
- `#inserted_ids`
122+
- `#errors` – array of `BulkInsertError`
123+
- `#successful?`, `#partial_success?`, `#complete_failure?`
124+
- `#finalize_result(duration_ms)` – called internally to compute `failed_count`.
125+
126+
`BulkInsertError` provides `index`, `error_message`, `error_class`, `validation_errors`, `database_error_code`, and helpers like `#constraint_violation?`.
127+
128+
## Errors
129+
130+
Defined under `src/crecto/errors/`:
131+
132+
- `Crecto::Errors::InvalidChangeset`
133+
- `Crecto::Errors::InvalidAdapter`
134+
- `Crecto::Errors::InvalidOption`
135+
- `Crecto::Errors::InvalidType`
136+
- `Crecto::Errors::AssociationError`
137+
- `Crecto::Errors::AssociationNotLoaded`
138+
- `Crecto::Errors::BulkError`
139+
- `Crecto::Errors::NoResults`
140+
- `Crecto::Errors::ConcurrentModificationError`
141+
- `Crecto::Errors::RecordNotFoundError`
142+
- `Crecto::Errors::IteratorError`
143+
144+
Handle them with Crystal's standard exception mechanisms (`rescue`).
145+
146+
## Logging
147+
148+
`Crecto::DbLogger` captures SQL statements, timings, and bulk operation diagnostics. Adapters call `DbLogger.log` and `DbLogger.log_error` internally. You can configure the logger by assigning `Crecto::DbLogger.logger = Log.for("crecto")`.
149+
150+
---
151+
152+
This reference reflects the behaviour of the code as of the current repository revision. If you add new features, update this document alongside the implementation to keep it trustworthy.

0 commit comments

Comments
 (0)