require "csv" require "erb" require "fileutils" require "json" module Aries class Index attr_reader :path, :name def initialize path, name @path = path @name = name end def refresh puts "Refreshing index..." File.readlines(File.join(@path, "#{@name}.csv")).each_with_index do |row, idx| if 1 > idx next end entity, attribute, value, timestamp, added = CSV.parse_line row self.commit idx, entity self.commit idx, attribute end puts "Index refreshed!" end def commit idx, target fp = File.join @path, ".#{@name}.index.#{target}" if File.exists?(fp) and File.foreach(fp).any? { |ln| ln[idx.to_s] } return end File.write fp, "#{idx.to_s}\n", mode: 'a+' end def check target fp = File.join @path, ".#{@name}.index.#{target}" if ! File.exists?(fp) return [] end return File.readlines(fp).map &:to_i end end class Trie attr_accessor :tree def initialize @tree = [] end def maker charlist, index, setter, trie if index == (charlist.length) return trie end exists = trie.filter {|n| n.first == charlist[index] }.first node = (! exists) ? [ charlist[index], # symbol, [], # items ] : exists node[1] = maker charlist, index + 1, setter, node[1] if setter && index == (charlist.length - 1) node[2] = setter.call node[2] end if ! exists trie << node end return trie end def fact value, setter, trie chars = value.to_s.chars maker chars, 0, setter, trie end def refresh path puts "Refreshing trie..." File.readlines(path).each_with_index do |row, idx| if 1 > idx next end entity, attribute, value, timestmap, added = CSV.parse_line row added = added == 'true' if 1 < value.to_s.length && value.to_s.length < 30 setter = added ? lambda { |leaf| (leaf or []) + [entity] } : lambda { |leaf| (leaf or []) - [entity] } @tree = fact "id_#{entity}", lambda { |leaf| (leaf or []) << idx }, @tree @tree = fact "kv_#{attribute}_#{value}", setter, @tree end end puts "Trie refreshed!" end def find trie, path, index = 0 if index == path.length - 1 return trie.first.last end node = trie.filter { |n| n.first == path[index] }.first if ! node return nil end return find(node[1], path, index + 1) end def ready? return true end def seek criteria, all = true hits = [] criteria.each do |k, v| if ! v.is_a?(NilClass) sequence = "kv_#{k}_#{v}" hit = find @tree, sequence if hit hits += hit end end end return hits end end class Database attr_reader :headers attr_accessor :index attr_accessor :used_at # @version ? def initialize path_part, name = nil path = File.join(Dir.home, "Databases/aries", path_part) FileUtils.mkdir_p path name = "db" unless name @headers = ['entity', 'attribute', 'value', 'timestamp', 'added'] @path = File.join path, "#{name}.csv" @index = Index.new path, name @trie = Trie.new if ! File.exists? @path CSV.open @path, "w" do |fh| fh << @headers end end @index.refresh @trie.refresh @path end # @brief appends to database # @version ? # # @todo append to file instead of loading a CSV object? def append entity, attribute, value, added = true idx = (File.foreach(@path).inject(0) { |c, ln| c+1 }) CSV.open @path, "ab" do |fh| @index.commit idx, entity @index.commit idx, attribute # obviously this should be within some Trie method if 1 < value.to_s.length && value.to_s.length < 30 setter = added ? lambda { |leaf| (leaf or []) + [entity] } : lambda { |leaf| (leaf or []) - [entity] } @trie.tree = @trie.fact "id_#{entity}", lambda { |leaf| (leaf or []) << idx }, @trie.tree @trie.tree = @trie.fact "kv_#{attribute}_#{value}", setter, @trie.tree end time = Time.now.to_i fh << [entity, attribute, value, time, added] end end # @version ? def add eav append eav['entity'], eav['attribute'], eav['value'] end def readline position line = nil File.open(@path, 'r') do |f| position.to_i.times { f.gets } line = f.gets end return line end # @brief finds (latest) exact attribute # @version ? def check entity_id, key rows = @index.check entity_id attr = @index.check key pos = rows & attr val = nil # 'random-'.concat (('a'..'z').to_a + ('A'..'Z').to_a + (0..9).to_a).shuffle[0..4].join File.readlines(@path).each_with_index do |row, idx| if 1 > idx next end if ! pos.include? idx next end entity, attribute, value, timestamp, added = CSV.parse_line row added = JSON.parse added val = added ? value : nil end return val end # @version ? def get entity_id created = updated = 0 rows = @index.check entity_id facts = {} File.readlines(@path).each_with_index do |row, idx| if 1 > idx next end if ! rows.empty? if ! rows.include? idx next end end parts = CSV.parse_line row entity, attribute, value, timestamp, added = parts if entity == entity_id added = JSON.parse added facts[attribute] = added ? value : nil created = [created, timestamp.to_i].min updated = [updated, timestamp.to_i].max end end facts['created'] = created facts['updated'] = updated facts['entity'] = entity_id return facts end # @return Array # # @version ? def scan criteria, all = true hits = [] seen = [] criteria = criteria.transform_keys &:to_sym File.readlines(@path).each_with_index do |row, idx| if 1 > idx next end entity = row.split(',').first if seen.include? entity next end seen.append entity entity, attribute, value, timestamp, added = CSV.parse_line row added = added == 'true' matches = {} criteria.keys.each do |k| v = criteria[k] if k == "$check" matches[k] = ERB.new(criteria[k]).result(binding) # @todo - this does not work end val = check entity, k matches[k] = val == v # pp e: entity, q: criteria, curr: {k: k, v: v}, db: val, eq: val == v end if matches.values.all? record = get entity record = record.transform_keys &:to_sym if ! all return [record] end hits.append record end end return hits end def matching record, criteria matches = {} criteria.keys.each do |k| v = criteria[k] if k == "$check" matches[k] = ERB.new(criteria[k]).result(binding) # @todo - this does not work end val = record[k] matches[k] = val == v # pp e: entity, q: criteria, curr: {k: k, v: v}, db: val, eq: val == v end return matches.values.all? end # @version ? def query criteria, all = true if criteria.empty? return scan criteria, all end if ! @trie.ready? return scan criteria, all end x = @trie.seek(criteria).uniq.map {|e| get(e)}.filter { |record| matching(record, criteria) }.map { |e| e.transform_keys(&:to_sym) } if ! all return [x.first] end return x end # @version ? def find criteria query criteria end # @version ? def first criteria matches = query criteria, false if 1 > matches.length return nil end matches.first end # @version ? def last criteria matches = query criteria, false if 1 > matches.length return nil end matches.last end end end