graph2relational 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ffd5eda800a15b74f1843d5e5e32c9063d8b2646
4
+ data.tar.gz: a10dc8539499cb64bbcbd88355bfa2a064ce5970
5
+ SHA512:
6
+ metadata.gz: aa6904f22f311b822dad0cfa1575b155cf1b3e03b58a1316eea3f026e4f69a7fc34984f242044c8ed2ebbf75dee188181a82c0f03d97dbb973a18d6cfb2bb6ea
7
+ data.tar.gz: 656a7e830a82e14f0fece65483f6efe32b9c82ba8af15f225839c04037623784ba6e9a09b3455fdbd5a97ec98365116837aab9d1c63366c609ebed3acc1c2fb1
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+ require 'graph2relational'
3
+
4
+ # parse command line input
5
+ options = G2R::Util::CommandLineParser.new.parse
6
+
7
+ # create database connections
8
+ neo4j = G2R::Neo4J::Database.new(options[:input])
9
+ rdbms = G2R::RDBMS::Database.new(options[:output], options)
10
+
11
+ # convert
12
+ rdbms.source = neo4j
13
+ rdbms.convert()
@@ -0,0 +1,22 @@
1
+ # external dependencies
2
+ require 'neography'
3
+ require 'sequel'
4
+ require 'optparse'
5
+ require 'set'
6
+
7
+ #
8
+ $:.unshift File.dirname(__FILE__)
9
+
10
+ # Utils
11
+ require 'graph2relational/util-commandline-parser'
12
+
13
+ # Neo4J require
14
+ require 'graph2relational/neo4j-database'
15
+ require 'graph2relational/neo4j-connection'
16
+
17
+ # RDBMS require
18
+ require 'graph2relational/rdbms-database'
19
+ require 'graph2relational/rdbms-connection'
20
+ require 'graph2relational/rdbms-table'
21
+ require 'graph2relational/rdbms-join-table'
22
+ require 'graph2relational/rdbms-column'
@@ -0,0 +1,26 @@
1
+ module G2R
2
+ module Neo4J
3
+ class Connection
4
+ def initialize(options)
5
+ @conn = Neography::Rest.new(options)
6
+ end
7
+
8
+ def query_data(cypher)
9
+ query(cypher)['data']
10
+ end
11
+
12
+ def query_columns(cypher)
13
+ columns = Set.new
14
+ query(cypher)['data'].each do |row|
15
+ columns += row[0]['data'].keys
16
+ end
17
+
18
+ columns.to_a
19
+ end
20
+
21
+ def query(cypher)
22
+ @conn.execute_query(cypher)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,81 @@
1
+ module G2R
2
+ module Neo4J
3
+
4
+ # The Neo4J database that will have its data converted. It is the source of the conversion.
5
+ class Database
6
+
7
+ def initialize(connection_options)
8
+ # services
9
+ @conn = Connection.new(connection_options)
10
+
11
+ # data
12
+ @labels = nil
13
+ @label_attributes = {}
14
+ @label_relationships = {}
15
+ @relationships = nil
16
+ @relationship_attributes = {}
17
+ end
18
+
19
+ # ==========================================================================
20
+ # NODES
21
+ # ==========================================================================
22
+ def labels
23
+ # lazy load labels
24
+ if @labels.nil?
25
+ rows = @conn.query_data("MATCH (n) UNWIND labels(n) AS label RETURN DISTINCT label ORDER BY label")
26
+ @labels = rows.map {|row| row[0]}
27
+ end
28
+
29
+ @labels
30
+ end
31
+
32
+ def label_attributes(label)
33
+ if not @label_attributes.key? label
34
+ @label_attributes[label] = @conn.query_columns("MATCH (n:#{label}) RETURN n LIMIT 1000")
35
+ end
36
+
37
+ @label_attributes[label]
38
+ end
39
+
40
+ def label_relationships(label)
41
+ if not @label_relationships.key? label
42
+ @label_relationships[label] = @conn.query_data("MATCH (n:#{label})-[r]->(m) UNWIND labels(m) AS label WITH label, type(r) as relationship RETURN DISTINCT relationship, label")
43
+ end
44
+
45
+ @label_relationships[label]
46
+ end
47
+
48
+ def label_data(label)
49
+ return_clause = label_attributes(label).map {|column| "n.#{column}"}.join(", ")
50
+ @conn.query_data("MATCH (n:#{label}) RETURN id(n), #{return_clause}")
51
+ end
52
+
53
+ # ==========================================================================
54
+ # RELATIONSIIPS
55
+ # ==========================================================================
56
+ def relationships
57
+ # lazy load relationships
58
+ if @relationships.nil?
59
+ rows = @conn.query_data("MATCH (n)-[r]-(m) RETURN DISTINCT type(r) AS relationship ORDER BY relationship")
60
+ @relationships = rows.map{|row| row[0]}
61
+ end
62
+
63
+ @relationships
64
+ end
65
+
66
+ def relationship_attributes(source, type, target)
67
+ key = "#{source}_#{type}_#{target}"
68
+ if not @relationship_attributes.has_key? key
69
+ @relationship_attributes[key] = @conn.query_columns("MATCH (n:#{source})-[r:#{type}]->(m:#{target}) RETURN r LIMIT 1000")
70
+ end
71
+
72
+ @relationship_attributes[key]
73
+ end
74
+
75
+ def relationship_data(source, type, target)
76
+ return_clause = relationship_attributes(source, type, target).map {|column| "r.#{column}"}.join(", ")
77
+ @conn.query_data("MATCH (n:#{source})-[r:#{type}]->(m:#{target}) RETURN id(n), id(m), #{return_clause}")
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,35 @@
1
+ module G2R
2
+ module RDBMS
3
+ class Column
4
+ attr_accessor :name
5
+
6
+ def initialize(name)
7
+ @name = name.downcase.to_sym
8
+ @primary_key = false
9
+ @foreign_key = false
10
+ end
11
+
12
+ # Define the column as a primary key
13
+ def primary_key
14
+ @primary_key = true
15
+ self
16
+ end
17
+
18
+ # Define the column as a foreign key
19
+ def foreign_key
20
+ @foreign_key = true
21
+ self
22
+ end
23
+
24
+ # Checks if the column is a primary key
25
+ def primary_key?
26
+ @primary_key
27
+ end
28
+
29
+ # Check if the column is a foreign key
30
+ def foreign_key?
31
+ @foreign_key
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,38 @@
1
+ module G2R
2
+ module RDBMS
3
+ class Connection
4
+
5
+ def initialize(options)
6
+ # sequel
7
+ @db = Sequel.connect(options)
8
+ end
9
+
10
+ def create_table(table)
11
+ @db.create_table! table.name do
12
+
13
+ table.columns.each do |column|
14
+ # primary key
15
+ if column.primary_key?
16
+ primary_key column.name
17
+ # foreign key
18
+ elsif column.foreign_key?
19
+ Integer column.name
20
+ # common column
21
+ else
22
+ String column.name
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ def insert_data(table)
29
+ @db.transaction do
30
+ table.data.each do |row|
31
+ @db[table.name.to_sym].insert(row)
32
+ end
33
+ end
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,146 @@
1
+ module G2R
2
+ module RDBMS
3
+
4
+ # The target RDBMS database that will have the schema and data generated for. It is the target of the conversion.
5
+ class Database
6
+
7
+ # ==========================================================================
8
+ # INITIALIZATION
9
+ # ==========================================================================
10
+ def initialize(connection_options, app_options = {})
11
+ # services
12
+ @options = app_options
13
+ @conn = Connection.new(connection_options)
14
+
15
+ # data
16
+ reset_data()
17
+ end
18
+
19
+ # ==========================================================================
20
+ # CONVERSION
21
+ # ==========================================================================
22
+ def source=(source)
23
+ @source = source
24
+ end
25
+
26
+ def source
27
+ @source
28
+ end
29
+
30
+ # Export the schema creation and data import scripts to the specified location
31
+ def convert
32
+ # prepare
33
+ reset_data()
34
+
35
+ # create tables
36
+ puts "Creating base tables"
37
+ base_tables.each do |table|
38
+ @conn.create_table(table)
39
+ end
40
+
41
+ puts "Creating relationship tables"
42
+ relationship_tables.each do |table|
43
+ @conn.create_table(table)
44
+ end
45
+
46
+ # insert data
47
+ puts "Inserting base tables data"
48
+ base_tables.each do |table|
49
+ @conn.insert_data(table)
50
+ end
51
+
52
+ puts "Inserting relationship tables data"
53
+ relationship_tables.each do |table|
54
+ @conn.insert_data(table)
55
+ end
56
+ end
57
+
58
+ # Reset the saved data to perform a new conversion
59
+ def reset_data
60
+ @base_tables = nil
61
+ @relationship_tables = nil
62
+ end
63
+
64
+ # ==========================================================================
65
+ # TABLES
66
+ # ==========================================================================
67
+ # Get all the base tables that will be generated. They are based on the Neo4J labels.
68
+ # Columns are based on the Neo4J node attributes.
69
+ def base_tables
70
+ # lazy initialization
71
+ if @base_tables.nil?
72
+ @base_tables = @source.labels.map do |label|
73
+
74
+ # if excluding label, do not transform into base table
75
+ next if exclude? label
76
+
77
+ # generate table
78
+ table = Table.new(label)
79
+
80
+ # generate primary key
81
+ table.add_columns(Column.new(:id).primary_key)
82
+
83
+ # generate columns
84
+ columns = @source.label_attributes(label).map {|attribute| Column.new(attribute)}
85
+ table.add_columns(columns)
86
+
87
+ # generate data
88
+ label_data = @source.label_data(label)
89
+ table.add_data(label_data)
90
+
91
+ # generated table
92
+ table
93
+ end.compact
94
+ end
95
+
96
+ @base_tables.compact
97
+ end
98
+
99
+ # Get all the relationships tables that will be generated. They are based in the found Neo4J relationships.
100
+ # Columss are based on the Neo4J relationship attributes.
101
+ def relationship_tables
102
+ # lazy initialization
103
+ if @relationship_tables.nil?
104
+ @relationship_tables = @source.labels.flat_map do |label|
105
+
106
+ # if excluding label, do not transform into relationship table
107
+ next if exclude? label
108
+
109
+ @source.label_relationships(label).map do |relationship|
110
+ relationship, target_label = relationship
111
+
112
+ # if excluding label, do not transform into relationship table
113
+ next if exclude? target_label
114
+
115
+ # gerarate table
116
+ table_name = "#{label}_#{relationship}_#{target_label}"
117
+ table = JoinTable.new(table_name)
118
+
119
+ # generate keys
120
+ table.source = label
121
+ table.target = target_label
122
+
123
+ # generate columns
124
+ columns = @source.relationship_attributes(label, relationship, target_label).map {|attribute| Column.new(attribute)}
125
+ table.add_columns(columns)
126
+
127
+ # generate data
128
+ relationship_data = @source.relationship_data(label, relationship, target_label)
129
+ table.add_data(relationship_data)
130
+
131
+ # generated table
132
+ table
133
+ end
134
+ end.compact
135
+ end
136
+
137
+ @relationship_tables
138
+ end
139
+
140
+ # Check if a label should be excluded and not be transformed into a table
141
+ def exclude?(label)
142
+ @options.has_key? :exclude and @options[:exclude].include? label
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,43 @@
1
+ module G2R
2
+ module RDBMS
3
+
4
+ class JoinTable < Table
5
+
6
+ # Additionally return the source and target column ids plus all defined columns
7
+ def columns
8
+ [source_column, target_column] + super
9
+ end
10
+
11
+ def source_column
12
+ Column.new(source + "_id").foreign_key
13
+ end
14
+
15
+ def target_column
16
+ if source != target
17
+ Column.new(target + "_id").foreign_key
18
+ else
19
+ Column.new(target + "_id_2").foreign_key
20
+ end
21
+ end
22
+
23
+ # ==========================================================================
24
+ # ACESSORS
25
+ # ==========================================================================
26
+ def source=(source)
27
+ @source = source
28
+ end
29
+
30
+ def source
31
+ @source
32
+ end
33
+
34
+ def target=(target)
35
+ @target = target
36
+ end
37
+
38
+ def target
39
+ @target
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,36 @@
1
+ module G2R
2
+ module RDBMS
3
+ class Table
4
+
5
+ def initialize(name)
6
+ @name = name.downcase.to_sym
7
+ @columns = []
8
+ @data = []
9
+ end
10
+
11
+ def name
12
+ @name
13
+ end
14
+
15
+ def add_columns(columns)
16
+ if columns.class == Array
17
+ @columns += columns
18
+ else
19
+ @columns << columns
20
+ end
21
+ end
22
+
23
+ def columns
24
+ @columns
25
+ end
26
+
27
+ def add_data(data)
28
+ @data += data
29
+ end
30
+
31
+ def data
32
+ @data
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,57 @@
1
+ module G2R
2
+ module Util
3
+
4
+ class CommandLineParser
5
+
6
+ def initialize()
7
+ @valid = true
8
+ end
9
+
10
+ def parse()
11
+ # default values
12
+ options = {:input => nil, :output => nil, :exclude => []}
13
+
14
+ # display help if no args
15
+ ARGV << '-h' if ARGV.empty?
16
+
17
+ # do parsing
18
+ OptionParser.new do |opts|
19
+ opts.banner = "Usage: graph2relational -i <neo4j_connection_url> -o <rdbms_connection_url> [optional_arguments]"
20
+
21
+ # input file
22
+ opts.on("-i CONNECTION", "--input CONNECTION", "Neo4J connection URI from where the schema and data will be geberated") do |input_connection|
23
+ options[:input] = input_connection
24
+ end
25
+
26
+ # output dir
27
+ opts.on("-o CONNECTION", "--output CONNECTION", "RDBMS connection URI to where the schema and data will be persisted") do |output_connection|
28
+ options[:output] = output_connection
29
+ end
30
+
31
+ # exclusion
32
+ opts.on("-x EXCLUSION", "--exclude EXCLUSION", "Neo4J labels (separated by comma) to exclude from the conversion") do |exclude|
33
+ options[:exclude] = exclude.split(",").map{|exclude| exclude.strip}
34
+ end
35
+
36
+ # help
37
+ opts.on_tail("-h", "--help", "Help message") do
38
+ puts opts
39
+ exit
40
+ end
41
+ end.parse!
42
+
43
+ # check mandatory args
44
+ if options[:input].nil?
45
+ puts "Missing parameter: specify Neo4J connection URI using -i <connection> parameter"
46
+ exit
47
+ end
48
+ if options[:output].nil?
49
+ puts "Missing parameter: specify RDBMS connection URI using -o <connection> parameter"
50
+ exit
51
+ end
52
+
53
+ return options
54
+ end
55
+ end
56
+ end
57
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: graph2relational
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Renato Dinhani Conceição
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-03-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: neography
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.8'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.8'
27
+ - !ruby/object:Gem::Dependency
28
+ name: sequel
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.32'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.32'
41
+ description: Converts a Neo4J graph database to a supported RDBMS database automatically
42
+ generating tables from its labels, relationships and attributes
43
+ email: renatodinhani@gmail.com
44
+ executables:
45
+ - graph2relational
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - bin/graph2relational
50
+ - lib/graph2relational.rb
51
+ - lib/graph2relational/neo4j-connection.rb
52
+ - lib/graph2relational/neo4j-database.rb
53
+ - lib/graph2relational/rdbms-column.rb
54
+ - lib/graph2relational/rdbms-connection.rb
55
+ - lib/graph2relational/rdbms-database.rb
56
+ - lib/graph2relational/rdbms-join-table.rb
57
+ - lib/graph2relational/rdbms-table.rb
58
+ - lib/graph2relational/util-commandline-parser.rb
59
+ homepage: https://github.com/renatodinhani/graph2relational
60
+ licenses:
61
+ - MIT
62
+ metadata: {}
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubyforge_project:
79
+ rubygems_version: 2.4.5.1
80
+ signing_key:
81
+ specification_version: 4
82
+ summary: Converts a Neo4J graph database to a RDBMS database
83
+ test_files: []