Something like Datalog in Ruby. Aries a placeholder name, can't think of something clever right now.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

397 lines
8.4KB

  1. require "csv"
  2. require "erb"
  3. require "fileutils"
  4. require "json"
  5. module Aries
  6. class Index
  7. attr_reader :path, :name
  8. def initialize path, name
  9. @path = path
  10. @name = name
  11. end
  12. def refresh
  13. puts "Refreshing index..."
  14. File.readlines(File.join(@path, "#{@name}.csv")).each_with_index do |row, idx|
  15. if 1 > idx
  16. next
  17. end
  18. entity, attribute, value, timestamp, added = CSV.parse_line row
  19. self.commit idx, entity
  20. self.commit idx, attribute
  21. end
  22. puts "Index refreshed!"
  23. end
  24. def commit idx, target
  25. fp = File.join @path, ".#{@name}.index.#{target}"
  26. if File.exists?(fp) and File.foreach(fp).any? { |ln| ln[idx.to_s] }
  27. return
  28. end
  29. File.write fp, "#{idx.to_s}\n", mode: 'a+'
  30. end
  31. def check target
  32. fp = File.join @path, ".#{@name}.index.#{target}"
  33. if ! File.exists?(fp)
  34. return []
  35. end
  36. return File.readlines(fp).map &:to_i
  37. end
  38. end
  39. class Trie
  40. attr_accessor :tree
  41. def initialize
  42. @tree = []
  43. end
  44. def maker charlist, index, setter, trie
  45. if index == (charlist.length)
  46. return trie
  47. end
  48. exists = trie.filter {|n| n.first == charlist[index] }.first
  49. node = (! exists) ? [
  50. charlist[index], # symbol,
  51. [], # items
  52. ] : exists
  53. node[1] = maker charlist, index + 1, setter, node[1]
  54. if setter && index == (charlist.length - 1)
  55. node[2] = setter.call node[2]
  56. end
  57. if ! exists
  58. trie << node
  59. end
  60. return trie
  61. end
  62. def fact value, setter, trie
  63. chars = value.to_s.chars
  64. maker chars, 0, setter, trie
  65. end
  66. def refresh path
  67. puts "Refreshing trie..."
  68. File.readlines(path).each_with_index do |row, idx|
  69. if 1 > idx
  70. next
  71. end
  72. entity, attribute, value, timestmap, added = CSV.parse_line row
  73. added = added == 'true'
  74. if 1 < value.to_s.length && value.to_s.length < 30
  75. setter = added ? lambda { |leaf| (leaf or []) + [entity] } : lambda { |leaf| (leaf or []) - [entity] }
  76. @tree = fact "id_#{entity}", lambda { |leaf| (leaf or []) << idx }, @tree
  77. @tree = fact "kv_#{attribute}_#{value}", setter, @tree
  78. end
  79. end
  80. puts "Trie refreshed!"
  81. end
  82. def find trie, path, index = 0
  83. if index == path.length - 1
  84. return trie.first.last
  85. end
  86. node = trie.filter { |n| n.first == path[index] }.first
  87. if ! node
  88. return nil
  89. end
  90. return find(node[1], path, index + 1)
  91. end
  92. def ready?
  93. return true
  94. end
  95. def seek criteria, all = true
  96. hits = []
  97. criteria.each do |k, v|
  98. if ! v.is_a?(NilClass)
  99. sequence = "kv_#{k}_#{v}"
  100. hit = find @tree, sequence
  101. if hit
  102. hits += hit
  103. end
  104. end
  105. end
  106. return hits
  107. end
  108. end
  109. class Database
  110. attr_reader :headers
  111. attr_accessor :index
  112. attr_accessor :used_at
  113. # @version ?
  114. def initialize path_part, name = nil
  115. path = File.join(Dir.home, "Databases/aries", path_part)
  116. FileUtils.mkdir_p path
  117. name = "db" unless name
  118. @headers = ['entity', 'attribute', 'value', 'timestamp', 'added']
  119. @path = File.join path, "#{name}.csv"
  120. @index = Index.new path, name
  121. @trie = Trie.new
  122. if ! File.exists? @path
  123. CSV.open @path, "w" do |fh|
  124. fh << @headers
  125. end
  126. end
  127. @index.refresh
  128. @trie.refresh @path
  129. end
  130. # @brief appends to database
  131. # @version ?
  132. #
  133. # @todo append to file instead of loading a CSV object?
  134. def append entity, attribute, value, added = true
  135. idx = (File.foreach(@path).inject(0) { |c, ln| c+1 })
  136. CSV.open @path, "ab" do |fh|
  137. @index.commit idx, entity
  138. @index.commit idx, attribute
  139. # obviously this should be within some Trie method
  140. if 1 < value.to_s.length && value.to_s.length < 30
  141. setter = added ? lambda { |leaf| (leaf or []) + [entity] } : lambda { |leaf| (leaf or []) - [entity] }
  142. @trie.tree = @trie.fact "id_#{entity}", lambda { |leaf| (leaf or []) << idx }, @trie.tree
  143. @trie.tree = @trie.fact "kv_#{attribute}_#{value}", setter, @trie.tree
  144. end
  145. time = Time.now.to_i
  146. fh << [entity, attribute, value, time, added]
  147. end
  148. end
  149. # @version ?
  150. def add eav
  151. append eav['entity'], eav['attribute'], eav['value']
  152. end
  153. def readline position
  154. line = nil
  155. File.open(@path, 'r') do |f|
  156. position.to_i.times { f.gets }
  157. line = f.gets
  158. end
  159. return line
  160. end
  161. # @brief finds (latest) exact attribute
  162. # @version ?
  163. def check entity_id, key
  164. rows = @index.check entity_id
  165. attr = @index.check key
  166. pos = rows & attr
  167. val = nil # 'random-'.concat (('a'..'z').to_a + ('A'..'Z').to_a + (0..9).to_a).shuffle[0..4].join
  168. File.readlines(@path).each_with_index do |row, idx|
  169. if 1 > idx
  170. next
  171. end
  172. if ! pos.include? idx
  173. next
  174. end
  175. entity, attribute, value, timestamp, added = CSV.parse_line row
  176. added = JSON.parse added
  177. val = added ? value : nil
  178. end
  179. return val
  180. end
  181. # @version ?
  182. def get entity_id
  183. created = updated = 0
  184. rows = @index.check entity_id
  185. facts = {}
  186. File.readlines(@path).each_with_index do |row, idx|
  187. if 1 > idx
  188. next
  189. end
  190. if ! rows.empty?
  191. if ! rows.include? idx
  192. next
  193. end
  194. end
  195. parts = CSV.parse_line row
  196. entity, attribute, value, timestamp, added = parts
  197. if entity == entity_id
  198. added = JSON.parse added
  199. facts[attribute] = added ? value : nil
  200. created = [created, timestamp.to_i].min
  201. updated = [updated, timestamp.to_i].max
  202. end
  203. end
  204. facts['created'] = created
  205. facts['updated'] = updated
  206. facts['entity'] = entity_id
  207. return facts
  208. end
  209. # @return Array<String>
  210. #
  211. # @version ?
  212. def scan criteria, all = true
  213. hits = []
  214. seen = []
  215. criteria = criteria.transform_keys &:to_sym
  216. File.readlines(@path).each_with_index do |row, idx|
  217. if 1 > idx
  218. next
  219. end
  220. entity = row.split(',').first
  221. if seen.include? entity
  222. next
  223. end
  224. seen.append entity
  225. entity, attribute, value, timestamp, added = CSV.parse_line row
  226. added = added == 'true'
  227. matches = {}
  228. criteria.keys.each do |k|
  229. v = criteria[k]
  230. if k == "$check"
  231. matches[k] = ERB.new(criteria[k]).result(binding) # @todo - this does not work
  232. end
  233. val = check entity, k
  234. matches[k] = val == v
  235. # pp e: entity, q: criteria, curr: {k: k, v: v}, db: val, eq: val == v
  236. end
  237. if matches.values.all?
  238. record = get entity
  239. record = record.transform_keys &:to_sym
  240. if ! all
  241. return [record]
  242. end
  243. hits.append record
  244. end
  245. end
  246. return hits
  247. end
  248. def matching record, criteria
  249. matches = {}
  250. criteria.keys.each do |k|
  251. v = criteria[k]
  252. if k == "$check"
  253. matches[k] = ERB.new(criteria[k]).result(binding) # @todo - this does not work
  254. end
  255. val = record[k]
  256. matches[k] = val == v
  257. # pp e: entity, q: criteria, curr: {k: k, v: v}, db: val, eq: val == v
  258. end
  259. return matches.values.all?
  260. end
  261. # @version ?
  262. def query criteria, all = true
  263. if criteria.empty?
  264. return scan criteria, all
  265. end
  266. if ! @trie.ready?
  267. return scan criteria, all
  268. end
  269. x = @trie.seek(criteria).uniq.map {|e| get(e)}.filter { |record| matching(record, criteria) }.map { |e| e.transform_keys(&:to_sym) }
  270. if ! all
  271. return [x.first]
  272. end
  273. return x
  274. end
  275. # @version ?
  276. def find criteria
  277. query criteria
  278. end
  279. # @version ?
  280. def first criteria
  281. matches = query criteria, false
  282. if 1 > matches.length
  283. return nil
  284. end
  285. matches.first
  286. end
  287. # @version ?
  288. def last criteria
  289. matches = query criteria, false
  290. if 1 > matches.length
  291. return nil
  292. end
  293. matches.last
  294. end
  295. end
  296. end