sunstone 0.1.0 → 1.0.0

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/lib/active_record/connection_adapters/sunstone/column.rb +19 -0
  3. data/lib/active_record/connection_adapters/sunstone/database_statements.rb +40 -0
  4. data/lib/active_record/connection_adapters/sunstone/schema_statements.rb +95 -0
  5. data/lib/active_record/connection_adapters/sunstone/type/date_time.rb +22 -0
  6. data/lib/active_record/connection_adapters/sunstone_adapter.rb +177 -0
  7. data/lib/arel/collectors/sunstone.rb +75 -0
  8. data/lib/arel/visitors/sunstone.rb +769 -0
  9. data/lib/ext/active_record/associations/builder/has_and_belongs_to_many.rb +48 -0
  10. data/lib/ext/active_record/relation.rb +26 -0
  11. data/lib/ext/active_record/statement_cache.rb +24 -0
  12. data/lib/sunstone.rb +37 -347
  13. data/lib/sunstone/connection.rb +337 -0
  14. data/sunstone.gemspec +3 -2
  15. data/test/sunstone/connection_test.rb +319 -0
  16. data/test/sunstone/parser_test.rb +21 -21
  17. metadata +30 -36
  18. data/lib/sunstone/model.rb +0 -23
  19. data/lib/sunstone/model/attributes.rb +0 -99
  20. data/lib/sunstone/model/persistence.rb +0 -168
  21. data/lib/sunstone/schema.rb +0 -38
  22. data/lib/sunstone/type/boolean.rb +0 -19
  23. data/lib/sunstone/type/date_time.rb +0 -20
  24. data/lib/sunstone/type/decimal.rb +0 -19
  25. data/lib/sunstone/type/integer.rb +0 -17
  26. data/lib/sunstone/type/mutable.rb +0 -16
  27. data/lib/sunstone/type/string.rb +0 -18
  28. data/lib/sunstone/type/value.rb +0 -97
  29. data/test/sunstone/model/associations_test.rb +0 -55
  30. data/test/sunstone/model/attributes_test.rb +0 -60
  31. data/test/sunstone/model/persistence_test.rb +0 -173
  32. data/test/sunstone/model_test.rb +0 -11
  33. data/test/sunstone/schema_test.rb +0 -25
  34. data/test/sunstone/type/boolean_test.rb +0 -24
  35. data/test/sunstone/type/date_time_test.rb +0 -31
  36. data/test/sunstone/type/decimal_test.rb +0 -27
  37. data/test/sunstone/type/integer_test.rb +0 -29
  38. data/test/sunstone/type/string_test.rb +0 -54
  39. data/test/sunstone/type/value_test.rb +0 -27
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5c574fe947ca4f3c808d4b1d843ae0e2bb4ce9ea
4
- data.tar.gz: a2410c511c64d0f2b7a85dbc5cbbff3b23a2f7a1
3
+ metadata.gz: c22d4667778ecfe53034454215950b7c93c82dac
4
+ data.tar.gz: fab3525c04c36627f5df0788ab9357fe5b020135
5
5
  SHA512:
6
- metadata.gz: 3837b844459bdf78f20fd3562f44b6a91cb53c3fa3f4474d32bf3e7375b96400a2b39a45e907ff0fc2cf0d60730d2984606f01a13bfa7b03a01e330878d5be75
7
- data.tar.gz: bf4c9c42165f389bc6e5e1475b19641a24d41ed6b7f8007fa37a549fdf6917656db7ebdcb609156b649d3c793eac053db84aa9fad2be3fabcc603430e8009d61
6
+ metadata.gz: 0758fa4348af24daae4be0a7217385e7e648dd8ff8790521a12411559b3b0f15892028eb6c2398b85e67546b40fc1c9c2edfa3a93d6bb2b07f920411e11cb17f
7
+ data.tar.gz: ef3c3385aa3502ce96e787e53b653cb8b9901fc9dcd11b756c9098e24ad3e3d2ea30abb8017913df15d6bcffc4b527d96f8185d4eb0d63415539193bbc510221
@@ -0,0 +1,19 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ # Sunstone-specific extensions to column definitions in a table.
4
+ class SunstoneColumn < Column #:nodoc:
5
+ attr_accessor :array
6
+
7
+ def initialize(name, cast_type, options={})
8
+ @primary_key = (options['primary_key'] == true)
9
+ @array = !!options['array']
10
+ super(name, options['default'], cast_type, nil, options['null'])
11
+ end
12
+
13
+ def primary_key?
14
+ @primary_key
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,40 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Sunstone
4
+ module DatabaseStatements
5
+
6
+ # Converts an arel AST to a Sunstone API Request
7
+ def to_sar(arel, bvs)
8
+ if arel.respond_to?(:ast)
9
+ collected = visitor.accept(arel.ast, collector)
10
+ collected.compile(bvs)
11
+ else
12
+ arel
13
+ end
14
+ end
15
+
16
+ # Returns an ActiveRecord::Result instance.
17
+ def select_all(arel, name = nil, binds = [], &block)
18
+ exec_query(arel, name, binds)
19
+ end
20
+
21
+ def exec_query(arel, name = 'SAR', binds = [])
22
+ result = exec(to_sar(arel, binds), name)
23
+
24
+ if result.is_a?(Array)
25
+ ActiveRecord::Result.new(result[0] ? result[0].keys : [], result.map{|r| r.values})
26
+ else
27
+ # this is a count.. yea i know..
28
+ ActiveRecord::Result.new(['all'], [[result]], {:all => type_map.lookup('integer')})
29
+ end
30
+ end
31
+
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+
38
+
39
+
40
+
@@ -0,0 +1,95 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Sunstone
4
+
5
+ module SchemaStatements
6
+
7
+ # Returns true if table exists.
8
+ # If the schema is not specified as part of +name+ then it will only find tables within
9
+ # the current schema search path (regardless of permissions to access tables in other schemas)
10
+ def table_exists?(name)
11
+ schema_definition[name] != nil
12
+ end
13
+
14
+ # Returns the list of all column definitions for a table.
15
+ def columns(table_name)
16
+ # Limit, precision, and scale are all handled by the superclass.
17
+ column_definitions(table_name).map do |column_name, options|
18
+ new_column(column_name, lookup_cast_type(options['type']), options)
19
+ end
20
+ end
21
+
22
+ # Returns the list of a table's column names, data types, and default values.
23
+ #
24
+ # Query implementation notes:
25
+ # - format_type includes the column size constraint, e.g. varchar(50)
26
+ # - ::regclass is a function that gives the id for a table name
27
+ def column_definitions(table_name) # :nodoc:
28
+ definition = schema_definition[table_name]
29
+ raise ActiveRecord::StatementInvalid, "Table \"#{table_name}\" does not exist" if definition.nil?
30
+
31
+ definition
32
+ end
33
+
34
+ def schema_definition
35
+ exec( Arel::Table.new(:schema).project )
36
+ end
37
+
38
+ def tables
39
+ Wankel.parse(@connection.get('/schema').body, :symbolize_keys => true).keys
40
+ end
41
+
42
+ def new_column(name, cast_type, options={}) # :nodoc:
43
+ SunstoneColumn.new(name, cast_type, options)
44
+ end
45
+
46
+ # TODO: def encoding
47
+
48
+ # Returns just a table's primary key
49
+ def primary_key(table)
50
+ columns(table).find{ |c| c.primary_key? }.name
51
+ end
52
+
53
+ # TODO: do we need this?
54
+ # Maps logical Rails types to PostgreSQL-specific data types.
55
+ # def type_to_sql(type, limit = nil, precision = nil, scale = nil)
56
+ # case type.to_s
57
+ # when 'binary'
58
+ # # PostgreSQL doesn't support limits on binary (bytea) columns.
59
+ # # The hard limit is 1Gb, because of a 32-bit size field, and TOAST.
60
+ # case limit
61
+ # when nil, 0..0x3fffffff; super(type)
62
+ # else raise(ActiveRecordError, "No binary type has byte size #{limit}.")
63
+ # end
64
+ # when 'text'
65
+ # # PostgreSQL doesn't support limits on text columns.
66
+ # # The hard limit is 1Gb, according to section 8.3 in the manual.
67
+ # case limit
68
+ # when nil, 0..0x3fffffff; super(type)
69
+ # else raise(ActiveRecordError, "The limit on text can be at most 1GB - 1byte.")
70
+ # end
71
+ # when 'integer'
72
+ # return 'integer' unless limit
73
+ #
74
+ # case limit
75
+ # when 1, 2; 'smallint'
76
+ # when 3, 4; 'integer'
77
+ # when 5..8; 'bigint'
78
+ # else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
79
+ # end
80
+ # when 'datetime'
81
+ # return super unless precision
82
+ #
83
+ # case precision
84
+ # when 0..6; "timestamp(#{precision})"
85
+ # else raise(ActiveRecordError, "No timestamp type has precision of #{precision}. The allowed range of precision is from 0 to 6")
86
+ # end
87
+ # else
88
+ # super
89
+ # end
90
+ # end
91
+
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,22 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Sunstone
4
+ module Type
5
+ class DateTime < ActiveRecord::Type::DateTime
6
+
7
+ def type_cast_for_database(value)
8
+ super(value).iso8601(3) if value
9
+ end
10
+
11
+ def cast_value(string)
12
+ return string unless string.is_a?(::String)
13
+ return if string.empty?
14
+
15
+ ::DateTime.iso8601(string) || fast_string_to_time(string) || fallback_string_to_time(string)
16
+ end
17
+
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,177 @@
1
+ require 'active_record/connection_adapters/abstract_adapter'
2
+ require 'active_record/connection_adapters/statement_pool'
3
+
4
+ require 'arel/visitors/sunstone'
5
+ require 'arel/collectors/sunstone'
6
+
7
+ require 'active_record/connection_adapters/sunstone/database_statements'
8
+ require 'active_record/connection_adapters/sunstone/schema_statements'
9
+ require 'active_record/connection_adapters/sunstone/column'
10
+
11
+ require 'active_record/connection_adapters/sunstone/type/date_time'
12
+
13
+ module ActiveRecord
14
+ module ConnectionHandling # :nodoc:
15
+
16
+ VALID_SUNSTONE_CONN_PARAMS = [:site, :host, :port, :api_key, :use_ssl]
17
+
18
+ # Establishes a connection to the database that's used by all Active Record
19
+ # objects
20
+ def sunstone_connection(config)
21
+ conn_params = config.symbolize_keys
22
+
23
+ conn_params.delete_if { |_, v| v.nil? }
24
+
25
+ # Map ActiveRecords param names to PGs.
26
+ conn_params[:user] = conn_params.delete(:username) if conn_params[:username]
27
+ conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database]
28
+ if conn_params[:site]
29
+ uri = URI.parse(conn_params.delete(:site))
30
+ conn_params[:api_key] ||= (uri.user ? CGI.unescape(uri.user) : nil)
31
+ conn_params[:host] ||= uri.host
32
+ conn_params[:port] ||= uri.port
33
+ conn_params[:use_ssl] ||= (uri.scheme == 'https')
34
+ end
35
+
36
+ # Forward only valid config params to PGconn.connect.
37
+ conn_params.keep_if { |k, _| VALID_SUNSTONE_CONN_PARAMS.include?(k) }
38
+
39
+ # The postgres drivers don't allow the creation of an unconnected PGconn object,
40
+ # so just pass a nil connection object for the time being.
41
+ ConnectionAdapters::SunstoneAPIAdapter.new(nil, logger, conn_params, config)
42
+ end
43
+ end
44
+
45
+ module ConnectionAdapters
46
+ # The SunstoneAPI adapter.
47
+ #
48
+ # Options:
49
+ #
50
+ # * <tt>:host</tt> - Defaults to a Unix-domain socket in /tmp. On machines
51
+ # without Unix-domain sockets, the default is to connect to localhost.
52
+ # * <tt>:port</tt> - Defaults to 5432.
53
+ # * <tt>:username</tt> - The API key to connect with
54
+ # * <tt>:encoding</tt> - An optional client encoding that is used in a <tt>SET client_encoding TO
55
+ # <encoding></tt> call on the connection.
56
+ class SunstoneAPIAdapter < AbstractAdapter
57
+ ADAPTER_NAME = 'Sunstone'
58
+
59
+ NATIVE_DATABASE_TYPES = {
60
+ string: { name: "string" },
61
+ number: { name: "number" },
62
+ json: { name: "json" },
63
+ boolean: { name: "boolean" }
64
+ }
65
+
66
+ include Sunstone::DatabaseStatements
67
+ include Sunstone::SchemaStatements
68
+
69
+ # Returns 'SunstoneAPI' as adapter name for identification purposes.
70
+ def adapter_name
71
+ ADAPTER_NAME
72
+ end
73
+
74
+ # Adds `:array` option to the default set provided by the AbstractAdapter
75
+ def prepare_column_options(column, types) # :nodoc:
76
+ spec = super
77
+ spec[:array] = 'true' if column.respond_to?(:array) && column.array
78
+ spec
79
+ end
80
+
81
+ # Initializes and connects a SunstoneAPI adapter.
82
+ def initialize(connection, logger, connection_parameters, config)
83
+ super(connection, logger)
84
+
85
+ @visitor = Arel::Visitors::Sunstone.new self
86
+ @connection_parameters, @config = connection_parameters, config
87
+
88
+ connect
89
+
90
+ @type_map = Type::HashLookupTypeMap.new
91
+ initialize_type_map(type_map)
92
+ end
93
+
94
+ # Is this connection alive and ready for queries?
95
+ def active?
96
+ @connection.ping
97
+ true
98
+ rescue Net::HTTPExceptions
99
+ false
100
+ end
101
+
102
+ # TODO: this doesn't work yet
103
+ # Close then reopen the connection.
104
+ def reconnect!
105
+ super
106
+ @connection.reset
107
+ configure_connection
108
+ end
109
+
110
+ # TODO don't know about this yet
111
+ def reset!
112
+ configure_connection
113
+ end
114
+
115
+ # TODO: deal with connection.close
116
+ # Disconnects from the database if already connected. Otherwise, this
117
+ # method does nothing.
118
+ def disconnect!
119
+ super
120
+ @connection.close rescue nil
121
+ end
122
+
123
+ def native_database_types #:nodoc:
124
+ NATIVE_DATABASE_TYPES
125
+ end
126
+
127
+ def use_insert_returning?
128
+ true
129
+ end
130
+
131
+ def valid_type?(type)
132
+ !native_database_types[type].nil?
133
+ end
134
+
135
+ def update_table_definition(table_name, base) #:nodoc:
136
+ SunstoneAPI::Table.new(table_name, base)
137
+ end
138
+
139
+ def collector
140
+ Arel::Collectors::Sunstone.new
141
+ end
142
+
143
+ def server_config
144
+ Wankel.parse(@connection.get("/configuration").body)
145
+ end
146
+
147
+ private
148
+
149
+ def initialize_type_map(m) # :nodoc:
150
+ m.register_type 'boolean', Type::Boolean.new
151
+ m.register_type 'string', Type::String.new
152
+ m.register_type 'integer', Type::Integer.new
153
+ m.register_type 'decimal', Type::Decimal.new
154
+ m.register_type 'datetime', Sunstone::Type::DateTime.new
155
+ m.register_type 'hash', Type::Value.new
156
+ end
157
+
158
+ def exec(arel, name='SAR', binds=[])
159
+ # result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) :
160
+ # exec_cache(sql, name, binds)
161
+ sar = to_sar(arel, binds)
162
+
163
+ log(sar.is_a?(String) ? sar : "#{sar.class} #{CGI.unescape(sar.path)}", name) { Wankel.parse(@connection.send_request(sar).body) }
164
+ end
165
+
166
+ # Connects to a Sunstone API server and sets up the adapter depending on
167
+ # the connected server's characteristics.
168
+ def connect
169
+ @connection = ::Sunstone::Connection.new(@connection_parameters)
170
+ end
171
+
172
+ def create_table_definition(name, temporary, options, as = nil) # :nodoc:
173
+ SunstoneAPI::TableDefinition.new native_database_types, name, temporary, options, as
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,75 @@
1
+ module Arel
2
+ module Collectors
3
+ class Sunstone < Arel::Collectors::Bind
4
+
5
+ attr_accessor :request_type, :table, :where, :limit, :offset, :order, :operation, :columns
6
+
7
+ def substitute_binds hash, bvs
8
+ if hash.is_a?(Array)
9
+ hash.map { |w| substitute_binds(w, bvs) }
10
+ else
11
+ newhash = {}
12
+ hash.each do |k, v|
13
+ if Arel::Nodes::BindParam === v
14
+ newhash[k] = bvs.shift.last
15
+ elsif v.is_a?(Hash)
16
+ newhash[k] = substitute_binds(v, bvs)
17
+ else
18
+ newhash[k] = v
19
+ end
20
+ end
21
+ newhash
22
+ end
23
+ end
24
+
25
+ def value
26
+ flatten_nested(where).flatten
27
+ end
28
+
29
+ def flatten_nested(obj)
30
+ if obj.is_a?(Array)
31
+ obj.map { |w| flatten_nested(w) }
32
+ elsif obj.is_a?(Hash)
33
+ obj.map{ |k,v| [k, flatten_nested(v)] }.flatten
34
+ else
35
+ obj
36
+ end
37
+ end
38
+
39
+ def compile bvs
40
+ path = "/#{table}"
41
+
42
+ case operation
43
+ when :count, :average, :min, :max
44
+ path += "/#{operation}"
45
+ end
46
+
47
+ get_params = {}
48
+
49
+ if where
50
+ get_params[:where] = substitute_binds(where.clone, bvs)
51
+ if get_params[:where].size == 1
52
+ get_params[:where] = get_params[:where].pop
53
+ end
54
+ end
55
+
56
+ get_params[:limit] = limit if limit
57
+ get_params[:offset] = offset if offset
58
+ get_params[:order] = order if order
59
+ get_params[:columns] = columns if columns
60
+
61
+ if get_params.size > 0
62
+ path += '?' + get_params.to_param
63
+ end
64
+
65
+ request = request_type.new(path)
66
+
67
+ request
68
+ end
69
+
70
+ end
71
+ end
72
+ end
73
+
74
+
75
+