sunstone 8.0.0 → 8.1.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 +4 -4
- data/CHANGELOG.md +43 -0
- data/README.md +221 -0
- data/ext/active_record/associations/collection_association.rb +45 -63
- data/ext/active_record/associations/has_many_through_association.rb +21 -0
- data/ext/active_record/associations.rb +12 -10
- data/ext/active_record/attribute_methods.rb +4 -2
- data/ext/active_record/base.rb +16 -0
- data/ext/active_record/callbacks.rb +4 -2
- data/ext/active_record/locking/optimistic.rb +60 -0
- data/ext/active_record/persistence.rb +35 -12
- data/ext/active_record/relation/calculations.rb +26 -24
- data/ext/active_record/relation/finder_methods.rb +165 -0
- data/ext/active_record/relation/predicate_builder.rb +91 -0
- data/ext/active_record/relation/query_methods.rb +6 -1
- data/ext/active_record/relation.rb +5 -0
- data/ext/active_record/statement_cache.rb +11 -3
- data/ext/active_record/transactions.rb +29 -15
- data/ext/active_support/core_ext/object/to_query.rb +2 -0
- data/ext/arel/attributes/empty_relation.rb +2 -0
- data/ext/arel/nodes/eager_load.rb +2 -0
- data/ext/arel/nodes/select_statement.rb +2 -0
- data/ext/arel/select_manager.rb +2 -0
- data/lib/active_record/connection_adapters/sunstone/column.rb +4 -1
- data/lib/active_record/connection_adapters/sunstone/database_statements.rb +12 -1
- data/lib/active_record/connection_adapters/sunstone/quoting.rb +2 -0
- data/lib/active_record/connection_adapters/sunstone/schema_dumper.rb +2 -0
- data/lib/active_record/connection_adapters/sunstone/schema_statements.rb +6 -4
- data/lib/active_record/connection_adapters/sunstone/type/array.rb +2 -0
- data/lib/active_record/connection_adapters/sunstone/type/binary.rb +2 -0
- data/lib/active_record/connection_adapters/sunstone/type/date_time.rb +2 -0
- data/lib/active_record/connection_adapters/sunstone/type/ewkb.rb +2 -0
- data/lib/active_record/connection_adapters/sunstone/type/json.rb +2 -0
- data/lib/active_record/connection_adapters/sunstone/type/uuid.rb +2 -0
- data/lib/active_record/connection_adapters/sunstone/type_metadata.rb +2 -0
- data/lib/active_record/connection_adapters/sunstone_adapter.rb +2 -0
- data/lib/arel/collectors/sunstone.rb +15 -5
- data/lib/arel/visitors/sunstone.rb +2 -3
- data/lib/sunstone/connection.rb +2 -0
- data/lib/sunstone/exception.rb +2 -0
- data/lib/sunstone/gis.rb +2 -0
- data/lib/sunstone/version.rb +3 -1
- data/lib/sunstone.rb +9 -1
- metadata +23 -16
- data/ext/active_record/finder_methods.rb +0 -246
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e9d85052377699e6030067ce89f33b964b2193a1cd345a2ed013682fd457ecb3
|
|
4
|
+
data.tar.gz: 6f1f0054a2d338d2aaa6dd7ab9528a4e8be697a72ff670772afd89fe5d1928f7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fcb5ec1577d0671923144212ab4048276db625873b9e1cdf4a6f5e8f655de64ee0c60593167efb5e04c04fa37214763184ac5f6f8e881ec1b2b952244d1b0b58
|
|
7
|
+
data.tar.gz: 1441be879d54bbaa74f3f980813809f08b4c5c4497e35da5e40fe4f64d2a4157482b5c06413840f75ee4e8af087e4b0525b2fa8fd1adf2d1ca6608441adb38ae
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
## [8.1.0] - 2026-08-27
|
|
2
|
+
|
|
3
|
+
### Changed
|
|
4
|
+
- Support both Rails 8.0 and 8.1. The ActiveRecord overrides in `ext/` had been
|
|
5
|
+
synced to Rails 8.1 and used 8.1-only APIs; they now work against 8.0 as well.
|
|
6
|
+
- Require Ruby >= 3.3. The gemspec previously declared `>= 2.6`, which the
|
|
7
|
+
`activerecord >= 8.0.1` dependency never actually permitted.
|
|
8
|
+
- Require `arel-extensions >= 9.0.0` and `activerecord-filter >= 9.0.0`; the
|
|
9
|
+
previous `>= 7.x` floors predated Rails 8 support.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Declare the MIT license in the gemspec.
|
|
13
|
+
- Add gemspec `metadata` (source code and changelog URIs, `rubygems_mfa_required`).
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Package `README.md` in the published gem. The gemspec referenced a
|
|
17
|
+
non-existent `README.rdoc`, so the README was omitted from the built gem.
|
|
18
|
+
|
|
19
|
+
### Internal
|
|
20
|
+
- Modernized CI: test Rails 8.0 and 8.1 across Ruby 3.3–4.0, run the upstream
|
|
21
|
+
ActiveRecord suites on Postgres 18, SQLite, and a MySQL service container,
|
|
22
|
+
auto-resolve the latest Rails patch per series, and guard every upstream
|
|
23
|
+
source patch so the build fails loudly if an anchor moves.
|
|
24
|
+
|
|
25
|
+
## Earlier releases
|
|
26
|
+
|
|
27
|
+
Prior versions tracked the matching Rails release. See the Git tags for the
|
|
28
|
+
full history; notable releases:
|
|
29
|
+
|
|
30
|
+
- [8.0.0] - 2025-01-24
|
|
31
|
+
- [7.1.0.1] - 2023-12-20
|
|
32
|
+
- [7.1.0] - 2023-12-15
|
|
33
|
+
- [6.1.0.2] - 2021-03-23
|
|
34
|
+
- [6.1.0] - 2021-01-14
|
|
35
|
+
- [6.0.0] - 2019-06-10
|
|
36
|
+
|
|
37
|
+
[8.1.0]: https://github.com/malomalo/sunstone/compare/v8.0.0...v8.1.0
|
|
38
|
+
[8.0.0]: https://github.com/malomalo/sunstone/releases/tag/v8.0.0
|
|
39
|
+
[7.1.0.1]: https://github.com/malomalo/sunstone/releases/tag/v7.1.0.1
|
|
40
|
+
[7.1.0]: https://github.com/malomalo/sunstone/releases/tag/v7.1.0
|
|
41
|
+
[6.1.0.2]: https://github.com/malomalo/sunstone/releases/tag/v6.1.0.2
|
|
42
|
+
[6.1.0]: https://github.com/malomalo/sunstone/releases/tag/v6.1.0
|
|
43
|
+
[6.0.0]: https://github.com/malomalo/sunstone/releases/tag/v6.0.0
|
data/README.md
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# Sunstone
|
|
2
|
+
|
|
3
|
+
Sunstone is an [ActiveRecord](https://rubygems.org/gems/activerecord) adapter for
|
|
4
|
+
querying APIs that conform to [StandardAPI](https://github.com/waratuman/standardapi).
|
|
5
|
+
|
|
6
|
+
It lets you talk to a remote HTTP API using the ActiveRecord query interface you
|
|
7
|
+
already know — `where`, `order`, `limit`, `find`, associations, and persistence
|
|
8
|
+
(`create`/`update`/`destroy`) — while Sunstone translates those calls into
|
|
9
|
+
StandardAPI HTTP requests instead of SQL.
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- Ruby >= 3.3
|
|
14
|
+
- ActiveRecord >= 8.0.1 (Rails 8.0 and 8.1)
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Add `sunstone` to your Gemfile:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
gem 'sunstone'
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Configuration
|
|
25
|
+
|
|
26
|
+
### Rails
|
|
27
|
+
|
|
28
|
+
Configure the connection in `config/database.yml`:
|
|
29
|
+
|
|
30
|
+
```yaml
|
|
31
|
+
development:
|
|
32
|
+
adapter: sunstone
|
|
33
|
+
url: https://my-api-key@mystanda.rd/api
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The connection accepts the following options:
|
|
37
|
+
|
|
38
|
+
| Option | Description |
|
|
39
|
+
| ------------- | ------------------------------------------------------------------ |
|
|
40
|
+
| `url` | Sets `host`, `port`, `use_ssl`, and (from userinfo) `api_key` |
|
|
41
|
+
| `host` | API host (defaults to `127.0.0.1`) |
|
|
42
|
+
| `port` | API port (defaults to `80`, or `443` when `use_ssl`) |
|
|
43
|
+
| `use_ssl` | Connect over HTTPS (inferred from an `https://` url) |
|
|
44
|
+
| `api_key` | Token sent in the `Api-Key` request header |
|
|
45
|
+
| `user_agent` | String prepended to Sunstone's own `User-Agent` |
|
|
46
|
+
| `ca_cert` | Path to a CA certificate file (used when `use_ssl` is set) |
|
|
47
|
+
|
|
48
|
+
`url` is a convenience that fills in the others, so this:
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
url: https://my-api-key@mystanda.rd:443/api
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
is equivalent to:
|
|
55
|
+
|
|
56
|
+
```yaml
|
|
57
|
+
host: mystanda.rd
|
|
58
|
+
port: 443
|
|
59
|
+
use_ssl: true
|
|
60
|
+
api_key: my-api-key
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Standalone ActiveRecord
|
|
64
|
+
|
|
65
|
+
Establish the connection on `ActiveRecord::Base` (or your abstract model, e.g.
|
|
66
|
+
`ApplicationRecord`):
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
ActiveRecord::Base.establish_connection(
|
|
70
|
+
adapter: 'sunstone',
|
|
71
|
+
url: 'https://my-api-key@mystanda.rd/api'
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Usage
|
|
76
|
+
|
|
77
|
+
The models below are used throughout the examples:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
class Fleet < ApplicationRecord
|
|
81
|
+
has_many :ships
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
class Ship < ApplicationRecord
|
|
85
|
+
belongs_to :fleet
|
|
86
|
+
has_and_belongs_to_many :sailors
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Querying
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
Ship.find(42) # where: {id: 42}, limit: 1
|
|
94
|
+
Ship.all # GET /ships
|
|
95
|
+
Ship.where(id: 10) # where: {id: 10}
|
|
96
|
+
Ship.where(fleet_id: nil) # where: {fleet_id: nil}
|
|
97
|
+
Ship.where(id: [10, 12]) # id IN (10, 12)
|
|
98
|
+
Ship.order(:id) # order: [{id: :asc}]
|
|
99
|
+
Ship.limit(5000) # limit: 5000
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`find_each` pages through the collection automatically:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
Ship.find_each { |ship| ... } # GET /ships in batches with limit/offset
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Arel predicates are supported for comparisons and boolean logic:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
Ship.where(Ship.arel_table[:id].gt(10))
|
|
112
|
+
Ship.where(Ship.arel_table[:id].eq(10).or(Ship.arel_table[:name].eq('name')))
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
You can inspect the query a relation will send:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
Ship.where(id: 10).to_sql
|
|
119
|
+
# => "SELECT ships.* FROM ships WHERE ships.id = 10"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Querying across associations
|
|
123
|
+
|
|
124
|
+
Pass a nested hash to filter on an associated resource:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
Ship.where(fleet: { id: 1 }) # belongs_to
|
|
128
|
+
Fleet.where(ships: { id: 1 }) # has_many
|
|
129
|
+
Ship.where(sailors: { id: 1 }) # has_and_belongs_to_many
|
|
130
|
+
Fleet.where(ships: { sailors: { id: 1 } }) # nested
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Aggregations
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
Ship.count # select: [{count: "*"}] (GET /ships/calculate)
|
|
137
|
+
Ship.count(:id) # select: [{count: "id"}]
|
|
138
|
+
Ship.sum(:weight) # select: [{sum: "weight"}]
|
|
139
|
+
Ship.distinct # distinct: true
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Eager loading
|
|
143
|
+
|
|
144
|
+
`eager_load` loads associations in a single request; `preload` issues a
|
|
145
|
+
separate request per association:
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
Fleet.eager_load(ships: :sailors) # one request, nested payload
|
|
149
|
+
Fleet.preload(ships: :sailors) # a request per association
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Persistence
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
fleet = Fleet.create(name: 'Armada Uno') # POST /fleets
|
|
156
|
+
ship = Ship.find(1)
|
|
157
|
+
ship.update(name: 'Definant') # PATCH /ships/1
|
|
158
|
+
ship.destroy # DELETE /ships/1
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Validation errors returned by the API (e.g. a `400` response) are written back
|
|
162
|
+
onto the record:
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
fleet = Fleet.create(name: nil)
|
|
166
|
+
fleet.errors[:name] # => [...] populated from the API response
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Associations can be written through the parent, matching ActiveRecord's nested
|
|
170
|
+
attributes and `*_ids` conventions:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
# belongs_to — creates the fleet alongside the ship
|
|
174
|
+
Ship.create(name: 'Definant', fleet: Fleet.new(name: 'Armada Duo'))
|
|
175
|
+
|
|
176
|
+
# has_many — assign by id, by records, or clear
|
|
177
|
+
Fleet.create(name: 'Spanish Armada', ship_ids: [2])
|
|
178
|
+
fleet.update(ships: fleet.ships + [Ship.find(3)])
|
|
179
|
+
fleet.update(ships: []) # clears the relation
|
|
180
|
+
|
|
181
|
+
# has_and_belongs_to_many
|
|
182
|
+
ship.update(sailors: [Sailor.find(1)])
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### RPC
|
|
186
|
+
|
|
187
|
+
`rpc` declares a custom, non-CRUD action that maps to a `POST` on a member
|
|
188
|
+
endpoint. Any attributes the API returns are written back onto the record:
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
class Ship < ApplicationRecord
|
|
192
|
+
rpc :self_destruct
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
ship.self_destruct! # POST /ships/:id/self_destruct
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Column types
|
|
199
|
+
|
|
200
|
+
Beyond the standard ActiveRecord types, the adapter registers:
|
|
201
|
+
|
|
202
|
+
- `array` — JSON-serialized arrays of any subtype
|
|
203
|
+
- `json`
|
|
204
|
+
- `uuid` — validated and cast as a UUID
|
|
205
|
+
- `binary` — Base64-encoded over the wire
|
|
206
|
+
- `datetime` — serialized as ISO 8601
|
|
207
|
+
|
|
208
|
+
Geospatial (GIS) support is opt-in. Require it to register the `geometry` /
|
|
209
|
+
`geography` (EWKB) types, which use [RGeo](https://rubygems.org/gems/rgeo):
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
require 'sunstone/gis'
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
Sunstone is released under the [MIT License](LICENSE).
|
|
218
|
+
|
|
219
|
+
## Changelog
|
|
220
|
+
|
|
221
|
+
See [CHANGELOG.md](CHANGELOG.md).
|
|
@@ -1,73 +1,55 @@
|
|
|
1
|
-
#
|
|
2
|
-
# ref: 9269f634d471ad6ca46752421eabd3e1c26220b5
|
|
3
|
-
module ActiveRecord
|
|
4
|
-
module Associations
|
|
5
|
-
class CollectionAssociation
|
|
6
|
-
|
|
7
|
-
def replace(other_array)
|
|
8
|
-
other_array.each { |val| raise_on_type_mismatch!(val) }
|
|
9
|
-
original_target = skip_strict_loading { load_target }.dup
|
|
10
|
-
|
|
11
|
-
if owner.new_record?
|
|
12
|
-
replace_records(other_array, original_target)
|
|
13
|
-
elsif owner.class.connection.is_a?(ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter) && owner.instance_variable_defined?(:@sunstone_updating) && owner.instance_variable_get(:@sunstone_updating)
|
|
14
|
-
replace_common_records_in_memory(other_array, original_target)
|
|
15
|
-
|
|
16
|
-
# Remove from target
|
|
17
|
-
records_for_removal = (original_target - other_array)
|
|
18
|
-
if !records_for_removal.empty?
|
|
19
|
-
self.instance_variable_set(:@sunstone_changed, true)
|
|
20
|
-
records_for_removal.each { |record| callback(:before_remove, record) }
|
|
21
|
-
records_for_removal.each { |record| target.delete(record) }
|
|
22
|
-
records_for_removal.each { |record| callback(:after_remove, record) }
|
|
23
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
24
2
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
3
|
+
# The last ref that this code was synced with Rails
|
|
4
|
+
# ref: 90a1eaa1b3
|
|
5
|
+
class ActiveRecord::Associations::CollectionAssociation
|
|
6
|
+
|
|
7
|
+
def replace(other_array)
|
|
8
|
+
other_array.each { |val| raise_on_type_mismatch!(val) }
|
|
9
|
+
original_target = skip_strict_loading { load_target }.dup
|
|
10
|
+
|
|
11
|
+
if owner.new_record?
|
|
12
|
+
replace_records(other_array, original_target)
|
|
13
|
+
elsif owner.sunstone? && owner.instance_variable_defined?(:@sunstone_updating) && owner.instance_variable_get(:@sunstone_updating)
|
|
14
|
+
replace_common_records_in_memory(other_array, original_target)
|
|
15
|
+
|
|
16
|
+
# Remove from target
|
|
17
|
+
records_for_removal = (original_target - other_array)
|
|
18
|
+
if !records_for_removal.empty?
|
|
19
|
+
self.instance_variable_set(:@sunstone_changed, true)
|
|
20
|
+
records_for_removal.each { |record| callback(:before_remove, record) }
|
|
21
|
+
records_for_removal.each { |record| target.delete(record) }
|
|
22
|
+
records_for_removal.each { |record| callback(:after_remove, record) }
|
|
43
23
|
end
|
|
44
24
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
record.save(validate: validate, &block)
|
|
25
|
+
# Add to target
|
|
26
|
+
records_for_addition = (other_array - original_target)
|
|
27
|
+
if !records_for_addition.empty?
|
|
28
|
+
self.instance_variable_set(:@sunstone_changed, true)
|
|
29
|
+
(other_array - original_target).each do |record|
|
|
30
|
+
add_to_target(record)
|
|
52
31
|
end
|
|
53
32
|
end
|
|
54
33
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
association = build_through_record(record)
|
|
63
|
-
if association.changed?
|
|
64
|
-
association.save!
|
|
65
|
-
end
|
|
66
|
-
ensure
|
|
67
|
-
@through_records.delete(record.object_id)
|
|
34
|
+
other_array
|
|
35
|
+
else
|
|
36
|
+
replace_common_records_in_memory(other_array, original_target)
|
|
37
|
+
if other_array != original_target
|
|
38
|
+
transaction { replace_records(other_array, original_target) }
|
|
39
|
+
else
|
|
40
|
+
other_array
|
|
68
41
|
end
|
|
69
|
-
|
|
70
42
|
end
|
|
43
|
+
end
|
|
71
44
|
|
|
45
|
+
def insert_record(record, validate = true, raise = false, &block)
|
|
46
|
+
if record.sunstone? && owner.instance_variable_defined?(:@sunstone_updating) && owner.instance_variable_get(:@sunstone_updating)
|
|
47
|
+
true
|
|
48
|
+
elsif raise
|
|
49
|
+
record.save!(validate: validate, &block)
|
|
50
|
+
else
|
|
51
|
+
record.save(validate: validate, &block)
|
|
52
|
+
end
|
|
72
53
|
end
|
|
73
|
-
|
|
54
|
+
|
|
55
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The last ref that this code was synced with Rails
|
|
4
|
+
# ref: 90a1eaa1b3
|
|
5
|
+
|
|
6
|
+
class ActiveRecord::Associations::CollectionAssociation::HasManyThroughAssociation
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def save_through_record(record)
|
|
11
|
+
return if record.sunstone?
|
|
12
|
+
|
|
13
|
+
association = build_through_record(record)
|
|
14
|
+
if association.changed?
|
|
15
|
+
association.save!
|
|
16
|
+
end
|
|
17
|
+
ensure
|
|
18
|
+
@through_records.delete(record)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# The last ref that this code was synced with Rails
|
|
2
|
-
# ref:
|
|
4
|
+
# ref: 90a1eaa1b3
|
|
3
5
|
|
|
4
6
|
require 'active_record'
|
|
5
7
|
require 'active_record/associations'
|
|
@@ -10,23 +12,23 @@ module ActiveRecord
|
|
|
10
12
|
def has_and_belongs_to_many(name, scope = nil, **options, &extension)
|
|
11
13
|
habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self)
|
|
12
14
|
|
|
13
|
-
builder = Builder::HasAndBelongsToMany.new
|
|
15
|
+
builder = Builder::HasAndBelongsToMany.new(name, self, options)
|
|
14
16
|
|
|
15
17
|
join_model = builder.through_model
|
|
16
18
|
|
|
17
|
-
const_set
|
|
18
|
-
private_constant
|
|
19
|
+
const_set(join_model.name, join_model)
|
|
20
|
+
private_constant(join_model.name)
|
|
19
21
|
|
|
20
|
-
middle_reflection = builder.middle_reflection
|
|
22
|
+
middle_reflection = builder.middle_reflection(join_model)
|
|
21
23
|
|
|
22
|
-
Builder::HasMany.define_callbacks
|
|
23
|
-
Reflection.add_reflection
|
|
24
|
+
Builder::HasMany.define_callbacks(self, middle_reflection)
|
|
25
|
+
Reflection.add_reflection(self, middle_reflection.name, middle_reflection)
|
|
24
26
|
middle_reflection.parent_reflection = habtm_reflection
|
|
25
27
|
|
|
26
28
|
include Module.new {
|
|
27
29
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
28
30
|
def destroy_associations
|
|
29
|
-
if !self.
|
|
31
|
+
if !self.sunstone?
|
|
30
32
|
association(:#{middle_reflection.name}).delete_all(:delete_all)
|
|
31
33
|
association(:#{name}).reset
|
|
32
34
|
end
|
|
@@ -39,8 +41,8 @@ module ActiveRecord
|
|
|
39
41
|
hm_options[:through] = middle_reflection.name
|
|
40
42
|
hm_options[:source] = join_model.right_reflection.name
|
|
41
43
|
|
|
42
|
-
[:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name, :extend, :strict_loading].each do |k|
|
|
43
|
-
hm_options[k] = options[k] if options.key?
|
|
44
|
+
[:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name, :extend, :strict_loading, :deprecated].each do |k|
|
|
45
|
+
hm_options[k] = options[k] if options.key?(k)
|
|
44
46
|
end
|
|
45
47
|
|
|
46
48
|
has_many name, scope, **hm_options, &extension
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# The last ref that this code was synced with Rails
|
|
2
|
-
# ref:
|
|
4
|
+
# ref: 90a1eaa1b3
|
|
3
5
|
|
|
4
6
|
module ActiveRecord
|
|
5
7
|
module AttributeMethods
|
|
@@ -11,7 +13,7 @@ module ActiveRecord
|
|
|
11
13
|
def attributes_with_values(attribute_names)
|
|
12
14
|
attrs = attribute_names.index_with { |name| @attributes[name] }
|
|
13
15
|
|
|
14
|
-
if self.
|
|
16
|
+
if self.sunstone?
|
|
15
17
|
self.class.reflect_on_all_associations.each do |reflection|
|
|
16
18
|
if reflection.belongs_to?
|
|
17
19
|
if association(reflection.name).loaded? && association(reflection.name).target == Thread.current[:sunstone_updating_model]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The last ref that this code was synced with Rails
|
|
4
|
+
# ref: 90a1eaa1b3
|
|
5
|
+
|
|
6
|
+
class ActiveRecord::Base
|
|
7
|
+
|
|
8
|
+
def self.sunstone?
|
|
9
|
+
connection_pool.db_config.adapter_class == ActiveRecord::ConnectionAdapters::SunstoneAPIAdapter
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def sunstone?
|
|
13
|
+
self.class.sunstone?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
end
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# The last ref that this code was synced with Rails
|
|
2
|
-
# ref:
|
|
4
|
+
# ref: 90a1eaa1b3
|
|
3
5
|
|
|
4
6
|
module ActiveRecord
|
|
5
7
|
module Callbacks
|
|
6
8
|
private
|
|
7
9
|
|
|
8
10
|
def create_or_update(**) #:nodoc:
|
|
9
|
-
if self.
|
|
11
|
+
if self.sunstone?
|
|
10
12
|
@_already_called ||= {}
|
|
11
13
|
self.class.reflect_on_all_associations.each do |r|
|
|
12
14
|
@_already_called[:"autosave_associated_records_for_#{r.name}"] = true
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The last ref that this code was synced with Rails
|
|
4
|
+
# ref: 90a1eaa1b3
|
|
5
|
+
|
|
6
|
+
module ActiveRecord
|
|
7
|
+
module Locking
|
|
8
|
+
module Optimistic
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def _update_row(attribute_values, attempted_action = "update")
|
|
13
|
+
return super unless locking_enabled?
|
|
14
|
+
|
|
15
|
+
begin
|
|
16
|
+
locking_column = self.class.locking_column
|
|
17
|
+
lock_attribute_was = @attributes[locking_column]
|
|
18
|
+
|
|
19
|
+
update_constraints = _query_constraints_hash
|
|
20
|
+
|
|
21
|
+
if self[locking_column].nil?
|
|
22
|
+
raise(<<-MSG.squish)
|
|
23
|
+
For optimistic locking, locking_column ('#{locking_column}') can't be nil.
|
|
24
|
+
Are you missing a default value or validation on '#{locking_column}'?
|
|
25
|
+
MSG
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
self[locking_column] += 1
|
|
29
|
+
|
|
30
|
+
attribute_values = if attribute_values.is_a?(Hash)
|
|
31
|
+
attribute_values.merge(attributes_with_values([locking_column]))
|
|
32
|
+
else
|
|
33
|
+
attribute_values = attribute_values.dup if attribute_values.frozen?
|
|
34
|
+
attribute_values << locking_column
|
|
35
|
+
attributes_with_values(attribute_values)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Suntone returns the row(s) not a int of afftecd_rows
|
|
39
|
+
result = self.class._update_record(
|
|
40
|
+
attribute_values,
|
|
41
|
+
update_constraints
|
|
42
|
+
)
|
|
43
|
+
affected_rows = sunstone? ? result.rows.size : result
|
|
44
|
+
|
|
45
|
+
if affected_rows != 1
|
|
46
|
+
raise ActiveRecord::StaleObjectError.new(self, attempted_action)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
affected_rows
|
|
50
|
+
|
|
51
|
+
# If something went wrong, revert the locking_column value.
|
|
52
|
+
rescue Exception
|
|
53
|
+
@attributes[locking_column] = lock_attribute_was
|
|
54
|
+
raise
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|