activecypher 0.6.3 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/active_cypher/associations.rb +0 -3
- data/lib/active_cypher/connection_adapters/memgraph_adapter.rb +40 -0
- data/lib/active_cypher/connection_adapters/neo4j_adapter.rb +33 -0
- data/lib/active_cypher/generators/migration_generator.rb +18 -0
- data/lib/active_cypher/generators/templates/migration.rb.erb +5 -0
- data/lib/active_cypher/generators/templates/relationship.rb.erb +3 -3
- data/lib/active_cypher/instrumentation.rb +1 -1
- data/lib/active_cypher/migration.rb +81 -0
- data/lib/active_cypher/migrator.rb +79 -0
- data/lib/active_cypher/railtie.rb +6 -0
- data/lib/active_cypher/schema/catalog.rb +14 -0
- data/lib/active_cypher/schema/dumper.rb +96 -0
- data/lib/active_cypher/schema/writer/cypher.rb +75 -0
- data/lib/active_cypher/version.rb +1 -1
- data/lib/cyrel/node.rb +0 -2
- data/lib/tasks/graphdb_migrate.rake +16 -0
- metadata +9 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 001236241c04680c15946488d5f7e915100342dad161abe512f5e1d1f47bb84f
|
4
|
+
data.tar.gz: e6cbcd8f09b83bdfbb90830aded7a39ad7afc91c7aef03704d7dba8a55bcc12d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2e4a8e7f74da14f8013152a7a4adebfbc0cab73be88919a3ae14994f2fbeaab1599832659fd31b6b4f7a6c5f14de14439aad29e913b17981bc62b2f46fdf824d
|
7
|
+
data.tar.gz: b856e47065693eaab0152246a2aa7ee475706ef9c0b54c61d8e3746438971c670cb0157087d6f4456ee3e5757619e114d796880d5213d72c62048228007bc9ba
|
@@ -1,8 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'active_support/concern'
|
4
|
-
require 'active_support/core_ext/string/inflections' # for camelize, singularize etc.
|
5
|
-
|
6
3
|
module ActiveCypher
|
7
4
|
# Module to handle association definitions (has_many, belongs_to, etc.)
|
8
5
|
# for ActiveCypher models.
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
require "active_cypher/schema/catalog"
|
2
3
|
|
3
4
|
module ActiveCypher
|
4
5
|
module ConnectionAdapters
|
@@ -6,6 +7,15 @@ module ActiveCypher
|
|
6
7
|
# Register this adapter with the registry
|
7
8
|
Registry.register('memgraph', self)
|
8
9
|
|
10
|
+
def vendor = :memgraph
|
11
|
+
|
12
|
+
def schema_catalog
|
13
|
+
rows = run('SHOW SCHEMA')
|
14
|
+
parse_schema(rows)
|
15
|
+
rescue StandardError
|
16
|
+
introspect_fallback
|
17
|
+
end
|
18
|
+
|
9
19
|
# Use id() for Memgraph instead of elementId()
|
10
20
|
ID_FUNCTION = 'id'
|
11
21
|
|
@@ -58,6 +68,36 @@ module ActiveCypher
|
|
58
68
|
|
59
69
|
protected
|
60
70
|
|
71
|
+
def parse_schema(rows)
|
72
|
+
nodes, edges, idx, cons = [], [], [], []
|
73
|
+
|
74
|
+
rows.each do |row|
|
75
|
+
case row['type']
|
76
|
+
when 'NODE'
|
77
|
+
nodes << Schema::NodeTypeDef.new(row['label'], row['properties'], row['primaryKey'])
|
78
|
+
when 'EDGE'
|
79
|
+
edges << Schema::EdgeTypeDef.new(row['label'], row['from'], row['to'], row['properties'])
|
80
|
+
when 'INDEX'
|
81
|
+
idx << Schema::IndexDef.new(row['name'], :node, row['label'], row['properties'], row['unique'], nil)
|
82
|
+
when 'CONSTRAINT'
|
83
|
+
cons << Schema::ConstraintDef.new(row['name'], row['label'], row['properties'], :unique)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
Schema::Catalog.new(indexes: idx, constraints: cons, node_types: nodes, edge_types: edges)
|
88
|
+
end
|
89
|
+
|
90
|
+
def introspect_fallback
|
91
|
+
labels = run('MATCH (n) RETURN DISTINCT labels(n) AS lbl').flat_map { |r| r['lbl'] }
|
92
|
+
|
93
|
+
nodes = labels.map do |lbl|
|
94
|
+
props = run("MATCH (n:`#{lbl}`) WITH n LIMIT 100 UNWIND keys(n) AS k RETURN DISTINCT k").map { |r| r['k'] }
|
95
|
+
Schema::NodeTypeDef.new(lbl, props, nil)
|
96
|
+
end
|
97
|
+
|
98
|
+
Schema::Catalog.new(indexes: [], constraints: [], node_types: nodes, edge_types: [])
|
99
|
+
end
|
100
|
+
|
61
101
|
def protocol_handler_class = ProtocolHandler
|
62
102
|
|
63
103
|
def validate_connection
|
@@ -1,10 +1,43 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
require "active_cypher/schema/catalog"
|
2
3
|
|
3
4
|
module ActiveCypher
|
4
5
|
module ConnectionAdapters
|
5
6
|
class Neo4jAdapter < AbstractBoltAdapter
|
6
7
|
Registry.register('neo4j', self)
|
7
8
|
|
9
|
+
def vendor = :neo4j
|
10
|
+
|
11
|
+
def schema_catalog
|
12
|
+
idx_rows = run('SHOW INDEXES')
|
13
|
+
con_rows = run('SHOW CONSTRAINTS')
|
14
|
+
|
15
|
+
idx_defs = idx_rows.map do |r|
|
16
|
+
Schema::IndexDef.new(
|
17
|
+
r['name'],
|
18
|
+
r['entityType'].downcase.to_sym,
|
19
|
+
r['labelsOrTypes'].first,
|
20
|
+
r['properties'],
|
21
|
+
r['uniqueness'] == 'UNIQUE',
|
22
|
+
r['type'] == 'VECTOR' ? r['options'] : nil
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
con_defs = con_rows.map do |r|
|
27
|
+
Schema::ConstraintDef.new(
|
28
|
+
r['name'],
|
29
|
+
r['labelsOrTypes'].first,
|
30
|
+
r['properties'],
|
31
|
+
r['type'].split('_').first.downcase.to_sym
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
Schema::Catalog.new(indexes: idx_defs, constraints: con_defs,
|
36
|
+
node_types: [], edge_types: [])
|
37
|
+
rescue StandardError
|
38
|
+
Schema::Catalog.new(indexes: [], constraints: [], node_types: [], edge_types: [])
|
39
|
+
end
|
40
|
+
|
8
41
|
# Use elementId() for Neo4j
|
9
42
|
ID_FUNCTION = 'elementId'
|
10
43
|
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators/named_base'
|
4
|
+
|
5
|
+
module ActiveCypher
|
6
|
+
module Generators
|
7
|
+
class MigrationGenerator < Rails::Generators::NamedBase
|
8
|
+
source_root File.expand_path('templates', __dir__)
|
9
|
+
|
10
|
+
def create_migration_file
|
11
|
+
timestamp = Time.now.utc.strftime('%Y%m%d%H%M%S')
|
12
|
+
dir = File.join('graphdb', 'migrate')
|
13
|
+
FileUtils.mkdir_p(dir)
|
14
|
+
template 'migration.rb.erb', File.join(dir, "#{timestamp}_#{file_name}.rb")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class <%= class_name %> < ApplicationGraphRelationship
|
4
|
-
from_class
|
5
|
-
to_class
|
6
|
-
type
|
4
|
+
from_class '<%= options[:from] %>'
|
5
|
+
to_class '<%= options[:to] %>'
|
6
|
+
type '<%= relationship_type %>'
|
7
7
|
<% if attributes.any? -%>
|
8
8
|
<% attributes.each do |attr| -%>
|
9
9
|
attribute :<%= attr.name %>, :<%= attr.type || "string" %>
|
@@ -132,7 +132,7 @@ module ActiveCypher
|
|
132
132
|
# @param key [String, Symbol] The key to check
|
133
133
|
# @return [Boolean] True if the key contains sensitive information
|
134
134
|
def sensitive_key?(key)
|
135
|
-
return true if key.to_s.match?(
|
135
|
+
return true if key.to_s.match?(/(^|[\-_])(?:password|token|secret|credential|key)($|[\-_])/i)
|
136
136
|
|
137
137
|
# Check against Rails filter parameters if available
|
138
138
|
if defined?(Rails) && Rails.application
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveCypher
|
4
|
+
# Base class for GraphDB migrations.
|
5
|
+
# Provides a small DSL for defining index and constraint operations.
|
6
|
+
class Migration
|
7
|
+
class << self
|
8
|
+
attr_reader :up_block
|
9
|
+
|
10
|
+
# Define the migration steps.
|
11
|
+
def up(&block)
|
12
|
+
@up_block = block if block_given?
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :connection, :operations
|
17
|
+
|
18
|
+
def initialize(connection = ActiveCypher::Base.connection)
|
19
|
+
@connection = connection
|
20
|
+
@operations = []
|
21
|
+
end
|
22
|
+
|
23
|
+
# Execute the migration.
|
24
|
+
def run
|
25
|
+
instance_eval(&self.class.up_block) if self.class.up_block
|
26
|
+
execute_operations
|
27
|
+
end
|
28
|
+
|
29
|
+
# DSL ---------------------------------------------------------------
|
30
|
+
|
31
|
+
def create_node_index(label, *props, unique: false, if_not_exists: true, name: nil)
|
32
|
+
props_clause = props.map { |p| "n.#{p}" }.join(', ')
|
33
|
+
cypher = +'CREATE '
|
34
|
+
cypher << 'UNIQUE ' if unique
|
35
|
+
cypher << 'INDEX'
|
36
|
+
cypher << " #{name}" if name
|
37
|
+
cypher << ' IF NOT EXISTS' if if_not_exists
|
38
|
+
cypher << " FOR (n:#{label}) ON (#{props_clause})"
|
39
|
+
operations << cypher
|
40
|
+
end
|
41
|
+
|
42
|
+
def create_rel_index(rel_type, *props, if_not_exists: true, name: nil)
|
43
|
+
props_clause = props.map { |p| "r.#{p}" }.join(', ')
|
44
|
+
cypher = +'CREATE INDEX'
|
45
|
+
cypher << " #{name}" if name
|
46
|
+
cypher << ' IF NOT EXISTS' if if_not_exists
|
47
|
+
cypher << " FOR ()-[r:#{rel_type}]-() ON (#{props_clause})"
|
48
|
+
operations << cypher
|
49
|
+
end
|
50
|
+
|
51
|
+
def create_uniqueness_constraint(label, *props, if_not_exists: true, name: nil)
|
52
|
+
props_clause = props.map { |p| "n.#{p}" }.join(', ')
|
53
|
+
cypher = +'CREATE CONSTRAINT'
|
54
|
+
cypher << " #{name}" if name
|
55
|
+
cypher << ' IF NOT EXISTS' if if_not_exists
|
56
|
+
cypher << " FOR (n:#{label}) REQUIRE (#{props_clause}) IS UNIQUE"
|
57
|
+
operations << cypher
|
58
|
+
end
|
59
|
+
|
60
|
+
def execute(cypher_string)
|
61
|
+
operations << cypher_string.strip
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def execute_operations
|
67
|
+
tx = connection.begin_transaction if connection.respond_to?(:begin_transaction)
|
68
|
+
operations.each do |cypher|
|
69
|
+
if tx
|
70
|
+
tx.run(cypher)
|
71
|
+
else
|
72
|
+
connection.execute_cypher(cypher)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
connection.commit_transaction(tx) if tx
|
76
|
+
rescue StandardError
|
77
|
+
connection.rollback_transaction(tx) if tx
|
78
|
+
raise
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveCypher
|
4
|
+
# Runs pending graph database migrations.
|
5
|
+
class Migrator
|
6
|
+
MIGRATE_DIR = File.join('graphdb', 'migrate')
|
7
|
+
|
8
|
+
def initialize(connection = ActiveCypher::Base.connection)
|
9
|
+
@connection = connection
|
10
|
+
end
|
11
|
+
|
12
|
+
def migrate!
|
13
|
+
ensure_schema_migration_constraint
|
14
|
+
applied = existing_versions
|
15
|
+
|
16
|
+
migration_files.each do |file|
|
17
|
+
version = File.basename(file)[0, 14]
|
18
|
+
next if applied.include?(version)
|
19
|
+
|
20
|
+
require file
|
21
|
+
class_name = File.basename(file, '.rb').split('_', 2).last.camelize
|
22
|
+
klass = Object.const_get(class_name)
|
23
|
+
klass.new(@connection).run
|
24
|
+
|
25
|
+
@connection.execute_cypher(<<~CYPHER)
|
26
|
+
CREATE (:SchemaMigration { version: '#{version}', executed_at: datetime() })
|
27
|
+
CYPHER
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def status
|
32
|
+
ensure_schema_migration_constraint
|
33
|
+
applied = existing_versions
|
34
|
+
migration_files.map do |file|
|
35
|
+
version = File.basename(file)[0, 14]
|
36
|
+
{
|
37
|
+
status: (applied.include?(version) ? 'up' : 'down'),
|
38
|
+
version: version,
|
39
|
+
name: File.basename(file)
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def adapter_dir
|
47
|
+
name = @connection.class.name.demodulize.sub('Adapter', '').downcase
|
48
|
+
File.join('graphdb', name)
|
49
|
+
end
|
50
|
+
|
51
|
+
def migration_dirs
|
52
|
+
dirs = [MIGRATE_DIR, adapter_dir]
|
53
|
+
extra = @connection.config[:migrations_paths]
|
54
|
+
dirs.concat(Array(extra)) if extra
|
55
|
+
dirs
|
56
|
+
end
|
57
|
+
|
58
|
+
def migration_files
|
59
|
+
migration_dirs.flat_map do |dir|
|
60
|
+
Dir[File.expand_path(File.join(dir, '*.rb'), Dir.pwd)]
|
61
|
+
end.sort
|
62
|
+
end
|
63
|
+
|
64
|
+
def existing_versions
|
65
|
+
@connection.execute_cypher('MATCH (m:SchemaMigration) RETURN m.version AS version')
|
66
|
+
.map { |r| r[:version].to_s }
|
67
|
+
rescue StandardError
|
68
|
+
[]
|
69
|
+
end
|
70
|
+
|
71
|
+
def ensure_schema_migration_constraint
|
72
|
+
@connection.execute_cypher(<<~CYPHER)
|
73
|
+
CREATE CONSTRAINT graph_schema_migration IF NOT EXISTS
|
74
|
+
FOR (m:SchemaMigration)
|
75
|
+
REQUIRE m.version IS UNIQUE
|
76
|
+
CYPHER
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -64,6 +64,12 @@ module ActiveCypher
|
|
64
64
|
require 'active_cypher/generators/install_generator'
|
65
65
|
require 'active_cypher/generators/node_generator'
|
66
66
|
require 'active_cypher/generators/relationship_generator'
|
67
|
+
require 'active_cypher/generators/migration_generator'
|
68
|
+
end
|
69
|
+
|
70
|
+
rake_tasks do
|
71
|
+
load File.expand_path('../tasks/graphdb_migrate.rake', __dir__)
|
72
|
+
load File.expand_path('../tasks/graphdb_schema.rake', __dir__)
|
67
73
|
end
|
68
74
|
end
|
69
75
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module ActiveCypher
|
2
|
+
module Schema
|
3
|
+
IndexDef = Data.define(:name, :element, :label, :props, :unique, :vector_opts)
|
4
|
+
ConstraintDef = Data.define(:name, :label, :props, :kind)
|
5
|
+
NodeTypeDef = Data.define(:label, :props, :primary_key)
|
6
|
+
EdgeTypeDef = Data.define(:type, :from, :to, :props)
|
7
|
+
|
8
|
+
Catalog = Data.define(:indexes, :constraints, :node_types, :edge_types) do
|
9
|
+
def empty?
|
10
|
+
indexes.empty? && constraints.empty? && node_types.empty? && edge_types.empty?
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'optparse'
|
3
|
+
|
4
|
+
module ActiveCypher
|
5
|
+
module Schema
|
6
|
+
# Dumps the graph schema to a Cypher script
|
7
|
+
class Dumper
|
8
|
+
DEFAULT_PATH = 'graphdb'.freeze
|
9
|
+
|
10
|
+
def initialize(connection = ActiveCypher::Base.connection, base_dir: Dir.pwd)
|
11
|
+
@connection = connection
|
12
|
+
@base_dir = base_dir
|
13
|
+
end
|
14
|
+
|
15
|
+
def dump_to_string
|
16
|
+
cat = @connection.schema_catalog
|
17
|
+
cat = catalog_from_migrations if cat.respond_to?(:empty?) && cat.empty?
|
18
|
+
Writer::Cypher.new(cat, @connection.vendor).to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
def dump_to_file(path)
|
22
|
+
FileUtils.mkdir_p(File.dirname(path))
|
23
|
+
File.write(path, dump_to_string)
|
24
|
+
path
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.run_from_cli(argv = ARGV)
|
28
|
+
opts = { stdout: false, connection: :primary }
|
29
|
+
OptionParser.new do |o|
|
30
|
+
o.on('--stdout') { opts[:stdout] = true }
|
31
|
+
o.on('--connection=NAME') { |v| opts[:connection] = v.to_sym }
|
32
|
+
end.parse!(argv)
|
33
|
+
|
34
|
+
pool = ActiveCypher::Base.connection_handler.pool(opts[:connection])
|
35
|
+
raise "Unknown connection: #{opts[:connection]}" unless pool
|
36
|
+
|
37
|
+
dumper = new(pool.connection)
|
38
|
+
file = output_file(opts[:connection])
|
39
|
+
if opts[:stdout]
|
40
|
+
puts dumper.dump_to_string
|
41
|
+
else
|
42
|
+
dumper.dump_to_file(file)
|
43
|
+
puts "Written #{file}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.output_file(conn)
|
48
|
+
case conn.to_sym
|
49
|
+
when :primary
|
50
|
+
File.join(DEFAULT_PATH, 'schema.cypher')
|
51
|
+
when :analytics
|
52
|
+
File.join(DEFAULT_PATH, 'schema.analytics.cypher')
|
53
|
+
else
|
54
|
+
File.join(DEFAULT_PATH, "schema.#{conn}.cypher")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def catalog_from_migrations
|
61
|
+
idx = []
|
62
|
+
cons = []
|
63
|
+
Dir[File.join(@base_dir, 'graphdb', 'migrate', '*.rb')].sort.each do |file|
|
64
|
+
require file
|
65
|
+
class_name = File.basename(file, '.rb').split('_', 2).last.camelize
|
66
|
+
klass = Object.const_get(class_name)
|
67
|
+
mig = klass.new(@connection)
|
68
|
+
mig.instance_eval(&klass.up_block) if klass.respond_to?(:up_block) && klass.up_block
|
69
|
+
mig.operations.each do |cy|
|
70
|
+
if cy =~ /CREATE\s+(UNIQUE\s+)?INDEX/i
|
71
|
+
unique = !Regexp.last_match(1).nil?
|
72
|
+
name = cy[/CREATE\s+(?:UNIQUE\s+)?INDEX\s+(\w+)/i, 1] || 'idx'
|
73
|
+
label = cy[/\(n:`?([^:`)]+)`?\)/, 1] || 'Unknown'
|
74
|
+
props = cy[/ON \(([^)]+)\)/i, 1].to_s.split(',').map { |p| p.strip.sub(/^n\./, '').sub(/^r\./, '') }
|
75
|
+
elem = cy.include?('-[r:') ? :relationship : :node
|
76
|
+
idx << IndexDef.new(name, elem, label, props, unique, nil)
|
77
|
+
elsif cy =~ /CREATE\s+CONSTRAINT/i
|
78
|
+
name = cy[/CREATE\s+CONSTRAINT\s+(\w+)/i, 1] || 'constraint'
|
79
|
+
label = cy[/\(n:`?([^:`)]+)`?\)/, 1] || 'Unknown'
|
80
|
+
if cy =~ /UNIQUE/i
|
81
|
+
props = cy[/\(([^)]+)\)/, 1].to_s.split(',').map { |p| p.strip.sub(/^n\./, '') }
|
82
|
+
kind = :unique
|
83
|
+
else
|
84
|
+
prop = cy[/n\.(\w+)\s+IS NOT NULL/i, 1]
|
85
|
+
props = [prop].compact
|
86
|
+
kind = :exists
|
87
|
+
end
|
88
|
+
cons << ConstraintDef.new(name, label, props, kind)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
Catalog.new(indexes: idx, constraints: cons, node_types: [], edge_types: [])
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module ActiveCypher
|
2
|
+
module Schema
|
3
|
+
module Writer
|
4
|
+
class Cypher
|
5
|
+
def initialize(catalog, vendor)
|
6
|
+
@catalog = catalog
|
7
|
+
@vendor = vendor
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_s
|
11
|
+
sections = []
|
12
|
+
sections << constraint_lines(@catalog.constraints)
|
13
|
+
sections << index_lines(@catalog.indexes.select { |i| i.element == :node }, :node)
|
14
|
+
sections << index_lines(@catalog.indexes.select { |i| i.element == :relationship }, :relationship)
|
15
|
+
if @vendor == :memgraph
|
16
|
+
sections << node_type_lines(@catalog.node_types)
|
17
|
+
sections << edge_type_lines(@catalog.edge_types)
|
18
|
+
end
|
19
|
+
sections.reject(&:empty?).join("\n")
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def constraint_lines(list)
|
25
|
+
list.sort_by(&:name).map do |c|
|
26
|
+
props = c.props.map { |p| "n.#{p}" }.join(', ')
|
27
|
+
case c.kind
|
28
|
+
when :unique
|
29
|
+
"CREATE CONSTRAINT #{c.name} FOR (n:`#{c.label}`) REQUIRE (#{props}) IS UNIQUE"
|
30
|
+
when :exists
|
31
|
+
"CREATE CONSTRAINT #{c.name} FOR (n:`#{c.label}`) REQUIRE n.#{c.props.first} IS NOT NULL"
|
32
|
+
else
|
33
|
+
"-- UNKNOWN CONSTRAINT #{c.name}"
|
34
|
+
end
|
35
|
+
end.join("\n")
|
36
|
+
end
|
37
|
+
|
38
|
+
def index_lines(list, element)
|
39
|
+
list.sort_by(&:name).map do |i|
|
40
|
+
if @vendor == :memgraph && i.vector_opts
|
41
|
+
"-- NOT-SUPPORTED ON MEMGRAPH 3.2: Vector index #{i.name}"
|
42
|
+
else
|
43
|
+
var = element == :node ? 'n' : 'r'
|
44
|
+
target = element == :node ? "(#{var}:`#{i.label}`)" : "()-[#{var}:`#{i.label}`]-()"
|
45
|
+
props = i.props.map { |p| "#{var}.#{p}" }.join(', ')
|
46
|
+
line = +'CREATE '
|
47
|
+
line << 'UNIQUE ' if i.unique
|
48
|
+
line << "INDEX #{i.name} FOR #{target} ON (#{props})"
|
49
|
+
if i.vector_opts && @vendor == :neo4j
|
50
|
+
opts = i.vector_opts.map { |k, v| "#{k}: #{v}" }.join(', ')
|
51
|
+
line << " OPTIONS { #{opts} }"
|
52
|
+
end
|
53
|
+
line
|
54
|
+
end
|
55
|
+
end.join("\n")
|
56
|
+
end
|
57
|
+
|
58
|
+
def node_type_lines(list)
|
59
|
+
list.sort_by(&:label).map do |nt|
|
60
|
+
props = nt.props.join(', ')
|
61
|
+
pk = nt.primary_key ? " PRIMARY KEY #{nt.primary_key}" : ''
|
62
|
+
"CREATE NODE TYPE #{nt.label}#{pk} PROPERTIES #{props}"
|
63
|
+
end.join("\n")
|
64
|
+
end
|
65
|
+
|
66
|
+
def edge_type_lines(list)
|
67
|
+
list.sort_by(&:type).map do |et|
|
68
|
+
props = et.props.join(', ')
|
69
|
+
"CREATE EDGE TYPE #{et.type} FROM #{et.from} TO #{et.to} PROPERTIES #{props}"
|
70
|
+
end.join("\n")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/lib/cyrel/node.rb
CHANGED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
namespace :graphdb do
|
4
|
+
desc 'Run graph database migrations'
|
5
|
+
task migrate: :environment do
|
6
|
+
ActiveCypher::Migrator.new.migrate!
|
7
|
+
puts 'GraphDB migrations complete'
|
8
|
+
end
|
9
|
+
|
10
|
+
desc 'Show graph database migration status'
|
11
|
+
task status: :environment do
|
12
|
+
ActiveCypher::Migrator.new.status.each do |m|
|
13
|
+
puts format('%-4s %s %s', m[:status], m[:version], m[:name])
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activecypher
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Abdelkader Boudih
|
@@ -132,15 +132,19 @@ files:
|
|
132
132
|
- lib/active_cypher/fixtures/registry.rb
|
133
133
|
- lib/active_cypher/fixtures/rel_builder.rb
|
134
134
|
- lib/active_cypher/generators/install_generator.rb
|
135
|
+
- lib/active_cypher/generators/migration_generator.rb
|
135
136
|
- lib/active_cypher/generators/node_generator.rb
|
136
137
|
- lib/active_cypher/generators/relationship_generator.rb
|
137
138
|
- lib/active_cypher/generators/templates/application_graph_node.rb
|
138
139
|
- lib/active_cypher/generators/templates/application_graph_relationship.rb
|
139
140
|
- lib/active_cypher/generators/templates/cypher_databases.yml
|
141
|
+
- lib/active_cypher/generators/templates/migration.rb.erb
|
140
142
|
- lib/active_cypher/generators/templates/node.rb.erb
|
141
143
|
- lib/active_cypher/generators/templates/relationship.rb.erb
|
142
144
|
- lib/active_cypher/instrumentation.rb
|
143
145
|
- lib/active_cypher/logging.rb
|
146
|
+
- lib/active_cypher/migration.rb
|
147
|
+
- lib/active_cypher/migrator.rb
|
144
148
|
- lib/active_cypher/model/abstract.rb
|
145
149
|
- lib/active_cypher/model/attributes.rb
|
146
150
|
- lib/active_cypher/model/callbacks.rb
|
@@ -158,6 +162,9 @@ files:
|
|
158
162
|
- lib/active_cypher/relation.rb
|
159
163
|
- lib/active_cypher/relationship.rb
|
160
164
|
- lib/active_cypher/runtime_registry.rb
|
165
|
+
- lib/active_cypher/schema/catalog.rb
|
166
|
+
- lib/active_cypher/schema/dumper.rb
|
167
|
+
- lib/active_cypher/schema/writer/cypher.rb
|
161
168
|
- lib/active_cypher/scoping.rb
|
162
169
|
- lib/active_cypher/utils/logger.rb
|
163
170
|
- lib/active_cypher/version.rb
|
@@ -205,6 +212,7 @@ files:
|
|
205
212
|
- lib/cyrel/return_only.rb
|
206
213
|
- lib/cyrel/types/hash_type.rb
|
207
214
|
- lib/cyrel/types/symbol_type.rb
|
215
|
+
- lib/tasks/graphdb_migrate.rake
|
208
216
|
- sig/activecypher.rbs
|
209
217
|
homepage: https://github.com/seuros/activecypher
|
210
218
|
licenses:
|