diff -r 000000000000 -r 3cc90e88c6ab lib/thingfish/metastore/pggraph.rb --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/thingfish/metastore/pggraph.rb Thu Nov 05 10:34:15 2015 -0800 @@ -0,0 +1,307 @@ +# -*- ruby -*- +#encoding: utf-8 + +require 'loggability' +require 'configurability' +require 'sequel' +require 'strelka' +require 'strelka/mixins' + +require 'thingfish' +require 'thingfish/mixins' +require 'thingfish/metastore' + +# Toplevel namespace +class Thingfish::Metastore::PgGraph < Thingfish::Metastore + extend Loggability, + Configurability, + Strelka::MethodUtilities + include Thingfish::Normalization + + + # Load Sequel extensions/plugins + Sequel.extension :migration + + + # Package version + VERSION = '0.0.1' + + # Version control revision + REVISION = %q$Revision$ + + # The data directory that contains migration files. + # + DATADIR = if ENV['THINGFISH_METASTORE_PGGRAPH_DATADIR'] + Pathname.new( ENV['THINGFISH_METASTORE_PGGRAPH_DATADIR'] ) + elsif Gem.datadir( 'thingfish-metastore-pggraph' ) + Pathname.new( Gem.datadir('thingfish-metastore-pggraph') ) + else + Pathname.new( __FILE__ ).dirname.parent.parent.parent + + 'data' + 'thingfish-metastore-pggraph' + end + + # The default config values + DEFAULT_CONFIG = { + uri: 'postgres:/thingfish', + slow_query_seconds: 0.01, + } + + + # Loggability API -- use a separate logger + log_as :thingfish_metastore_pggraph + + # Configurability API -- load the `pg_metastore` + config_key :pggraph_metastore + + ## + # The URI of the database to use for the metastore + singleton_attr_accessor :uri + + ## + # The Sequel::Database that's used to access the metastore tables + singleton_attr_accessor :db + + ## + # The number of seconds to consider a "slow" query + singleton_attr_accessor :slow_query_seconds + + + ### Set up the metastore database and migrate to the latest version. + def self::setup_database + Sequel.extension :pg_json_ops + + self.db = Sequel.connect( self.uri ) + self.db.logger = Loggability[ Thingfish::Metastore::PgGraph ] + self.db.extension :pg_streaming + self.db.stream_all_queries = true + self.db.optimize_model_load = true + self.db.sql_log_level = :debug + self.db.extension( :pg_json ) + self.db.log_warn_duration = self.slow_query_seconds + + # Ensure the database is current. + # + unless Sequel::Migrator.is_current?( self.db, self.migrations_dir.to_s ) + self.log.info "Installing database schema..." + Sequel::Migrator.apply( self.db, self.migrations_dir.to_s ) + end + end + + + ### Tear down the configured metastore database. + def self::teardown_database + self.log.info "Tearing down database schema..." + Sequel::Migrator.apply( self.db, self.migrations_dir.to_s, 0 ) + end + + + ### Return the current database migrations directory as a Pathname + def self::migrations_dir + return DATADIR + 'migrations' + end + + + ### Configurability API -- set up the metastore with the `pg_metastore` section of + ### the config file. + def self::configure( config=nil ) + config = self.defaults.merge( config || {} ) + + self.uri = config[:uri] + self.slow_query_seconds = config[:slow_query_seconds] + + self.setup_database + end + + + ### Set up the metastore. + def initialize( * ) # :notnew: + require 'thingfish/metastore/pggraph/node' + require 'thingfish/metastore/pggraph/edge' + Thingfish::Metastore::PgGraph::Node.db = self.class.db + Thingfish::Metastore::PgGraph::Edge.db = self.class.db + @model = Thingfish::Metastore::PgGraph::Node + end + + + ###### + public + ###### + + ## + # The Sequel model representing the metadata rows. + attr_reader :model + + + # + # :section: Thingfish::Metastore API + # + + ### Return an Array of all stored oids. + def oids + return self.each_oid.to_a + end + + + ### Iterate over each of the store's oids, yielding to the block if one is given + ### or returning an Enumerator if one is not. + def each_oid( &block ) + return self.model.select_map( :id ).each( &block ) + end + + + ### Save the +metadata+ Hash for the specified +oid+. + def save( oid, metadata ) + md = self.model.from_hash( metadata ) + md.id = oid + md.save + end + + + ### Fetch the data corresponding to the given +oid+ as a Hash-ish object. + def fetch( oid, *keys ) + metadata = self.model[ oid ] or return nil + + if keys.empty? + return metadata.to_hash + else + keys = normalize_keys( keys ) + values = metadata.to_hash.values_at( *keys ) + return Hash[ [keys, values].transpose ] + end + end + + + ### Fetch the value of the metadata associated with the given +key+ for the + ### specified +oid+. + def fetch_value( oid, key ) + metadata = self.model[ oid ] or return nil + return metadata.send( key ) + end + + + ### Fetch UUIDs related to the given +oid+. + def fetch_related_oids( oid ) + oid = normalize_oid( oid ) + oid = self.model[ oid ] + return [] unless oid + return oid.related_nodes.map( &:id_c ) + end + + + ### Search the metastore for UUIDs which match the specified +criteria+ and + ### return them as an iterator. + def search( options={} ) + ds = self.model.naked.select( :id ) + self.log.debug "Starting search with %p" % [ ds ] + + ds = self.omit_related_resources( ds, options ) + ds = self.apply_search_criteria( ds, options ) + ds = self.apply_search_order( ds, options ) + ds = self.apply_search_direction( ds, options ) + ds = self.apply_search_limit( ds, options ) + + return ds.map {|row| row[:id] } + end + + + ### Update the metadata for the given +oid+ with the specified +values+ hash. + def merge( oid, values ) + values = normalize_keys( values ) + + md = self.model[ oid ] or return nil + md.merge!( values ) + md.save + end + + + ### Remove all metadata associated with +oid+ from the Metastore. + def remove( oid, *keys ) + self.model[ id: oid ].destroy + end + + + ### Remove all metadata associated with +oid+ except for the specified +keys+. + def remove_except( oid, *keys ) + keys = normalize_keys( keys ) + + md = self.model[ oid ] or return nil + md.user_metadata.keep_if {|key,_| keys.include?(key) } + md.save + end + + + ### Returns +true+ if the metastore has metadata associated with the specified +oid+. + def include?( oid ) + return self.model.count( id: oid ).nonzero? + end + + + ### Returns the number of objects the store contains. + def size + return self.model.count + end + + + ######### + protected + ######### + + ### Omit related resources from the search dataset +ds+ unless the given + ### +options+ specify otherwise. + def omit_related_resources( ds, options ) + unless options[:include_related] + self.log.debug " omitting entries for related resources" + ds = ds.unrelated + end + return ds + end + + + ### Apply the search :criteria from the specified +options+ to the collection + ### in +ds+ and return the modified dataset. + def apply_search_criteria( ds, options ) + if (( criteria = options[:criteria] )) + criteria.each do |field, value| + self.log.debug " applying criteria: %p => %p" % [ field.to_s, value ] + ds = ds.where_metadata( field => value ) + end + end + + return ds + end + + + ### Apply the search :order from the specified +options+ to the collection in + ### +ds+ and return the modified dataset. + def apply_search_order( ds, options ) + if options[:order] + columns = Array( options[:order] ) + ds = ds.order( columns.map(&:to_sym) ) + end + + return ds + end + + + ### Apply the search :direction from the specified +options+ to the collection + ### in +ds+ and return the modified dataset. + def apply_search_direction( ds, options ) + ds = ds.reverse if options[:direction] && options[:direction] == 'desc' + return ds + end + + + ### Apply the search :limit from the specified +options+ to the collection in + ### +ds+ and return the modified dataset. + def apply_search_limit( ds, options ) + if (( limit = options[:limit] )) + self.log.debug " limiting to %s results" % [ limit ] + offset = options[:offset] || 0 + ds = ds.limit( limit, offset ) + end + + return ds + end + +end # class Thingfish::Metastore::PgGraph +