active_record_cr
Version, currently 0.1.911 versions
github.com/OrelSokolov/active_record.cr
No description declared in shard.yml.
Nothing has been indexed for 0.1.9 yet. The tag is recorded, its shard.yml has not been read, so the manifest and dependency list below are empty because they are unknown rather than because they are absent.
Installation
# Add this to your shard.yml
dependencies:
active_record_cr:
github: OrelSokolov/active_record.cr
version: ~> 0.1.9Then run:
shards installshard.yml
No shard.yml has been indexed for 0.1.9. You can read it on the repository.
Dependencies
Unknown: the shard.yml for this version has not been read yet.
README
This README is the one indexed from the repository at its latest ref, not from the tag for this version.
active_record.cr
A Crystal port of Rails ActiveRecord 8.1.4 for PostgreSQL — models,
Relation querying, validations, callbacks, associations, transactions,
migrations, schema dumping, STI, enums/stores and ActiveRecord::Encryption.
Namespace ActiveRecord mirrors the original 1:1. Status: 292/292
oracle specs green (crud/dirty/callbacks/validations/associations/
transactions/persistence/proxy, through + polymorphic + HABTM +
counter_cache/touch/inverse_of, includes/preload/eager_load, optimistic
locking, STI, enum/store/alias_attribute/attr_readonly/cache_key/
has_secure_token, nested attributes, serialize, autosave), plus
migrations, db/schema.cr dump (byte-identical to the schema.rb
oracle) and encryption.
⚠️ EXPERIMENTAL FRAMEWORK, DO NOT USE IN PRODUCTION YET.
How it works
bin/ar_schema_dumpdumps the DB schema todb/schema.yml(columns/types/nullability) — input for compile-time codegen.- A model class
< ActiveRecord::Baseregisters itself at compile time (macro inherited); at the end of the program a{{ run(...) }}runner readsdb/schema.yml(envCRAILS_SCHEMA) and generates attributes, the row mapper,initialize(**attrs),attributes/[]/inspect/reload!. Ruby's runtimedefine_methodbecomes compile-time code generation — this is the key mechanism of the whole port. - PostgreSQL → Crystal type mapping:
int*→Int64,float8→Float64,numeric→PG::Numeric,timestamp*→Time,json/jsonb→JSON::Any,bytea→Bytes, arrays →Array(...). Caveat: columns named after Crystal keywords (until) get getters/ setters but are excluded fromnew(**kwargs).
Quick start
shards install
# 1) dump the DB schema to db/schema.yml (needed for codegen)
DATABASE_URL='postgres://root@/data_lake_development?host=/var/run/postgresql' \
bin/ar_schema_dump
# 2) demo read
crystal run examples/datalake_read.cr
Feature comparison vs activerecord 8.1.4
Connections & schema
| Feature | Original (activerecord) | Port (active_record.cr) | Status |
|---|---|---|---|
establish_connection (URL / config / database.yml subset), DATABASE_URL, unix sockets, schema_search_path, connection pool (pool: Rails semantics, default 5) | ✅ | Same | ✅ |
Attribute codegen name / name= / name? / name_was / name_changed? | ✅ | {{ run }} from schema.yml | ✅ |
table_name=, primary_key= | ✅ | Custom PK lives in @id (w.uid → w.id) | 🔶 |
attribute :x, default: | ✅ | Applies to new records only (NULL from DB stays nil, oracle-verified); type argument accepted but ignored (types come from the schema) | 🔶 |
| Runtime attribute methods without recompilation | ✅ | Not possible — equivalent: crails watch (rebuild) | ❌ |
Relation / querying
| Feature | Original | Port | Status |
|---|---|---|---|
where/not/order/limit/offset/reverse_order (String+args, hash, Range→BETWEEN, Array→IN, nil→IS NULL) | ✅ | Same | ✅ |
each, empty?, count(+group), size, pluck, pick, ids | ✅ | count with GROUP BY → Hash | ✅ |
take, first, first!, last, sole, to_sql | ✅ | Same | ✅ |
exists? (no args / id / conditions) | ✅ | Same | ✅ |
find_each, find_in_batches, in_batches(of:) | ✅ | Same | ✅ |
find, find?, find_by, find_by!, first_or_create, find_or_create_by | ✅ | Nilable finders cannot be chained (types) | 🔶 |
sum / average / minimum / maximum | ✅ | average → PG::Numeric (2 digits) | ✅ |
or, merge, group, having, distinct | ✅ | or checks structural compatibility | ✅ |
joins, left_outer_joins | ✅ | has_many/has_one FK convention | ✅ |
includes, preload, eager_load, references | ✅ | IN-queries + memory preload; eager_load as one LEFT OUTER JOIN with AS c_<col> aliases and owner dedup; references switches strategy (Rails semantics); polymorphic/through load lazily | ✅ |
select, reselect | ✅ | Simple columns; unloaded attribute → nil (Rails: MissingAttributeError); raw SQL fragments cannot be materialized into records | 🔶 |
reorder, in_order_of | ✅ | ARRAY_POSITION | ✅ |
update_all, delete_all, destroy_all, touch_all | ✅ | Relation and class level | ✅ |
none, except/only, annotate, explain | ✅ | explain → String (Rails: ExplainProxy) | 🔶 |
lock ("FOR UPDATE"), readonly, strict_loading | ✅ | readonly save/destroy → ReadOnlyRecord; strict_loading → StrictLoadingViolationError | ✅ |
scope :name do ... end (with parameters) | ✅ | Block-body splice | ✅ |
default_scope, unscoped | ✅ | default_scope: single where(...) only (macro recursion limit) | 🔶 |
Dynamic finders find_by_email | ✅ | No method_missing — use find_by(email:) | ❌ |
Validations
| Feature | Original | Port | Status |
|---|---|---|---|
presence / length / numericality / format / uniqueness (+scope:) | ✅ | Same | ✅ |
| inclusion, exclusion, acceptance, confirmation, comparison | ✅ | Rails 8 acceptance skips nil | ✅ |
validate (method/block), valid?(context) / invalid? | ✅ | Default context :create/:update | ✅ |
on:, allow_blank:, allow_nil:, strict: | ✅ | strict → StrictValidationFailed | ✅ |
if: / unless: | ✅ | Ruby keywords — named args condition: / unless_condition: | 🔶 |
errors: add/full_messages/where/details/group_by_attribute | ✅ | details keys are Strings (no runtime Symbols) | 🔶 |
around_validation | — | Does not exist in Rails 8 | — |
| numericality of a String into an Int column | ✅ | Will not compile (documented) | ❌ |
Callbacks
| Feature | Original | Port | Status |
|---|---|---|---|
before/after _validation/_save/_create/_update/_destroy | ✅ | Order oracle-verified | ✅ |
around_save/create/update/destroy | ✅ | Block or symbol form, order 1:1 | ✅ |
after_initialize, after_find, after_touch | ✅ | after_find fires before after_initialize on load (1:1) | ✅ |
after_commit, after_rollback (+ on: filter) | ✅ | Real COMMIT/ROLLBACK incl. savepoints | ✅ |
Associations
| Feature | Original | Port | Status |
|---|---|---|---|
belongs_to (+ build_/create_), has_many, has_one (+dependent), options foreign_key, class_name, optional | ✅ | Same | ✅ |
CollectionProxy each/count/size/empty?/ids/pluck/build/create/create!/destroy_all/delete_all/order?/where? | ✅ | Same | ✅ |
CollectionProxy <<, concat, include?, destroy, delete, replace | ✅ | delete/replace (nullify) implemented but not oracle-pinned (FK NOT NULL in scratch schema) | 🔶 |
<assoc>_ids | ✅ | Codegen | ✅ |
through / source | ✅ | Subquery mid_fk IN (SELECT …) | ✅ |
Polymorphic (as:) | ✅ | Param as_name: (as is a keyword); compile-time POLY_OWNERS/POLY_USERS registry | 🔶 |
has_and_belongs_to_many | ✅ | Join naming from both table_names | ✅ |
counter_cache, inverse_of, autosave, touch | ✅ | autosave stores children in @loaded_assocs (semantics 1:1 on oracle cases) | ✅ |
primary_key: / validate: association options | ✅ | Not ported | ⬜ |
Runtime class_name: "Foo" for arbitrary strings | ✅ | Compile-time known classes only | ❌ |
Persistence & dirty
| Feature | Original | Port | Status |
|---|---|---|---|
new, create(_!), save(_!), update(_!), assign_attributes, attributes | ✅ | Persistence methods take a Hash, not keywords | 🔶 |
destroy, persisted?, new_record?, frozen? | ✅ | Same | ✅ |
dirty: changed?/changed/changes/saved_changes/will_save_change_to_x? | ✅ | Same | ✅ |
update_columns, touch(*cols), insert_all(_!), upsert_all | ✅ | Same | ✅ |
reload, update_attribute, increment/decrement(+!), toggle(+!), restore_attributes, previous_changes, instance delete, becomes | ✅ | Return-value semantics 1:1 | ✅ |
| Virtual attributes | ✅ | Direct setter only (no runtime method magic) | 🔶 |
Transactions & locking
| Feature | Original | Port | Status |
|---|---|---|---|
transaction (requires_new:, isolation:) | ✅ | Connection pinned to the fiber; joined-nested Rollback does not roll back (matches 8.1.4, verified on live Rails) | ✅ |
with_lock, lock! | ✅ | SELECT FOR UPDATE on the pinned connection | ✅ |
Optimistic locking (lock_version) | ✅ | UPDATE ... AND lock_version = $N; 0 rows → StaleObjectError | ✅ |
Migrations & schema
| Feature | Original | Port | Status |
|---|---|---|---|
create_table (all types, timestamps, references, index), id: false, column ops, add_index (unique:, name:, where:), add_reference, FKs, execute, reversible change / up-down, change_table, create_join_table, check constraints, reversible blocks | ✅ | Same; partial indexes oracle-verified via dumps | ✅ |
crails db:migrate / rollback / schema:dump / schema:load | ✅ | Dumps db/schema.cr (primary) + db/schema.yml (codegen input; columns + indexes with name/columns/unique/where, implicit PK skipped); schema:load recompiles a runner (~1 s, deliberate deviation from instant ruby db/schema.rb) | 🔶 |
db/schema.cr ↔ schema.rb | ✅ | Byte-identical syntax to 8.1.4 (ActiveRecord::Schema[8.1].define, force: :cascade, t.index, t.check_constraint, enable_extension, add_foreign_key); differences: require "active_record_cr" header, typed empty literals ([] of Int64); expression defaults (now()) are not dumped | 🔶 |
Enums, stores, STI, misc
| Feature | Original | Port | Status |
|---|---|---|---|
enum status: [...] → predicates/bang-methods/scopes | ✅ | Macro ar_enum (enum is a keyword); getter returns String, unmapped DB values read as nil (8.1, oracle-verified); int and string values (index_by(&:itself)) | ✅ |
store / store_accessor | ✅ | jsonb out of the box, StoreProxy container | ✅ |
serialize | ✅ | JSON coder only (Rails' YAML default not ported); text column ↔ JSON roundtrip; mutating a read container is not tracked — assign a new one | 🔶 |
alias_attribute | ✅ | Getter/setter + where resolution | ✅ |
accepts_nested_attributes_for | ✅ | has_many with autosave semantics, allow_destroy:, reject_if: :all_blank; belongs_to form ⬜; setter is explicit (model.n_posts_attributes = [...]) | 🔶 |
cache_key, has_secure_token, attr_readonly, to_param, signed_id | ✅ | cache_key ✅, has_secure_token ✅ (24 hex), attr_readonly ✅ (silently ignored, oracle-verified); to_param ⬜; signed_id ❌ (MessageVerifier not available outside a Rails app) | 🔶 |
STI (type column) | ✅ | One inheritance level: __read_row dispatch + becomes | ✅ |
Runtime constantize | ✅ | Compile-time registry only | ❌ |
Encryption (phase G)
| Feature | Original | activerecord encryption | Port | Status |
|---|---|---|---|---|
| AES-256-GCM cipher, deterministic IV (HMAC-SHA256), Message + serializer (byte-exact JSON envelope), compression >140 B | ✅ | Same (EVP aes-256-gcm) | ✅ | |
| Key providers: DerivedSecretKeyProvider / DeterministicKeyProvider (PBKDF2-HMAC-SHA1 2**16, AS::KeyGenerator) | ✅ | Same | ✅ | |
encrypts macro, Model.encrypted_attributes, #ciphertext_for, Model.where_encrypted | ✅ | Deterministic lookup (analog of ExtendedDeterministicQueries) | ✅ | |
Config: configure || ENV ACTIVE_RECORD_ENCRYPTION_* || defaults | ✅ | Same (ENV support is an extension) | ✅ | |
Envelope encryption / key references (k/i headers), previous: schemes, ignore_case original_* columns, NullEncryptor contexts | ✅ | Not ported | ⬜ |
Deviations & Crystal-specific notes
- No
method_missing, no runtime Symbols, noconstantize, static typing — these power most ❌ rows above; equivalents are compile-time registries and codegen. - Nilable finders cannot be unwrapped into a chain; persistence methods
accept
Hash, not keywords; virtual attributes via direct setters (details in the linked SEMANTICS doc of the umbrella project). - Class variables are not shared with subclasses — state is accessed via
explicit
Base.receivers; transactions pin a connection to the fiber.
Testing
# fixture DB crails_ar_test + datalake read-only
psql -h /var/run/postgresql -U root -d postgres -c 'create database crails_ar_test owner root'
psql -h /var/run/postgresql -U root -d crails_ar_test -f spec/fixtures/seed.sql
crystal spec
Oracle-first development: oracle/cases.rb + oracle/migrations.rb /
oracle/schema.rb are recorded against real activerecord 8.1.4;
mirror specs in spec/oracle_spec.cr / spec/migrations_spec.cr /
spec/schema_spec.cr reproduce the expected files without editing the
cases. Encryption parity: 26 Ruby cases + a reverse check where the real
gem decrypts port ciphertexts (oracle/verify_encryption.rb).
Documentation
Built from the current release. The first visit to a release nobody has asked for starts its build.
Links
This release
- Version
0.1.9- Tagged
- Oct 19, 2026
- Commit
4acd7dec992c- Indexed
- not yet
Dependents
- turbo_cr
- once_campfire_crails
- active_storage_cr
- action_cable_cr
- will_paginate_cr
- rails_settings_cached_cr
- rolify_cr
- factory_bot_cr
- cancancan_cr
- devise_cr
- action_text_cr
- action_mailbox_cr
and 1 more
Repository
github.com/OrelSokolov/active_record.cr
Metadata
- Created
- Oct 15, 2026
- Updated
- Oct 20, 2026
- Synced
- Oct 20, 2026
- Versions
- 11