diametric 0.1.1-java

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +28 -0
  3. data/Jarfile +20 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +264 -0
  6. data/Rakefile +49 -0
  7. data/bin/datomic-rest +33 -0
  8. data/bin/download-datomic +13 -0
  9. data/datomic_version.yml +4 -0
  10. data/diametric-java.gemspec +39 -0
  11. data/ext/diametric/DiametricCollection.java +147 -0
  12. data/ext/diametric/DiametricConnection.java +113 -0
  13. data/ext/diametric/DiametricDatabase.java +107 -0
  14. data/ext/diametric/DiametricEntity.java +90 -0
  15. data/ext/diametric/DiametricListenableFuture.java +47 -0
  16. data/ext/diametric/DiametricObject.java +66 -0
  17. data/ext/diametric/DiametricPeer.java +414 -0
  18. data/ext/diametric/DiametricService.java +102 -0
  19. data/ext/diametric/DiametricUUID.java +61 -0
  20. data/ext/diametric/DiametricUtils.java +183 -0
  21. data/lib/boolean_type.rb +3 -0
  22. data/lib/diametric.rb +42 -0
  23. data/lib/diametric/config.rb +54 -0
  24. data/lib/diametric/config/environment.rb +42 -0
  25. data/lib/diametric/entity.rb +659 -0
  26. data/lib/diametric/errors.rb +13 -0
  27. data/lib/diametric/generators/active_model.rb +42 -0
  28. data/lib/diametric/persistence.rb +48 -0
  29. data/lib/diametric/persistence/common.rb +82 -0
  30. data/lib/diametric/persistence/peer.rb +154 -0
  31. data/lib/diametric/persistence/rest.rb +107 -0
  32. data/lib/diametric/query.rb +259 -0
  33. data/lib/diametric/railtie.rb +52 -0
  34. data/lib/diametric/rest_service.rb +74 -0
  35. data/lib/diametric/service_base.rb +77 -0
  36. data/lib/diametric/transactor.rb +86 -0
  37. data/lib/diametric/version.rb +3 -0
  38. data/lib/diametric_service.jar +0 -0
  39. data/lib/tasks/create_schema.rb +27 -0
  40. data/lib/tasks/diametric_config.rb +45 -0
  41. data/lib/value_enums.rb +8 -0
  42. data/spec/conf_helper.rb +55 -0
  43. data/spec/config/free-transactor-template.properties +73 -0
  44. data/spec/config/logback.xml +59 -0
  45. data/spec/data/seattle-data0.dtm +452 -0
  46. data/spec/data/seattle-data1.dtm +326 -0
  47. data/spec/developer_create_sample.rb +39 -0
  48. data/spec/developer_query_spec.rb +120 -0
  49. data/spec/diametric/config_spec.rb +60 -0
  50. data/spec/diametric/entity_spec.rb +476 -0
  51. data/spec/diametric/peer_api_spec.rb +147 -0
  52. data/spec/diametric/persistence/peer_many2many_spec.rb +76 -0
  53. data/spec/diametric/persistence/peer_spec.rb +27 -0
  54. data/spec/diametric/persistence/rest_spec.rb +30 -0
  55. data/spec/diametric/persistence_spec.rb +59 -0
  56. data/spec/diametric/query_spec.rb +118 -0
  57. data/spec/diametric/rest_service_spec.rb +56 -0
  58. data/spec/diametric/transactor_spec.rb +68 -0
  59. data/spec/integration_spec.rb +107 -0
  60. data/spec/parent_child_sample.rb +42 -0
  61. data/spec/peer_integration_spec.rb +121 -0
  62. data/spec/peer_seattle_spec.rb +200 -0
  63. data/spec/rc2013_seattle_big.rb +82 -0
  64. data/spec/rc2013_seattle_small.rb +60 -0
  65. data/spec/rc2013_simple_sample.rb +72 -0
  66. data/spec/seattle_integration_spec.rb +106 -0
  67. data/spec/simple_validation_sample.rb +31 -0
  68. data/spec/spec_helper.rb +63 -0
  69. data/spec/support/entities.rb +157 -0
  70. data/spec/support/gen_entity_class.rb +9 -0
  71. data/spec/support/persistence_examples.rb +104 -0
  72. data/spec/test_version_file.yml +4 -0
  73. metadata +290 -0
@@ -0,0 +1,259 @@
1
+ require 'diametric'
2
+
3
+ module Diametric
4
+ # +Query+ objects are used to generate Datomic queries, whether to
5
+ # send via an external client or via the persistence API. The two
6
+ # methods used to generate a query are +.where+ and +.filter+, both
7
+ # of which are chainable. To get the query data and arguments for a
8
+ # +Query+, use the +data+ method.
9
+ #
10
+ # If you are using a persistence API, you can ask +Query+ to get the
11
+ # results of a Datomic query. +Diametric::Query+ is an
12
+ # +Enumerable+. To get the results of a query, use +Enumerable+
13
+ # methods such as +.each+ or +.first+. +Query+ also provides a
14
+ # +.all+ method to run the query and get the results.
15
+ class Query
16
+ include Enumerable
17
+
18
+ attr_reader :conditions, :filters, :filter_attrs, :model, :connection, :resolve
19
+
20
+ # Create a new Datomic query.
21
+ #
22
+ # @param model [Entity] This model must include +Datomic::Entity+. Including
23
+ # a persistence module is optional.
24
+ def initialize(model, connection_or_database=nil, resolve=false)
25
+ @model = model
26
+ @conditions = {}
27
+ @filters = []
28
+ @filter_attrs = []
29
+ @conn_or_db = connection_or_database
30
+ @resolve = resolve
31
+ end
32
+
33
+ # Add conditions to your Datomic query. Conditions check for equality
34
+ # against entity attributes. In addition, you can add conditions for
35
+ # use as variables in filters.
36
+ #
37
+ # @example Looking for mice named Wilbur.
38
+ # Query.new(Mouse).conditions(:name => "Wilbur")
39
+ #
40
+ # @param conditions [Hash] Datomic variables and values.
41
+ # @return [Query]
42
+ def where(conditions)
43
+ query = self.dup
44
+ query.conditions = query.conditions.merge(conditions)
45
+ query
46
+ end
47
+
48
+ # Add a filter to your Datomic query. Filters are known as expression clause
49
+ # predicates in the {Datomic query documentation}[http://docs.datomic.com/query.html].
50
+ #
51
+ # A filter can be in one of two forms. In the first, you pass a
52
+ # series of arguments. Any Ruby symbol given in this form will be
53
+ # converted to a EDN symbol. If the symbol is the same as one of
54
+ # the queried model's attributes or as a key passed to +where+, it
55
+ # will be prefixed with a +?+ so that it becomes a Datalog
56
+ # variable. In the second form, you pass
57
+ # {EDN}[https://github.com/relevance/edn-ruby] representing a
58
+ # Datomic predicate to +filter+. No conversion is done on this
59
+ # filter and it must be an EDN list.
60
+ #
61
+ # @example Passing arguments to be converted.
62
+ # query.filter(:>, :age, 21)
63
+ #
64
+ # @example Passing EDN, which will not be converted.
65
+ # query.filter(EDN::Type::List.new(EDN::Type::Symbol(">"),
66
+ # EDN::Type::Symbol("?age"),
67
+ # 21))
68
+ # # or, more simply
69
+ # query.filter(~[~">", ~"?age", 21])
70
+ #
71
+ # @param filter [Array] Either one +EDN::Type::List+ or a number of arguments
72
+ # that will be converted into a Datalog query.
73
+ # @return [Query]
74
+ def filter(*filter)
75
+ return peer_filter(filter) if self.model.instance_variable_get("@peer")
76
+ query = self.dup
77
+
78
+ if filter.first.is_a?(EDN::Type::List)
79
+ filter = filter.first
80
+ else
81
+ filter = filter.map { |e| convert_filter_element(e) }
82
+ filter = EDN::Type::List.new(*filter)
83
+ end
84
+
85
+ query.filters += [[filter]]
86
+ query
87
+ end
88
+
89
+ def peer_filter(*filter)
90
+ query = self.dup
91
+
92
+ if filter.first.is_a?(Array)
93
+ filter = filter.first
94
+ end
95
+ filter_attrs << filter[1].to_s
96
+ filter[1] = "?#{filter[1].to_s}"
97
+ filter = filter.tap {|e| e.to_s }.join(" ")
98
+ filter = "[(#{filter})]"
99
+
100
+ query.filters << filter
101
+ query
102
+ end
103
+
104
+
105
+ # Loop through the query results. In order to use +each+, your model *must*
106
+ # include a persistence API. At a minimum, it must have a +.q+ method that
107
+ # returns an +Enumerable+ object.
108
+ #
109
+ # @yield [Entity] An instance of the model passed to +Query+.
110
+ def each
111
+ # TODO check to see if the model has a `.q` method and give
112
+ # an appropriate error if not.
113
+ res = model.q(*data, @conn_or_db)
114
+ collapse_results(res).each do |entity|
115
+ if self.model.instance_variable_get("@peer") && @resolve
116
+ yield model.from_dbid_or_entity(entity.first.to_java, @conn_or_db, @resolve)
117
+ elsif self.model.instance_variable_get("@peer")
118
+ yield entity.first.to_java
119
+ # The map is for compatibility with Java peer persistence.
120
+ # TODO remove if possible
121
+ else
122
+ yield model.from_query(entity.map { |x| x }, @conn_or_db, @resolve)
123
+ end
124
+ end
125
+ end
126
+
127
+ # Return all query results.
128
+ #
129
+ # @return [Array<Entity>] Query results.
130
+ def all
131
+ map { |x| x }
132
+ end
133
+
134
+ # Create a Datomic query from the conditions and filters passed to this
135
+ # +Query+ object.
136
+ #
137
+ # @return [Array(Array, Array)] The first element of the array returned
138
+ # is the Datomic query composed of Ruby data. The second element is
139
+ # the arguments that used with the query.
140
+ def data
141
+ return peer_data if self.model.instance_variable_get("@peer")
142
+
143
+ vars = model.attribute_names.map { |attribute| ~"?#{attribute}" }
144
+
145
+ from = conditions.map { |k, _| ~"?#{k}" }
146
+
147
+ clauses = model.attribute_names.map { |attribute|
148
+ [~"?e", model.namespace(model.prefix, attribute), ~"?#{attribute}"]
149
+ }
150
+ clauses += filters
151
+
152
+ args = conditions.map { |_, v| v }
153
+
154
+ query = [
155
+ :find, ~"?e", *vars,
156
+ :in, ~"\$", *from,
157
+ :where, *clauses
158
+ ]
159
+
160
+ [query, args]
161
+ end
162
+
163
+ def peer_data
164
+ if conditions.empty? && filters.empty?
165
+ args = [model.prefix]
166
+ query = <<-EOQ
167
+ [:find ?e
168
+ :in $ [?include-ns ...]
169
+ :where
170
+ [?e ?aid ?v]
171
+ [?aid :db/ident ?a]
172
+ [(namespace ?a) ?ns]
173
+ [(= ?ns ?include-ns)]]
174
+ EOQ
175
+ else
176
+ unless conditions.empty?
177
+ clauses = conditions.keys.inject("") do |memo, attribute|
178
+ memo + "[?e " + model.namespace(model.prefix, attribute) + " ?#{attribute} ] "
179
+ end
180
+ from = conditions.inject("[") {|memo, kv| memo + "?#{kv.shift} "} +"]"
181
+ args = conditions.map { |_, v| v }
182
+ end
183
+
184
+ unless filter_attrs.empty?
185
+ clauses ||= ""
186
+ clauses = filter_attrs.inject(clauses) do |memo, attribute|
187
+ memo + "[?e " + model.namespace(model.prefix, attribute) + " ?#{attribute} ] "
188
+ end
189
+ end
190
+ query = "[:find ?e :in $ #{from} :where #{clauses} #{filters.join}]"
191
+ end
192
+
193
+ [query, args]
194
+ end
195
+
196
+ protected
197
+
198
+ def conditions=(conditions)
199
+ @conditions = conditions
200
+ end
201
+
202
+ def filters=(filters)
203
+ @filters = filters
204
+ end
205
+
206
+ def convert_filter_element(element)
207
+ if element.is_a?(Symbol)
208
+ if model.attribute_names.include?(element) || @conditions.keys.include?(element)
209
+ EDN::Type::Symbol.new("?#{element}")
210
+ else
211
+ EDN::Type::Symbol.new(element.to_s)
212
+ end
213
+ else
214
+ element
215
+ end
216
+ end
217
+
218
+ def collapse_results(res)
219
+ return res unless @resolve
220
+
221
+ res.group_by(&:first).map do |dbid, results|
222
+ # extract dbid from results
223
+ # NOTE: we prefer to_a.drop(1) over block arg decomposition because
224
+ # result may be a Java::ClojureLang::PersistentVector
225
+ results = results.map {|result| result.to_a.drop(1) }
226
+
227
+ unless results.flatten.empty?
228
+ # Group values from all results into one result set
229
+ # [["b", 123], ["c", 123]] #=> [["b", "c"], [123, 123]]
230
+ grouped_values = results.transpose
231
+ attr_grouped_values = grouped_values[0...model.attributes.size]
232
+ enum_grouped_values = grouped_values[model.attributes.size..-1]
233
+
234
+ # Attach attribute names to each collection of values
235
+ # => [[:letters, ["b", "c"]], [:number, [123, 123]]]
236
+ attr_to_values = model.attributes.keys.zip(attr_grouped_values)
237
+
238
+ # Retain cardinality/many attributes as a collection,
239
+ # but pick only one value for cardinality/one attributes
240
+ collapsed_values = attr_to_values.map do |attr, values|
241
+ if model.attributes[attr][:cardinality] == :many
242
+ values
243
+ elsif model.attributes[attr][:value_type] == "ref" &&
244
+ model.enum_names.include?(attr)
245
+ enum_grouped_values.shift.first
246
+ else
247
+ values.first
248
+ end
249
+ end
250
+
251
+ # Returning a singular result for each dbid
252
+ [dbid, *collapsed_values]
253
+ else
254
+ [dbid]
255
+ end
256
+ end
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,52 @@
1
+ # encoding: utf-8
2
+ require "diametric"
3
+ require "diametric/config"
4
+
5
+ require "rails"
6
+
7
+ require 'diametric/generators/active_model'
8
+
9
+ module Rails
10
+ module Diametric
11
+ class Railtie < Rails::Railtie
12
+
13
+ config.app_generators.orm :diametric
14
+
15
+ # Initialize Diametric. This will look for a diametric.yml in the config
16
+ # directory and configure diametric appropriately.
17
+ initializer "setup database" do
18
+ config_file = Rails.root.join("config", "diametric.yml")
19
+ if config_file.file?
20
+ begin
21
+ ::Diametric::Config.load_and_connect!(config_file)
22
+ rescue Exception => e
23
+ handle_configuration_error(e)
24
+ end
25
+ end
26
+ end
27
+
28
+ # After initialization we will warn the user if we can't find a diametric.yml and
29
+ # alert to create one.
30
+ initializer "warn when configuration is missing" do
31
+ config.after_initialize do
32
+ unless Rails.root.join("config", "diametric.yml").file? || ::Diametric::Config.configured?
33
+ puts "\nDiametric config not found. Create a config file at: config/diametric.yml"
34
+ puts "to generate one run: rails generate diametric:config\n\n"
35
+ end
36
+ end
37
+ end
38
+ rake_tasks do
39
+ require "#{File.join(File.dirname(__FILE__), "..", "tasks", "create_schema.rb")}"
40
+ require "#{File.join(File.dirname(__FILE__), "..", "tasks", "diametric_config.rb")}"
41
+ end
42
+ # Rails runs all initializers first before getting into any generator
43
+ # code, so we have no way in the initializer to know if we are
44
+ # generating a diametric.yml. So instead of failing, we catch all the
45
+ # errors and print them out.
46
+ def handle_configuration_error(e)
47
+ puts "There is a configuration error with the current diametric.yml."
48
+ puts e.message
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,74 @@
1
+ require 'diametric/service_base'
2
+
3
+ module Diametric
4
+ class RestService
5
+ include ::Diametric::ServiceBase
6
+ class << self
7
+ def datomic_command(datomic_home)
8
+ classpath = datomic_classpath(datomic_home)
9
+ command = ["java -server -Xmx1g", "-cp", classpath, "clojure.main", "-i", "#{datomic_home}/bin/bridge.clj", "--main datomic.rest"].flatten.join(" ")
10
+ end
11
+ end
12
+
13
+ attr_accessor :datomic_version, :datomic_version_no, :datomic_home, :pid
14
+ attr_accessor :host, :port, :db_alias, :uri
15
+
16
+ def initialize(conf="datomic_version.yml", dest="vendor/datomic")
17
+ @conf = conf
18
+ @dest = dest
19
+ @datomic_version = RestService.datomic_version(conf)
20
+ @datomic_home = File.join(File.dirname(__FILE__), "../..", dest, @datomic_version)
21
+ @datomic_version_no = RestService.datomic_version_no(@datomic_version)
22
+ @pid = nil
23
+ end
24
+
25
+ def start(opts={})
26
+ return if @pid
27
+ RestService.download(@conf, @dest)
28
+ command = RestService.datomic_command(@datomic_home)
29
+
30
+ require 'socket'
31
+ @host = opts[:host] ? opts[:host] : Socket.gethostname
32
+ @port = opts[:port] ? opts[:port] : 9000
33
+ @db_alias = opts[:db_alias] ? opts[:db_alias] : "free"
34
+ @uri = opts[:uri] ? opts[:uri] : "datomic:mem://"
35
+
36
+ uri = URI("http://#{@host}:#{@port}/")
37
+
38
+ unless port_available?(uri)
39
+ puts "Somebody is using #{@port}. Choose other."
40
+ return
41
+ end
42
+
43
+ temp_pid = spawn("#{command} -p #{@port} #{@db_alias} #{@uri}")
44
+
45
+ @pid = temp_pid if ready?(uri)
46
+ end
47
+
48
+ def stop
49
+ Process.kill("HUP", @pid) if @pid
50
+ @pid = nil
51
+ end
52
+
53
+ def port_available?(uri)
54
+ response = Net::HTTP.get_response(uri)
55
+ false
56
+ rescue Errno::ECONNREFUSED
57
+ true
58
+ end
59
+
60
+ def ready?(uri)
61
+ while true
62
+ begin
63
+ response = Net::HTTP.get_response(uri)
64
+ return true
65
+ rescue
66
+ sleep 1
67
+ redo
68
+ end
69
+ end
70
+ true
71
+ end
72
+
73
+ end
74
+ end
@@ -0,0 +1,77 @@
1
+ require 'net/http'
2
+ require 'pathname'
3
+ require 'yaml'
4
+
5
+ module Diametric
6
+ module ServiceBase
7
+ def self.included(base)
8
+ base.send(:extend, ClassMethods)
9
+ end
10
+
11
+ module ClassMethods
12
+ def datomic_conf_file?(file="datomic_version.yml")
13
+ if Pathname.new(file).relative?
14
+ file = File.join(File.dirname(__FILE__), "../..", file)
15
+ end
16
+ return File.exists?(file)
17
+ end
18
+
19
+ def datomic_version(conf="datomic_version.yml")
20
+ if datomic_conf_file?(conf)
21
+ datomic_names = File.read(File.join(File.dirname(__FILE__), "../..", conf))
22
+ datomic_versions = YAML.load(datomic_names)
23
+ if ENV['DIAMETRIC_ENV'] && (ENV['DIAMETRIC_ENV'] == "pro")
24
+ datomic_versions["pro"]
25
+ else
26
+ datomic_versions["free"]
27
+ end
28
+ else
29
+ conf
30
+ end
31
+ end
32
+
33
+ def datomic_version_no(datomic_version_str)
34
+ /(\d|\.)+/.match(datomic_version_str)[0]
35
+ end
36
+
37
+ def downloaded?(conf="datomic_version.yml", dest="vendor/datomic")
38
+ datomic_home = datomic_version(conf)
39
+ if Pathname.new(dest).relative?
40
+ dest = File.join(File.dirname(__FILE__), "..", "..", dest)
41
+ end
42
+ File.exists?(File.join(dest, datomic_home))
43
+ end
44
+
45
+ def download(conf="datomic_version.yml", dest="vendor/datomic")
46
+ return true if downloaded?(conf, dest)
47
+ version = datomic_version(conf)
48
+ url = "http://downloads.datomic.com/#{datomic_version_no(version)}/#{version}.zip"
49
+ if Pathname.new(dest).relative?
50
+ dest = File.join(File.dirname(__FILE__), "../..", dest)
51
+ end
52
+ require 'open-uri'
53
+ require 'zip/zipfilesystem'
54
+ open(url) do |zip_file|
55
+ Zip::ZipFile.open(zip_file.path) do |zip_path|
56
+ zip_path.each do |zip_entry|
57
+ file_path = File.join(dest, zip_entry.to_s)
58
+ FileUtils.mkdir_p(File.dirname(file_path))
59
+ zip_path.extract(zip_entry, file_path) { true }
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ def datomic_classpath(datomic_home)
66
+ # Find jar archives
67
+ jars = Dir["#{datomic_home}/lib/*.jar"]
68
+ jars += Dir["#{datomic_home}/*transactor*.jar"]
69
+
70
+ # Setup CLASSPATH
71
+ classpath = jars.join(File::PATH_SEPARATOR)
72
+ files = ["samples/clj", "bin", "resources"]
73
+ classpath += File::PATH_SEPARATOR + files.collect {|f| datomic_home + "/" + f}.join(File::PATH_SEPARATOR)
74
+ end
75
+ end
76
+ end
77
+ end