activerecord4-redshift-adapter 0.1.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.
@@ -0,0 +1,30 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ class RedshiftAdapter < AbstractAdapter
4
+ module ReferentialIntegrity
5
+ def supports_disable_referential_integrity? #:nodoc:
6
+ true
7
+ end
8
+
9
+ def disable_referential_integrity #:nodoc:
10
+ if supports_disable_referential_integrity?
11
+ begin
12
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
13
+ rescue
14
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER USER" }.join(";"))
15
+ end
16
+ end
17
+ yield
18
+ ensure
19
+ if supports_disable_referential_integrity?
20
+ begin
21
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
22
+ rescue
23
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER USER" }.join(";"))
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,435 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ class RedshiftAdapter < AbstractAdapter
4
+ class SchemaCreation < AbstractAdapter::SchemaCreation
5
+ private
6
+
7
+ def visit_AddColumn(o)
8
+ sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale)
9
+ sql = "ADD COLUMN #{quote_column_name(o.name)} #{sql_type}"
10
+ add_column_options!(sql, column_options(o))
11
+ end
12
+
13
+ def visit_ColumnDefinition(o)
14
+ sql = super
15
+ if o.primary_key? && o.type == :uuid
16
+ sql << " PRIMARY KEY "
17
+ add_column_options!(sql, column_options(o))
18
+ end
19
+ sql
20
+ end
21
+
22
+ def add_column_options!(sql, options)
23
+ if options[:array] || options[:column].try(:array)
24
+ sql << '[]'
25
+ end
26
+
27
+ column = options.fetch(:column) { return super }
28
+ if column.type == :uuid && options[:default] =~ /\(\)/
29
+ sql << " DEFAULT #{options[:default]}"
30
+ else
31
+ super
32
+ end
33
+ end
34
+ end
35
+
36
+ def schema_creation
37
+ SchemaCreation.new self
38
+ end
39
+
40
+ module SchemaStatements
41
+ # Drops the database specified on the +name+ attribute
42
+ # and creates it again using the provided +options+.
43
+ def recreate_database(name, options = {}) #:nodoc:
44
+ drop_database(name)
45
+ create_database(name, options)
46
+ end
47
+
48
+ # Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>,
49
+ # <tt>:encoding</tt>, <tt>:collation</tt>, <tt>:ctype</tt>,
50
+ # <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses
51
+ # <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>).
52
+ #
53
+ # Example:
54
+ # create_database config[:database], config
55
+ # create_database 'foo_development', encoding: 'unicode'
56
+ def create_database(name, options = {})
57
+ options = { encoding: 'utf8' }.merge!(options.symbolize_keys)
58
+
59
+ option_string = options.sum do |key, value|
60
+ case key
61
+ when :owner
62
+ " OWNER = \"#{value}\""
63
+ when :template
64
+ " TEMPLATE = \"#{value}\""
65
+ when :encoding
66
+ " ENCODING = '#{value}'"
67
+ when :collation
68
+ " LC_COLLATE = '#{value}'"
69
+ when :ctype
70
+ " LC_CTYPE = '#{value}'"
71
+ when :tablespace
72
+ " TABLESPACE = \"#{value}\""
73
+ when :connection_limit
74
+ " CONNECTION LIMIT = #{value}"
75
+ else
76
+ ""
77
+ end
78
+ end
79
+
80
+ execute "CREATE DATABASE #{quote_table_name(name)}#{option_string}"
81
+ end
82
+
83
+ # Drops a PostgreSQL database.
84
+ #
85
+ # Example:
86
+ # drop_database 'matt_development'
87
+ def drop_database(name) #:nodoc:
88
+ execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
89
+ end
90
+
91
+ # Returns the list of all tables in the schema search path or a specified schema.
92
+ def tables(name = nil)
93
+ query(<<-SQL, 'SCHEMA').map { |row| "#{row[0]}.#{row[1]}" }
94
+ SELECT tablename
95
+ FROM pg_tables
96
+ WHERE schemaname = ANY (current_schemas(false))
97
+ SQL
98
+ end
99
+
100
+ # Returns true if table exists.
101
+ # If the schema is not specified as part of +name+ then it will only find tables within
102
+ # the current schema search path (regardless of permissions to access tables in other schemas)
103
+ def table_exists?(name)
104
+ schema, table = Utils.extract_schema_and_table(name.to_s)
105
+ return false unless table
106
+
107
+ binds = [[nil, table]]
108
+ binds << [nil, schema] if schema
109
+
110
+ exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
111
+ SELECT COUNT(*)
112
+ FROM pg_class c
113
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
114
+ WHERE c.relkind in ('v','r')
115
+ AND c.relname = '#{table.gsub(/(^"|"$)/,'')}'
116
+ AND n.nspname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'}
117
+ SQL
118
+ end
119
+
120
+ # Returns true if schema exists.
121
+ def schema_exists?(name)
122
+ exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
123
+ SELECT COUNT(*)
124
+ FROM pg_namespace
125
+ WHERE nspname = '#{name}'
126
+ SQL
127
+ end
128
+
129
+ # Returns an array of indexes for the given table.
130
+ def indexes(table_name, name = nil)
131
+ []
132
+ end
133
+
134
+ # Returns the list of all column definitions for a table.
135
+ def columns(table_name)
136
+ # Limit, precision, and scale are all handled by the superclass.
137
+ column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod|
138
+ oid = OID::TYPE_MAP.fetch(oid.to_i, fmod.to_i) {
139
+ OID::Identity.new
140
+ }
141
+ RedshiftColumn.new(column_name, default, oid, type, notnull == 'f')
142
+ end
143
+ end
144
+
145
+ # Returns the current database name.
146
+ def current_database
147
+ query('select current_database()', 'SCHEMA')[0][0]
148
+ end
149
+
150
+ # Returns the current schema name.
151
+ def current_schema
152
+ query('SELECT current_schema', 'SCHEMA')[0][0]
153
+ end
154
+
155
+ # Returns the current database encoding format.
156
+ def encoding
157
+ query(<<-end_sql, 'SCHEMA')[0][0]
158
+ SELECT pg_encoding_to_char(pg_database.encoding) FROM pg_database
159
+ WHERE pg_database.datname LIKE '#{current_database}'
160
+ end_sql
161
+ end
162
+
163
+ # Returns the current database collation.
164
+ def collation
165
+ query(<<-end_sql, 'SCHEMA')[0][0]
166
+ SELECT pg_database.datcollate FROM pg_database WHERE pg_database.datname LIKE '#{current_database}'
167
+ end_sql
168
+ end
169
+
170
+ # Returns the current database ctype.
171
+ def ctype
172
+ query(<<-end_sql, 'SCHEMA')[0][0]
173
+ SELECT pg_database.datctype FROM pg_database WHERE pg_database.datname LIKE '#{current_database}'
174
+ end_sql
175
+ end
176
+
177
+ # Returns an array of schema names.
178
+ def schema_names
179
+ query(<<-SQL, 'SCHEMA').flatten
180
+ SELECT nspname
181
+ FROM pg_namespace
182
+ WHERE nspname !~ '^pg_.*'
183
+ AND nspname NOT IN ('information_schema')
184
+ ORDER by nspname;
185
+ SQL
186
+ end
187
+
188
+ # Creates a schema for the given schema name.
189
+ def create_schema schema_name
190
+ execute "CREATE SCHEMA #{schema_name}"
191
+ end
192
+
193
+ # Drops the schema for the given schema name.
194
+ def drop_schema schema_name
195
+ execute "DROP SCHEMA #{schema_name} CASCADE"
196
+ end
197
+
198
+ # Sets the schema search path to a string of comma-separated schema names.
199
+ # Names beginning with $ have to be quoted (e.g. $user => '$user').
200
+ # See: http://www.postgresql.org/docs/current/static/ddl-schemas.html
201
+ #
202
+ # This should be not be called manually but set in database.yml.
203
+ def schema_search_path=(schema_csv)
204
+ if schema_csv
205
+ execute("SET search_path TO #{schema_csv}", 'SCHEMA')
206
+ @schema_search_path = schema_csv
207
+ end
208
+ end
209
+
210
+ # Returns the active schema search path.
211
+ def schema_search_path
212
+ @schema_search_path ||= query('SHOW search_path', 'SCHEMA')[0][0]
213
+ end
214
+
215
+ # Returns the sequence name for a table's primary key or some other specified key.
216
+ def default_sequence_name(table_name, pk = nil) #:nodoc:
217
+ result = serial_sequence(table_name, pk || 'id')
218
+ return nil unless result
219
+ result.split('.').last
220
+ rescue ActiveRecord::StatementInvalid
221
+ "#{table_name}_#{pk || 'id'}_seq"
222
+ end
223
+
224
+ def serial_sequence(table, column)
225
+ result = exec_query(<<-eosql, 'SCHEMA')
226
+ SELECT pg_get_serial_sequence('#{table}', '#{column}')
227
+ eosql
228
+ result.rows.first.first
229
+ end
230
+
231
+ # Resets the sequence of a table's primary key to the maximum value.
232
+ def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc:
233
+ unless pk and sequence
234
+ default_pk, default_sequence = pk_and_sequence_for(table)
235
+
236
+ pk ||= default_pk
237
+ sequence ||= default_sequence
238
+ end
239
+
240
+ if @logger && pk && !sequence
241
+ @logger.warn "#{table} has primary key #{pk} with no default sequence"
242
+ end
243
+
244
+ if pk && sequence
245
+ quoted_sequence = quote_table_name(sequence)
246
+
247
+ select_value <<-end_sql, 'SCHEMA'
248
+ SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false)
249
+ end_sql
250
+ end
251
+ end
252
+
253
+ # Returns a table's primary key and belonging sequence.
254
+ def pk_and_sequence_for(table) #:nodoc:
255
+ # First try looking for a sequence with a dependency on the
256
+ # given table's primary key.
257
+ result = query(<<-end_sql, 'SCHEMA')[0]
258
+ SELECT attr.attname, seq.relname
259
+ FROM pg_class seq,
260
+ pg_attribute attr,
261
+ pg_depend dep,
262
+ pg_constraint cons
263
+ WHERE seq.oid = dep.objid
264
+ AND seq.relkind = 'S'
265
+ AND attr.attrelid = dep.refobjid
266
+ AND attr.attnum = dep.refobjsubid
267
+ AND attr.attrelid = cons.conrelid
268
+ AND attr.attnum = cons.conkey[1]
269
+ AND cons.contype = 'p'
270
+ AND dep.refobjid = '#{quote_table_name(table)}'::regclass
271
+ end_sql
272
+
273
+ if result.nil? or result.empty?
274
+ result = query(<<-end_sql, 'SCHEMA')[0]
275
+ SELECT attr.attname,
276
+ CASE
277
+ WHEN split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2) ~ '.' THEN
278
+ substr(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2),
279
+ strpos(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2), '.')+1)
280
+ ELSE split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2)
281
+ END
282
+ FROM pg_class t
283
+ JOIN pg_attribute attr ON (t.oid = attrelid)
284
+ JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum)
285
+ JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1])
286
+ WHERE t.oid = '#{quote_table_name(table)}'::regclass
287
+ AND cons.contype = 'p'
288
+ AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval'
289
+ end_sql
290
+ end
291
+
292
+ [result.first, result.last]
293
+ rescue
294
+ nil
295
+ end
296
+
297
+ # Returns just a table's primary key
298
+ def primary_key(table)
299
+ row = exec_query(<<-end_sql, 'SCHEMA').rows.first
300
+ SELECT DISTINCT attr.attname
301
+ FROM pg_attribute attr
302
+ INNER JOIN pg_depend dep ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid
303
+ INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1]
304
+ WHERE cons.contype = 'p'
305
+ AND dep.refobjid = '#{quote_table_name(table)}'::regclass
306
+ end_sql
307
+
308
+ row && row.first
309
+ end
310
+
311
+ # Renames a table.
312
+ # Also renames a table's primary key sequence if the sequence name matches the
313
+ # Active Record default.
314
+ #
315
+ # Example:
316
+ # rename_table('octopuses', 'octopi')
317
+ def rename_table(table_name, new_name)
318
+ clear_cache!
319
+ execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
320
+ pk, seq = pk_and_sequence_for(new_name)
321
+ if seq == "#{table_name}_#{pk}_seq"
322
+ new_seq = "#{new_name}_#{pk}_seq"
323
+ execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}"
324
+ end
325
+
326
+ rename_table_indexes(table_name, new_name)
327
+ end
328
+
329
+ # Adds a new column to the named table.
330
+ # See TableDefinition#column for details of the options you can use.
331
+ def add_column(table_name, column_name, type, options = {})
332
+ clear_cache!
333
+ super
334
+ end
335
+
336
+ # Changes the column of a table.
337
+ def change_column(table_name, column_name, type, options = {})
338
+ clear_cache!
339
+ quoted_table_name = quote_table_name(table_name)
340
+
341
+ execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
342
+
343
+ change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
344
+ change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
345
+ end
346
+
347
+ # Changes the default value of a table column.
348
+ def change_column_default(table_name, column_name, default)
349
+ clear_cache!
350
+ execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}"
351
+ end
352
+
353
+ def change_column_null(table_name, column_name, null, default = nil)
354
+ clear_cache!
355
+ unless null || default.nil?
356
+ execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
357
+ end
358
+ execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL")
359
+ end
360
+
361
+ # Renames a column in a table.
362
+ def rename_column(table_name, column_name, new_column_name)
363
+ clear_cache!
364
+ execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
365
+ rename_column_indexes(table_name, column_name, new_column_name)
366
+ end
367
+
368
+ def add_index(*args)
369
+ # ignore
370
+ end
371
+
372
+ def remove_index!(table_name, index_name) #:nodoc:
373
+ # ignore
374
+ end
375
+
376
+ def rename_index(table_name, old_name, new_name)
377
+ # ignore
378
+ end
379
+
380
+ def index_name_length
381
+ 63
382
+ end
383
+
384
+ # Maps logical Rails types to PostgreSQL-specific data types.
385
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil)
386
+ case type.to_s
387
+ when 'binary'
388
+ # PostgreSQL doesn't support limits on binary (bytea) columns.
389
+ # The hard limit is 1Gb, because of a 32-bit size field, and TOAST.
390
+ case limit
391
+ when nil, 0..0x3fffffff; super(type)
392
+ else raise(ActiveRecordError, "No binary type has byte size #{limit}.")
393
+ end
394
+ when 'integer'
395
+ return 'integer' unless limit
396
+
397
+ case limit
398
+ when 1, 2; 'smallint'
399
+ when 3, 4; 'integer'
400
+ when 5..8; 'bigint'
401
+ else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
402
+ end
403
+ when 'datetime'
404
+ return super unless precision
405
+
406
+ case precision
407
+ when 0..6; "timestamp(#{precision})"
408
+ else raise(ActiveRecordError, "No timestamp type has precision of #{precision}. The allowed range of precision is from 0 to 6")
409
+ end
410
+ else
411
+ super
412
+ end
413
+ end
414
+
415
+ # Returns a SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
416
+ #
417
+ # PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and
418
+ # requires that the ORDER BY include the distinct column.
419
+ #
420
+ # distinct("posts.id", ["posts.created_at desc"])
421
+ # # => "DISTINCT posts.id, posts.created_at AS alias_0"
422
+ def distinct(columns, orders) #:nodoc:
423
+ order_columns = orders.map{ |s|
424
+ # Convert Arel node to string
425
+ s = s.to_sql unless s.is_a?(String)
426
+ # Remove any ASC/DESC modifiers
427
+ s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '')
428
+ }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
429
+
430
+ [super].concat(order_columns).join(', ')
431
+ end
432
+ end
433
+ end
434
+ end
435
+ end