rails_age 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 349925c4b715d64987c69a6a20b23e67c9076d91152477a976ade763805fdbf7
4
- data.tar.gz: 4c7f1e430fe94ac3cb59eac8e6cb94bc03edab9532074c0df75be233b1210973
3
+ metadata.gz: 272f8908dc8fb8a98adb443411e4b439eff914c8173b5eb64b53fa5362fa1572
4
+ data.tar.gz: bf1921611698ffc1b651787f2e1b1a5d1bc503cbe1300639b2f9dada8202cdde
5
5
  SHA512:
6
- metadata.gz: af26f1a10253235703203804e508aa0d0dfbe7e44cc92097f70111587663c9af75e6cfd730fcb0986dc6093bb9bb16c847ab66ad7d3249057d3fe8adf443c78e
7
- data.tar.gz: f98980a1937efeef70c1e548b2f62f5b2a7e2f383f707a92e8392a8f45b1bc9ac882a1f478fff0f24f1fcb3cb145e0f037202356ea4a2e76737ee81f4c70b459
6
+ metadata.gz: 51592c31e872a9b8dc6e96fe8fa42374241496e4da6a17ce70bf63f5f118ac078e81200259c048529e7e16b2147c8c5bbef00315cee8f26237295d04ffaea887
7
+ data.tar.gz: 0b24560f7703e4dc4f97d3d36de626906a47a2cda5f5137cc6c1840a5281bbcf2693743666fb6301b26723c9723244bd1854c9c8b21d5d657eb621d4caf0d864
data/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Change Log
2
2
 
3
- ## VERSION 0.3.0 - 2024-xx-xx
3
+ ## VERSION 0.4.0 - 2024-xx-xx
4
4
 
5
5
  - **cypher**
6
6
  * query support
@@ -9,10 +9,33 @@
9
9
  - **Paths**
10
10
  * ?
11
11
 
12
+ ## VERSION 0.3.1 - 2024-xx-xx
13
+
14
+ - **Generators**
15
+ * add `rails generate apache_age:node` to create a node model (with its type in initializer)
16
+ * add `rails generate apache_age:edge` to create an edge model (with its type in initializer)
17
+ - **Installer**
18
+ * refactor into multiple independent tasks with tests
19
+ - **Documentation**
20
+ * updated README with additional information
21
+ * added `db/structure.sql` config to README
22
+
23
+ ## VERSION 0.3.0 - 2024-05-28
24
+
25
+ - **Edges**
26
+ * `find_by(start_node:, :end_node:, properties:)` to find an edge with specific nodes & properties (deprecated `find_edge`)
27
+ - **Installer** (`rails generate apache_age:install`)
28
+ * copy Age PG Extenstion migration to `db/migrate`
29
+ * run the AGE PG Migration
30
+ * repair `db/schema.rb` (rails mangles schema after running pg extension)
31
+ * update `database.yml` with schema search paths
32
+
33
+ NOTE: the `rails generate apache_age:install` can be run at any time to repair the schema (or other config) file if needed.
34
+
12
35
  ## VERSION 0.2.0 - 2024-05-26
13
36
 
14
37
  - **Edges**
15
- * add class methods to find_edge(with {properties, end_id, start_id})
38
+ * add class methods to `find_edge` (with {properties, end_id, start_id})
16
39
  * add missing methods to use in rails controllers
17
40
  * validate edge start- & end-nodes are valid
18
41
  * add unique edge validations
@@ -25,13 +48,13 @@
25
48
  Initial release has the following features:
26
49
 
27
50
  - **Nodes:**
28
- * Create, Read, Update, Delete & .find(by id), .find_by(age_property), .all
51
+ * `.create`, `.read`, `.update`, `.delete`, `.all`, `.find(by id)`, `.find_by(age_properties)`
29
52
  * verified with usage in a controller and views
30
53
  - **Edges:**
31
- * Create, Read, Update, Delete & .find(by id), .find_by(age_property), .all
54
+ *`.create`, `.read`, `.update`, `.delete`, `.all`, `.find(by id)`, `.find_by(age_properties)`
32
55
  * verified with usage in a controller and views
33
56
  - **Entities:**
34
- * find (by id), find_by(age_property), all; when class/label and/or edge/node is unknown)
57
+ * `.all`, `.find(id)`, `.find_by(age_property)` use these when class, label, edge, node
35
58
 
36
59
  These can be used within Rails applications using a Rails APIs including within controllers and views.
37
60
  See the [README](README.md) for more information.
data/README.md CHANGED
@@ -12,24 +12,30 @@ Add this line to your application's Gemfile:
12
12
  gem "rails_age"
13
13
  ```
14
14
 
15
- And then execute:
15
+ ### Quick Install
16
+
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.
16
18
 
17
19
  ```bash
18
20
  $ bundle
21
+ $ bin/rails apache_age:install
22
+ $ git add .
23
+ $ git commit -m "Add Apache Age to Rails"
19
24
  ```
20
25
 
21
- Or install it yourself as:
26
+ 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`). **You can run `bin/rails apache_age:install` at any time to repair the schema file as needed.**
22
27
 
23
- ```bash
24
- $ gem install rails_age
25
- ```
28
+ For now, if you are using `db/structure.sql` you will need to manually configure Apache Age (RailsAge) as described below.
26
29
 
27
- finally (tempoarily you need to copy and run the migration)
28
- https://github.com/marpori/rails_age/blob/main/db/migrate/20240521062349_configure_apache_age.rb
30
+ ### Manual Install
29
31
 
32
+ create a migration to add the Apache Age extension to your database
30
33
  ```bash
31
- # db/migrate/20240521062349_configure_apache_age.rb
32
- class ConfigureApacheAge < ActiveRecord::Migration[7.1]
34
+ $ bin/rails g migration AddApacheAge
35
+ ```
36
+ copy the contents of https://github.com/marpori/rails_age/blob/main/db/migrate/20240521062349_add_apache_age.rb
37
+ ```ruby
38
+ class AddApacheAge < ActiveRecord::Migration[7.1]
33
39
  def up
34
40
  # Allow age extension
35
41
  execute('CREATE EXTENSION IF NOT EXISTS age;')
@@ -65,16 +71,48 @@ class ConfigureApacheAge < ActiveRecord::Migration[7.1]
65
71
  end
66
72
  end
67
73
  ```
74
+ into your new migration file
68
75
 
69
- and fix the TOP of `schema.rb` file to match the following (note: the version number should be the same as the LARGEST version number in your `db/migrations` folder)
70
- https://github.com/marpori/rails_age/blob/main/db/schema.rb
76
+ then run the migration
77
+ ```bash
78
+ $ bin/rails db:migrate
79
+ ```
71
80
 
81
+ Rails migrate will mangle the schema `db/schema.rb` file. You need to remove the lines that look like:
72
82
  ```ruby
73
- # db/schema.rb
74
83
  ActiveRecord::Schema[7.1].define(version: 2024_05_21_062349) do
84
+ create_schema "ag_catalog"
85
+ create_schema "age_schema"
86
+
75
87
  # These are extensions that must be enabled in order to support this database
88
+ enable_extension "age"
76
89
  enable_extension "plpgsql"
77
90
 
91
+ # Could not dump table "_ag_label_edge" because of following StandardError
92
+ # Unknown type 'graphid' for column 'id'
93
+
94
+ # Could not dump table "_ag_label_vertex" because of following StandardError
95
+ # Unknown type 'graphid' for column 'id'
96
+
97
+ # Could not dump table "ag_graph" because of following StandardError
98
+ # Unknown type 'regnamespace' for column 'namespace'
99
+
100
+ # Could not dump table "ag_label" because of following StandardError
101
+ # Unknown type 'regclass' for column 'relation'
102
+
103
+ add_foreign_key "ag_label", "ag_graph", column: "graph", primary_key: "graphid", name: "fk_graph_oid"
104
+
105
+ # other migrations
106
+ # ...
107
+ end
108
+ ```
109
+
110
+ and replace them with the following lines:
111
+ ```ruby
112
+ ActiveRecord::Schema[7.1].define(version: 2024_05_21_062349) do
113
+ # These are extensions that must be enabled in order to support this database
114
+ enable_extension 'plpgsql'
115
+
78
116
  # Allow age extension
79
117
  execute('CREATE EXTENSION IF NOT EXISTS age;')
80
118
 
@@ -92,9 +130,35 @@ ActiveRecord::Schema[7.1].define(version: 2024_05_21_062349) do
92
130
  end
93
131
  ```
94
132
 
133
+ NOTE: if using `db/structure.sql` use:
134
+ ```sql
135
+ -- These are extensions that must be enabled in order to support this database
136
+ CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA public;
137
+
138
+ -- Allow age extension (if not already enabled), this builds the age_catalog schema
139
+ CREATE EXTENSION IF NOT EXISTS age;
140
+
141
+ -- Load the age module
142
+ LOAD 'age';
143
+
144
+ -- Load the ag_catalog into the search path
145
+ SET search_path = ag_catalog, "$user", public;
146
+
147
+ -- Create age_schema graph if it doesn't exist
148
+ SELECT create_graph('age_schema');
149
+
150
+ # other migrations
151
+ # ...
152
+
153
+ INSERT INTO "schema_migrations" (version) VALUES
154
+ ('20110315075839'),
155
+ --- ...
156
+ ('20240521062349');
157
+ ```
158
+
95
159
  ## Contributing
96
160
 
97
- Create an MR and tests and I will review it.
161
+ Create an merge request (with tests) and I will review it/merge it when ready.
98
162
 
99
163
  ## License
100
164
 
@@ -102,9 +166,12 @@ The gem is available as open source under the terms of the [MIT License](https:/
102
166
 
103
167
  ## Usage
104
168
 
169
+ I suggest you creat a folder create a folder called `app/nodes` and `app/edges` to keep the code organized.
170
+ 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))
171
+
105
172
  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.
106
173
 
107
- A full sample app can be found [here](https://github.com/marpori/rails_age_demo_app) the summary usage is described below.
174
+ 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.
108
175
 
109
176
  ### Nodes
110
177
 
@@ -115,7 +182,12 @@ module Nodes
115
182
  include ApacheAge::Entities::Vertex
116
183
 
117
184
  attribute :company_name, :string
185
+
118
186
  validates :company_name, presence: true
187
+ validates_with(
188
+ ApacheAge::Validators::UniqueVertexValidator,
189
+ attributes: [:company_name]
190
+ )
119
191
  end
120
192
  end
121
193
  ```
@@ -148,15 +220,30 @@ end
148
220
  ### Edges
149
221
 
150
222
  ```ruby
151
- # app/graphs/edges/works_at.rb
223
+ # app/graphs/edges/has_job.rb
152
224
  module Edges
153
225
  class HasJob
154
226
  include ApacheAge::Entities::Edge
155
227
 
156
228
  attribute :employee_role, :string
157
- attribute :start_node, :person # if using optional age types
158
- # attribute :end_node, :person # if using optional age types
229
+ attribute :start_node, :person
230
+ attribute :end_node, :company
231
+
159
232
  validates :employee_role, presence: true
233
+ validate :validate_unique
234
+ # or with a one-liner
235
+ # validates_with(
236
+ # ApacheAge::Validators::UniqueEdgeValidator,
237
+ # attributes: %i[employee_role start_node end_node]
238
+ # )
239
+
240
+ private
241
+
242
+ def validate_unique
243
+ ApacheAge::Validators::UniqueEdgeValidator
244
+ .new(attributes: %i[employee_role start_node end_node])
245
+ .validate(self)
246
+ end
160
247
  end
161
248
  end
162
249
  ```
@@ -1,4 +1,4 @@
1
- class ConfigureApacheAge < ActiveRecord::Migration[7.1]
1
+ class AddApacheAge < ActiveRecord::Migration[7.1]
2
2
  def up
3
3
  # Allow age extension
4
4
  execute('CREATE EXTENSION IF NOT EXISTS age;')
data/db/schema.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  ActiveRecord::Schema[7.1].define(version: 2024_05_21_062349) do
2
2
  # These are extensions that must be enabled in order to support this database
3
- enable_extension "plpgsql"
3
+ enable_extension 'plpgsql'
4
4
 
5
5
  # Allow age extension
6
6
  execute('CREATE EXTENSION IF NOT EXISTS age;')
@@ -8,29 +8,15 @@ module ApacheAge
8
8
  instance
9
9
  end
10
10
 
11
- def find_edge(attributes)
12
- where_attribs =
13
- attributes
14
- .compact
15
- .except(:end_id, :start_id, :end_node, :start_node)
16
- .map { |k, v| "find.#{k} = '#{v}'" }.join(' AND ')
17
- where_attribs = where_attribs.empty? ? nil : where_attribs
18
-
19
- end_id = attributes[:end_id] || attributes[:end_node]&.id
20
- start_id = attributes[:start_id] || attributes[:start_node]&.id
21
- where_end_id = end_id ? "id(end_node) = #{end_id}" : nil
22
- where_start_id = start_id ? "id(start_node) = #{start_id}" : nil
23
-
24
- where_clause = [where_attribs, where_start_id, where_end_id].compact.join(' AND ')
25
- return nil if where_clause.empty?
11
+ def find_by(attributes)
12
+ return nil if attributes.reject{ |k,v| v.blank? }.empty?
26
13
 
27
- cypher_sql = find_edge_sql(where_clause)
28
- execute_find(cypher_sql)
29
- end
14
+ edge_keys = [:start_id, :start_node, :end_id, :end_node]
15
+ return find_edge(attributes) if edge_keys.any? { |key| attributes.include?(key) }
30
16
 
31
- def find_by(attributes)
32
17
  where_clause = attributes.map { |k, v| "find.#{k} = '#{v}'" }.join(' AND ')
33
18
  cypher_sql = find_sql(where_clause)
19
+
34
20
  execute_find(cypher_sql)
35
21
  end
36
22
 
@@ -55,6 +41,27 @@ module ApacheAge
55
41
 
56
42
  # Private stuff
57
43
 
44
+ def find_edge(attributes)
45
+ where_attribs =
46
+ attributes
47
+ .compact
48
+ .except(:end_id, :start_id, :end_node, :start_node)
49
+ .map { |k, v| "find.#{k} = '#{v}'" }.join(' AND ')
50
+ where_attribs = where_attribs.empty? ? nil : where_attribs
51
+
52
+ end_id = attributes[:end_id] || attributes[:end_node]&.id
53
+ start_id = attributes[:start_id] || attributes[:start_node]&.id
54
+ where_end_id = end_id ? "id(end_node) = #{end_id}" : nil
55
+ where_start_id = start_id ? "id(start_node) = #{start_id}" : nil
56
+
57
+ where_clause = [where_attribs, where_start_id, where_end_id].compact.join(' AND ')
58
+ return nil if where_clause.empty?
59
+
60
+ cypher_sql = find_edge_sql(where_clause)
61
+
62
+ execute_find(cypher_sql)
63
+ end
64
+
58
65
  def age_graph = 'age_schema'
59
66
  def age_label = name.gsub('::', '__')
60
67
  def age_type = name.constantize.new.age_type
@@ -32,7 +32,7 @@ module ApacheAge
32
32
  end
33
33
  return if attributes.blank? && (end_query.blank? || start_query.blank?)
34
34
 
35
- query = record.class.find_edge(edge_attribs.compact)
35
+ query = record.class.find_by(edge_attribs.compact)
36
36
  return if query.blank? || (query.id == record.id)
37
37
 
38
38
  record.errors.add(:base, 'attribute combination not unique')
@@ -1,3 +1,3 @@
1
1
  module RailsAge
2
- VERSION = "0.2.0"
2
+ VERSION = '0.3.1'
3
3
  end
@@ -0,0 +1,33 @@
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
@@ -0,0 +1,99 @@
1
+ # lib/tasks/install.rake
2
+ # Usage: `rake apache_age:copy_migrations`
3
+ #
4
+ namespace :apache_age do
5
+ desc "Ensure the database.yml file is properly configured for Apache Age"
6
+ task :database_config => :environment do
7
+
8
+ db_config_file = File.expand_path("#{Rails.root}/config/database.yml", __FILE__)
9
+
10
+ # Read the file
11
+ lines = File.readlines(db_config_file)
12
+
13
+ # any uncommented "schema_search_path:" lines?
14
+ path_index = lines.find_index { |line| !line.include?('#') && line.include?('schema_search_path:') }
15
+ default_start_index = lines.index { |line| line.strip.start_with?('default:') }
16
+
17
+ # when it finds an existing schema_search_path, it updates it
18
+ if path_index && lines[path_index].include?('ag_catalog,age_schema')
19
+ puts "the schema_search_path in config/database.yml is already properly set, nothing to do."
20
+ else
21
+ if path_index
22
+ key, val = lines[path_index].split(': ')
23
+ # remove any unwanted characters
24
+ val = val.gsub(/[ "\s\"\"'\n]/, '')
25
+ lines[path_index] = "#{key}: ag_catalog,age_schema,#{val}\n"
26
+ puts "added ag_catalog,age_schema to schema_search_path in config/database.yml"
27
+ elsif default_start_index
28
+ puts "the schema_search_path in config/database.yml is now properly set."
29
+ sections_index = lines.map.with_index { |line, index| index if !line.start_with?(' ') }.compact.sort
30
+
31
+ # find the start of the default section
32
+ next_section_in_list = sections_index.index(default_start_index) + 1
33
+
34
+ # find the end of the default section (before the next section starts)
35
+ path_insert_index = sections_index[next_section_in_list]
36
+
37
+ lines.insert(path_insert_index, " schema_search_path: ag_catalog,age_schema,public\n")
38
+ else
39
+ puts "didn't find a default section in database.yml, please add the following line:"
40
+ puts " schema_search_path: ag_catalog,age_schema,public"
41
+ puts "to the apprpriate section of your database.yml"
42
+ end
43
+
44
+ # Write the modified lines back to the file
45
+ File.open(db_config_file, 'w') { |file| file.write(lines.join) }
46
+ end
47
+ end
48
+ end
49
+
50
+ # # lib/tasks/install.rake
51
+ # # Usage: `rake apache_age:copy_migrations`
52
+ # #
53
+ # namespace :apache_age do
54
+ # desc "Ensure the database.yml file is properly configured for Apache Age"
55
+ # task :database_config, [:destination_path] => :environment do |t, args|
56
+ # destination_path =
57
+ # File.expand_path(args[:destination_path].presence || "#{Rails.root}/config", __FILE__)
58
+
59
+ # db_config_file = File.expand_path("#{destination_path.to_s}/database.yml", __FILE__)
60
+
61
+ # # Read the file
62
+ # lines = File.readlines(db_config_file)
63
+
64
+ # # any uncommented "schema_search_path:" lines?
65
+ # path_index = lines.find_index { |line| !line.include?('#') && line.include?('schema_search_path:') }
66
+ # default_start_index = lines.index { |line| line.strip.start_with?('default:') }
67
+
68
+ # # when it finds an existing schema_search_path, it updates it
69
+ # if path_index && lines[path_index].include?('ag_catalog,age_schema')
70
+ # puts "the schema_search_path in config/database.yml is already properly set, nothing to do."
71
+ # else
72
+ # if path_index
73
+ # key, val = lines[path_index].split(': ')
74
+ # # remove any unwanted characters
75
+ # val = val.gsub(/[ "\s\"\"'\n]/, '')
76
+ # lines[path_index] = "#{key}: ag_catalog,age_schema,#{val}\n"
77
+ # puts "added ag_catalog,age_schema to schema_search_path in config/database.yml"
78
+ # elsif default_start_index
79
+ # puts "the schema_search_path in config/database.yml is now properly set."
80
+ # sections_index = lines.map.with_index { |line, index| index if !line.start_with?(' ') }.compact.sort
81
+
82
+ # # find the start of the default section
83
+ # next_section_in_list = sections_index.index(default_start_index) + 1
84
+
85
+ # # find the end of the default section (before the next section starts)
86
+ # path_insert_index = sections_index[next_section_in_list]
87
+
88
+ # lines.insert(path_insert_index, " schema_search_path: ag_catalog,age_schema,public\n")
89
+ # else
90
+ # puts "didn't find a default section in database.yml, please add the following line:"
91
+ # puts " schema_search_path: ag_catalog,age_schema,public"
92
+ # puts "to the apprpriate section of your database.yml"
93
+ # end
94
+
95
+ # # Write the modified lines back to the file
96
+ # File.open(db_config_file, 'w') { |file| file.write(lines.join) }
97
+ # end
98
+ # end
99
+ # end
@@ -0,0 +1,257 @@
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
@@ -1,91 +1,19 @@
1
1
  # lib/tasks/install.rake
2
- # Usage: `rake rails_age:install`
2
+ # Usage: `rake apache_age:install`
3
3
  #
4
- namespace :rails_age do
5
- desc "Copy migrations from rails_age to application and update schema"
4
+ namespace :apache_age do
5
+ desc "Install & configure Apache Age within Rails (updates migrations, schema & database.yml)"
6
6
  task :install => :environment do
7
- source = File.expand_path('../../../db/migrate', __FILE__)
8
- destination = File.expand_path('../../../../db/migrate', __FILE__)
7
+ # copy our migrations to the application (if needed)
8
+ Rake::Task["apache_age:copy_migrations"].invoke
9
9
 
10
- FileUtils.mkdir_p(destination) unless File.exists?(destination)
10
+ # run any new migrations
11
+ Rake::Task["db:migrate"].invoke
11
12
 
12
- Dir.glob("#{source}/*.rb").each do |file|
13
- filename = File.basename(file)
14
- destination_file = File.join(destination, filename)
13
+ # adjust the schema file (unfortunately rails mangles the schema file)
14
+ Rake::Task["apache_age:schema_config"].invoke
15
15
 
16
- if File.exists?(destination_file)
17
- puts "Skipping #{filename}, it already exists"
18
- else
19
- FileUtils.cp(file, destination_file)
20
- puts "Copied #{filename} to #{destination}"
21
- end
22
- end
23
-
24
- # Update the schema.rb file
25
- schema_file = File.expand_path('../../../../db/schema.rb', __FILE__)
26
- if File.exists?(schema_file)
27
- content = File.read(schema_file)
28
-
29
- # Add the necessary extensions and configurations at the top of the schema
30
- insert_statements = <<-RUBY
31
-
32
- # These are extensions that must be enabled in order to support this database
33
- enable_extension "plpgsql"
34
-
35
- # Allow age extension
36
- execute('CREATE EXTENSION IF NOT EXISTS age;')
37
-
38
- # Load the age code
39
- execute("LOAD 'age';")
40
-
41
- # Load the ag_catalog into the search path
42
- execute('SET search_path = ag_catalog, "$user", public;')
43
-
44
- # Create age_schema graph if it doesn't exist
45
- execute("SELECT create_graph('age_schema');")
46
-
47
- RUBY
48
-
49
- unless content.include?(insert_statements.strip)
50
- content.sub!(/^# These are extensions that must be enabled in order to support this database.*?\n\n/m, insert_statements)
51
- end
52
-
53
- # Remove unwanted schema statements
54
- content.gsub!(/^.*?create_schema "ag_catalog".*?\n\n/m, '')
55
- content.gsub!(/^.*?create_schema "age_schema".*?\n\n/m, '')
56
- content.gsub!(/^.*?enable_extension "age".*?\n\n/m, '')
57
- content.gsub!(/^.*?# Could not dump table "_ag_label_edge" because of following StandardError.*?\n\n/m, '')
58
- content.gsub!(/^.*?# Could not dump table "_ag_label_vertex" because of following StandardError.*?\n\n/m, '')
59
- content.gsub!(/^.*?# Could not dump table "ag_graph" because of following StandardError.*?\n\n/m, '')
60
- content.gsub!(/^.*?# Could not dump table "ag_label" because of following StandardError.*?\n\n/m, '')
61
- content.gsub!(/^.*?add_foreign_key "ag_label", "ag_graph".*?\n\n/m, '')
62
-
63
- File.write(schema_file, content)
64
- puts "Updated #{schema_file} with necessary extensions and configurations."
65
- else
66
- puts "schema.rb file not found. Please ensure migrations have been run."
67
- end
16
+ # ensure the config/database.yml file has the proper configurations
17
+ Rake::Task["apache_age:database_config"].invoke
68
18
  end
69
19
  end
70
-
71
- # namespace :rails_age do
72
- # desc "Copy migrations from rails_age to application"
73
- # task :install => :environment do
74
- # source = File.expand_path('../../../db/migrate', __FILE__)
75
- # destination = File.expand_path('../../../../db/migrate', __FILE__)
76
-
77
- # FileUtils.mkdir_p(destination) unless File.exists?(destination)
78
-
79
- # Dir.glob("#{source}/*.rb").each do |file|
80
- # filename = File.basename(file)
81
- # destination_file = File.join(destination, filename)
82
-
83
- # if File.exists?(destination_file)
84
- # puts "Skipping #{filename}, it already exists"
85
- # else
86
- # FileUtils.cp(file, destination_file)
87
- # puts "Copied #{filename} to #{destination}"
88
- # end
89
- # end
90
- # end
91
- # end
@@ -0,0 +1,95 @@
1
+ # lib/tasks/install.rake
2
+ # Usage: `rake apache_age:schema_config`
3
+ #
4
+ namespace :apache_age do
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__)
8
+ destination_schema = File.expand_path("#{Rails.root}/db/schema.rb", __FILE__)
9
+
10
+ unless File.exist?(destination_schema)
11
+ puts "local db/schema.rb file not found. please run db:create and db:migrate first"
12
+ else
13
+ destination_content = File.read(destination_schema)
14
+
15
+ # Remove unwanted schema statements
16
+ destination_content.gsub!(%r{^.*?create_schema "ag_catalog".*?\n}, '')
17
+ destination_content.gsub!(%r{^.*?create_schema "age_schema".*?\n}, '')
18
+ destination_content.gsub!(%r{^.*?enable_extension "age".*?\n}, '')
19
+ destination_content.gsub!(%r{^.*?enable_extension "plpgsql".*?\n}, '')
20
+ destination_content.gsub!(%r{^.*?# Could not dump table "ag_graph" because of following StandardError.*?\n}, '')
21
+ destination_content.gsub!(%r{^.*?# Unknown type 'regnamespace' for column 'namespace'.*?\n}, '')
22
+ destination_content.gsub!(%r{^.*?# Could not dump table "ag_label" because of following StandardError.*?\n}, '')
23
+ destination_content.gsub!(%r{^.*?# Unknown type 'regclass' for column 'relation'.*?\n}, '')
24
+ destination_content.gsub!(%r{^.*?# Unknown type 'graphid' for column 'id'.*?\n}, '')
25
+ destination_content.gsub!(
26
+ %r{^.*?# Could not dump table "_ag_label_edge" because of following StandardError.*?\n}, ''
27
+ )
28
+ destination_content.gsub!(
29
+ %r{^.*?# Could not dump table "_ag_label_vertex" because of following StandardError.*?\n}, ''
30
+ )
31
+ destination_content.gsub!(%r{^.*?# Could not dump table "ag_graph" because of following StandardError.*?\n}, '')
32
+ destination_content.gsub!(%r{^.*?# Could not dump table "ag_label" because of following StandardError.*?\n}, '')
33
+ destination_content.gsub!(%r{^.*?add_foreign_key "ag_label", "ag_graph".*?\n}, '')
34
+
35
+ # add necessary contents (as needed)
36
+ if destination_content.include?(%{execute("LOAD 'age';")}) &&
37
+ destination_content.include?(%{enable_extension 'plpgsql'}) &&
38
+ destination_content.include?(%{execute("SELECT create_graph('age_schema');")}) &&
39
+ destination_content.include?(%{execute('CREATE EXTENSION IF NOT EXISTS age;')}) &&
40
+ destination_content.include?(%{execute('SET search_path = ag_catalog, "$user", public;')})
41
+ puts "schema.rb is properly configured, nothing to do"
42
+ else
43
+ # if not all are found then remove any found
44
+ destination_content.gsub!(%r{^.*?execute("LOAD 'age';")*?\n}, '')
45
+ destination_content.gsub!(%r{^.*?enable_extension 'plpgsql'*?\n}, '')
46
+ destination_content.gsub!(%r{^.*?execute("SELECT create_graph('age_schema');")*?\n}, '')
47
+ destination_content.gsub!(%r{^.*?execute('CREATE EXTENSION IF NOT EXISTS age;')*?\n}, '')
48
+ destination_content.gsub!(%r{^.*?execute('SET search_path = ag_catalog, "$user", public;')*?\n}, '')
49
+ destination_content.gsub!(%r{^.*?# Allow age extension*?\n}, '')
50
+ destination_content.gsub!(%r{^.*?# Load the ag_catalog into the search path*?\n}, '')
51
+ destination_content.gsub!(%r{^.*?# Create age_schema graph if it doesn't exist*?\n}, '')
52
+ destination_content.gsub!(%r{^.*?# These are extensions that must be enabled in order to support this database*?\n}, '')
53
+
54
+ # add all of the correct settings back in
55
+ # source_content = File.read(source_schema)
56
+ source_content =
57
+ <<~RUBY
58
+ ActiveRecord::Schema[7.1].define(version: 2024_05_21_062349) do
59
+ # These are extensions that must be enabled in order to support this database
60
+ enable_extension 'plpgsql'
61
+
62
+ # Allow age extension
63
+ execute('CREATE EXTENSION IF NOT EXISTS age;')
64
+
65
+ # Load the age code
66
+ execute("LOAD 'age';")
67
+
68
+ # Load the ag_catalog into the search path
69
+ execute('SET search_path = ag_catalog, "$user", public;')
70
+
71
+ # Create age_schema graph if it doesn't exist
72
+ execute("SELECT create_graph('age_schema');")
73
+ end
74
+ RUBY
75
+
76
+ age_config_contents =
77
+ source_content.gsub(
78
+ /.*ActiveRecord::Schema\[\d+\.\d+\]\.define\(version: \d{4}(?:_\d{2}){2}(?:_\d{6})?\) do\n|\nend$/,
79
+ ''
80
+ )
81
+
82
+ destination_content.sub!(
83
+ %r{(ActiveRecord::Schema\[\d+\.\d+\]\.define\(version: \d{4}(?:_\d{2}){2}(?:_\d{6})?\) do\n)},
84
+ "\\1#{age_config_contents}\n"
85
+ )
86
+ end
87
+
88
+ # Remove multiple consecutive empty lines
89
+ destination_content.gsub!(/\n{2,}/, "\n\n")
90
+
91
+ File.write(destination_schema, destination_content)
92
+ puts "The schema '#{destination_schema}' is ready to work with Apache Age."
93
+ end
94
+ end
95
+ 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.2.0
4
+ version: 0.3.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-05-26 00:00:00.000000000 Z
11
+ date: 2024-06-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -44,8 +44,8 @@ dependencies:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
46
  version: '6.0'
47
- description: This plugin integrates Apache AGE with Rails 7.x, providing tools and
48
- helpers for working with graph databases within a Rails application.
47
+ description: This plugin integrates Apache AGE for PostgreSQL with Rails 7.x, providing
48
+ tools and helpers for working with graph databases within a Rails application.
49
49
  email:
50
50
  - btihen@gmail.com
51
51
  executables: []
@@ -65,7 +65,7 @@ files:
65
65
  - app/models/rails_age/application_record.rb
66
66
  - app/views/layouts/rails_age/application.html.erb
67
67
  - config/routes.rb
68
- - db/migrate/20240521062349_configure_apache_age.rb
68
+ - db/migrate/20240521062349_add_apache_age.rb
69
69
  - db/schema.rb
70
70
  - lib/apache_age/entities/class_methods.rb
71
71
  - lib/apache_age/entities/common_methods.rb
@@ -79,8 +79,11 @@ files:
79
79
  - lib/rails_age.rb
80
80
  - lib/rails_age/engine.rb
81
81
  - lib/rails_age/version.rb
82
+ - lib/tasks/copy_migrations.rake
83
+ - lib/tasks/database_config.rake
84
+ - lib/tasks/install.original.rake
82
85
  - lib/tasks/install.rake
83
- - lib/tasks/rails_age_tasks.rake
86
+ - lib/tasks/schema_config.rake
84
87
  homepage: https://github.com/marpori/rails_age
85
88
  licenses:
86
89
  - MIT
@@ -1,4 +0,0 @@
1
- # desc "Explaining what the task does"
2
- # task :rails_age do
3
- # # Task goes here
4
- # end