postshift 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +28 -0
- data/.gitignore +19 -0
- data/.rspec +2 -0
- data/.rubocop.yml +1156 -0
- data/Appraisals +9 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +54 -0
- data/README.md +111 -0
- data/Rakefile +34 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/circle.yml +9 -0
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/ar_5.0.gemfile +8 -0
- data/gemfiles/ar_5.0.gemfile.lock +70 -0
- data/gemfiles/ar_5.1.gemfile +8 -0
- data/gemfiles/ar_5.1.gemfile.lock +70 -0
- data/lib/active_record/connection_adapters/redshift/column.rb +7 -0
- data/lib/active_record/connection_adapters/redshift/referential_integrity.rb +11 -0
- data/lib/active_record/connection_adapters/redshift/schema_definitions.rb +43 -0
- data/lib/active_record/connection_adapters/redshift/schema_dumper.rb +25 -0
- data/lib/active_record/connection_adapters/redshift/schema_statements.rb +154 -0
- data/lib/active_record/connection_adapters/redshift/type_metadata.rb +18 -0
- data/lib/active_record/connection_adapters/redshift_adapter.rb +220 -0
- data/lib/postshift.rb +5 -0
- data/lib/postshift/version.rb +3 -0
- data/postshift.gemspec +37 -0
- metadata +199 -0
@@ -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
|