|
- 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<String>
- #
- # @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
|