activerecord4-redshift-adapter 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +54 -0
- data/README.md +40 -0
- data/lib/active_record/connection_adapters/redshift/array_parser.rb +97 -0
- data/lib/active_record/connection_adapters/redshift/cast.rb +156 -0
- data/lib/active_record/connection_adapters/redshift/database_statements.rb +242 -0
- data/lib/active_record/connection_adapters/redshift/oid.rb +366 -0
- data/lib/active_record/connection_adapters/redshift/quoting.rb +172 -0
- data/lib/active_record/connection_adapters/redshift/referential_integrity.rb +30 -0
- data/lib/active_record/connection_adapters/redshift/schema_statements.rb +435 -0
- data/lib/active_record/connection_adapters/redshift_adapter.rb +909 -0
- metadata +87 -0
@@ -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
|