rails_age 0.3.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: aa956147280cd1ef2125eb39f3dd7867eadafde15806d95b90dab30d1290b135
4
- data.tar.gz: a5b3a61d2fcf3026f9503b1bb4a9c6e60c6317f92266501f2890c2a02af1a01d
3
+ metadata.gz: 272f8908dc8fb8a98adb443411e4b439eff914c8173b5eb64b53fa5362fa1572
4
+ data.tar.gz: bf1921611698ffc1b651787f2e1b1a5d1bc503cbe1300639b2f9dada8202cdde
5
5
  SHA512:
6
- metadata.gz: a2dbb2a72e8f64056bf6531fab29da715bdc89d1748dce2fe92a44de11b5a8c74dbc354c18a903472c996c90e01f7947c921819d3cded09b4ffbfe1cbd0da33c
7
- data.tar.gz: d5265daf4c4a1928bdd8f7f08a1efd884f8c4ce5da38d56321f58f6f507905a24d7587d2f92bacfe5a4141e500f7f81060726a2723368b443d8892bf2e66a9b3
6
+ metadata.gz: 51592c31e872a9b8dc6e96fe8fa42374241496e4da6a17ce70bf63f5f118ac078e81200259c048529e7e16b2147c8c5bbef00315cee8f26237295d04ffaea887
7
+ data.tar.gz: 0b24560f7703e4dc4f97d3d36de626906a47a2cda5f5137cc6c1840a5281bbcf2693743666fb6301b26723c9723244bd1854c9c8b21d5d657eb621d4caf0d864
data/CHANGELOG.md CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  ## VERSION 0.4.0 - 2024-xx-xx
4
4
 
5
- - **Edges**
6
- * `find_edge` is deprecated - use `find_by` with :start_node, :end_node to find an edge with specific nodes
7
5
  - **cypher**
8
6
  * query support
9
7
  * paths support
@@ -13,14 +11,19 @@
13
11
 
14
12
  ## VERSION 0.3.1 - 2024-xx-xx
15
13
 
16
- - **Genetator**
14
+ - **Generators**
17
15
  * add `rails generate apache_age:node` to create a node model (with its type in initializer)
18
16
  * add `rails generate apache_age:edge` to create an edge model (with its type in initializer)
19
17
  - **Installer**
20
- * refactored into multiple independent tasks?
18
+ * refactor into multiple independent tasks with tests
19
+ - **Documentation**
20
+ * updated README with additional information
21
+ * added `db/structure.sql` config to README
21
22
 
22
23
  ## VERSION 0.3.0 - 2024-05-28
23
24
 
25
+ - **Edges**
26
+ * `find_by(start_node:, :end_node:, properties:)` to find an edge with specific nodes & properties (deprecated `find_edge`)
24
27
  - **Installer** (`rails generate apache_age:install`)
25
28
  * copy Age PG Extenstion migration to `db/migrate`
26
29
  * run the AGE PG Migration
data/README.md CHANGED
@@ -14,6 +14,8 @@ gem "rails_age"
14
14
 
15
15
  ### Quick Install
16
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.
18
+
17
19
  ```bash
18
20
  $ bundle
19
21
  $ bin/rails apache_age:install
@@ -21,7 +23,9 @@ $ git add .
21
23
  $ git commit -m "Add Apache Age to Rails"
22
24
  ```
23
25
 
24
- NOTE: it is important to add the db/schema.rb to your git repository because `rails db:migrate` will inappropriately modify the schema file. However, you can run `bin/rails apache_age:install` at any time to repair the schema file if needed.
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.**
27
+
28
+ For now, if you are using `db/structure.sql` you will need to manually configure Apache Age (RailsAge) as described below.
25
29
 
26
30
  ### Manual Install
27
31
 
@@ -76,6 +80,7 @@ $ bin/rails db:migrate
76
80
 
77
81
  Rails migrate will mangle the schema `db/schema.rb` file. You need to remove the lines that look like:
78
82
  ```ruby
83
+ ActiveRecord::Schema[7.1].define(version: 2024_05_21_062349) do
79
84
  create_schema "ag_catalog"
80
85
  create_schema "age_schema"
81
86
 
@@ -99,12 +104,14 @@ Rails migrate will mangle the schema `db/schema.rb` file. You need to remove th
99
104
 
100
105
  # other migrations
101
106
  # ...
107
+ end
102
108
  ```
103
109
 
104
110
  and replace them with the following lines:
105
111
  ```ruby
112
+ ActiveRecord::Schema[7.1].define(version: 2024_05_21_062349) do
106
113
  # These are extensions that must be enabled in order to support this database
107
- enable_extension "plpgsql"
114
+ enable_extension 'plpgsql'
108
115
 
109
116
  # Allow age extension
110
117
  execute('CREATE EXTENSION IF NOT EXISTS age;')
@@ -120,14 +127,38 @@ and replace them with the following lines:
120
127
 
121
128
  # other migrations
122
129
  # ...
130
+ end
123
131
  ```
124
132
 
125
- NOTE: I like to add the schema.rb to git so that it is easy to revert the unwanted changes and keep the desired changes.
126
- ALSO note that running: `bin/rails apache_age:install` will check and non-destructively repair any config files at any time (however a git commit before hand as a backup is a good idea incase something goes wrong!)
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
+ ```
127
158
 
128
159
  ## Contributing
129
160
 
130
- 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.
131
162
 
132
163
  ## License
133
164
 
@@ -135,9 +166,12 @@ The gem is available as open source under the terms of the [MIT License](https:/
135
166
 
136
167
  ## Usage
137
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
+
138
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.
139
173
 
140
- 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.
141
175
 
142
176
  ### Nodes
143
177
 
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;')
@@ -1,3 +1,3 @@
1
1
  module RailsAge
2
- VERSION = '0.3.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
@@ -2,209 +2,18 @@
2
2
  # Usage: `rake apache_age:install`
3
3
  #
4
4
  namespace :apache_age do
5
- desc "Copy migrations from rails_age to application and update schema"
5
+ desc "Install & configure Apache Age within Rails (updates migrations, schema & database.yml)"
6
6
  task :install => :environment do
7
- source_schema = File.expand_path('../../../db/schema.rb', __FILE__)
8
- destination_schema = File.expand_path("#{Rails.root}/db/schema.rb", __FILE__)
7
+ # copy our migrations to the application (if needed)
8
+ Rake::Task["apache_age:copy_migrations"].invoke
9
9
 
10
- # ensure we have a schema file
11
- run_migrations
12
-
13
- # copy our migrations to the application
14
- last_migration_version = copy_migrations
15
-
16
- # check if the schema is blank (NEW) before running migrations!
17
- is_schema_blank = blank_schema?(destination_schema)
18
- puts "Schema is blank: #{is_schema_blank}"
19
-
20
- # run our new migrations
21
- run_migrations
22
-
23
- # adjust the schema file (unfortunately rails mangles the schema file)
24
- if is_schema_blank
25
- puts "creating new schema..."
26
- create_new_schema(last_migration_version, destination_schema, source_schema)
27
- else
28
- puts "updating existing schema..."
29
- update_existing_schema(last_migration_version, destination_schema, source_schema)
30
- end
31
-
32
- update_database_yml
33
- end
34
-
35
- def copy_migrations
36
- migration_version = nil
37
-
38
- source = File.expand_path('../../../db/migrate', __FILE__)
39
- destination = File.expand_path("#{Rails.root}/db/migrate", __FILE__)
40
-
41
- FileUtils.mkdir_p(destination) unless File.exist?(destination)
42
- existing_migrations =
43
- Dir.glob("#{destination}/*.rb").map { |file| File.basename(file).sub(/^\d+/, '') }
44
-
45
- Dir.glob("#{source}/*.rb").each do |file|
46
- filename = File.basename(file)
47
- test_name = filename.sub(/^\d+/, '')
48
-
49
- if existing_migrations.include?(test_name)
50
- puts "Skipping #{filename}, it already exists"
51
- else
52
- migration_version = Time.now.utc.strftime("%Y_%m_%d_%H%M%S")
53
- file_version = migration_version.delete('_')
54
- new_filename = filename.sub(/^\d+/, file_version)
55
- destination_file = File.join(destination, new_filename)
56
- FileUtils.cp(file, destination_file)
57
- puts "Copied #{filename} to #{destination} as #{new_filename}"
58
- end
59
- end
60
- migration_version
61
- end
62
-
63
- def blank_schema?(destination_schema)
64
- return false unless File.exist?(destination_schema)
65
-
66
- content = File.read(destination_schema)
67
- content.include?('define(version: 0)') &&
68
- content.include?('enable_extension "plpgsql"') &&
69
- content.scan(/enable_extension/).size == 1
70
- end
71
-
72
- def run_migrations
73
- puts "Running migrations..."
10
+ # run any new migrations
74
11
  Rake::Task["db:migrate"].invoke
75
- end
76
-
77
- def extract_rails_version(destination_schema)
78
- if File.exist?(destination_schema)
79
- content = File.read(destination_schema)
80
- version_match = content.match(/ActiveRecord::Schema\[(.*?)\]/)
81
- return version_match[1] if version_match
82
- else
83
- full_version = Rails.version
84
- primary_secondary_version = full_version.split('.')[0..1].join('.')
85
- primary_secondary_version
86
- end
87
- end
88
12
 
89
- def create_new_schema(last_migration_version, destination_schema, source_schema)
90
- if File.exist?(source_schema) && File.exist?(destination_schema)
91
- rails_version = extract_rails_version(destination_schema)
92
- source_content = File.read(source_schema)
93
-
94
- # ensure we use the Rails version from the destination schema
95
- source_content.gsub!(
96
- /ActiveRecord::Schema\[\d+\.\d+\]/,
97
- "ActiveRecord::Schema[#{rails_version}]"
98
- )
99
- # ensure we use the last migration version (not the source schema version)
100
- source_content.gsub!(
101
- /define\(version: \d{4}(?:_\d{2}){2}(?:_\d{6})?\) do/,
102
- "define(version: #{last_migration_version}) do"
103
- )
104
-
105
- File.write(destination_schema, source_content)
106
- puts "Created new schema in #{destination_schema} with necessary extensions and configurations."
107
- else
108
- puts "local db/schema.rb file not found."
109
- end
110
- end
111
-
112
- def update_existing_schema(last_migration_version, destination_schema, source_schema)
113
- if File.exist?(source_schema) && File.exist?(destination_schema)
114
- rails_version = extract_rails_version(destination_schema)
115
- source_content = File.read(source_schema)
116
- new_content =
117
- source_content.gsub(
118
- /.*ActiveRecord::Schema\[\d+\.\d+\]\.define\(version: \d{4}(?:_\d{2}){2}(?:_\d{6})?\) do\n|\nend$/,
119
- ''
120
- )
121
-
122
- destination_content = File.read(destination_schema)
123
-
124
- # Remove unwanted schema statements
125
- destination_content.gsub!(%r{^.*?# These are extensions that must be enabled in order to support this database.*?\n}, '')
126
-
127
- destination_content.gsub!(%r{^.*?create_schema "ag_catalog".*?\n}, '')
128
- destination_content.gsub!(%r{^.*?create_schema "age_schema".*?\n}, '')
129
- destination_content.gsub!(%r{^.*?enable_extension "age".*?\n}, '')
130
- destination_content.gsub!(%r{^.*?enable_extension "plpgsql".*?\n}, '')
131
- destination_content.gsub!(%r{^.*?# Could not dump table "ag_graph" because of following StandardError.*?\n}, '')
132
- destination_content.gsub!(%r{^.*?# Unknown type 'regnamespace' for column 'namespace'.*?\n}, '')
133
- destination_content.gsub!(%r{^.*?# Could not dump table "ag_label" because of following StandardError.*?\n}, '')
134
- destination_content.gsub!(%r{^.*?# Unknown type 'regclass' for column 'relation'.*?\n}, '')
135
- destination_content.gsub!(%r{^.*?# Unknown type 'graphid' for column 'id'.*?\n}, '')
136
- destination_content.gsub!(
137
- %r{^.*?# Could not dump table "_ag_label_edge" because of following StandardError.*?\n}, ''
138
- )
139
- destination_content.gsub!(
140
- %r{^.*?# Could not dump table "_ag_label_vertex" because of following StandardError.*?\n}, ''
141
- )
142
- destination_content.gsub!(%r{^.*?# Could not dump table "ag_graph" because of following StandardError.*?\n}, '')
143
- destination_content.gsub!(%r{^.*?# Could not dump table "ag_label" because of following StandardError.*?\n}, '')
144
- destination_content.gsub!(%r{^.*?add_foreign_key "ag_label", "ag_graph".*?\n}, '')
145
-
146
- # add new wanted schema statements (at the top of the schema)
147
- destination_content.sub!(
148
- %r{(ActiveRecord::Schema\[\d+\.\d+\]\.define\(version: \d{4}(?:_\d{2}){2}(?:_\d{6})?\) do\n)},
149
- "\\1#{new_content}\n"
150
- )
151
-
152
- existing_version = destination_content.match(/define\(version: (\d{4}(?:_\d{2}){2}(?:_\d{6})?)\)/)[1].gsub('_', '')
153
- current_version = last_migration_version ? last_migration_version.gsub('_', '') : existing_version
154
-
155
- # ensure we use the last migration version (not the source schema version)
156
- if current_version.to_i > existing_version.to_i
157
- destination_content.gsub!(
158
- /define\(version: \d{4}(?:_\d{2}){2}(?:_\d{6})?\) do/,
159
- "define(version: #{last_migration_version}) do"
160
- )
161
- end
162
-
163
- File.write(destination_schema, destination_content)
164
- puts "Updated #{destination_schema} with necessary extensions and configurations."
165
- else
166
- puts "local db/schema.rb file not found."
167
- end
168
- end
169
-
170
- def update_database_yml
171
- db_config_file = File.expand_path("#{Rails.root}/config/database.yml", __FILE__)
172
-
173
- # Read the file
174
- lines = File.readlines(db_config_file)
175
-
176
- # any uncommented "schema_search_path:" lines?
177
- path_index = lines.find_index { |line| !line.include?('#') && line.include?('schema_search_path:') }
178
- default_start_index = lines.index { |line| line.strip.start_with?('default:') }
179
-
180
- # when it finds an existing schema_search_path, it updates it
181
- if path_index && lines[path_index].include?('ag_catalog,age_schema')
182
- puts "schema_search_path already set to ag_catalog,age_schema nothing to do."
183
- return
184
- elsif path_index
185
- key, val = lines[path_index].split(': ')
186
- # remove any unwanted characters
187
- val = val.gsub(/[ "\s\"\"'\n]/, '')
188
- lines[path_index] = "#{key}: ag_catalog,age_schema,#{val}\n"
189
- puts "add ag_catalog,age_schema to schema_search_path"
190
- elsif default_start_index
191
- puts "add ag_catalog,age_schema,public to schema_search_path in the default section of database.yml"
192
- sections_index = lines.map.with_index { |line, index| index if !line.start_with?(' ') }.compact.sort
193
-
194
- # find the start of the default section
195
- next_section_in_list = sections_index.index(default_start_index) + 1
196
-
197
- # find the end of the default section (before the next section starts)
198
- path_insert_index = sections_index[next_section_in_list]
199
-
200
- lines.insert(path_insert_index, " schema_search_path: ag_catalog,age_schema,public\n")
201
- else
202
- puts "didn't find a default section in database.yml, please add the following line:"
203
- puts " schema_search_path: ag_catalog,age_schema,public"
204
- puts "to the apprpriate section of your database.yml"
205
- end
13
+ # adjust the schema file (unfortunately rails mangles the schema file)
14
+ Rake::Task["apache_age:schema_config"].invoke
206
15
 
207
- # Write the modified lines back to the file
208
- File.open(db_config_file, 'w') { |file| file.write(lines.join) }
16
+ # ensure the config/database.yml file has the proper configurations
17
+ Rake::Task["apache_age:database_config"].invoke
209
18
  end
210
19
  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.3.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-28 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
@@ -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