Something like Datalog in Ruby. Aries a placeholder name, can't think of something clever right now.
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

397 rindas
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