sunstone 0.1.0 → 1.0.0

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