postshift 0.1.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.
@@ -0,0 +1,7 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ class RedshiftColumn < PostgreSQLColumn #:nodoc:
4
+ delegate :encoding, to: :sql_type_metadata
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,11 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Redshift
4
+ module ReferentialIntegrity # :nodoc:
5
+ def disable_referential_integrity # :nodoc:
6
+ yield
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,43 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Redshift
4
+ if ActiveRecord.version < Gem::Version.new('5.1')
5
+ # All this to add 'encoding' to Structure
6
+ class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :collation, :sql_type, :comment, :encoding)
7
+ # From PostgreSQL to maintain compatability
8
+ attr_accessor :array
9
+
10
+ # From Abstract to maintain compatability
11
+ def primary_key?
12
+ primary_key || type.to_sym == :primary_key
13
+ end
14
+ end
15
+
16
+ class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
17
+ include ActiveRecord::ConnectionAdapters::PostgreSQL::ColumnMethods
18
+
19
+ def new_column_definition(name, type, options) # :nodoc:
20
+ super.tap do |column|
21
+ column.encoding = options[:encoding]
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def create_column_definition(name, type, options=nil)
28
+ ColumnDefinition.new name, type, options
29
+ end
30
+ end
31
+ else
32
+ class TableDefinition < ActiveRecord::ConnectionAdapters::PostgreSQL::TableDefinition
33
+ def primary_key(name, type=:primary_key, **options)
34
+ ints = %i(integer bigint)
35
+ options[:auto_increment] ||= true if ints.include?(type) && !options.key?(:default)
36
+ type = :primary_key if ints.include?(type) && options.delete(:auto_increment) == true
37
+ super
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,25 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Redshift
4
+ module ColumnDumper
5
+ def column_spec_for_primary_key(column)
6
+ super.tap do |spec|
7
+ spec[:id] = ':primary_key' if column.sql_type == 'primary_key'
8
+ end
9
+ end
10
+
11
+ # Adds +:encoding+ option to the default set
12
+ def prepare_column_options(column)
13
+ super.tap do |spec|
14
+ spec[:encoding] = "'#{column.sql_type_metadata.encoding}'" if column.sql_type_metadata.encoding.present?
15
+ end
16
+ end
17
+
18
+ # Adds +:encoding+ as a valid migration key
19
+ def migration_keys
20
+ super + [:encoding]
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,154 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Redshift
4
+ class SchemaCreation < PostgreSQL::SchemaCreation
5
+ private
6
+
7
+ def add_column_options!(sql, options)
8
+ sql = super
9
+ if (encoding = encoding_option(options)).present?
10
+ sql << " ENCODE #{encoding}"
11
+ end
12
+ sql
13
+ end
14
+
15
+ def encoding_option(options)
16
+ if ActiveRecord.version < Gem::Version.new('5.1')
17
+ options[:column].encoding
18
+ else
19
+ options[:encoding]
20
+ end
21
+ end
22
+ end
23
+
24
+ module SchemaStatements
25
+ # Create a new Redshift database. Options include <tt>:owner</tt> and <tt>:connection_limit</tt>
26
+ # Example:
27
+ # create_database config[:database], config
28
+ # create_database 'foo_development', encoding: 'unicode'
29
+ def create_database(name, options = {})
30
+ options = { encoding: 'utf8' }.merge!(options.symbolize_keys)
31
+
32
+ option_string = options.inject("") do |memo, (key, value)|
33
+ memo += case key
34
+ when :owner
35
+ " OWNER = \"#{value}\""
36
+ when :connection_limit
37
+ " CONNECTION LIMIT = #{value}"
38
+ else
39
+ ''
40
+ end
41
+ end
42
+
43
+ execute "CREATE DATABASE #{quote_table_name(name)}#{option_string}"
44
+ end
45
+
46
+ def create_table(table_name, comment: nil, **options)
47
+ options[:options] ||= ''
48
+ options[:options] += "DISTKEY(#{options.delete(:distkey)}) " if options.key?(:distkey)
49
+ options[:options] += "SORTKEY(#{options.delete(:sortkey)}) " if options.key?(:sortkey)
50
+ super
51
+ end
52
+
53
+ def indexes(*)
54
+ []
55
+ end
56
+
57
+ # Returns the list of all column definitions for a table.
58
+ def columns(table_name)
59
+ column_definitions(table_name.to_s).map do |column_name, type, default, notnull, oid, fmod, encoding|
60
+ default_value = extract_value_from_default(default)
61
+ type = determine_primary_key_type_conversion(type, default)
62
+ type_metadata = fetch_type_metadata(column_name, type, oid, fmod, encoding)
63
+ default_function = extract_default_function(default_value, default)
64
+ new_column(column_name, default_value, type_metadata, notnull == false, table_name, default_function)
65
+ end
66
+ end
67
+
68
+ def determine_primary_key_type_conversion(type, default)
69
+ return 'primary_key' if (type == 'integer' && default.to_s.starts_with?('"identity"'))
70
+ type
71
+ end
72
+
73
+ def new_column(name, default, sql_type_metadata = nil, null = true, table_name = nil, default_function = nil) # :nodoc:
74
+ RedshiftColumn.new(name, default, sql_type_metadata, null, table_name, default_function)
75
+ end
76
+
77
+ def table_options(table_name) # :nodoc:
78
+ {}.tap do |options|
79
+ if (distkey = table_distkey(table_name)).present?
80
+ options[:distkey] = distkey
81
+ end
82
+ if (sortkey = table_sortkey(table_name)).present?
83
+ options[:sortkey] = sortkey
84
+ end
85
+ end
86
+ end
87
+
88
+ def table_distkey(table_name) # :nodoc:
89
+ select_value("SELECT \"column\" FROM pg_table_def WHERE tablename = #{quote(table_name)} AND distkey = true")
90
+ end
91
+
92
+ def table_sortkey(table_name) # :nodoc:
93
+ columns = select_values("SELECT \"column\" FROM pg_table_def WHERE tablename = #{quote(table_name)} AND sortkey > 0 ORDER BY sortkey ASC")
94
+ columns.present? ? columns.join(', ') : nil
95
+ end
96
+
97
+ # Returns just a table's primary key
98
+ def primary_keys(table)
99
+ pks = query(<<-end_sql, 'SCHEMA')
100
+ SELECT DISTINCT attr.attname
101
+ FROM pg_attribute attr
102
+ INNER JOIN pg_depend dep ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid
103
+ INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey)
104
+ WHERE cons.contype = 'p'
105
+ AND dep.refobjid = '#{quote_table_name(table)}'::regclass
106
+ end_sql
107
+ pks.present? ? pks[0] : pks
108
+ end
109
+
110
+ # TODO: This entire method block for 't2.oid::regclass::text' to 't2.relname'
111
+ def foreign_keys(table_name)
112
+ fk_info = select_all(<<-SQL.strip_heredoc, 'SCHEMA')
113
+ SELECT t2.relname AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete
114
+ FROM pg_constraint c
115
+ JOIN pg_class t1 ON c.conrelid = t1.oid
116
+ JOIN pg_class t2 ON c.confrelid = t2.oid
117
+ JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
118
+ JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
119
+ JOIN pg_namespace t3 ON c.connamespace = t3.oid
120
+ WHERE c.contype = 'f'
121
+ AND t1.relname = #{quote(table_name)}
122
+ AND t3.nspname = ANY (current_schemas(false))
123
+ ORDER BY c.conname
124
+ SQL
125
+
126
+ fk_info.map do |row|
127
+ options = {
128
+ column: row['column'],
129
+ name: row['name'],
130
+ primary_key: row['primary_key']
131
+ }
132
+
133
+ options[:on_delete] = extract_foreign_key_action(row['on_delete'])
134
+ options[:on_update] = extract_foreign_key_action(row['on_update'])
135
+
136
+ ForeignKeyDefinition.new(table_name, row['to_table'], options)
137
+ end
138
+ end
139
+
140
+ def fetch_type_metadata(column_name, sql_type, oid, fmod, encoding)
141
+ cast_type = get_oid_type(oid, fmod, column_name, sql_type)
142
+ simple_type = SqlTypeMetadata.new(
143
+ sql_type: sql_type,
144
+ type: cast_type.type,
145
+ limit: cast_type.limit,
146
+ precision: cast_type.precision,
147
+ scale: cast_type.scale,
148
+ )
149
+ RedshiftSQLTypeMetadata.new(simple_type, oid: oid, fmod: fmod, encoding: encoding)
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,18 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ class RedshiftSQLTypeMetadata < PostgreSQLTypeMetadata
4
+ attr_reader :encoding
5
+
6
+ def initialize(type_metadata, oid: nil, fmod: nil, encoding: nil)
7
+ super(type_metadata, oid: oid, fmod: fmod)
8
+ @encoding = encoding unless encoding == 'none'
9
+ end
10
+
11
+ protected
12
+
13
+ def attributes_for_hash
14
+ super << encoding
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,220 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext/module/deprecation'
3
+
4
+ require 'active_record'
5
+ require 'active_record/connection_adapters/postgresql_adapter'
6
+
7
+ require 'active_record/connection_adapters/redshift/column'
8
+ require 'active_record/connection_adapters/redshift/referential_integrity'
9
+ require 'active_record/connection_adapters/redshift/schema_definitions'
10
+ require 'active_record/connection_adapters/redshift/schema_dumper'
11
+ require 'active_record/connection_adapters/redshift/schema_statements'
12
+ require 'active_record/connection_adapters/redshift/type_metadata'
13
+
14
+ module ActiveRecord
15
+ module ConnectionHandling # :nodoc
16
+ RS_VALID_CONN_PARAMS = [:host, :hostaddr, :port, :dbname, :user, :password, :connect_timeout,
17
+ :client_encoding, :options, :application_name, :fallback_application_name,
18
+ :keepalives, :keepalives_idle, :keepalives_interval, :keepalives_count,
19
+ :tty, :sslmode, :requiressl, :sslcompression, :sslcert, :sslkey,
20
+ :sslrootcert, :sslcrl, :requirepeer, :krbsrvname, :gsslib, :service]
21
+
22
+ # Establishes a connection to the database that's used by all Active Record objects
23
+ def redshift_connection(config)
24
+ conn_params = config.symbolize_keys
25
+
26
+ conn_params.delete_if { |_, v| v.nil? }
27
+
28
+ # Map ActiveRecords param names to PGs.
29
+ conn_params[:user] = conn_params.delete(:username) if conn_params[:username]
30
+ conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database]
31
+
32
+ # Forward only valid config params to PGconn.connect.
33
+ conn_params.keep_if { |k, _| RS_VALID_CONN_PARAMS.include?(k) }
34
+
35
+ # The postgres drivers don't allow the creation of an unconnected PGconn object,
36
+ # so just pass a nil connection object for the time being.
37
+ ConnectionAdapters::RedshiftAdapter.new(nil, logger, conn_params, config)
38
+ end
39
+ end
40
+
41
+ module ConnectionAdapters
42
+ class RedshiftAdapter < PostgreSQLAdapter
43
+ ADAPTER_NAME = 'Redshift'.freeze
44
+
45
+ NATIVE_DATABASE_TYPES = {
46
+ primary_key: 'integer identity primary key',
47
+ string: { name: 'varchar' },
48
+ text: { name: 'varchar' },
49
+ integer: { name: 'integer' },
50
+ float: { name: 'float' },
51
+ decimal: { name: 'decimal' },
52
+ datetime: { name: 'timestamp' },
53
+ time: { name: 'timestamptz' },
54
+ date: { name: 'date' },
55
+ bigint: { name: 'bigint' },
56
+ boolean: { name: 'boolean' },
57
+ }.freeze
58
+
59
+ include Redshift::ColumnDumper
60
+ include Redshift::ReferentialIntegrity
61
+ include Redshift::SchemaStatements
62
+
63
+ def schema_creation # :nodoc:
64
+ Redshift::SchemaCreation.new self
65
+ end
66
+
67
+ def supports_index_sort_order?
68
+ false
69
+ end
70
+
71
+ def supports_partial_index?
72
+ false
73
+ end
74
+
75
+ def supports_expression_index?
76
+ false
77
+ end
78
+
79
+ def supports_transaction_isolation?
80
+ false
81
+ end
82
+
83
+ def supports_json?
84
+ false
85
+ end
86
+
87
+ def supports_savepoints?
88
+ false
89
+ end
90
+
91
+ def native_database_types #:nodoc:
92
+ NATIVE_DATABASE_TYPES
93
+ end
94
+
95
+ def supports_extensions?
96
+ false
97
+ end
98
+
99
+ def use_insert_returning?
100
+ false
101
+ end
102
+
103
+ def supports_advisory_locks?
104
+ false
105
+ end
106
+
107
+ def supports_ranges?
108
+ false
109
+ end
110
+
111
+ def supports_materialized_views?
112
+ false
113
+ end
114
+
115
+ def postgresql_version
116
+ # Will pass all inernal version support checks
117
+ Float::INFINITY
118
+ end
119
+
120
+ private
121
+
122
+ # TODO: Copied from PostgreSQL with minor registration changes. If broken out, could override segments, etc
123
+ def initialize_type_map(m) # :nodoc:
124
+ register_class_with_limit m, 'int2', Type::Integer
125
+ register_class_with_limit m, 'int4', Type::Integer
126
+ register_class_with_limit m, 'int8', Type::Integer
127
+ m.alias_type 'oid', 'int2'
128
+ m.register_type 'float4', Type::Float.new
129
+ m.alias_type 'float8', 'float4'
130
+ m.register_type 'text', Type::Text.new
131
+ register_class_with_limit m, 'varchar', Type::String
132
+ m.alias_type 'char', 'varchar'
133
+ m.alias_type 'name', 'varchar'
134
+ m.alias_type 'bpchar', 'varchar'
135
+ m.register_type 'bool', Type::Boolean.new
136
+ m.alias_type 'timestamptz', 'timestamp'
137
+ m.register_type 'date', Type::Date.new
138
+
139
+ m.register_type 'timestamp' do |_, _, sql_type|
140
+ precision = extract_precision(sql_type)
141
+ OID::DateTime.new(precision: precision)
142
+ end
143
+
144
+ m.register_type 'numeric' do |_, fmod, sql_type|
145
+ precision = extract_precision(sql_type)
146
+ scale = extract_scale(sql_type)
147
+
148
+ # The type for the numeric depends on the width of the field,
149
+ # so we'll do something special here.
150
+ #
151
+ # When dealing with decimal columns:
152
+ #
153
+ # places after decimal = fmod - 4 & 0xffff
154
+ # places before decimal = (fmod - 4) >> 16 & 0xffff
155
+ if fmod && (fmod - 4 & 0xffff).zero?
156
+ # FIXME: Remove this class, and the second argument to
157
+ # lookups on PG
158
+ Type::DecimalWithoutScale.new(precision: precision)
159
+ else
160
+ OID::Decimal.new(precision: precision, scale: scale)
161
+ end
162
+ end
163
+ end
164
+
165
+ def configure_connection
166
+ if @config[:encoding]
167
+ @connection.set_client_encoding(@config[:encoding])
168
+ end
169
+ self.schema_search_path = @config[:schema_search_path] || @config[:schema_order]
170
+
171
+ # SET statements from :variables config hash
172
+ # http://www.postgresql.org/docs/8.3/static/sql-set.html
173
+ variables = @config[:variables] || {}
174
+ variables.map do |k, v|
175
+ if v == ':default' || v == :default
176
+ # Sets the value to the global or compile default
177
+ execute("SET SESSION #{k} TO DEFAULT", 'SCHEMA')
178
+ elsif !v.nil?
179
+ execute("SET SESSION #{k} TO #{quote(v)}", 'SCHEMA')
180
+ end
181
+ end
182
+ end
183
+
184
+ # Returns the list of a table's column names, data types, and default values.
185
+ #
186
+ # The underlying query is roughly:
187
+ # SELECT column.name, column.type, default.value
188
+ # FROM column LEFT JOIN default
189
+ # ON column.table_id = default.table_id
190
+ # AND column.num = default.column_num
191
+ # WHERE column.table_id = get_table_id('table_name')
192
+ # AND column.num > 0
193
+ # AND NOT column.is_dropped
194
+ # ORDER BY column.num
195
+ #
196
+ # If the table name is not prefixed with a schema, the database will
197
+ # take the first match from the schema search path.
198
+ #
199
+ # Query implementation notes:
200
+ # - format_type includes the column size constraint, e.g. varchar(50)
201
+ # - ::regclass is a function that gives the id for a table name
202
+ def column_definitions(table_name) # :nodoc:
203
+ query(<<-end_sql, 'SCHEMA')
204
+ SELECT a.attname, format_type(a.atttypid, a.atttypmod),
205
+ pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod,
206
+ format_encoding(a.attencodingtype::integer)
207
+ FROM pg_attribute a LEFT JOIN pg_attrdef d
208
+ ON a.attrelid = d.adrelid AND a.attnum = d.adnum
209
+ WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass
210
+ AND a.attnum > 0 AND NOT a.attisdropped
211
+ ORDER BY a.attnum
212
+ end_sql
213
+ end
214
+
215
+ def create_table_definition(*args) # :nodoc:
216
+ Redshift::TableDefinition.new(*args)
217
+ end
218
+ end
219
+ end
220
+ end