lib/thingfish/metastore/pggraph.rb
changeset 0 3cc90e88c6ab
child 1 60b437614edd
equal deleted inserted replaced
-1:000000000000 0:3cc90e88c6ab
       
     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.0.1'
       
    28 
       
    29 	# Version control revision
       
    30 	REVISION = %q$Revision$
       
    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 		return metadata.send( key )
       
   179 	end
       
   180 
       
   181 
       
   182 	### Fetch UUIDs related to the given +oid+.
       
   183 	def fetch_related_oids( oid )
       
   184 		oid = normalize_oid( oid )
       
   185 		oid = self.model[ oid ]
       
   186 		return [] unless oid
       
   187 		return oid.related_nodes.map( &:id_c )
       
   188 	end
       
   189 
       
   190 
       
   191 	### Search the metastore for UUIDs which match the specified +criteria+ and
       
   192 	### return them as an iterator.
       
   193 	def search( options={} )
       
   194 		ds = self.model.naked.select( :id )
       
   195 		self.log.debug "Starting search with %p" % [ ds ]
       
   196 
       
   197 		ds = self.omit_related_resources( ds, options )
       
   198 		ds = self.apply_search_criteria( ds, options )
       
   199 		ds = self.apply_search_order( ds, options )
       
   200 		ds = self.apply_search_direction( ds, options )
       
   201 		ds = self.apply_search_limit( ds, options )
       
   202 
       
   203 		return ds.map {|row| row[:id] }
       
   204 	end
       
   205 
       
   206 
       
   207 	### Update the metadata for the given +oid+ with the specified +values+ hash.
       
   208 	def merge( oid, values )
       
   209 		values = normalize_keys( values )
       
   210 
       
   211 		md = self.model[ oid ] or return nil
       
   212 		md.merge!( values )
       
   213 		md.save
       
   214 	end
       
   215 
       
   216 
       
   217 	### Remove all metadata associated with +oid+ from the Metastore.
       
   218 	def remove( oid, *keys )
       
   219 		self.model[ id: oid ].destroy
       
   220 	end
       
   221 
       
   222 
       
   223 	### Remove all metadata associated with +oid+ except for the specified +keys+.
       
   224 	def remove_except( oid, *keys )
       
   225 		keys = normalize_keys( keys )
       
   226 
       
   227 		md = self.model[ oid ] or return nil
       
   228 		md.user_metadata.keep_if {|key,_| keys.include?(key) }
       
   229 		md.save
       
   230 	end
       
   231 
       
   232 
       
   233 	### Returns +true+ if the metastore has metadata associated with the specified +oid+.
       
   234 	def include?( oid )
       
   235 		return self.model.count( id: oid ).nonzero?
       
   236 	end
       
   237 
       
   238 
       
   239 	### Returns the number of objects the store contains.
       
   240 	def size
       
   241 		return self.model.count
       
   242 	end
       
   243 
       
   244 
       
   245 	#########
       
   246 	protected
       
   247 	#########
       
   248 
       
   249 	### Omit related resources from the search dataset +ds+ unless the given
       
   250 	### +options+ specify otherwise.
       
   251 	def omit_related_resources( ds, options )
       
   252 		unless options[:include_related]
       
   253 			self.log.debug "  omitting entries for related resources"
       
   254 			ds = ds.unrelated
       
   255 		end
       
   256 		return ds
       
   257 	end
       
   258 
       
   259 
       
   260 	### Apply the search :criteria from the specified +options+ to the collection
       
   261 	### in +ds+ and return the modified dataset.
       
   262 	def apply_search_criteria( ds, options )
       
   263 		if (( criteria = options[:criteria] ))
       
   264 			criteria.each do |field, value|
       
   265 				self.log.debug "  applying criteria: %p => %p" % [ field.to_s, value ]
       
   266 				ds = ds.where_metadata( field => value )
       
   267 			end
       
   268 		end
       
   269 
       
   270 		return ds
       
   271 	end
       
   272 
       
   273 
       
   274 	### Apply the search :order from the specified +options+ to the collection in
       
   275 	### +ds+ and return the modified dataset.
       
   276 	def apply_search_order( ds, options )
       
   277 		if options[:order]
       
   278 			columns = Array( options[:order] )
       
   279 			ds = ds.order( columns.map(&:to_sym) )
       
   280 		end
       
   281 
       
   282 		return ds
       
   283 	end
       
   284 
       
   285 
       
   286 	### Apply the search :direction from the specified +options+ to the collection
       
   287 	### in +ds+ and return the modified dataset.
       
   288 	def apply_search_direction( ds, options )
       
   289 		ds = ds.reverse if options[:direction] && options[:direction] == 'desc'
       
   290 		return ds
       
   291 	end
       
   292 
       
   293 
       
   294 	### Apply the search :limit from the specified +options+ to the collection in
       
   295 	### +ds+ and return the modified dataset.
       
   296 	def apply_search_limit( ds, options )
       
   297 		if (( limit = options[:limit] ))
       
   298 			self.log.debug "  limiting to %s results" % [ limit ]
       
   299 			offset = options[:offset] || 0
       
   300 			ds = ds.limit( limit, offset )
       
   301 		end
       
   302 
       
   303 		return ds
       
   304 	end
       
   305 
       
   306 end # class Thingfish::Metastore::PgGraph
       
   307