rails_age 0.2.0 → 0.3.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: 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