declare_schema 3.1.0.colin.3 → 4.0.1.colin.1
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 +4 -4
- data/.github/workflows/pipeline.yml +1 -1
- data/CHANGELOG.md +13 -2
- data/Gemfile.lock +1 -1
- data/lib/declare_schema/model/deferred_field_spec.rb +18 -6
- data/lib/declare_schema/model.rb +23 -9
- data/lib/declare_schema/version.rb +1 -1
- data/lib/generators/declare_schema/migration/migrator.rb +10 -1
- data/spec/lib/declare_schema/deferred_field_spec_spec.rb +56 -0
- data/spec/lib/generators/declare_schema/migration/migrator_default_pk_type_spec.rb +5 -5
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '09079a0e965a85eba22eade7bc713fcacf35d4fd6f874e9d8b41eb36f8e6901e'
|
|
4
|
+
data.tar.gz: b7f0fa06ec76c298d2b9248ef10b6a1010d36bf2edb3c3a667f57790d98bb6b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5cb66b34c4d70e0673106ee94fec485e7e5eec25e34b75a64a94e0d3fea9f6f822157dadc8b662ba57af9878c3686807be1ab97b01ce89b8de3b3354c2dfd3a9
|
|
7
|
+
data.tar.gz: 6359500ff616315a48dd3921a9fac1b9d51e3a24c1dfb1b761160b048da1ef8222d036f60b6e5d2d5e5b74de5f12e44c2db70717a985257be420d78fcbe7f64d
|
data/CHANGELOG.md
CHANGED
|
@@ -4,9 +4,20 @@ Inspired by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
4
4
|
|
|
5
5
|
Note: this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
-
## [
|
|
7
|
+
## [4.0.1] - 2026-05-06
|
|
8
|
+
### Fixed
|
|
9
|
+
- `DeferredFieldSpec` (the lazy stand-in introduced in 4.0.0 for `belongs_to`
|
|
10
|
+
foreign-key field specs) now transparently delegates the `FieldSpec` read
|
|
11
|
+
API (`.options`, `.type`, `.limit`, `.null`, etc.) to its memoized resolved
|
|
12
|
+
spec. Application code that reads `Model.field_specs[name].options` at
|
|
13
|
+
runtime would raise `NoMethodError: undefined method 'options' for an
|
|
14
|
+
instance of DeclareSchema::Model::DeferredFieldSpec`.
|
|
15
|
+
|
|
16
|
+
## [4.0.0] - 2026-05-05
|
|
8
17
|
### Added
|
|
9
|
-
-
|
|
18
|
+
- Generalized `belongs_to` foreign keys to always match the primary key they point at, including
|
|
19
|
+
in HABTM intersection tables. (Previously this was just a special case that matched up
|
|
20
|
+
:integer vs :bigint.)
|
|
10
21
|
|
|
11
22
|
### Removed
|
|
12
23
|
- Drop support for Rails 6.x. Minimum supported Rails is now 7.0.
|
data/Gemfile.lock
CHANGED
|
@@ -12,7 +12,7 @@ module DeclareSchema
|
|
|
12
12
|
# The migrator calls {#resolve} on every value in `field_specs` at the start
|
|
13
13
|
# of migration generation (see `Migrator#generate`); for plain {FieldSpec}s
|
|
14
14
|
# `#resolve` is a no-op returning self, while for instances of this class it
|
|
15
|
-
# invokes the block
|
|
15
|
+
# invokes the resolver block with the default spec and returns the produced
|
|
16
16
|
# {FieldSpec}.
|
|
17
17
|
class DeferredFieldSpec
|
|
18
18
|
# @param default_spec [FieldSpec] the eager placeholder spec
|
|
@@ -24,14 +24,26 @@ module DeclareSchema
|
|
|
24
24
|
@resolver = resolver
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
27
|
+
# Resolve and memoize the produced FieldSpec. Memoization matters because
|
|
28
|
+
# application code can hit several FieldSpec accessors per request (e.g.
|
|
29
|
+
# ModelReport reads `.options`, ApplicationModel reads `.limit`); without
|
|
30
|
+
# it each one would re-run the resolver and re-touch `reflection.klass`.
|
|
31
31
|
#
|
|
32
32
|
# @return [FieldSpec]
|
|
33
33
|
def resolve
|
|
34
|
-
@resolver.call(@default_spec)
|
|
34
|
+
@resolved ||= @resolver.call(@default_spec)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def respond_to_missing?(name, include_private = false)
|
|
38
|
+
resolve.respond_to?(name, include_private) || super
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def method_missing(name, *args, **kwargs, &)
|
|
42
|
+
if resolve.respond_to?(name)
|
|
43
|
+
resolve.public_send(name, *args, **kwargs, &)
|
|
44
|
+
else
|
|
45
|
+
super
|
|
46
|
+
end
|
|
35
47
|
end
|
|
36
48
|
end
|
|
37
49
|
end
|
data/lib/declare_schema/model.rb
CHANGED
|
@@ -371,13 +371,15 @@ module DeclareSchema
|
|
|
371
371
|
|
|
372
372
|
# @param klass [Class] any ActiveRecord class
|
|
373
373
|
# @return [ActiveRecord::ConnectionAdapters::Column, nil] the live PK column, or
|
|
374
|
-
# nil when the table doesn't exist yet or can't otherwise be inspected
|
|
375
|
-
#
|
|
376
|
-
#
|
|
377
|
-
#
|
|
374
|
+
# nil when the table doesn't exist yet or can't otherwise be inspected, or
|
|
375
|
+
# when the model has a compound primary key (Array PK name misses
|
|
376
|
+
# `columns_hash`). Reads via `columns_hash` directly (not `_column`, whose
|
|
377
|
+
# `@table_exists` memoization can pin a stale answer) so a parent table
|
|
378
|
+
# being created in this same migration run is honored.
|
|
378
379
|
def _pk_column_for(klass)
|
|
379
|
-
pk_name = klass.
|
|
380
|
-
|
|
380
|
+
if (pk_name = klass.try(:_declared_primary_key) || klass.primary_key)
|
|
381
|
+
klass.columns_hash[pk_name]
|
|
382
|
+
end
|
|
381
383
|
rescue ActiveRecord::StatementInvalid, ActiveRecord::NoDatabaseError
|
|
382
384
|
nil
|
|
383
385
|
end
|
|
@@ -391,13 +393,25 @@ module DeclareSchema
|
|
|
391
393
|
# the column type doesn't round-trip cleanly
|
|
392
394
|
def _field_spec_options_from_pk_column(column)
|
|
393
395
|
if column&.type == :integer
|
|
394
|
-
column.limit == 8
|
|
396
|
+
if column.limit == 8
|
|
397
|
+
[:bigint, {}]
|
|
398
|
+
else
|
|
399
|
+
[:integer, { limit: column.limit || 4 }]
|
|
400
|
+
end
|
|
395
401
|
end
|
|
396
402
|
end
|
|
397
403
|
|
|
398
404
|
# `declare_schema id: ...` accepts either a Hash (`id: { type: :integer, limit: 4 }`)
|
|
399
|
-
# or a bare type Symbol (`id: :integer`).
|
|
400
|
-
#
|
|
405
|
+
# or a bare type Symbol (`id: :integer`). Normalizes both shapes -- plus the
|
|
406
|
+
# absent / unrecognized case -- into a uniform `[type, options]` pair so the
|
|
407
|
+
# caller (`_primary_key_field_spec_from_table_options`) can dispatch on
|
|
408
|
+
# `type.nil?` to decide whether to fall back to live-PK inspection or the
|
|
409
|
+
# gem default.
|
|
410
|
+
#
|
|
411
|
+
# @param value [Hash, Symbol, nil] the per-PK entry from `_table_options`
|
|
412
|
+
# @return [Array(Symbol, Hash), Array(nil, Hash)] `[type, options]` where
|
|
413
|
+
# `type` is nil when `value` is neither a Hash nor a Symbol (signal to
|
|
414
|
+
# caller to fall back). `options` always excludes `:type`.
|
|
401
415
|
def _parse_pk_table_options(value)
|
|
402
416
|
case value
|
|
403
417
|
when Hash then [value[:type], value.except(:type)]
|
|
@@ -80,7 +80,16 @@ module Generators
|
|
|
80
80
|
self.class.native_types
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
-
#
|
|
83
|
+
# Collect every HABTM reflection across the application, keyed by its join
|
|
84
|
+
# table name. Each join table is shared by both sides of a HABTM
|
|
85
|
+
# association, so iterating all descendants would yield two reflections
|
|
86
|
+
# per join table (one from each parent model). Keying by `join_table` and
|
|
87
|
+
# using `||=` keeps the first occurrence and silently drops the duplicate
|
|
88
|
+
# from the other side, so the migrator emits exactly one shim per join
|
|
89
|
+
# table.
|
|
90
|
+
#
|
|
91
|
+
# @return [Hash{String => ActiveRecord::Reflection::HasAndBelongsToManyReflection}]
|
|
92
|
+
# join table name -> the (first-seen) HABTM reflection that owns it
|
|
84
93
|
def habtm_tables
|
|
85
94
|
ActiveRecord::Base.send(:descendants).each_with_object({}) do |c, result|
|
|
86
95
|
c.reflect_on_all_associations(:has_and_belongs_to_many).each do |a|
|
|
@@ -24,5 +24,61 @@ RSpec.describe DeclareSchema::Model::DeferredFieldSpec do
|
|
|
24
24
|
expect(deferred.resolve).to equal(mirrored_spec)
|
|
25
25
|
expect(captured).to equal(default_spec)
|
|
26
26
|
end
|
|
27
|
+
|
|
28
|
+
it 'memoizes the resolver result so repeated calls invoke it only once' do
|
|
29
|
+
call_count = 0
|
|
30
|
+
deferred = described_class.new(default_spec) do |_|
|
|
31
|
+
call_count += 1
|
|
32
|
+
mirrored_spec
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
3.times { deferred.resolve }
|
|
36
|
+
|
|
37
|
+
expect(call_count).to eq(1)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Application code (e.g. ModelReport reading `field_specs[name].options[:rr_report_options]`)
|
|
42
|
+
# reads from `field_specs` without first calling `.resolve`. The deferred spec must
|
|
43
|
+
# therefore quack like the resolved FieldSpec; otherwise we get NoMethodError at runtime
|
|
44
|
+
# in apps that read FieldSpec attributes, AND we'd return a wrong default-typed answer
|
|
45
|
+
# (e.g. :bigint when the parent's PK is actually :integer or :binary).
|
|
46
|
+
describe 'FieldSpec API delegation' do
|
|
47
|
+
let(:deferred) { described_class.new(default_spec) { mirrored_spec } }
|
|
48
|
+
|
|
49
|
+
it 'delegates #options to the resolved spec (parent-mirrored, not default)' do
|
|
50
|
+
expect(deferred.options).to eq(mirrored_spec.options)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'delegates #type to the resolved spec' do
|
|
54
|
+
expect(deferred.type).to eq(:string)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'delegates #limit to the resolved spec' do
|
|
58
|
+
expect(deferred.limit).to eq(36)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'delegates #null to the resolved spec' do
|
|
62
|
+
expect(deferred.null).to eq(false)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'reports respond_to? for delegated methods' do
|
|
66
|
+
expect(deferred).to respond_to(:options, :type, :limit, :null, :name, :position)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'invokes the resolver only once across many delegated reads' do
|
|
70
|
+
call_count = 0
|
|
71
|
+
deferred = described_class.new(default_spec) do |_|
|
|
72
|
+
call_count += 1
|
|
73
|
+
mirrored_spec
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
deferred.options
|
|
77
|
+
deferred.type
|
|
78
|
+
deferred.limit
|
|
79
|
+
deferred.null
|
|
80
|
+
|
|
81
|
+
expect(call_count).to eq(1)
|
|
82
|
+
end
|
|
27
83
|
end
|
|
28
84
|
end
|
|
@@ -12,11 +12,11 @@ RSpec.describe 'DeclareSchema.default_generated_primary_key_type integration wit
|
|
|
12
12
|
include_context 'prepare test app'
|
|
13
13
|
|
|
14
14
|
before do
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
# Cross-adapter DDL: `integer PRIMARY KEY NOT NULL` works on sqlite, mysql, and
|
|
16
|
+
# postgres. AUTOINCREMENT (sqlite) / AUTO_INCREMENT (mysql) / SERIAL (postgres)
|
|
17
|
+
# would all be valid, but the migrator inspects type/null, not auto-increment
|
|
18
|
+
# metadata, so we can skip the auto-increment dance entirely.
|
|
19
|
+
ActiveRecord::Base.connection.execute("CREATE TABLE foos (id integer PRIMARY KEY NOT NULL, name varchar(250))")
|
|
20
20
|
ActiveRecord::Base.connection.schema_cache.clear!
|
|
21
21
|
|
|
22
22
|
allow_any_instance_of(DeclareSchema::Support::ThorShell)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: declare_schema
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 4.0.1.colin.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Invoca Development adapted from hobo_fields by Tom Locke
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|