diametric 0.1.1-java

Sign up to get free protection for your applications and to get access to all the features.
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