dalton 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.jrubyrc +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +8 -0
- data/Jarfile +7 -0
- data/README.md +35 -0
- data/Rakefile +7 -0
- data/dalton.gemspec +23 -0
- data/epl-v10.html +261 -0
- data/examples/posts_controller.rb +48 -0
- data/lib/dalton.rb +37 -0
- data/lib/dalton/attribute.rb +16 -0
- data/lib/dalton/connection.rb +104 -0
- data/lib/dalton/database.rb +48 -0
- data/lib/dalton/datomization.rb +18 -0
- data/lib/dalton/entity.rb +75 -0
- data/lib/dalton/exception.rb +68 -0
- data/lib/dalton/transaction_result.rb +33 -0
- data/lib/dalton/translation.rb +65 -0
- data/lib/dalton/undatomization.rb +11 -0
- data/lib/dalton/utility.rb +91 -0
- data/lib/dalton/version.rb +3 -0
- data/spec/dalton/connection_spec.rb +133 -0
- data/spec/dalton/database_spec.rb +85 -0
- data/spec/dalton/datomization_spec.rb +94 -0
- data/spec/dalton/entity_spec.rb +37 -0
- data/spec/dalton/translation_spec.rb +126 -0
- data/spec/dalton/utility_spec.rb +37 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/support/clojure_equal_matcher.rb +52 -0
- data/spec/support/datomic_context.rb +19 -0
- metadata +101 -0
@@ -0,0 +1,16 @@
|
|
1
|
+
module Dalton
|
2
|
+
class Attribute
|
3
|
+
def initialize(datomic_attribute)
|
4
|
+
@datomic_attribute = datomic_attribute
|
5
|
+
end
|
6
|
+
|
7
|
+
def method_missing(name, *args, &block)
|
8
|
+
if @datomic_attribute.respond_to?(name)
|
9
|
+
Translation.from_clj(@datomic_attribute.send(name, *args, &block))
|
10
|
+
else
|
11
|
+
super
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
java_import "clojure.lang.Keyword"
|
2
|
+
java_import "datomic.Peer"
|
3
|
+
|
4
|
+
module Dalton
|
5
|
+
class Connection
|
6
|
+
|
7
|
+
include Dalton::Datomization
|
8
|
+
|
9
|
+
def initialize(uri)
|
10
|
+
@uri = uri
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.connect(uri)
|
14
|
+
Peer.createDatabase(uri)
|
15
|
+
database = new(uri)
|
16
|
+
database.connect
|
17
|
+
return database
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :uri, :datomic_connection, :db
|
21
|
+
|
22
|
+
def create
|
23
|
+
Peer.createDatabase(uri) or
|
24
|
+
raise DatomicError, "Unable to create database at \"#{uri}\"."
|
25
|
+
end
|
26
|
+
|
27
|
+
def destroy
|
28
|
+
Peer.deleteDatabase(uri) or
|
29
|
+
raise DatomicError, "Unable to destroy database at \"#{uri}\"."
|
30
|
+
end
|
31
|
+
|
32
|
+
def connect
|
33
|
+
@datomic_connection = Peer.connect(uri) or
|
34
|
+
raise DatomicError, "Unable to connect to database at \"#{uri}\"."
|
35
|
+
refresh
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
39
|
+
def db=(new_db)
|
40
|
+
@db = new_db.is_a?(Database) ? new_db : Database.new(new_db)
|
41
|
+
end
|
42
|
+
|
43
|
+
def refresh
|
44
|
+
self.db = @datomic_connection.db
|
45
|
+
db
|
46
|
+
end
|
47
|
+
|
48
|
+
def transact(datoms)
|
49
|
+
data = self.class.convert_datoms(datoms)
|
50
|
+
# STDERR.puts "data=#{data.to_edn}"
|
51
|
+
result = TransactionResult.new(@datomic_connection.transact(data).get)
|
52
|
+
self.db = result.db_after
|
53
|
+
Translation.from_clj(result)
|
54
|
+
rescue Java::JavaUtilConcurrent::ExecutionException => e
|
55
|
+
cause = e.getCause
|
56
|
+
if cause.respond_to?(:data)
|
57
|
+
err_data = Translation.from_clj(cause.data)
|
58
|
+
case err_data[:'db/error']
|
59
|
+
when :'db.error/unique-conflict'
|
60
|
+
raise UniqueConflict.parse(cause.getMessage)
|
61
|
+
when :'db.error/wrong-type-for-attribute'
|
62
|
+
raise TypeError.parse(cause.getMessage)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
raise DatomicError, "Transaction failed: #{e.getMessage}"
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
def retract(entity)
|
71
|
+
entity_id = entity.is_a?(Entity) ? entity.id : entity
|
72
|
+
transact([[:'db.fn/retractEntity', entity_id]])
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.convert_datoms(datoms)
|
76
|
+
case datoms
|
77
|
+
when Array
|
78
|
+
Translation.from_ruby(datoms)
|
79
|
+
when String
|
80
|
+
Utility.read_edn(datoms)
|
81
|
+
else
|
82
|
+
raise ArgumentError, 'datoms must be an Array or a String containing EDN.'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.tempid(partition=:'db.part/user', id=nil)
|
87
|
+
partition = Keyword.intern(partition.to_s.sub(/^:/, ''))
|
88
|
+
if id
|
89
|
+
Peer.tempid(partition, id)
|
90
|
+
else
|
91
|
+
Peer.tempid(partition)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.tempid?(id)
|
96
|
+
0 > case id
|
97
|
+
when Numeric
|
98
|
+
id
|
99
|
+
when Java::DatomicDb::DbId
|
100
|
+
id.get(Utility::kw('idx'))
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Dalton
|
2
|
+
|
3
|
+
class Database
|
4
|
+
|
5
|
+
include Dalton::Undatomization
|
6
|
+
|
7
|
+
def initialize(datomic_database_value)
|
8
|
+
@datomic_db = datomic_database_value
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :datomic_db
|
12
|
+
|
13
|
+
def ==(other)
|
14
|
+
datomic_db == other.datomic_db
|
15
|
+
end
|
16
|
+
|
17
|
+
def q(query, *args)
|
18
|
+
translated_query = Translation.from_ruby(query)
|
19
|
+
# STDERR.puts "translated_query=#{translated_query.to_edn}"
|
20
|
+
result = Peer.q(translated_query, datomic_db, *args)
|
21
|
+
Translation.from_clj(result)
|
22
|
+
rescue Java::JavaUtilConcurrent::ExecutionException => e
|
23
|
+
raise DatomicError, "Query failed: #{e.getMessage}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def entity(entity_id)
|
27
|
+
Entity.new(datomic_db.entity(Translation.from_ruby(entity_id)))
|
28
|
+
rescue Java::JavaUtilConcurrent::ExecutionException => e
|
29
|
+
raise DatomicError, "Entity retrieval failed: #{e.getMessage}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def retrieve(query, *inputs)
|
33
|
+
q(query, *inputs).lazy.map { |result| entity(result.first) }
|
34
|
+
end
|
35
|
+
|
36
|
+
def attribute(id)
|
37
|
+
Attribute.new(datomic_db.attribute(Translation.from_ruby(id)))
|
38
|
+
end
|
39
|
+
|
40
|
+
def basis_t
|
41
|
+
datomic_db.basisT
|
42
|
+
end
|
43
|
+
|
44
|
+
def ==(other)
|
45
|
+
self.datomic_db == other.datomic_db
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Dalton
|
2
|
+
module Datomization
|
3
|
+
|
4
|
+
Utility.require_clojure('goodguide.datomizer.datomize.setup')
|
5
|
+
Utility.require_clojure('goodguide.datomizer.datomize.decode')
|
6
|
+
|
7
|
+
def set_up_datomizer
|
8
|
+
Utility.run_clojure_function('goodguide.datomizer.datomize.setup/load-datomizer-schema', datomic_connection)
|
9
|
+
Utility.run_clojure_function('goodguide.datomizer.datomize.setup/load-datomizer-functions', datomic_connection)
|
10
|
+
end
|
11
|
+
|
12
|
+
def datomize(data)
|
13
|
+
result = transact([[:'dmzr/datomize', data]])
|
14
|
+
result.resolve_tempid(data[:'db/id'])
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Dalton
|
2
|
+
class Entity
|
3
|
+
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
def initialize(datomic_entity)
|
7
|
+
@datomic_entity = datomic_entity
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :datomic_entity
|
11
|
+
|
12
|
+
def db
|
13
|
+
Database.new(datomic_entity.db)
|
14
|
+
end
|
15
|
+
|
16
|
+
def get(key)
|
17
|
+
Translation.from_clj(datomic_entity.get(Translation.from_ruby(key)))
|
18
|
+
end
|
19
|
+
|
20
|
+
alias_method :[], :get
|
21
|
+
|
22
|
+
def touch
|
23
|
+
@datomic_entity.touch
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def keys
|
28
|
+
datomic_entity.keySet.map{|x| x.sub(/^:/, '').to_sym}.to_a
|
29
|
+
end
|
30
|
+
|
31
|
+
def id
|
32
|
+
get(:'db/id')
|
33
|
+
end
|
34
|
+
|
35
|
+
def each
|
36
|
+
if block_given?
|
37
|
+
keys.each do |key|
|
38
|
+
yield [key, get(key)]
|
39
|
+
end
|
40
|
+
self
|
41
|
+
else
|
42
|
+
Enumerator.new(self)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
alias_method :each_pair, :each
|
46
|
+
|
47
|
+
def to_h
|
48
|
+
Hash[map {|key, value|
|
49
|
+
[key, decode(value)]
|
50
|
+
}]
|
51
|
+
end
|
52
|
+
|
53
|
+
def ==(other)
|
54
|
+
other.instance_of?(self.class) && Utility.clojure_equal?(datomic_entity, other.datomic_entity)
|
55
|
+
end
|
56
|
+
|
57
|
+
def decode(value)
|
58
|
+
case value
|
59
|
+
when Dalton::Entity
|
60
|
+
value.to_h
|
61
|
+
when Set
|
62
|
+
Set.new(value.map{|x| decode(x)})
|
63
|
+
else
|
64
|
+
Translation.from_clj(value)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
DB_FUNCTION_KEY = ::Keyword.intern('db/fn')
|
69
|
+
|
70
|
+
def fn
|
71
|
+
@datomic_entity.get(DB_FUNCTION_KEY)
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Dalton
|
2
|
+
class UniqueConflict < DatomicError
|
3
|
+
# TODO: [jneen] this is terrible, but error handling is not implemented at the moment.
|
4
|
+
# eventually all this data should be accessible via (ex-data e).
|
5
|
+
MESSAGE_RE =
|
6
|
+
%r(^:db[.]error/unique-conflict Unique conflict: :([a-z./-]+), value: (.*?) already held by: (\d+) asserted for: (\d+)$)o
|
7
|
+
|
8
|
+
def self.parse(message)
|
9
|
+
message =~ MESSAGE_RE
|
10
|
+
raise ArgumentError, "invalid format: #{message.inspect}" unless $~
|
11
|
+
new(
|
12
|
+
attribute: $1.to_sym,
|
13
|
+
value: $2,
|
14
|
+
existing_id: Integer($3),
|
15
|
+
new_id: Integer($4),
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :message, :attribute, :value, :existing_id, :new_id
|
20
|
+
|
21
|
+
def initialize(opts={})
|
22
|
+
@attribute = opts.fetch(:attribute)
|
23
|
+
@value = opts.fetch(:value)
|
24
|
+
@existing_id = opts.fetch(:existing_id)
|
25
|
+
@new_id = opts.fetch(:new_id)
|
26
|
+
@message = "Unique conflict: tried to assign duplicate #@attribute to #@new_id, already held by #@existing_id. value: #@value"
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
"#{self.class.name}: #@message"
|
31
|
+
end
|
32
|
+
|
33
|
+
def inspect
|
34
|
+
"#<#{self.class.name}: @attribute=#@attribute @value=#@value @existing_id=#@existing_id @new_id=#@new_id>"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class TypeError < DatomicError
|
39
|
+
MESSAGE_RE = %r(^:db[.]error/wrong-type-for-attribute Value (.*?) is not a valid :(\w+) for attribute :([a-z./-]+)$)
|
40
|
+
|
41
|
+
def self.parse(message)
|
42
|
+
message =~ MESSAGE_RE
|
43
|
+
raise ArgumentError, "invalid format: #{message.inspect}" unless $~
|
44
|
+
new(
|
45
|
+
value: $1,
|
46
|
+
type: $2.to_sym,
|
47
|
+
attribute: $3.to_sym
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
attr_reader :message, :value, :type, :attribute
|
52
|
+
|
53
|
+
def initialize(opts={})
|
54
|
+
@value = opts.fetch(:value)
|
55
|
+
@type = opts.fetch(:type)
|
56
|
+
@attribute = opts.fetch(:attribute)
|
57
|
+
@message = "Type error: tried to set #@attribute as #@value, expected type #@type"
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_s
|
61
|
+
"#{self.class.name}: #@message"
|
62
|
+
end
|
63
|
+
|
64
|
+
def inspect
|
65
|
+
"#<#{self.class.name}: @attribute=#@attribute @value=#@value @type=#@type>"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
java_import "datomic.Peer"
|
2
|
+
|
3
|
+
module Dalton
|
4
|
+
class TransactionResult
|
5
|
+
def initialize(result_map)
|
6
|
+
@result_map = result_map
|
7
|
+
end
|
8
|
+
|
9
|
+
def db_before
|
10
|
+
Dalton::Database.new(@result_map.get(Java::Datomic::Connection.DB_BEFORE))
|
11
|
+
end
|
12
|
+
|
13
|
+
def db_after
|
14
|
+
Dalton::Database.new(@result_map.get(Java::Datomic::Connection.DB_AFTER))
|
15
|
+
end
|
16
|
+
|
17
|
+
def tx_data
|
18
|
+
Translation.from_clj(@result_map.get(Java::Datomic::Connection.TX_DATA))
|
19
|
+
end
|
20
|
+
|
21
|
+
def raw_tempids
|
22
|
+
@result_map.get(Java::Datomic::Connection.TEMPIDS)
|
23
|
+
end
|
24
|
+
|
25
|
+
def tempids
|
26
|
+
Translation.from_clj(raw_tempids)
|
27
|
+
end
|
28
|
+
|
29
|
+
def resolve_tempid(tempid)
|
30
|
+
Peer.resolve_tempid(db_after.datomic_db, raw_tempids, tempid)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
java_import "clojure.lang.PersistentHashSet"
|
2
|
+
java_import "clojure.lang.Keyword"
|
3
|
+
|
4
|
+
module Zweikopf
|
5
|
+
module Primitive
|
6
|
+
def self.is_primitive_type?(obj) # monkey patch to remove DateTime from list of primitives and allow them to be converted. :-/
|
7
|
+
[String, Fixnum, Integer, Float, TrueClass, FalseClass].include?(obj.class)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module Keyword
|
12
|
+
# Monkey patch special handling for datalog variables
|
13
|
+
def self.from_ruby(keyword)
|
14
|
+
if keyword.to_s =~ /^[?$]/
|
15
|
+
Java::ClojureLang::Symbol.intern(keyword.to_s)
|
16
|
+
else
|
17
|
+
::Keyword.intern(keyword.to_s)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module Dalton
|
24
|
+
module Translation
|
25
|
+
|
26
|
+
module_function
|
27
|
+
|
28
|
+
#TODO: Fork Zweikopf, add Set handling, submit pull request
|
29
|
+
|
30
|
+
def from_clj(object)
|
31
|
+
Zweikopf::Transformer.from_clj(object) do |value|
|
32
|
+
case value
|
33
|
+
when Java::ClojureLang::Symbol
|
34
|
+
value.to_s.to_sym
|
35
|
+
when Java::JavaUtil::Set
|
36
|
+
Set.new(value.map{|x| from_clj(x)})
|
37
|
+
when Java::JavaUtil::ArrayList
|
38
|
+
value.map { |x| from_clj(x) }
|
39
|
+
when Java::Datomic::Entity, Java::DatomicQuery::EntityMap
|
40
|
+
Dalton::Entity.new(value)
|
41
|
+
when Java::JavaUtil::Date
|
42
|
+
Time.at(value.getTime / 1000).to_datetime
|
43
|
+
else
|
44
|
+
value
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def from_ruby(object)
|
50
|
+
Zweikopf::Transformer.from_ruby(object) do |value|
|
51
|
+
case value
|
52
|
+
when ::Set
|
53
|
+
PersistentHashSet.create(value.map{|x| from_ruby(x)})
|
54
|
+
when Dalton::Entity
|
55
|
+
value.datomic_entity
|
56
|
+
when DateTime
|
57
|
+
Java::JavaUtil::Date.new(value.to_time.to_i * 1000)
|
58
|
+
else
|
59
|
+
value
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|