thingfish-metastore-pggraph 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: aa350287482cc8fd51a482da2133f97e31b01d9d
4
+ data.tar.gz: ad7d4eeeb9db0849b5fa90b9f76633075fb81134
5
+ SHA512:
6
+ metadata.gz: b2b3bad22b9291d94bd5c9ce659fd2ce3cedb9956aad6b35d1dee21421d97bbca90ceb96450d91ff58e2f8f1bc1a1c1e15ad2f99444606807b4833e0fb037746
7
+ data.tar.gz: 30b2e55811bd4ade8794f3407a47476e57175245a9914fc45b0cf529dfaecb139ca2017c224612dc7b8c9bd71381aa4de7537cdb71b8ac3fe12ab7d6d32d275c
@@ -0,0 +1,45 @@
1
+ # vim: set nosta noet ts=4 sw=4:
2
+
3
+ ### The initial Thingfish::Metastore::PgGraph DDL.
4
+ ###
5
+ Sequel.migration do
6
+ up do
7
+ create_table( :nodes ) do
8
+ uuid :id, primary_key: true
9
+ text :format, null: false
10
+ int :extent, null: false
11
+ timestamptz :created, null: false, default: Sequel.function(:now)
12
+ inet :uploadaddress, null: false
13
+ jsonb :user_metadata, null: false, default: '{}'
14
+ end
15
+
16
+ create_table( :edges ) do
17
+ uuid :id_p, null: false
18
+ uuid :id_c, null: false
19
+ jsonb :prop, null: false, default: '{}'
20
+
21
+ index :id_p
22
+ index :id_c
23
+
24
+ # Remove relationships when a node is deleted.
25
+ foreign_key [:id_p], :nodes, name: 'edges_p_fkey', key: :id, on_delete: :cascade, on_update: :cascade
26
+ foreign_key [:id_c], :nodes, name: 'edges_c_fkey', key: :id, on_delete: :cascade, on_update: :cascade
27
+ end
28
+
29
+ # Add functional index from JSON edge props
30
+ run "CREATE INDEX relation_idx ON edges ( (prop->>'relationship') )"
31
+
32
+ # Disallow a node linking to itself -- no self loops.
33
+ run 'ALTER TABLE edges ADD CONSTRAINT no_self_edge_chk CHECK ( id_p <> id_c )'
34
+
35
+ # Allow only a single link between any two nodes, enforcing relations
36
+ # to be directional parent->child.
37
+ run 'CREATE UNIQUE INDEX pair_unique_idx ON edges USING btree ((LEAST(id_p, id_c)), (GREATEST(id_p, id_c)))'
38
+ end
39
+
40
+ down do
41
+ drop_table( :nodes, cascade: true )
42
+ drop_table( :edges, cascade: true )
43
+ end
44
+ end
45
+
@@ -0,0 +1,311 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'loggability'
5
+ require 'configurability'
6
+ require 'sequel'
7
+ require 'strelka'
8
+ require 'strelka/mixins'
9
+
10
+ require 'thingfish'
11
+ require 'thingfish/mixins'
12
+ require 'thingfish/metastore'
13
+
14
+ # Toplevel namespace
15
+ class Thingfish::Metastore::PgGraph < Thingfish::Metastore
16
+ extend Loggability,
17
+ Configurability,
18
+ Strelka::MethodUtilities
19
+ include Thingfish::Normalization
20
+
21
+
22
+ # Load Sequel extensions/plugins
23
+ Sequel.extension :migration
24
+
25
+
26
+ # Package version
27
+ VERSION = '0.1.3'
28
+
29
+ # Version control revision
30
+ REVISION = %q$Revision: 1ad0d5bc5083 $
31
+
32
+ # The data directory that contains migration files.
33
+ #
34
+ DATADIR = if ENV['THINGFISH_METASTORE_PGGRAPH_DATADIR']
35
+ Pathname.new( ENV['THINGFISH_METASTORE_PGGRAPH_DATADIR'] )
36
+ elsif Gem.datadir( 'thingfish-metastore-pggraph' )
37
+ Pathname.new( Gem.datadir('thingfish-metastore-pggraph') )
38
+ else
39
+ Pathname.new( __FILE__ ).dirname.parent.parent.parent +
40
+ 'data' + 'thingfish-metastore-pggraph'
41
+ end
42
+
43
+ # The default config values
44
+ DEFAULT_CONFIG = {
45
+ uri: 'postgres:/thingfish',
46
+ slow_query_seconds: 0.01,
47
+ }
48
+
49
+
50
+ # Loggability API -- use a separate logger
51
+ log_as :thingfish_metastore_pggraph
52
+
53
+ # Configurability API -- load the `pg_metastore`
54
+ config_key :pggraph_metastore
55
+
56
+ ##
57
+ # The URI of the database to use for the metastore
58
+ singleton_attr_accessor :uri
59
+
60
+ ##
61
+ # The Sequel::Database that's used to access the metastore tables
62
+ singleton_attr_accessor :db
63
+
64
+ ##
65
+ # The number of seconds to consider a "slow" query
66
+ singleton_attr_accessor :slow_query_seconds
67
+
68
+
69
+ ### Set up the metastore database and migrate to the latest version.
70
+ def self::setup_database
71
+ Sequel.extension :pg_json_ops
72
+
73
+ self.db = Sequel.connect( self.uri )
74
+ self.db.logger = Loggability[ Thingfish::Metastore::PgGraph ]
75
+ self.db.extension :pg_streaming
76
+ self.db.stream_all_queries = true
77
+ self.db.optimize_model_load = true
78
+ self.db.sql_log_level = :debug
79
+ self.db.extension( :pg_json )
80
+ self.db.log_warn_duration = self.slow_query_seconds
81
+
82
+ # Ensure the database is current.
83
+ #
84
+ unless Sequel::Migrator.is_current?( self.db, self.migrations_dir.to_s )
85
+ self.log.info "Installing database schema..."
86
+ Sequel::Migrator.apply( self.db, self.migrations_dir.to_s )
87
+ end
88
+ end
89
+
90
+
91
+ ### Tear down the configured metastore database.
92
+ def self::teardown_database
93
+ self.log.info "Tearing down database schema..."
94
+ Sequel::Migrator.apply( self.db, self.migrations_dir.to_s, 0 )
95
+ end
96
+
97
+
98
+ ### Return the current database migrations directory as a Pathname
99
+ def self::migrations_dir
100
+ return DATADIR + 'migrations'
101
+ end
102
+
103
+
104
+ ### Configurability API -- set up the metastore with the `pg_metastore` section of
105
+ ### the config file.
106
+ def self::configure( config=nil )
107
+ config = self.defaults.merge( config || {} )
108
+
109
+ self.uri = config[:uri]
110
+ self.slow_query_seconds = config[:slow_query_seconds]
111
+
112
+ self.setup_database
113
+ end
114
+
115
+
116
+ ### Set up the metastore.
117
+ def initialize( * ) # :notnew:
118
+ require 'thingfish/metastore/pggraph/node'
119
+ require 'thingfish/metastore/pggraph/edge'
120
+ Thingfish::Metastore::PgGraph::Node.db = self.class.db
121
+ Thingfish::Metastore::PgGraph::Edge.db = self.class.db
122
+ @model = Thingfish::Metastore::PgGraph::Node
123
+ end
124
+
125
+
126
+ ######
127
+ public
128
+ ######
129
+
130
+ ##
131
+ # The Sequel model representing the metadata rows.
132
+ attr_reader :model
133
+
134
+
135
+ #
136
+ # :section: Thingfish::Metastore API
137
+ #
138
+
139
+ ### Return an Array of all stored oids.
140
+ def oids
141
+ return self.each_oid.to_a
142
+ end
143
+
144
+
145
+ ### Iterate over each of the store's oids, yielding to the block if one is given
146
+ ### or returning an Enumerator if one is not.
147
+ def each_oid( &block )
148
+ return self.model.select_map( :id ).each( &block )
149
+ end
150
+
151
+
152
+ ### Save the +metadata+ Hash for the specified +oid+.
153
+ def save( oid, metadata )
154
+ md = self.model.from_hash( metadata )
155
+ md.id = oid
156
+ md.save
157
+ end
158
+
159
+
160
+ ### Fetch the data corresponding to the given +oid+ as a Hash-ish object.
161
+ def fetch( oid, *keys )
162
+ metadata = self.model[ oid ] or return nil
163
+
164
+ if keys.empty?
165
+ return metadata.to_hash
166
+ else
167
+ keys = normalize_keys( keys )
168
+ values = metadata.to_hash.values_at( *keys )
169
+ return Hash[ [keys, values].transpose ]
170
+ end
171
+ end
172
+
173
+
174
+ ### Fetch the value of the metadata associated with the given +key+ for the
175
+ ### specified +oid+.
176
+ def fetch_value( oid, key )
177
+ metadata = self.model[ oid ] or return nil
178
+ key = key.to_sym
179
+ return metadata[ key ] || metadata.user_metadata[ key ]
180
+ end
181
+
182
+
183
+ ### Fetch UUIDs related to the given +oid+.
184
+ def fetch_related_oids( oid )
185
+ oid = normalize_oid( oid )
186
+ oid = self.model[ oid ]
187
+ return [] unless oid
188
+ return oid.related_nodes.map( &:id_c )
189
+ end
190
+
191
+
192
+ ### Search the metastore for UUIDs which match the specified +criteria+ and
193
+ ### return them as an iterator.
194
+ def search( options={} )
195
+ ds = self.model.naked.select( :id )
196
+ self.log.debug "Starting search with %p" % [ ds ]
197
+
198
+ ds = self.omit_related_resources( ds, options )
199
+ ds = self.apply_search_criteria( ds, options )
200
+ ds = self.apply_search_order( ds, options )
201
+ ds = self.apply_search_direction( ds, options )
202
+ ds = self.apply_search_limit( ds, options )
203
+
204
+ self.log.debug "Dataset for search is: %s" % [ ds.sql ]
205
+
206
+ return ds.map {|row| row[:id] }
207
+ end
208
+
209
+
210
+ ### Update the metadata for the given +oid+ with the specified +values+ hash.
211
+ def merge( oid, values )
212
+ values = normalize_keys( values )
213
+
214
+ md = self.model[ oid ] or return nil
215
+ md.merge!( values )
216
+ md.save
217
+ end
218
+
219
+
220
+ ### Remove all metadata associated with +oid+ from the Metastore.
221
+ def remove( oid, *keys )
222
+ self.model[ id: oid ].destroy
223
+ end
224
+
225
+
226
+ ### Remove all metadata associated with +oid+ except for the specified +keys+.
227
+ def remove_except( oid, *keys )
228
+ keys = normalize_keys( keys )
229
+
230
+ md = self.model[ oid ] or return nil
231
+ md.user_metadata.keep_if {|key,_| keys.include?(key) }
232
+ md.save
233
+ end
234
+
235
+
236
+ ### Returns +true+ if the metastore has metadata associated with the specified +oid+.
237
+ def include?( oid )
238
+ return self.model.count( id: oid ).nonzero?
239
+ end
240
+
241
+
242
+ ### Returns the number of objects the store contains.
243
+ def size
244
+ return self.model.count
245
+ end
246
+
247
+
248
+ #########
249
+ protected
250
+ #########
251
+
252
+ ### Omit related resources from the search dataset +ds+ unless the given
253
+ ### +options+ specify otherwise.
254
+ def omit_related_resources( ds, options )
255
+ unless options[:include_related]
256
+ self.log.debug " omitting entries for related resources"
257
+ ds = ds.unrelated
258
+ end
259
+ return ds
260
+ end
261
+
262
+
263
+ ### Apply the search :criteria from the specified +options+ to the collection
264
+ ### in +ds+ and return the modified dataset.
265
+ def apply_search_criteria( ds, options )
266
+ if (( criteria = options[:criteria] ))
267
+ criteria.each do |field, value|
268
+ self.log.debug " applying criteria: %p => %p" % [ field.to_s, value ]
269
+ ds = ds.where_metadata( field => value )
270
+ end
271
+ end
272
+
273
+ return ds
274
+ end
275
+
276
+
277
+ ### Apply the search :order from the specified +options+ to the collection in
278
+ ### +ds+ and return the modified dataset.
279
+ def apply_search_order( ds, options )
280
+ if options[:order]
281
+ columns = Array( options[:order] )
282
+ self.log.debug " ordering results by columns: %p" % [ columns ]
283
+ ds = ds.order_metadata( columns )
284
+ end
285
+
286
+ return ds
287
+ end
288
+
289
+
290
+ ### Apply the search :direction from the specified +options+ to the collection
291
+ ### in +ds+ and return the modified dataset.
292
+ def apply_search_direction( ds, options )
293
+ ds = ds.reverse if options[:direction] && options[:direction] == 'desc'
294
+ return ds
295
+ end
296
+
297
+
298
+ ### Apply the search :limit from the specified +options+ to the collection in
299
+ ### +ds+ and return the modified dataset.
300
+ def apply_search_limit( ds, options )
301
+ if (( limit = options[:limit] ))
302
+ self.log.debug " limiting to %s results" % [ limit ]
303
+ offset = options[:offset] || 0
304
+ ds = ds.limit( limit, offset )
305
+ end
306
+
307
+ return ds
308
+ end
309
+
310
+ end # class Thingfish::Metastore::PgGraph
311
+
@@ -0,0 +1,52 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'sequel/model'
5
+
6
+ require 'thingfish/mixins'
7
+ require 'thingfish/metastore/pggraph' unless defined?( Thingfish::Metastore::PgGraph )
8
+
9
+
10
+ ### A row representing a relationship between two node objects.
11
+ ###
12
+ class Thingfish::Metastore::PgGraph::Edge < Sequel::Model( :edges )
13
+
14
+ # Related resource associations
15
+ many_to_one :node, :key => :id_p
16
+
17
+ # Dataset methods
18
+ #
19
+ dataset_module do
20
+ #########
21
+ protected
22
+ #########
23
+
24
+ ### Returns a Sequel expression suitable for use as the key of a query against
25
+ ### the specified property field.
26
+ ###
27
+ def prop_expr( field )
28
+ return Sequel.pg_jsonb( :prop ).get_text( field.to_s )
29
+ end
30
+ end
31
+
32
+
33
+ ### Do some initial attribute setup for new objects.
34
+ ###
35
+ def initialize( * )
36
+ super
37
+ self[ :prop ] ||= Sequel.pg_jsonb({})
38
+ end
39
+
40
+
41
+ #########
42
+ protected
43
+ #########
44
+
45
+ ### Proxy method -- fetch a value from the edge property hash if it exists.
46
+ ###
47
+ def method_missing( sym, *args, &block )
48
+ return self.prop[ sym.to_s ] || super
49
+ end
50
+
51
+ end # Thingfish::Metastore::PgGraph::Edge
52
+
@@ -0,0 +1,202 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'sequel/model'
5
+
6
+ require 'thingfish/mixins'
7
+ require 'thingfish/metastore/pggraph' unless defined?( Thingfish::Metastore::PgGraph )
8
+
9
+
10
+ # A row of metadata describing an asset in a Thingfish store.
11
+ class Thingfish::Metastore::PgGraph::Node < Sequel::Model( :nodes )
12
+ include Thingfish::Normalization
13
+
14
+ # Related resources for this node
15
+ one_to_many :related_nodes, :key => :id_p, :class => 'Thingfish::Metastore::PgGraph::Edge'
16
+
17
+ # Edge relation if this node is a related resource
18
+ one_to_one :related_to, :key => :id_c, :class => 'Thingfish::Metastore::PgGraph::Edge'
19
+
20
+ # Allow instances to be created with a primary key
21
+ unrestrict_primary_key
22
+
23
+
24
+ # Dataset methods
25
+ dataset_module do
26
+
27
+ ### Dataset method: Limit results to metadata which is for a related resource.
28
+ ###
29
+ def related
30
+ return self.join_edges( :rel ).exclude( :rel__id_c => nil )
31
+ end
32
+
33
+
34
+ ### Dataset method: Limit results to metadata which is not for a related resource.
35
+ ###
36
+ def unrelated
37
+ return self.join_edges( :notrel ).filter( :notrel__id_c => nil )
38
+ end
39
+
40
+
41
+ ### Dataset method: Limit results to records whose operational or user
42
+ ### metadata matches the values from the specified +hash+.
43
+ ###
44
+ def where_metadata( hash )
45
+ ds = self
46
+ hash.each do |field, value|
47
+
48
+ # Direct DB column
49
+ #
50
+ if self.model.metadata_columns.include?( field.to_sym )
51
+ ds = ds.where( field.to_sym => value )
52
+
53
+ # User metadata or edge relationship
54
+ #
55
+ else
56
+ if field.to_sym == :relationship
57
+ ds = ds.join_edges unless ds.joined_dataset?
58
+ ds = ds.filter( Sequel.pg_jsonb( :edges__prop ).get_text( field.to_s ) => value )
59
+
60
+ elsif field.to_sym == :relation
61
+ ds = ds.join_edges unless ds.joined_dataset?
62
+ ds = self.join_edges.filter( :edges__id_p => value )
63
+
64
+ else
65
+ ds = ds.where( self.user_metadata_expr(field) => value )
66
+ end
67
+ end
68
+ end
69
+
70
+ return ds
71
+ end
72
+
73
+
74
+ ### Dataset method: Order results by the specified +columns+.
75
+ ###
76
+ def order_metadata( *columns )
77
+ columns.flatten!
78
+ ds = self
79
+ columns.each do |column|
80
+ if Thingfish::Metastore::PgGraph::Node.metadata_columns.include?( column.to_sym )
81
+ ds = ds.order_append( column.to_sym )
82
+ else
83
+ ds = ds.order_append( self.user_metadata_expr(column) )
84
+ end
85
+ end
86
+
87
+ return ds
88
+ end
89
+
90
+
91
+
92
+ #########
93
+ protected
94
+ #########
95
+
96
+ ### Return a dataset linking related nodes to edges.
97
+ ###
98
+ def join_edges( aka=nil )
99
+ return self.join_table( :left, :edges, { :id_c => :nodes__id }, { :table_alias => aka } )
100
+ end
101
+
102
+
103
+ ### Returns a Sequel expression suitable for use as the key of a query against
104
+ ### the specified user metadata field.
105
+ def user_metadata_expr( field )
106
+ return Sequel.pg_jsonb( :user_metadata ).get_text( field.to_s )
107
+ end
108
+
109
+ end # dataset_module
110
+
111
+
112
+ ### Return a new Metadata object from the given +oid+ and one-dimensional +hash+
113
+ ### used by Thingfish.
114
+ def self::from_hash( hash )
115
+ metadata = Thingfish::Normalization.normalize_keys( hash )
116
+
117
+ md = new
118
+
119
+ md.format = metadata.delete( 'format' )
120
+ md.extent = metadata.delete( 'extent' )
121
+ md.created = metadata.delete( 'created' )
122
+ md.uploadaddress = metadata.delete( 'uploadaddress' ).to_s
123
+
124
+ md.user_metadata = Sequel.pg_jsonb( metadata )
125
+
126
+ return md
127
+ end
128
+
129
+
130
+ ### Return the columns of the table that are used for resource metadata.
131
+ def self::metadata_columns
132
+ return self.columns - [self.primary_key, :user_metadata]
133
+ end
134
+
135
+
136
+ ### Do some initial attribute setup for new objects.
137
+ def initialize( * )
138
+ super
139
+ self[ :user_metadata ] ||= Sequel.pg_jsonb({})
140
+ end
141
+
142
+
143
+ ### Return the metadata as a Hash; overridden from Sequel::Model to
144
+ ### merge the user and system pairs together.
145
+ def to_hash
146
+ hash = self.values.dup
147
+
148
+ hash.delete( :id )
149
+ hash.merge!( hash.delete(:user_metadata) )
150
+
151
+ if related_to = self.related_to
152
+ hash.merge!( related_to.prop )
153
+ hash[ :relation ] = related_to.id_p
154
+ end
155
+
156
+ return normalize_keys( hash )
157
+ end
158
+
159
+
160
+ ### Merge new metadata +values+ into the metadata for the resource
161
+ def merge!( values )
162
+
163
+ # Extract and set the column-metadata values first
164
+ self.class.metadata_columns.each do |col|
165
+ next unless values.key?( col.to_s )
166
+ self[ col ] = values.delete( col.to_s )
167
+ end
168
+
169
+ self.user_metadata.merge!( values )
170
+ end
171
+
172
+
173
+ ### Hook creation for new related resources, divert relation data to
174
+ ### a new edge row.
175
+ ###
176
+ def around_save
177
+ relationship = self.user_metadata.delete( 'relationship' )
178
+ relation = self.user_metadata.delete( 'relation' )
179
+
180
+ super
181
+
182
+ if relation
183
+ edge = Thingfish::Metastore::PgGraph::Edge.new
184
+ edge.prop[ 'relationship' ] = relationship
185
+ edge.id_p = relation
186
+ edge.id_c = self.id
187
+ edge.save
188
+ end
189
+ end
190
+
191
+
192
+ #########
193
+ protected
194
+ #########
195
+
196
+ ### Proxy method -- fetch a value from the metadata hash if it exists.
197
+ def method_missing( sym, *args, &block )
198
+ return self.user_metadata[ sym.to_s ] || super
199
+ end
200
+
201
+ end # Thingfish::Metastore::PgGraph::Node
202
+
metadata ADDED
@@ -0,0 +1,124 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: thingfish-metastore-pggraph
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.3
5
+ platform: ruby
6
+ authors:
7
+ - Mahlon E. Smith <mahlon@martini.nu>
8
+ - Michael Granger <ged@faeriemud.org>
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2016-11-14 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: thingfish
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '0.5'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '0.5'
28
+ - !ruby/object:Gem::Dependency
29
+ name: loggability
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '0.11'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '0.11'
42
+ - !ruby/object:Gem::Dependency
43
+ name: configurability
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '2.2'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '2.2'
56
+ - !ruby/object:Gem::Dependency
57
+ name: sequel
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '4.35'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '4.35'
70
+ - !ruby/object:Gem::Dependency
71
+ name: pg
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '0.19'
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '0.19'
84
+ description: |
85
+ This is a metadata storage plugin for the Thingfish digital asset
86
+ manager. It provides persistent storage for uploaded data to PostgreSQL
87
+ tables.
88
+
89
+ It is heavily based on the regular PG metastore, however it differs by
90
+ storing objects as nodes, and their relations as edges.
91
+ email: mahlon@martini.nu
92
+ executables: []
93
+ extensions: []
94
+ extra_rdoc_files: []
95
+ files:
96
+ - data/thingfish-metastore-pggraph/migrations/20151102_initial.rb
97
+ - lib/thingfish/metastore/pggraph.rb
98
+ - lib/thingfish/metastore/pggraph/edge.rb
99
+ - lib/thingfish/metastore/pggraph/node.rb
100
+ homepage: https://bitbucket.org/mahlon/thingfish-metastore-pggraph
101
+ licenses:
102
+ - BSD-3-Clause
103
+ metadata: {}
104
+ post_install_message:
105
+ rdoc_options: []
106
+ require_paths:
107
+ - lib
108
+ required_ruby_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '2.3'
113
+ required_rubygems_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ requirements: []
119
+ rubyforge_project:
120
+ rubygems_version: 2.6.8
121
+ signing_key:
122
+ specification_version: 4
123
+ summary: Graph DDL storage for Thingfish metadata.
124
+ test_files: []