rails_age 0.3.2 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dc953f22e786b1ce4280d8ced0bcb6ca768f89773c49a4ad0d7ddd3e3505e91e
4
- data.tar.gz: fff1d2809ce6f439196bfbc8a50b7990453d17def541829ef332ee2711a7fd3a
3
+ metadata.gz: 03e419984be6f29b1da74d0de6e31ccb218ba966e3818537884fcff715024e02
4
+ data.tar.gz: cb5a30c1e17ee7bf30394691232e3185e317ef5e827ece3ab02ef7d14cb0aef5
5
5
  SHA512:
6
- metadata.gz: e03cb391c29274953ab63badc3da19a9a965d53b23844086775d5668e61f32feba68605078b506c2a1ef8bde9d6627c67c087b7b1752acd2ccff681a55d4eec2
7
- data.tar.gz: 6373605ea72f6d85babde6ba167135f3098bb6b04c60a56015a04670e48f40e452a2473ce974de2e5577d2930d267aff72e253fc0a1fe10f740def2b28c0cf1b
6
+ metadata.gz: 555b80c544a8d5217d5c40ef3f24c15cd8fbf0c13bf96d1445845d9afc21d5e2ab835a60ac678ba5177fa5562ef1b87d7f293226783c94f1470d8b64e7a56fd9
7
+ data.tar.gz: 303f2776ebc5320711f50ec54e9a136ba7d9727fcb28c8d0038ffdcb0edb94d6715b93f40598a70d3eb3b8f1405a05610a707458f91209288414f0a4a7251374
data/CHANGELOG.md CHANGED
@@ -1,26 +1,58 @@
1
1
  # Change Log
2
2
 
3
- ## VERSION 0.4.0 - 2024-xx-xx
3
+ ## VERSION 0.5.1 - 2024-xx-xx
4
4
 
5
- - **cypher**
5
+ - **cypher queries** (like active record queries)
6
+ * schema override
6
7
  * query support
7
8
  * paths support
8
9
  * select attributes support
9
- - **Paths**
10
- * ?
11
10
 
12
- ## VERSION 0.3.4 - 2024-xx-xx
11
+ ## VERSION 0.5.0 - 2024-xx-xx
13
12
 
14
- - **Type Generators**
15
- * add `rails generate apache_age:type`
16
- * add `rails generate apache_age:node_type`
17
- * add `rails generate apache_age:edge_type`
13
+ - **Age Path**
18
14
 
19
- ## VERSION 0.3.3 - 2024-xx-xx
15
+ ## VERSION 0.4.3 - 2024-xx-xx
16
+
17
+ - **AGE Schema override**
18
+
19
+ - **multiple AGE Schema**
20
+
21
+ ## VERSION 0.4.3 - 2024-xx-xx
22
+
23
+ - **Edge Scaffold** (generates edge, type, view and controller)
24
+ * add `rails generate apache_age:edge_scaffold HasJob employee_role start_node:person end_node:company`
25
+
26
+ ## VERSION 0.4.2 - 2024-xx-xx
27
+
28
+ - **Node Scaffold** (generates node, type, view and controller)
29
+ * add `rails generate apache_age:node_scaffold Person first_name last_name age:integer`
30
+
31
+ ## VERSION 0.4.2 - 2024-xx-xx
20
32
 
21
33
  - **Edge Generator**
22
- * add `rails generate apache_age:edge` to create an edge model
34
+ * add `rails generate apache_age:edge HasPet owner_role` just a property
35
+ * add `rails generate apache_age:edge HasPet owner_role start_node:person end_node:pet`
36
+ with property and specified start-/end-nodes (person and pet nodes must have already been created)
37
+
38
+ ## VERSION 0.4.1 - 2024-06-15
39
+
40
+ - **Installer**
41
+ * add optional: safe migrations with `bin/rails db:migrate` instead of using `bin/rails apache_age:migrate`
42
+ (install using `bin/rails apache_age:override_db_migrate`)
43
+
44
+ ## VERSION 0.4.0 - 2024-06-14
23
45
 
46
+ Minor breaking change: type (:vertix) is now required in core for edges
47
+
48
+ - **Installer**
49
+ * AGE types added to installer (with tests)
50
+
51
+ - **Node Generator**
52
+ * add also creates node types (with tests)
53
+
54
+ - **Apache AGE Migrate**
55
+ * add `bin/rails apache_age:migrate` runs `bin/rails db:migrate` followed by `bin/rails apache_age:config_schema` to fix the schema file after `bin/rails db:migrate`
24
56
 
25
57
  ## VERSION 0.3.2 - 2024-06-08
26
58
 
@@ -33,6 +65,7 @@
33
65
 
34
66
  - **Installer**
35
67
  * refactor into multiple independent tasks with tests
68
+
36
69
  - **Documentation**
37
70
  * updated README with additional information
38
71
  * added `db/structure.sql` config to README
@@ -41,6 +74,7 @@
41
74
 
42
75
  - **Edges**
43
76
  * `find_by(start_node:, :end_node:, properties:)` to find an edge with specific nodes & properties (deprecated `find_edge`)
77
+
44
78
  - **Installer** (`rails generate apache_age:install`)
45
79
  * copy Age PG Extenstion migration to `db/migrate`
46
80
  * run the AGE PG Migration
@@ -56,6 +90,7 @@ NOTE: the `rails generate apache_age:install` can be run at any time to repair t
56
90
  * add missing methods to use in rails controllers
57
91
  * validate edge start- & end-nodes are valid
58
92
  * add unique edge validations
93
+
59
94
  - **Nodes**
60
95
  * add missing methods to use in rails controllers
61
96
  * add unique node validations
@@ -67,9 +102,11 @@ Initial release has the following features:
67
102
  - **Nodes:**
68
103
  * `.create`, `.read`, `.update`, `.delete`, `.all`, `.find(by id)`, `.find_by(age_properties)`
69
104
  * verified with usage in a controller and views
105
+
70
106
  - **Edges:**
71
107
  *`.create`, `.read`, `.update`, `.delete`, `.all`, `.find(by id)`, `.find_by(age_properties)`
72
108
  * verified with usage in a controller and views
109
+
73
110
  - **Entities:**
74
111
  * `.all`, `.find(id)`, `.find_by(age_property)` use these when class, label, edge, node
75
112
 
data/README.md CHANGED
@@ -12,371 +12,109 @@ Add this line to your application's Gemfile:
12
12
  gem "rails_age"
13
13
  ```
14
14
 
15
- ### Quick Install
15
+ ## Quick Start
16
16
 
17
17
  using the installer, creates the migration to install age, runs the migration, and adjusts the schema file, and updates the `config/database.yml` file.
18
18
 
19
+ setup (& Test) postgresql with AGE (using the docker version of AGE DB may be the easiest way to get started)
20
+ using the docker version of AGE DB, you can confirm psql AGE with the following commands:
19
21
  ```bash
20
- $ bundle
21
- $ bin/rails apache_age:install
22
- $ git add .
23
- $ git commit -m "Add Apache Age to Rails"
24
- $ rails generate apache_age:node Company company_name
25
- $ rails generate apache_age:node Person first_name last_name age:integer
22
+ psql -h localhost -p 5455 -U docker_username
23
+ > CREATE EXTENSION IF NOT EXISTS age;
24
+ > LOAD 'age';
25
+ > SET search_path = ag_catalog, "$user", public;
26
+ > SELECT create_graph('age_schema');
27
+ > \q
26
28
  ```
27
29
 
28
- NOTE: it is important to commit the `db/schema.rb` to git because `rails db:migrate` inappropriately modifies the schema file (I haven't yet tested `db/structure.sql`).
29
-
30
- **You can run `bin/rails apache_age:install` at any time to repair the schema file as needed.**
31
-
32
- For now, if you are using `db/structure.sql` you will need to manually configure Apache Age (RailsAge) as described below.
33
-
34
- ### Manual Install
35
-
36
- create a migration to add the Apache Age extension to your database
30
+ create a new Rails app (WITH POSTGRESQL!)
37
31
  ```bash
38
- $ bin/rails g migration AddApacheAge
32
+ rails new age_demo -d postgresql
33
+ cd age_demo
34
+ git add .
35
+ git commit -m "Initial Rails App"
39
36
  ```
40
- copy the contents of https://github.com/marpori/rails_age/blob/main/db/migrate/20240521062349_add_apache_age.rb
41
- ```ruby
42
- class AddApacheAge < ActiveRecord::Migration[7.1]
43
- def up
44
- # Allow age extension
45
- execute('CREATE EXTENSION IF NOT EXISTS age;')
46
-
47
- # Load the age code
48
- execute("LOAD 'age';")
49
-
50
- # Load the ag_catalog into the search path
51
- execute('SET search_path = ag_catalog, "$user", public;')
52
-
53
- # Create age_schema graph if it doesn't exist
54
- execute("SELECT create_graph('age_schema');")
55
- end
56
-
57
- def down
58
- execute <<-SQL
59
- DO $$
60
- BEGIN
61
- IF EXISTS (
62
- SELECT 1
63
- FROM pg_constraint
64
- WHERE conname = 'fk_graph_oid'
65
- ) THEN
66
- ALTER TABLE ag_catalog.ag_label
67
- DROP CONSTRAINT fk_graph_oid;
68
- END IF;
69
- END $$;
70
- SQL
71
-
72
- execute("SELECT drop_graph('age_schema', true);")
73
- execute('DROP SCHEMA IF EXISTS ag_catalog CASCADE;')
74
- execute('DROP EXTENSION IF EXISTS age;')
75
- end
76
- end
37
+ configure `config/database.yml` when using the docker version of AGE DB my config looks like:
38
+ ```yaml
39
+ port: 5455
40
+ host: localhost
41
+ username: docker_username
42
+ password: dockerized_password
77
43
  ```
78
- into your new migration file
79
44
 
80
- then run the migration
45
+ now you should be able to create the rails database:
81
46
  ```bash
82
- $ bin/rails db:migrate
83
- ```
84
-
85
- Rails migrate will mangle the schema `db/schema.rb` file. You need to remove the lines that look like:
86
- ```ruby
87
- ActiveRecord::Schema[7.1].define(version: 2024_05_21_062349) do
88
- create_schema "ag_catalog"
89
- create_schema "age_schema"
90
-
91
- # These are extensions that must be enabled in order to support this database
92
- enable_extension "age"
93
- enable_extension "plpgsql"
94
-
95
- # Could not dump table "_ag_label_edge" because of following StandardError
96
- # Unknown type 'graphid' for column 'id'
97
-
98
- # Could not dump table "_ag_label_vertex" because of following StandardError
99
- # Unknown type 'graphid' for column 'id'
100
-
101
- # Could not dump table "ag_graph" because of following StandardError
102
- # Unknown type 'regnamespace' for column 'namespace'
103
-
104
- # Could not dump table "ag_label" because of following StandardError
105
- # Unknown type 'regclass' for column 'relation'
106
-
107
- add_foreign_key "ag_label", "ag_graph", column: "graph", primary_key: "graphid", name: "fk_graph_oid"
108
-
109
- # other migrations
110
- # ...
111
- end
112
- ```
113
-
114
- and replace them with the following lines:
115
- ```ruby
116
- ActiveRecord::Schema[7.1].define(version: 2024_05_21_062349) do
117
- # These are extensions that must be enabled in order to support this database
118
- enable_extension 'plpgsql'
119
-
120
- # Allow age extension
121
- execute('CREATE EXTENSION IF NOT EXISTS age;')
122
-
123
- # Load the age code
124
- execute("LOAD 'age';")
125
-
126
- # Load the ag_catalog into the search path
127
- execute('SET search_path = ag_catalog, "$user", public;')
128
-
129
- # Create age_schema graph if it doesn't exist
130
- execute("SELECT create_graph('age_schema');")
131
-
132
- # other migrations
133
- # ...
134
- end
47
+ rails db:create
48
+ rails db:migrate
49
+ git add .
50
+ git commit -m "Add Apache Age Postgres DB configured with Rails App"
135
51
  ```
136
52
 
137
- NOTE: if using `db/structure.sql` use:
138
- ```sql
139
- -- These are extensions that must be enabled in order to support this database
140
- CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA public;
141
-
142
- -- Allow age extension (if not already enabled), this builds the age_catalog schema
143
- CREATE EXTENSION IF NOT EXISTS age;
144
-
145
- -- Load the age module
146
- LOAD 'age';
147
-
148
- -- Load the ag_catalog into the search path
149
- SET search_path = ag_catalog, "$user", public;
150
-
151
- -- Create age_schema graph if it doesn't exist
152
- SELECT create_graph('age_schema');
153
-
154
- # other migrations
155
- # ...
156
-
157
- INSERT INTO "schema_migrations" (version) VALUES
158
- ('20110315075839'),
159
- --- ...
160
- ('20240521062349');
53
+ install Apache Age (you can ignore the `unknown OID` warnings)
54
+ ```bash
55
+ bundle add rails_age
56
+ bundle install
57
+ bin/rails apache_age:install
58
+ git add .
59
+ git commit -m "Add Apache Age to Rails"
161
60
  ```
162
61
 
163
- ## Contributing
164
-
165
- Create an merge request (with tests) and I will review it/merge it when ready.
166
-
167
- ## License
168
-
169
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
170
-
171
- ## Usage
172
-
173
- I suggest you creat a folder create a folder called `app/nodes` and `app/edges` to keep the code organized.
174
- I frequently use the `app/graphs` folder to keep all the graph related code together in a Module (as is done in the [rails age demo app](https://github.com/marpori/rails_age_demo_app))
175
-
176
- I suggest you creat a folder within app called `graphs` and under that create a folder called `nodes` and `edges`. This will help you keep your code organized.
177
-
178
- A trival, but fully functional [rails age demo app](https://github.com/marpori/rails_age_demo_app), based on the Flintstones Commic, is available for reference.
179
-
180
- ### Nodes
181
-
182
- ```ruby
183
- # app/graphs/nodes/company.rb
184
- module Nodes
185
- class Company
186
- include ApacheAge::Entities::Vertex
187
-
188
- attribute :company_name, :string
189
-
190
- validates :company_name, presence: true
191
- validates_with(
192
- ApacheAge::Validators::UniqueVertexValidator,
193
- attributes: [:company_name]
194
- )
195
- end
196
- end
62
+ make some nodes :string is the default type
63
+ ```bash
64
+ rails generate apache_age:node Company company_name
65
+ rails generate apache_age:node Person first_name last_name
66
+ rails generate apache_age:node Pet pet_name:string age:integer
197
67
  ```
68
+ make some edges (`:vertex` is the default type) for start_node and end_node
69
+ ```bash
70
+ # when start node and end node are not specified they are of type `:vertex`
71
+ # this is generally not recommended - exept when very generic relationships are needed
72
+ rails generate apache_age:edge HasJob employee_role
198
73
 
199
- ```ruby
200
- # app/graphs/nodes/person.rb
201
- module Nodes
202
- class Person
203
- include ApacheAge::Entities::Vertex
204
-
205
- attribute :first_name, :string, default: nil
206
- attribute :last_name, :string, default: nil
207
- attribute :given_name, :string, default: nil
208
- attribute :nick_name, :string, default: nil
209
-
210
- validates :first_name, :last_name, :given_name, :nick_name,
211
- presence: true
212
-
213
- def initialize(**attributes)
214
- super
215
- # use unless present? since attributes when empty sets to "" by default
216
- self.nick_name = first_name unless nick_name.present?
217
- self.given_name = last_name unless given_name.present?
218
- end
219
- end
220
- end
74
+ # this is recommended - use explicit start_node and end_node types
75
+ rails generate apache_age:node HasPet start_node:person end_node:pet caretaker_role
221
76
  ```
222
77
 
223
- ### Edges
78
+ **NOTE:** the default `rails db:migrate` inappropriately modifies the schema file. This installer patches the migration to prevent this (however this might break on rails updates, etc). **You can run `bin/rails apache_age:install` at any time to repair the schema file as needed.**
224
79
 
225
- ```ruby
226
- # app/graphs/edges/has_job.rb
227
- module Edges
228
- class HasJob
229
- include ApacheAge::Entities::Edge
230
-
231
- attribute :employee_role, :string
232
- attribute :start_node, :person
233
- attribute :end_node, :company
234
-
235
- validates :employee_role, presence: true
236
- validate :validate_unique
237
- # or with a one-liner
238
- # validates_with(
239
- # ApacheAge::Validators::UniqueEdgeValidator,
240
- # attributes: %i[employee_role start_node end_node]
241
- # )
242
-
243
- private
244
-
245
- def validate_unique
246
- ApacheAge::Validators::UniqueEdgeValidator
247
- .new(attributes: %i[employee_role start_node end_node])
248
- .validate(self)
249
- end
250
- end
251
- end
252
- ```
80
+ For now, if you are using `db/structure.sql` you will need to manually configure Apache Age (RailsAge) as described below.
253
81
 
254
82
  ### Rails Console Usage
255
83
 
256
84
  ```ruby
257
- fred = Nodes::Person.create(first_name: 'Fredrick Jay', nick_name: 'Fred', last_name: 'Flintstone', gender: 'male')
258
- fred.to_h
259
-
260
- quarry = Nodes::Company.create(company_name: 'Bedrock Quarry')
261
- quarry.to_h
262
-
263
- job = Edges::HasJob.create(start_node: fred, end_node: quarry, employee_role: 'Crane Operator')
264
- job.to_h
265
- ```
266
-
267
- ### Update Routes
268
-
269
- ```ruby
270
- Rails.application.routes.draw do
271
- # mount is not needed with the engine
272
- # mount RailsAge::Engine => "/rails_age"
273
-
274
- # defines the route for the people controller
275
- resources :people
276
-
277
- # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
278
- # Can be used by load balancers and uptime monitors to verify that the app is live.
279
- get 'up' => 'rails/health#show', as: :rails_health_check
280
-
281
- # Defines the root path route ("/")
282
- root 'people#index'
283
- end
284
- ```
285
-
286
- ### Types (Optional)
287
-
288
- ```ruby
289
- # spec/dummy/config/initializers/types.rb
290
- require 'apache_age/types/age_type_generator'
291
-
292
- Rails.application.config.to_prepare do
293
- # Ensure the files are loaded
294
- require_dependency 'nodes/company'
295
- require_dependency 'nodes/person'
296
-
297
- # Register the custom types
298
- ActiveModel::Type.register(:company, ApacheAge::Types::AgeTypeGenerator.create_type_for(Nodes::Company))
299
- ActiveModel::Type.register(:person, ApacheAge::Types::AgeTypeGenerator.create_type_for(Nodes::Person))
300
- end
301
- ```
302
-
303
- ### Controller Usage
304
-
305
- ```ruby
306
- # app/controllers/people_controller.rb
307
- class PeopleController < ApplicationController
308
- before_action :set_person, only: %i[show edit update destroy]
309
-
310
- # GET /people or /people.json
311
- def index
312
- @people = Nodes::Person.all
313
- end
314
-
315
- # GET /people/1 or /people/1.json
316
- def show; end
317
-
318
- # GET /people/new
319
- def new
320
- @person = Nodes::Person.new
321
- end
322
-
323
- # GET /people/1/edit
324
- def edit; end
325
-
326
- # POST /people or /people.json
327
- def create
328
- @person = Nodes::Person.new(**person_params)
329
- respond_to do |format|
330
- if @person.save
331
- format.html { redirect_to person_url(@person), notice: 'Person was successfully created.' }
332
- format.json { render :show, status: :created, location: @person }
333
- else
334
- format.html { render :new, status: :unprocessable_entity }
335
- format.json { render json: @person.errors, status: :unprocessable_entity }
336
- end
337
- end
338
- end
339
-
340
- # PATCH/PUT /people/1 or /people/1.json
341
- def update
342
- respond_to do |format|
343
- if @person.update(**person_params)
344
- format.html { redirect_to person_url(@person), notice: 'Person was successfully updated.' }
345
- format.json { render :show, status: :ok, location: @person }
346
- else
347
- format.html { render :edit, status: :unprocessable_entity }
348
- format.json { render json: @person.errors, status: :unprocessable_entity }
349
- end
350
- end
351
- end
352
-
353
- # DELETE /people/1 or /people/1.json
354
- def destroy
355
- @person.destroy!
356
-
357
- respond_to do |format|
358
- format.html { redirect_to people_url, notice: 'Person was successfully destroyed.' }
359
- format.json { head :no_content }
360
- end
361
- end
362
-
363
- private
364
-
365
- # Use callbacks to share common setup or constraints between actions.
366
- def set_person
367
- @person = Nodes::Person.find(params[:id])
368
- end
369
-
370
- # Only allow a list of trusted parameters through.
371
- def person_params
372
- # params.fetch(:person, {})
373
- params.require(:nodes_person).permit(:first_name, :last_name, :nick_name, :given_name, :gender)
374
- end
375
- end
376
- ```
377
-
378
- ### Views
379
-
380
- ```erb
381
-
382
- ```
85
+ bin/rails c
86
+
87
+ fred = Person.new(first_name: 'Fredrick Jay', last_name: 'Flintstone')
88
+ fred.valid?
89
+ fred.save
90
+ fred.to_h # should have an ID
91
+
92
+ # fails because of a missing required field (Property)
93
+ incomplete = Person.new(first_name: 'Fredrick Jay')
94
+ incomplete.valid?
95
+ incomplete.errors
96
+ incomplete.to_h
97
+
98
+ # fails because of uniqueness constraints
99
+ jay = Person.create(first_name: 'Fredrick Jay', last_name: 'Flintstone')
100
+ jay.to_h
101
+ => {:id=>nil, :first_name=>"Fredrick Jay", :last_name=>"Flintstone"}
102
+ jay.valid?
103
+ => false
104
+ jay.errors
105
+ => #<ActiveModel::Errors [#<ActiveModel::Error attribute=base, type=record not unique, options={}>, #<ActiveModel::Error attribute=first_name, type=property combination not unique, options={}>, #<ActiveModel::Error attribute=last_name, type=property combination not unique, options={}>]>
106
+ irb(main):008> jav.to_h
107
+ => {:id=>nil, :first_name=>"Fredrick Jay", :last_name=>"Flintstone"}
108
+
109
+ # .create is a shortcut for .new and .save
110
+ quarry = Company.create(company_name: 'Bedrock Quarry')
111
+ quarry.to_h # should have an ID
112
+
113
+ # create an edge (no generator yet)
114
+ job = HasJob.create(start_node: fred, end_node: quarry, employee_role: 'Crane Operator')
115
+ job.to_h # should have an ID
116
+ ```
117
+
118
+ ## Manual Install, Config and Usage
119
+
120
+ see [Manuel Installation, Configuration and Usage](MANUAL_INSTALL.md)
@@ -11,9 +11,9 @@ module ApacheAge
11
11
  attribute :id, :integer
12
12
  attribute :end_id, :integer
13
13
  attribute :start_id, :integer
14
- # allow user to optionally specify the class type (or not) thus not adding: `:vertex`
15
- attribute :end_node
16
- attribute :start_node
14
+ # type: `:vertex` can be overriden with a specific node type
15
+ attribute :end_node, :vertex
16
+ attribute :start_node, :vertex
17
17
 
18
18
  validates :end_node, :start_node, presence: true
19
19
  validate :validate_nodes
@@ -35,10 +35,10 @@ module ApacheAge
35
35
  query = record.class.find_by(edge_attribs.compact)
36
36
  return if query.blank? || (query.id == record.id)
37
37
 
38
- record.errors.add(:base, 'attribute combination not unique')
39
- record.errors.add(:end_node, 'attribute combination not unique')
40
- record.errors.add(:start_node, 'attribute combination not unique')
41
- attributes.each { record.errors.add(_1, 'attribute combination not unique') }
38
+ record.errors.add(:base, 'record not unique')
39
+ record.errors.add(:end_node, 'node combination not unique')
40
+ record.errors.add(:start_node, 'node combination not unique')
41
+ attributes.each { record.errors.add(_1, 'prpoerty combination not unique') }
42
42
  end
43
43
 
44
44
  private
@@ -20,8 +20,8 @@ module ApacheAge
20
20
  # if no match is found or if it finds itself, it's valid
21
21
  return if query.blank? || (query.id == record.id)
22
22
 
23
- record.errors.add(:base, 'attribute combination not unique')
24
- attributes.each { record.errors.add(_1, 'attribute combination not unique') }
23
+ record.errors.add(:base, 'record not unique')
24
+ attributes.each { record.errors.add(_1, 'property combination not unique') }
25
25
  end
26
26
  end
27
27
  end
@@ -14,11 +14,13 @@ module ApacheAge
14
14
 
15
15
  def create_node_file
16
16
  template "node.rb.tt", File.join(destination_root, "app/nodes", class_path, "#{file_name}.rb")
17
+ add_type_config
17
18
  end
18
19
 
19
20
  def destroy_node_file
20
21
  file_path = File.join(destination_root, "app/nodes", class_path, "#{file_name}.rb")
21
22
  File.delete(file_path) if File.exist?(file_path)
23
+ remove_type_config
22
24
  end
23
25
 
24
26
  def attributes_list
@@ -52,5 +54,41 @@ module ApacheAge
52
54
  "#{' ' * (parent_module.split('::').length - 1 - index)}end"
53
55
  end.join("\n") + "\n"
54
56
  end
57
+
58
+ def add_type_config
59
+ return unless File.exist?(types_config_file)
60
+
61
+ types_content = File.read(types_config_file)
62
+ types_content.sub!(/^end\s*$/, "#{new_type_content}end")
63
+ File.open(types_config_file, 'w') { |file| file.write(types_content) }
64
+ puts " modified: config/initializers/types.rb"
65
+ end
66
+
67
+ def remove_type_config
68
+ return unless File.exist?(types_config_file)
69
+
70
+ type_to_remove = new_type_content
71
+
72
+ types_content = File.read(types_config_file)
73
+ types_content.gsub!(type_to_remove, '')
74
+ File.open(types_config_file, 'w') { |file| file.write(types_content) }
75
+ end
76
+
77
+ def types_config_file = File.join(Rails.root, 'config/initializers/types.rb')
78
+
79
+ def new_type_content
80
+ file_path = [class_path, file_name].reject(&:blank?).join('/').downcase
81
+ node_namespace = class_path.map(&:capitalize).join('::')
82
+ node_class_name = file_name.split('_').map(&:capitalize).join
83
+ node_namespaced_class = [node_namespace, node_class_name].reject(&:blank?).join('::')
84
+ type_name = [class_path.join('_'), file_name].reject(&:blank?).join('_')
85
+ content =
86
+ <<-RUBY
87
+ require_dependency '#{file_path}'
88
+ ActiveModel::Type.register(
89
+ :#{type_name}, ApacheAge::Types::AgeTypeGenerator.create_type_for(#{node_namespaced_class})
90
+ )
91
+ RUBY
92
+ end
55
93
  end
56
94
  end
@@ -1,3 +1,3 @@
1
1
  module RailsAge
2
- VERSION = '0.3.2'
2
+ VERSION = '0.4.1'
3
3
  end
@@ -0,0 +1,69 @@
1
+ # lib/tasks/install.rake
2
+ # Usage:
3
+ # * `bin/rails apache_age:add_age_migration`
4
+ # * `bin/rails apache_age:add_age_migration[destination_path.to_s]`
5
+ # * `bundle exec rails apache_age:add_age_migration.invoke(destination_path.to_s)`
6
+ namespace :apache_age do
7
+ desc "Copy migrations from rails_age to application and update schema"
8
+ task :add_age_migration, [:destination_path] => :environment do |t, args|
9
+
10
+ base_name = 'add_apache_age'
11
+ destination_path =
12
+ File.expand_path(args[:destination_path].presence || "#{Rails.root}/db/migrate", __FILE__)
13
+
14
+ FileUtils.mkdir_p(destination_path) unless File.exist?(destination_path)
15
+ existing_migrations =
16
+ Dir.glob("#{destination_path}/*.rb").map { |file| File.basename(file).sub(/^\d+/, '') }
17
+
18
+ if existing_migrations.any? { |migration| migration.include?(base_name) }
19
+ puts "Skipping migration AddApacheAge, it already exists"
20
+ else
21
+ age_migration_contents =
22
+ <<~RUBY
23
+ class AddApacheAge < ActiveRecord::Migration[7.1]
24
+ def up
25
+ # Allow age extension
26
+ execute('CREATE EXTENSION IF NOT EXISTS age;')
27
+
28
+ # Load the age code
29
+ execute("LOAD 'age';")
30
+
31
+ # Load the ag_catalog into the search path
32
+ execute('SET search_path = ag_catalog, "$user", public;')
33
+
34
+ # Create age_schema graph if it doesn't exist
35
+ execute("SELECT create_graph('age_schema');")
36
+ end
37
+
38
+ def down
39
+ execute <<-SQL
40
+ DO $$
41
+ BEGIN
42
+ IF EXISTS (
43
+ SELECT 1
44
+ FROM pg_constraint
45
+ WHERE conname = 'fk_graph_oid'
46
+ ) THEN
47
+ ALTER TABLE ag_catalog.ag_label
48
+ DROP CONSTRAINT fk_graph_oid;
49
+ END IF;
50
+ END $$;
51
+ SQL
52
+
53
+ execute("SELECT drop_graph('age_schema', true);")
54
+ execute('DROP SCHEMA IF EXISTS ag_catalog CASCADE;')
55
+ execute('DROP EXTENSION IF EXISTS age;')
56
+ end
57
+ end
58
+ RUBY
59
+
60
+ migration_version = Time.now.utc.strftime("%Y%m%d%H%M%S")
61
+ file_version = migration_version.delete('_')
62
+ new_filename = "#{file_version}_#{base_name}.rb"
63
+ destination_file = File.join(destination_path, new_filename)
64
+
65
+ File.write(destination_file, age_migration_contents)
66
+ puts "Created migration AddApacheAge"
67
+ end
68
+ end
69
+ end
@@ -3,7 +3,7 @@
3
3
  #
4
4
  namespace :apache_age do
5
5
  desc "Ensure the database.yml file is properly configured for Apache Age"
6
- task :database_config => :environment do
6
+ task :config_database => :environment do
7
7
 
8
8
  db_config_file = File.expand_path("#{Rails.root}/config/database.yml", __FILE__)
9
9
 
@@ -3,8 +3,7 @@
3
3
  #
4
4
  namespace :apache_age do
5
5
  desc "Copy migrations from rails_age to application and update schema"
6
- task :schema_config => :environment do
7
- # source_schema = File.expand_path('../../../db/schema.rb', __FILE__)
6
+ task :config_schema => :environment do
8
7
  destination_schema = File.expand_path("#{Rails.root}/db/schema.rb", __FILE__)
9
8
 
10
9
  unless File.exist?(destination_schema)
@@ -0,0 +1,85 @@
1
+ # lib/tasks/install.rake
2
+ # Usage: `rake apache_age:config_types`
3
+ #
4
+ namespace :apache_age do
5
+ desc "Install AGE types from rails_age into the rails initializers"
6
+ task :config_types => :environment do
7
+ types_file_path = File.expand_path("#{Rails.root}/config/initializers/types.rb", __FILE__)
8
+ required_file_path = "require 'apache_age/types/age_type_generator'"
9
+ required_file_content =
10
+ <<~RUBY
11
+ require 'apache_age/types/age_type_generator'
12
+ # AGE Type Definition Usage (edges/nodes):
13
+ # require_dependency 'nodes/company'
14
+ # ActiveModel::Type.register(
15
+ # :company, ApacheAge::Types::AgeTypeGenerator.create_type_for(Nodes::Company)
16
+ # )
17
+ RUBY
18
+ node_type_content =
19
+ <<-RUBY
20
+ require_dependency 'apache_age/entities/vertex'
21
+ ActiveModel::Type.register(
22
+ :vertex, ApacheAge::Types::AgeTypeGenerator.create_type_for(ApacheAge::Entities::Vertex)
23
+ )
24
+ RUBY
25
+ edge_type_content =
26
+ <<-RUBY
27
+ require_dependency 'apache_age/entities/edge'
28
+ ActiveModel::Type.register(
29
+ :edge, ApacheAge::Types::AgeTypeGenerator.create_type_for(ApacheAge::Entities::Edge)
30
+ )
31
+ RUBY
32
+
33
+ unless File.exist?(types_file_path)
34
+ source_content =
35
+ <<~RUBY
36
+ # config/initializers/types.rb
37
+
38
+ #{required_file_content}
39
+ Rails.application.config.to_prepare do
40
+ # Register AGE types
41
+ #{node_type_content}
42
+ #{edge_type_content}
43
+ end
44
+ RUBY
45
+ File.open(types_file_path, 'w') { |file| file.write(source_content) }
46
+ puts "config/initializers/types.rb file created with AGE base types"
47
+ else
48
+ destination_content = File.read(types_file_path)
49
+ original_content = destination_content.dup
50
+
51
+ unless destination_content.include?(required_file_path)
52
+ destination_content.sub!(
53
+ /^(\s*Rails\.application\.config\.to_prepare do\n)/,
54
+ "#{required_file_content}\n\\1"
55
+ )
56
+ end
57
+
58
+ unless destination_content.include?('# Register AGE types')
59
+ destination_content.sub!(
60
+ /^(\s*Rails\.application\.config\.to_prepare do\n)/,
61
+ "\\1 # Register AGE types\n"
62
+ )
63
+ end
64
+
65
+ unless destination_content.include?(edge_type_content)
66
+ destination_content.sub!(
67
+ /^(\s*Rails\.application\.config\.to_prepare do\n # Register AGE types\n)/,
68
+ "\\1#{edge_type_content}"
69
+ )
70
+ end
71
+
72
+ unless destination_content.include?(node_type_content)
73
+ destination_content.sub!(
74
+ /^(\s*Rails\.application\.config\.to_prepare do\n # Register AGE types\n)/,
75
+ "\\1#{node_type_content}"
76
+ )
77
+ end
78
+
79
+ if destination_content != original_content
80
+ File.open(types_file_path, 'w') { |file| file.write(destination_content) }
81
+ puts "modified: config/initializers/types.rb"
82
+ end
83
+ end
84
+ end
85
+ end
@@ -4,16 +4,19 @@
4
4
  namespace :apache_age do
5
5
  desc "Install & configure Apache Age within Rails (updates migrations, schema & database.yml)"
6
6
  task :install => :environment do
7
- # copy our migrations to the application (if needed)
8
- Rake::Task["apache_age:copy_migrations"].invoke
7
+ # Ensure the AGE migration is in place
8
+ Rake::Task["apache_age:add_age_migration"].invoke
9
9
 
10
10
  # run any new migrations
11
11
  Rake::Task["db:migrate"].invoke
12
12
 
13
+ # ensure the config/database.yml file has the proper configurations
14
+ Rake::Task["apache_age:config_database"].invoke
15
+
13
16
  # adjust the schema file (unfortunately rails mangles the schema file)
14
- Rake::Task["apache_age:schema_config"].invoke
17
+ Rake::Task["apache_age:config_schema"].invoke
15
18
 
16
- # ensure the config/database.yml file has the proper configurations
17
- Rake::Task["apache_age:database_config"].invoke
19
+ # ensure the config/initializers/types.rb file has the base AGE Types
20
+ Rake::Task["apache_age:config_types"].invoke
18
21
  end
19
22
  end
@@ -0,0 +1,7 @@
1
+ namespace :apache_age do
2
+ desc "Ensure 'db:migrate' is followed by 'apache_age:config_schema' to repair 'schema.rb' after migrations"
3
+ task :migrate do
4
+ Rake::Task['db:migrate'].invoke
5
+ Rake::Task["apache_age:config_schema"].invoke
6
+ end
7
+ end
@@ -0,0 +1,50 @@
1
+ namespace :apache_age do
2
+ desc "Ensure 'db:migrate' is followed by 'apache_age:config_schema' to repair the schema.rb file after migration mangles it."
3
+ task :override_db_migrate, [:destination_path] => :environment do |t, args|
4
+ destination_path = (args[:destination_path].presence || "#{Rails.root}") + "/bin"
5
+ FileUtils.mkdir_p(destination_path) unless File.exist?(destination_path)
6
+ bin_rails_path = File.expand_path("#{destination_path}/rails", __FILE__)
7
+
8
+ original_content = File.read(bin_rails_path)
9
+ destination_content = original_content.dup
10
+
11
+ unless destination_content.include?("#!/usr/bin/env ruby\nrails_cmd = ARGV.first")
12
+ capture_rails_cmd =
13
+ <<~RUBY
14
+ rails_cmd = ARGV.first # must be first (otherwise consumed by rails:commands)
15
+ RUBY
16
+
17
+ # add to the top of the file (with gsub)
18
+ destination_content.sub!(
19
+ %r{#!/usr/bin/env ruby\n},
20
+ "#!/usr/bin/env ruby\n#{capture_rails_cmd}\n"
21
+ )
22
+ end
23
+
24
+ # Check if the migration hook is already present
25
+ unless destination_content.include?('Rake::Task["apache_age:config_schema"].invoke')
26
+ override_migrate =
27
+ <<~RUBY
28
+
29
+ # ensure db:migrate is followed with: `Rake::Task["apache_age:config_schema"].invoke`
30
+ # to the schema.rb file after the migration mangles it
31
+ if rails_cmd == 'db:migrate'
32
+ require 'rake'
33
+ Rails.application.load_tasks
34
+ Rake::Task['db:migrate'].invoke
35
+ Rake::Task["apache_age:config_schema"].invoke
36
+ end
37
+ RUBY
38
+
39
+ # append to the end of the file
40
+ destination_content << override_migrate
41
+ end
42
+
43
+ if destination_content == original_content
44
+ puts "AGE Safe Migration is already present in bin/rails. (If its not working inspect the bin/rails file)"
45
+ else
46
+ File.write(bin_rails_path, destination_content)
47
+ puts "AGE Safe Migration added to bin/rails. Now run `bin/rails db:migrate`, then run your tests (or inspect the schema.rb file)."
48
+ end
49
+ end
50
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_age
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bill Tihen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-06-08 00:00:00.000000000 Z
11
+ date: 2024-06-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -85,11 +85,13 @@ files:
85
85
  - lib/rails_age.rb
86
86
  - lib/rails_age/engine.rb
87
87
  - lib/rails_age/version.rb
88
- - lib/tasks/copy_migrations.rake
89
- - lib/tasks/database_config.rake
90
- - lib/tasks/install.original.rake
88
+ - lib/tasks/add_age_migration.rake
89
+ - lib/tasks/config_database.rake
90
+ - lib/tasks/config_schema.rake
91
+ - lib/tasks/config_types.rake
91
92
  - lib/tasks/install.rake
92
- - lib/tasks/schema_config.rake
93
+ - lib/tasks/migrate.rake
94
+ - lib/tasks/override_db_migrate.rake
93
95
  homepage: https://github.com/marpori/rails_age
94
96
  licenses:
95
97
  - MIT
@@ -1,33 +0,0 @@
1
- # lib/tasks/install.rake
2
- # Usage:
3
- # * `bin/rails apache_age:copy_migrations`
4
- # * `bundle exec rails apache_age:copy_migrations[destination_path.to_s]`
5
- # * `bundle exec rails apache_age:copy_migrations.invoke(destination_path.to_s)`
6
- namespace :apache_age do
7
- desc "Copy migrations from rails_age to application and update schema"
8
- task :copy_migrations, [:destination_path] => :environment do |t, args|
9
- source = File.expand_path('../../../db/migrate', __FILE__)
10
- destination_path =
11
- File.expand_path(args[:destination_path].presence || "#{Rails.root}/db/migrate", __FILE__)
12
-
13
- FileUtils.mkdir_p(destination_path) unless File.exist?(destination_path)
14
- existing_migrations =
15
- Dir.glob("#{destination_path}/*.rb").map { |file| File.basename(file).sub(/^\d+/, '') }
16
-
17
- Dir.glob("#{source}/*.rb").each do |file|
18
- filename = File.basename(file)
19
- test_name = filename.sub(/^\d+/, '')
20
-
21
- if existing_migrations.include?(test_name)
22
- puts "Skipping migration: '#{filename}', it already exists"
23
- else
24
- migration_version = Time.now.utc.strftime("%Y_%m_%d_%H%M%S")
25
- file_version = migration_version.delete('_')
26
- new_filename = filename.sub(/^\d+/, file_version)
27
- destination_file = File.join(destination_path, new_filename)
28
- FileUtils.cp(file, destination_file)
29
- puts "Created migration: '#{new_filename}'"
30
- end
31
- end
32
- end
33
- end
@@ -1,257 +0,0 @@
1
- # lib/tasks/install.rake
2
- # Usage: `rake apache_age:install`
3
- #
4
- namespace :apache_age do
5
- desc "Install & configure Apache Age within Rails (updates migrations, schema & database.yml)"
6
- task :install_old => :environment do
7
- source_schema = File.expand_path('../../../db/schema.rb', __FILE__)
8
- destination_schema = File.expand_path("#{Rails.root}/db/schema.rb", __FILE__)
9
- source_migrations = File.expand_path('../../../db/migrate', __FILE__)
10
- destination_migrations = File.expand_path("#{Rails.root}/db/migrate", __FILE__)
11
- # create the migrations folder if needed
12
- FileUtils.mkdir_p(destination_migrations) unless File.exist?(destination_migrations)
13
- original_migrations =
14
- Dir.glob("#{destination_migrations}/*.rb").map { |file| File.basename(file).sub(/^\d+/, '') }
15
-
16
- # # check if the schema is non-existent or blank (NEW) we need to know how to handle schema
17
- # is_schema_blank = !File.exist?(destination_schema) || blank_schema?(destination_schema)
18
- # puts "Schema is blank: #{is_schema_blank}"
19
-
20
- # ensure we have a schema file
21
- unless File.exist?(destination_schema)
22
- run_db_create
23
- run_db_migrate
24
- end
25
-
26
- # copy our migrations to the application (last_migration_version is nil if no migration necessary)
27
- # last_migration_version = copy_migrations
28
- Rake::Task["apache_age:copy_migrations"].invoke
29
-
30
- updated_migrations =
31
- Dir.glob("#{destination_migrations}/*.rb").map { |file| File.basename(file).sub(/^\d+/, '') }
32
-
33
- # # run our new migrations (unless we have not added any new migrations)
34
- # if original_migrations == updated_migrations
35
- # puts "no new migrations were copied, skipping migrations"
36
- # else
37
- # puts "added Apache Age migrations, running migrations"
38
- # run_db_migrate
39
- # end
40
- run_db_migrate
41
-
42
- # adjust the schema file (unfortunately rails mangles the schema file)
43
- # if is_schema_blank
44
- # puts "creating new schema..."
45
- # create_new_schema(last_migration_version, destination_schema, source_schema)
46
- # else
47
- # puts "updating existing schema..."
48
- # update_existing_schema(last_migration_version, destination_schema, source_schema)
49
- # end
50
- Rake::Task["apache_age:schema_config"].invoke
51
-
52
- # ensure the config/database.yml file has the proper configurations
53
- # update_database_yml
54
- Rake::Task["apache_age:database_config"].invoke
55
- end
56
-
57
- def run_db_create
58
- puts "Running db:create..."
59
- Rake::Task["db:create"].invoke
60
- end
61
-
62
- def run_db_migrate
63
- puts "Running db:migrate..."
64
- Rake::Task["db:migrate"].invoke
65
- end
66
-
67
- def copy_migrations
68
- migration_version = nil
69
-
70
- source = File.expand_path('../../../db/migrate', __FILE__)
71
- destination = File.expand_path("#{Rails.root}/db/migrate", __FILE__)
72
-
73
- FileUtils.mkdir_p(destination) unless File.exist?(destination)
74
- existing_migrations =
75
- Dir.glob("#{destination}/*.rb").map { |file| File.basename(file).sub(/^\d+/, '') }
76
-
77
- Dir.glob("#{source}/*.rb").each do |file|
78
- filename = File.basename(file)
79
- test_name = filename.sub(/^\d+/, '')
80
-
81
- if existing_migrations.include?(test_name)
82
- puts "Skipping #{filename}, it already exists"
83
- else
84
- migration_version = Time.now.utc.strftime("%Y_%m_%d_%H%M%S")
85
- file_version = migration_version.delete('_')
86
- new_filename = filename.sub(/^\d+/, file_version)
87
- destination_file = File.join(destination, new_filename)
88
- FileUtils.cp(file, destination_file)
89
- puts "Copied #{filename} to #{destination} as #{new_filename}"
90
- end
91
- end
92
- migration_version
93
- end
94
-
95
- def blank_schema?(destination_schema)
96
- return false unless File.exist?(destination_schema)
97
-
98
- content = File.read(destination_schema)
99
- content.include?('define(version: 0)') &&
100
- (content.include?("enable_extension 'plpgsql'") || content.include?('enable_extension "plpgsql"')) &&
101
- content.scan(/enable_extension/).size == 1
102
- end
103
-
104
- def schema_rails_version(destination_schema)
105
- if File.exist?(destination_schema)
106
- content = File.read(destination_schema)
107
- version_match = content.match(/ActiveRecord::Schema\[(.*?)\]/)
108
- return version_match[1] if version_match
109
- else
110
- full_version = Rails.version
111
- primary_secondary_version = full_version.split('.')[0..1].join('.')
112
- primary_secondary_version
113
- end
114
- end
115
-
116
- def create_new_schema(last_migration_version, destination_schema, source_schema)
117
- if File.exist?(source_schema) && File.exist?(destination_schema)
118
- rails_version = schema_rails_version(destination_schema)
119
- source_content = File.read(source_schema)
120
-
121
- # ensure we use the Rails version from the destination schema
122
- source_content.gsub!(
123
- /ActiveRecord::Schema\[\d+\.\d+\]/,
124
- "ActiveRecord::Schema[#{rails_version}]"
125
- )
126
- # ensure we use the last migration version (not the source schema version)
127
- source_content.gsub!(
128
- /define\(version: \d{4}(?:_\d{2}){2}(?:_\d{6})?\) do/,
129
- "define(version: #{last_migration_version}) do"
130
- )
131
-
132
- File.write(destination_schema, source_content)
133
- puts "Created new schema in #{destination_schema} with necessary extensions and configurations."
134
- else
135
- puts "local db/schema.rb file not found."
136
- end
137
- end
138
-
139
- def update_existing_schema(last_migration_version, destination_schema, source_schema)
140
- if File.exist?(source_schema) && File.exist?(destination_schema)
141
- rails_version = schema_rails_version(destination_schema)
142
- source_content = File.read(source_schema)
143
- new_content =
144
- source_content.gsub(
145
- /.*ActiveRecord::Schema\[\d+\.\d+\]\.define\(version: \d{4}(?:_\d{2}){2}(?:_\d{6})?\) do\n|\nend$/,
146
- ''
147
- )
148
-
149
- destination_content = File.read(destination_schema)
150
-
151
- # Remove unwanted schema statements
152
- destination_content.gsub!(%r{^.*?create_schema "ag_catalog".*?\n}, '')
153
- destination_content.gsub!(%r{^.*?create_schema "age_schema".*?\n}, '')
154
- destination_content.gsub!(%r{^.*?enable_extension "age".*?\n}, '')
155
- destination_content.gsub!(%r{^.*?enable_extension "plpgsql".*?\n}, '')
156
- destination_content.gsub!(%r{^.*?# Could not dump table "ag_graph" because of following StandardError.*?\n}, '')
157
- destination_content.gsub!(%r{^.*?# Unknown type 'regnamespace' for column 'namespace'.*?\n}, '')
158
- destination_content.gsub!(%r{^.*?# Could not dump table "ag_label" because of following StandardError.*?\n}, '')
159
- destination_content.gsub!(%r{^.*?# Unknown type 'regclass' for column 'relation'.*?\n}, '')
160
- destination_content.gsub!(%r{^.*?# Unknown type 'graphid' for column 'id'.*?\n}, '')
161
- destination_content.gsub!(
162
- %r{^.*?# Could not dump table "_ag_label_edge" because of following StandardError.*?\n}, ''
163
- )
164
- destination_content.gsub!(
165
- %r{^.*?# Could not dump table "_ag_label_vertex" because of following StandardError.*?\n}, ''
166
- )
167
- destination_content.gsub!(%r{^.*?# Could not dump table "ag_graph" because of following StandardError.*?\n}, '')
168
- destination_content.gsub!(%r{^.*?# Could not dump table "ag_label" because of following StandardError.*?\n}, '')
169
- destination_content.gsub!(%r{^.*?add_foreign_key "ag_label", "ag_graph".*?\n}, '')
170
-
171
- # add new wanted schema statements (at the top of the schema)
172
- unless destination_content.include?(%{execute("LOAD 'age';")}) &&
173
- destination_content.include?(%{enable_extension 'plpgsql'}) &&
174
- destination_content.include?(%{execute("SELECT create_graph('age_schema');")}) &&
175
- destination_content.include?(%{execute('CREATE EXTENSION IF NOT EXISTS age;')}) &&
176
- destination_content.include?(%{execute('SET search_path = ag_catalog, "$user", public;')})
177
- # if not all are found then remove any found
178
- destination_content.gsub!(%r{^.*?execute("LOAD 'age';")*?\n}, '')
179
- destination_content.gsub!(%r{^.*?enable_extension 'plpgsql'*?\n}, '')
180
- destination_content.gsub!(%r{^.*?execute("SELECT create_graph('age_schema');")*?\n}, '')
181
- destination_content.gsub!(%r{^.*?execute('CREATE EXTENSION IF NOT EXISTS age;')*?\n}, '')
182
- destination_content.gsub!(%r{^.*?execute('SET search_path = ag_catalog, "$user", public;')*?\n}, '')
183
- destination_content.gsub!(%r{^.*?# Allow age extension*?\n}, '')
184
- destination_content.gsub!(%r{^.*?# Load the ag_catalog into the search path*?\n}, '')
185
- destination_content.gsub!(%r{^.*?# Create age_schema graph if it doesn't exist*?\n}, '')
186
- destination_content.gsub!(%r{^.*?# These are extensions that must be enabled in order to support this database*?\n}, '')
187
-
188
- # add all of the correct settings back in
189
- destination_content.sub!(
190
- %r{(ActiveRecord::Schema\[\d+\.\d+\]\.define\(version: \d{4}(?:_\d{2}){2}(?:_\d{6})?\) do\n)},
191
- "\\1#{new_content}\n"
192
- )
193
- puts 'db/schema.rb has been updated with the necessary configuration.'
194
- else
195
- puts 'db/schema.rb has the necessary configuration, no adjustments necessary'
196
- end
197
-
198
- existing_version = destination_content.match(/define\(version: (\d{4}(?:_\d{2}){2}(?:_\d{6})?)\)/)[1].gsub('_', '')
199
- current_version = last_migration_version ? last_migration_version.gsub('_', '') : existing_version
200
-
201
- # ensure we use the last migration version (not the source schema version)
202
- if current_version.to_i > existing_version.to_i
203
- destination_content.gsub!(
204
- /define\(version: \d{4}(?:_\d{2}){2}(?:_\d{6})?\) do/,
205
- "define(version: #{last_migration_version}) do"
206
- )
207
- puts "Updated schema version to the migration version #{last_migration_version}"
208
- end
209
-
210
- File.write(destination_schema, destination_content)
211
- puts "Updated #{destination_schema} with necessary extensions and configurations."
212
- else
213
- puts "local db/schema.rb file not found."
214
- end
215
- end
216
-
217
- def update_database_yml
218
- db_config_file = File.expand_path("#{Rails.root}/config/database.yml", __FILE__)
219
-
220
- # Read the file
221
- lines = File.readlines(db_config_file)
222
-
223
- # any uncommented "schema_search_path:" lines?
224
- path_index = lines.find_index { |line| !line.include?('#') && line.include?('schema_search_path:') }
225
- default_start_index = lines.index { |line| line.strip.start_with?('default:') }
226
-
227
- # when it finds an existing schema_search_path, it updates it
228
- if path_index && lines[path_index].include?('ag_catalog,age_schema')
229
- puts "schema_search_path already set to ag_catalog,age_schema nothing to do."
230
- return
231
- elsif path_index
232
- key, val = lines[path_index].split(': ')
233
- # remove any unwanted characters
234
- val = val.gsub(/[ "\s\"\"'\n]/, '')
235
- lines[path_index] = "#{key}: ag_catalog,age_schema,#{val}\n"
236
- puts "add ag_catalog,age_schema to schema_search_path"
237
- elsif default_start_index
238
- puts "add ag_catalog,age_schema,public to schema_search_path in the default section of database.yml"
239
- sections_index = lines.map.with_index { |line, index| index if !line.start_with?(' ') }.compact.sort
240
-
241
- # find the start of the default section
242
- next_section_in_list = sections_index.index(default_start_index) + 1
243
-
244
- # find the end of the default section (before the next section starts)
245
- path_insert_index = sections_index[next_section_in_list]
246
-
247
- lines.insert(path_insert_index, " schema_search_path: ag_catalog,age_schema,public\n")
248
- else
249
- puts "didn't find a default section in database.yml, please add the following line:"
250
- puts " schema_search_path: ag_catalog,age_schema,public"
251
- puts "to the apprpriate section of your database.yml"
252
- end
253
-
254
- # Write the modified lines back to the file
255
- File.open(db_config_file, 'w') { |file| file.write(lines.join) }
256
- end
257
- end