activerecord4-redshift-adapter 0.1.1

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