activerecord-oracle_enhanced-adapter 8.1.0-java

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.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/History.md +1971 -0
  3. data/License.txt +20 -0
  4. data/README.md +947 -0
  5. data/VERSION +1 -0
  6. data/lib/active_record/connection_adapters/emulation/oracle_adapter.rb +7 -0
  7. data/lib/active_record/connection_adapters/oracle_enhanced/column.rb +24 -0
  8. data/lib/active_record/connection_adapters/oracle_enhanced/connection.rb +137 -0
  9. data/lib/active_record/connection_adapters/oracle_enhanced/context_index.rb +359 -0
  10. data/lib/active_record/connection_adapters/oracle_enhanced/database_limits.rb +47 -0
  11. data/lib/active_record/connection_adapters/oracle_enhanced/database_statements.rb +325 -0
  12. data/lib/active_record/connection_adapters/oracle_enhanced/database_tasks.rb +63 -0
  13. data/lib/active_record/connection_adapters/oracle_enhanced/dbms_output.rb +71 -0
  14. data/lib/active_record/connection_adapters/oracle_enhanced/jdbc_connection.rb +629 -0
  15. data/lib/active_record/connection_adapters/oracle_enhanced/jdbc_quoting.rb +38 -0
  16. data/lib/active_record/connection_adapters/oracle_enhanced/lob.rb +57 -0
  17. data/lib/active_record/connection_adapters/oracle_enhanced/oci_connection.rb +465 -0
  18. data/lib/active_record/connection_adapters/oracle_enhanced/oci_quoting.rb +44 -0
  19. data/lib/active_record/connection_adapters/oracle_enhanced/procedures.rb +195 -0
  20. data/lib/active_record/connection_adapters/oracle_enhanced/quoting.rb +186 -0
  21. data/lib/active_record/connection_adapters/oracle_enhanced/schema_creation.rb +95 -0
  22. data/lib/active_record/connection_adapters/oracle_enhanced/schema_definitions.rb +99 -0
  23. data/lib/active_record/connection_adapters/oracle_enhanced/schema_dumper.rb +197 -0
  24. data/lib/active_record/connection_adapters/oracle_enhanced/schema_statements.rb +739 -0
  25. data/lib/active_record/connection_adapters/oracle_enhanced/structure_dump.rb +394 -0
  26. data/lib/active_record/connection_adapters/oracle_enhanced/type_metadata.rb +34 -0
  27. data/lib/active_record/connection_adapters/oracle_enhanced/version.rb +3 -0
  28. data/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb +886 -0
  29. data/lib/active_record/type/oracle_enhanced/boolean.rb +19 -0
  30. data/lib/active_record/type/oracle_enhanced/character_string.rb +36 -0
  31. data/lib/active_record/type/oracle_enhanced/integer.rb +14 -0
  32. data/lib/active_record/type/oracle_enhanced/json.rb +10 -0
  33. data/lib/active_record/type/oracle_enhanced/national_character_string.rb +26 -0
  34. data/lib/active_record/type/oracle_enhanced/national_character_text.rb +36 -0
  35. data/lib/active_record/type/oracle_enhanced/raw.rb +25 -0
  36. data/lib/active_record/type/oracle_enhanced/string.rb +29 -0
  37. data/lib/active_record/type/oracle_enhanced/text.rb +32 -0
  38. data/lib/active_record/type/oracle_enhanced/timestampltz.rb +25 -0
  39. data/lib/active_record/type/oracle_enhanced/timestamptz.rb +25 -0
  40. data/lib/activerecord-oracle_enhanced-adapter.rb +25 -0
  41. data/lib/arel/visitors/oracle.rb +216 -0
  42. data/lib/arel/visitors/oracle12.rb +121 -0
  43. data/lib/arel/visitors/oracle_common.rb +51 -0
  44. data/spec/active_record/connection_adapters/emulation/oracle_adapter_spec.rb +24 -0
  45. data/spec/active_record/connection_adapters/oracle_enhanced/compatibility_spec.rb +40 -0
  46. data/spec/active_record/connection_adapters/oracle_enhanced/composite_spec.rb +84 -0
  47. data/spec/active_record/connection_adapters/oracle_enhanced/connection_spec.rb +589 -0
  48. data/spec/active_record/connection_adapters/oracle_enhanced/context_index_spec.rb +431 -0
  49. data/spec/active_record/connection_adapters/oracle_enhanced/database_tasks_spec.rb +122 -0
  50. data/spec/active_record/connection_adapters/oracle_enhanced/dbconsole_spec.rb +63 -0
  51. data/spec/active_record/connection_adapters/oracle_enhanced/dbms_output_spec.rb +69 -0
  52. data/spec/active_record/connection_adapters/oracle_enhanced/procedures_spec.rb +362 -0
  53. data/spec/active_record/connection_adapters/oracle_enhanced/quoting_spec.rb +181 -0
  54. data/spec/active_record/connection_adapters/oracle_enhanced/schema_dumper_spec.rb +492 -0
  55. data/spec/active_record/connection_adapters/oracle_enhanced/schema_statements_spec.rb +1318 -0
  56. data/spec/active_record/connection_adapters/oracle_enhanced/structure_dump_spec.rb +485 -0
  57. data/spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb +815 -0
  58. data/spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb +230 -0
  59. data/spec/active_record/oracle_enhanced/type/binary_spec.rb +119 -0
  60. data/spec/active_record/oracle_enhanced/type/boolean_spec.rb +206 -0
  61. data/spec/active_record/oracle_enhanced/type/character_string_spec.rb +67 -0
  62. data/spec/active_record/oracle_enhanced/type/custom_spec.rb +90 -0
  63. data/spec/active_record/oracle_enhanced/type/decimal_spec.rb +56 -0
  64. data/spec/active_record/oracle_enhanced/type/dirty_spec.rb +141 -0
  65. data/spec/active_record/oracle_enhanced/type/float_spec.rb +48 -0
  66. data/spec/active_record/oracle_enhanced/type/integer_spec.rb +101 -0
  67. data/spec/active_record/oracle_enhanced/type/json_spec.rb +56 -0
  68. data/spec/active_record/oracle_enhanced/type/national_character_string_spec.rb +55 -0
  69. data/spec/active_record/oracle_enhanced/type/national_character_text_spec.rb +230 -0
  70. data/spec/active_record/oracle_enhanced/type/raw_spec.rb +137 -0
  71. data/spec/active_record/oracle_enhanced/type/text_spec.rb +295 -0
  72. data/spec/active_record/oracle_enhanced/type/timestamp_spec.rb +107 -0
  73. data/spec/spec_config.yaml.template +11 -0
  74. data/spec/spec_helper.rb +225 -0
  75. data/spec/support/alter_system_set_open_cursors.sql +1 -0
  76. data/spec/support/alter_system_user_password.sql +2 -0
  77. data/spec/support/create_oracle_enhanced_users.sql +31 -0
  78. metadata +181 -0
@@ -0,0 +1,739 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module ActiveRecord
6
+ module ConnectionAdapters
7
+ module OracleEnhanced
8
+ module SchemaStatements
9
+ # SCHEMA STATEMENTS ========================================
10
+ #
11
+ # see: abstract/schema_statements.rb
12
+
13
+ def tables # :nodoc:
14
+ select_values(<<~SQL.squish, "SCHEMA")
15
+ SELECT
16
+ DECODE(table_name, UPPER(table_name), LOWER(table_name), table_name)
17
+ FROM all_tables
18
+ WHERE owner = SYS_CONTEXT('userenv', 'current_schema')
19
+ AND secondary = 'N'
20
+ minus
21
+ SELECT DECODE(mview_name, UPPER(mview_name), LOWER(mview_name), mview_name)
22
+ FROM all_mviews
23
+ WHERE owner = SYS_CONTEXT('userenv', 'current_schema')
24
+ SQL
25
+ end
26
+
27
+ def data_sources
28
+ super | synonyms.map(&:name)
29
+ end
30
+
31
+ def table_exists?(table_name)
32
+ table_name = table_name.to_s
33
+ if table_name.include?("@")
34
+ # db link is not table
35
+ false
36
+ else
37
+ default_owner = current_schema
38
+ end
39
+ real_name = OracleEnhanced::Quoting.valid_table_name?(table_name) ?
40
+ table_name.upcase : table_name
41
+ if real_name.include?(".")
42
+ table_owner, table_name = real_name.split(".")
43
+ else
44
+ table_owner, table_name = default_owner, real_name
45
+ end
46
+
47
+ select_values(<<~SQL.squish, "SCHEMA", [bind_string("owner", table_owner), bind_string("table_name", table_name)]).any?
48
+ SELECT owner, table_name
49
+ FROM all_tables
50
+ WHERE owner = :owner
51
+ AND table_name = :table_name
52
+ SQL
53
+ end
54
+
55
+ def data_source_exists?(table_name)
56
+ (_owner, _table_name) = _connection.describe(table_name)
57
+ true
58
+ rescue
59
+ false
60
+ end
61
+
62
+ def views # :nodoc:
63
+ select_values(<<~SQL.squish, "SCHEMA")
64
+ SELECT
65
+ LOWER(view_name) FROM all_views WHERE owner = SYS_CONTEXT('userenv', 'current_schema')
66
+ SQL
67
+ end
68
+
69
+ def materialized_views # :nodoc:
70
+ select_values(<<~SQL.squish, "SCHEMA")
71
+ SELECT
72
+ LOWER(mview_name) FROM all_mviews WHERE owner = SYS_CONTEXT('userenv', 'current_schema')
73
+ SQL
74
+ end
75
+
76
+ # get synonyms for schema dump
77
+ def synonyms
78
+ result = select_all(<<~SQL.squish, "SCHEMA")
79
+ SELECT synonym_name, table_owner, table_name
80
+ FROM all_synonyms where owner = SYS_CONTEXT('userenv', 'current_schema')
81
+ SQL
82
+
83
+ result.collect do |row|
84
+ OracleEnhanced::SynonymDefinition.new(oracle_downcase(row["synonym_name"]),
85
+ oracle_downcase(row["table_owner"]), oracle_downcase(row["table_name"]))
86
+ end
87
+ end
88
+
89
+ def indexes(table_name) # :nodoc:
90
+ (_owner, table_name) = _connection.describe(table_name)
91
+ default_tablespace_name = default_tablespace
92
+
93
+ result = select_all(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table_name)])
94
+ SELECT LOWER(i.table_name) AS table_name, LOWER(i.index_name) AS index_name, i.uniqueness,
95
+ i.index_type, i.ityp_owner, i.ityp_name, i.parameters,
96
+ LOWER(i.tablespace_name) AS tablespace_name,
97
+ LOWER(c.column_name) AS column_name, e.column_expression,
98
+ atc.virtual_column
99
+ FROM all_indexes i
100
+ JOIN all_ind_columns c ON c.index_name = i.index_name AND c.index_owner = i.owner
101
+ LEFT OUTER JOIN all_ind_expressions e ON e.index_name = i.index_name AND
102
+ e.index_owner = i.owner AND e.column_position = c.column_position
103
+ LEFT OUTER JOIN all_tab_cols atc ON i.table_name = atc.table_name AND
104
+ c.column_name = atc.column_name AND i.owner = atc.owner AND atc.hidden_column = 'NO'
105
+ WHERE i.owner = SYS_CONTEXT('userenv', 'current_schema')
106
+ AND i.table_owner = SYS_CONTEXT('userenv', 'current_schema')
107
+ AND i.table_name = :table_name
108
+ AND NOT EXISTS (SELECT uc.index_name FROM all_constraints uc
109
+ WHERE uc.index_name = i.index_name AND uc.owner = i.owner AND uc.constraint_type = 'P')
110
+ ORDER BY i.index_name, c.column_position
111
+ SQL
112
+
113
+ current_index = nil
114
+ all_schema_indexes = []
115
+
116
+ result.each do |row|
117
+ # have to keep track of indexes because above query returns dups
118
+ # there is probably a better query we could figure out
119
+ if current_index != row["index_name"]
120
+ statement_parameters = nil
121
+ if row["index_type"] == "DOMAIN" && row["ityp_owner"] == "CTXSYS" && row["ityp_name"] == "CONTEXT"
122
+ procedure_name = default_datastore_procedure(row["index_name"])
123
+ source = select_values(<<~SQL.squish, "SCHEMA", [bind_string("procedure_name", procedure_name.upcase)]).join
124
+ SELECT text
125
+ FROM all_source
126
+ WHERE owner = SYS_CONTEXT('userenv', 'current_schema')
127
+ AND name = :procedure_name
128
+ ORDER BY line
129
+ SQL
130
+ if source =~ /-- add_context_index_parameters (.+)\n/
131
+ statement_parameters = $1
132
+ end
133
+ end
134
+ all_schema_indexes << OracleEnhanced::IndexDefinition.new(
135
+ row["table_name"],
136
+ row["index_name"],
137
+ row["uniqueness"] == "UNIQUE",
138
+ [],
139
+ {},
140
+ row["index_type"] == "DOMAIN" ? "#{row['ityp_owner']}.#{row['ityp_name']}" : nil,
141
+ row["parameters"],
142
+ statement_parameters,
143
+ row["tablespace_name"] == default_tablespace_name ? nil : row["tablespace_name"])
144
+ current_index = row["index_name"]
145
+ end
146
+
147
+ # Functional index columns and virtual columns both get stored as column expressions,
148
+ # but re-creating a virtual column index as an expression (instead of using the virtual column's name)
149
+ # results in a ORA-54018 error. Thus, we only want the column expression value returned
150
+ # when the column is not virtual.
151
+ if row["column_expression"] && row["virtual_column"] != "YES"
152
+ all_schema_indexes.last.columns << row["column_expression"]
153
+ else
154
+ all_schema_indexes.last.columns << row["column_name"].downcase
155
+ end
156
+ end
157
+
158
+ # Return the indexes just for the requested table, since AR is structured that way
159
+ table_name = table_name.downcase
160
+ all_schema_indexes.select { |i| i.table == table_name }
161
+ end
162
+
163
+ def columns(table_name)
164
+ table_name = table_name.to_s
165
+ if @columns_cache[table_name]
166
+ @columns_cache[table_name]
167
+ else
168
+ @columns_cache[table_name] = super(table_name)
169
+ end
170
+ end
171
+ # Additional options for +create_table+ method in migration files.
172
+ #
173
+ # You can specify individual starting value in table creation migration file, e.g.:
174
+ #
175
+ # create_table :users, :sequence_start_value => 100 do |t|
176
+ # # ...
177
+ # end
178
+ #
179
+ # You can also specify other sequence definition additional parameters, e.g.:
180
+ #
181
+ # create_table :users, :sequence_start_value => “100 NOCACHE INCREMENT BY 10” do |t|
182
+ # # ...
183
+ # end
184
+ #
185
+ # Create primary key trigger (so that you can skip primary key value in INSERT statement).
186
+ # By default trigger name will be "table_name_pkt", you can override the name with
187
+ # :trigger_name option (but it is not recommended to override it as then this trigger will
188
+ # not be detected by ActiveRecord model and it will still do prefetching of sequence value).
189
+ # Example:
190
+ #
191
+ # create_table :users, :primary_key_trigger => true do |t|
192
+ # # ...
193
+ # end
194
+ #
195
+ # It is possible to add table and column comments in table creation migration files:
196
+ #
197
+ # create_table :employees, :comment => “Employees and contractors” do |t|
198
+ # t.string :first_name, :comment => “Given name”
199
+ # t.string :last_name, :comment => “Surname”
200
+ # end
201
+
202
+ def create_table(table_name, id: :primary_key, primary_key: nil, force: nil, **options)
203
+ create_sequence = id != false
204
+ td = create_table_definition(
205
+ table_name, **options.extract!(:temporary, :options, :as, :comment, :tablespace, :organization)
206
+ )
207
+
208
+ if id && !td.as
209
+ pk = primary_key || Base.get_primary_key(table_name.to_s.singularize)
210
+
211
+ if pk.is_a?(Array)
212
+ td.primary_keys pk
213
+ else
214
+ td.primary_key pk, id, **options
215
+ end
216
+ end
217
+
218
+ # store that primary key was defined in create_table block
219
+ unless create_sequence
220
+ class << td
221
+ attr_accessor :create_sequence
222
+ def primary_key(name, type = :primary_key, **options)
223
+ self.create_sequence = true
224
+ super(name, type, **options)
225
+ end
226
+ end
227
+ end
228
+
229
+ yield td if block_given?
230
+ create_sequence = create_sequence || td.create_sequence
231
+
232
+ if force && data_source_exists?(table_name)
233
+ drop_table(table_name, force: force, if_exists: true)
234
+ else
235
+ schema_cache.clear_data_source_cache!(table_name.to_s)
236
+ end
237
+
238
+ execute schema_creation.accept td
239
+
240
+ create_sequence_and_trigger(table_name, options) if create_sequence
241
+
242
+ if supports_comments? && !supports_comments_in_create?
243
+ if table_comment = td.comment.presence
244
+ change_table_comment(table_name, table_comment)
245
+ end
246
+ td.columns.each do |column|
247
+ change_column_comment(table_name, column.name, column.comment) if column.comment.present?
248
+ end
249
+ end
250
+ td.indexes.each { |c, o| add_index table_name, c, **o }
251
+
252
+ rebuild_primary_key_index_to_default_tablespace(table_name, options)
253
+ end
254
+
255
+ def rename_table(table_name, new_name, **options) # :nodoc:
256
+ if new_name.to_s.length > DatabaseLimits::IDENTIFIER_MAX_LENGTH
257
+ raise ArgumentError, "New table name '#{new_name}' is too long; the limit is #{DatabaseLimits::IDENTIFIER_MAX_LENGTH} characters"
258
+ end
259
+ schema_cache.clear_data_source_cache!(table_name.to_s)
260
+ schema_cache.clear_data_source_cache!(new_name.to_s)
261
+ execute "RENAME #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
262
+ execute "RENAME #{quote_table_name("#{table_name}_seq")} TO #{default_sequence_name(new_name)}" rescue nil
263
+
264
+ rename_table_indexes(table_name, new_name, **options)
265
+ end
266
+
267
+ def drop_table(table_name, **options) # :nodoc:
268
+ schema_cache.clear_data_source_cache!(table_name.to_s)
269
+ execute "DROP TABLE #{quote_table_name(table_name)}#{' CASCADE CONSTRAINTS' if options[:force] == :cascade}"
270
+ seq_name = options[:sequence_name] || default_sequence_name(table_name)
271
+ execute "DROP SEQUENCE #{quote_table_name(seq_name)}" rescue nil
272
+ rescue ActiveRecord::StatementInvalid => e
273
+ raise e unless options[:if_exists]
274
+ ensure
275
+ clear_table_columns_cache(table_name)
276
+ end
277
+
278
+ def insert_versions_sql(versions) # :nodoc:
279
+ sm_table = quote_table_name(ActiveRecord::Tasks::DatabaseTasks.migration_connection_pool.schema_migration.table_name)
280
+
281
+ if supports_multi_insert?
282
+ versions.inject(+"INSERT ALL\n") { |sql, version|
283
+ sql << "INTO #{sm_table} (version) VALUES (#{quote(version)})\n"
284
+ } << "SELECT * FROM DUAL\n"
285
+ else
286
+ if versions.is_a?(Array)
287
+ # called from ActiveRecord::Base.connection#dump_schema_versions
288
+ versions.map { |version|
289
+ "INSERT INTO #{sm_table} (version) VALUES (#{quote(version)})"
290
+ }.join("\n\n/\n\n")
291
+ else
292
+ # called from ActiveRecord::Base.connection#assume_migrated_upto_version
293
+ "INSERT INTO #{sm_table} (version) VALUES (#{quote(versions)})"
294
+ end
295
+ end
296
+ end
297
+
298
+ def add_index(table_name, column_name, **options) # :nodoc:
299
+ index_name, index_type, quoted_column_names, tablespace, index_options = add_index_options(table_name, column_name, **options)
300
+ execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{quoted_column_names})#{tablespace} #{index_options}"
301
+ if index_type == "UNIQUE"
302
+ unless /\(.*\)/.match?(quoted_column_names)
303
+ execute "ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{quote_column_name(index_name)} #{index_type} (#{quoted_column_names}) USING INDEX #{quote_column_name(index_name)}"
304
+ end
305
+ end
306
+ end
307
+
308
+ def add_index_options(table_name, column_name, comment: nil, **options) # :nodoc:
309
+ column_names = Array(column_name)
310
+ index_name = index_name(table_name, column: column_names)
311
+
312
+ options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :tablespace, :options, :using)
313
+
314
+ index_type = options[:unique] ? "UNIQUE" : ""
315
+ index_name = options[:name].to_s if options.key?(:name)
316
+ tablespace = tablespace_for(:index, options[:tablespace])
317
+ # TODO: This option is used for NOLOGGING, needs better argument name
318
+ index_options = options[:options]
319
+
320
+ validate_index_length!(table_name, index_name, options.fetch(:internal, false))
321
+
322
+ if table_exists?(table_name) && index_name_exists?(table_name, index_name)
323
+ raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
324
+ end
325
+
326
+ quoted_column_names = column_names.map { |e| quote_column_name_or_expression(e) }.join(", ")
327
+ [index_name, index_type, quoted_column_names, tablespace, index_options]
328
+ end
329
+
330
+ # Remove the given index from the table.
331
+ # Gives warning if index does not exist
332
+ def remove_index(table_name, column_name = nil, **options) # :nodoc:
333
+ return if options[:if_exists] && !index_exists?(table_name, column_name, **options)
334
+
335
+ index_name = index_name_for_remove(table_name, column_name, options)
336
+ # TODO: It should execute only when index_type == "UNIQUE"
337
+ execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{quote_column_name(index_name)}" rescue nil
338
+ execute "DROP INDEX #{quote_column_name(index_name)}"
339
+ end
340
+
341
+ # returned shortened index name if default is too large
342
+ def index_name(table_name, options) # :nodoc:
343
+ default_name = super(table_name, options).to_s
344
+ # sometimes options can be String or Array with column names
345
+ options = {} unless options.is_a?(Hash)
346
+ identifier_max_length = options[:identifier_max_length] || index_name_length
347
+ return default_name if default_name.length <= identifier_max_length
348
+
349
+ # remove 'index', 'on' and 'and' keywords
350
+ shortened_name = "i_#{table_name}_#{Array(options[:column]) * '_'}"
351
+
352
+ # leave just first three letters from each word
353
+ if shortened_name.length > identifier_max_length
354
+ shortened_name = shortened_name.split("_").map { |w| w[0, 3] }.join("_")
355
+ end
356
+ # generate unique name using hash function
357
+ if shortened_name.length > identifier_max_length
358
+ shortened_name = "i" + OpenSSL::Digest::SHA1.hexdigest(default_name)[0, identifier_max_length - 1]
359
+ end
360
+ @logger.warn "#{adapter_name} shortened default index name #{default_name} to #{shortened_name}" if @logger
361
+ shortened_name
362
+ end
363
+
364
+ # Verify the existence of an index with a given name.
365
+ #
366
+ # The default argument is returned if the underlying implementation does not define the indexes method,
367
+ # as there's no way to determine the correct answer in that case.
368
+ #
369
+ # Will always query database and not index cache.
370
+ def index_name_exists?(table_name, index_name)
371
+ (_owner, table_name) = _connection.describe(table_name)
372
+ result = select_value(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table_name), bind_string("index_name", index_name.to_s.upcase)])
373
+ SELECT 1 FROM all_indexes i
374
+ WHERE i.owner = SYS_CONTEXT('userenv', 'current_schema')
375
+ AND i.table_owner = SYS_CONTEXT('userenv', 'current_schema')
376
+ AND i.table_name = :table_name
377
+ AND i.index_name = :index_name
378
+ SQL
379
+ result == 1
380
+ end
381
+
382
+ def rename_index(table_name, old_name, new_name) # :nodoc:
383
+ validate_index_length!(table_name, new_name)
384
+ execute "ALTER INDEX #{quote_column_name(old_name)} rename to #{quote_column_name(new_name)}"
385
+ end
386
+
387
+ # Add synonym to existing table or view or sequence. Can be used to create local synonym to
388
+ # remote table in other schema or in other database
389
+ # Examples:
390
+ #
391
+ # add_synonym :posts, "blog.posts"
392
+ # add_synonym :posts_seq, "blog.posts_seq"
393
+ # add_synonym :employees, "hr.employees", :force => true
394
+ #
395
+ def add_synonym(name, table_name, options = {})
396
+ sql = +"CREATE"
397
+ if options[:force] == true
398
+ sql << " OR REPLACE"
399
+ end
400
+ sql << " SYNONYM #{quote_table_name(name)} FOR #{quote_table_name(table_name)}"
401
+ execute sql
402
+ end
403
+
404
+ # Remove existing synonym to table or view or sequence
405
+ # Example:
406
+ #
407
+ # remove_synonym :posts, "blog.posts"
408
+ #
409
+ def remove_synonym(name)
410
+ execute "DROP SYNONYM #{quote_table_name(name)}"
411
+ end
412
+
413
+ def add_reference(table_name, ref_name, **options)
414
+ OracleEnhanced::ReferenceDefinition.new(ref_name, **options).add_to(update_table_definition(table_name, self))
415
+ end
416
+
417
+ def add_column(table_name, column_name, type, **options) # :nodoc:
418
+ type = aliased_types(type.to_s, type)
419
+ at = create_alter_table table_name
420
+ at.add_column(column_name, type, **options)
421
+ add_column_sql = schema_creation.accept at
422
+ add_column_sql << tablespace_for((type_to_sql(type).downcase.to_sym), nil, table_name, column_name)
423
+ execute add_column_sql
424
+ create_sequence_and_trigger(table_name, options) if type && type.to_sym == :primary_key
425
+ change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment)
426
+ ensure
427
+ clear_table_columns_cache(table_name)
428
+ end
429
+
430
+ def aliased_types(name, fallback)
431
+ fallback
432
+ end
433
+
434
+ def change_column_default(table_name, column_name, default_or_changes) # :nodoc:
435
+ default = extract_new_default_value(default_or_changes)
436
+ execute "ALTER TABLE #{quote_table_name(table_name)} MODIFY #{quote_column_name(column_name)} DEFAULT #{quote(default)}"
437
+ ensure
438
+ clear_table_columns_cache(table_name)
439
+ end
440
+
441
+ def change_column_null(table_name, column_name, null, default = nil) # :nodoc:
442
+ column = column_for(table_name, column_name)
443
+
444
+ unless null || default.nil?
445
+ execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
446
+ end
447
+
448
+ change_column table_name, column_name, column.sql_type, null: null
449
+ end
450
+
451
+ def change_column(table_name, column_name, type, **options) # :nodoc:
452
+ column = column_for(table_name, column_name)
453
+
454
+ # remove :null option if its value is the same as current column definition
455
+ # otherwise Oracle will raise error
456
+ if options.has_key?(:null) && options[:null] == column.null
457
+ options[:null] = nil
458
+ end
459
+ if type.to_sym == :virtual
460
+ type = options[:type]
461
+ end
462
+
463
+ td = create_table_definition(table_name)
464
+ cd = td.new_column_definition(column.name, type, **options)
465
+ change_column_stmt = schema_creation.accept cd
466
+ change_column_stmt << tablespace_for((type_to_sql(type).downcase.to_sym), nil, options[:table_name], options[:column_name]) if type
467
+ change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} MODIFY #{change_column_stmt}"
468
+ execute(change_column_sql)
469
+
470
+ change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment)
471
+ ensure
472
+ clear_table_columns_cache(table_name)
473
+ end
474
+
475
+ def rename_column(table_name, column_name, new_column_name) # :nodoc:
476
+ execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} to #{quote_column_name(new_column_name)}"
477
+ rename_column_indexes(table_name, column_name, new_column_name)
478
+ ensure
479
+ clear_table_columns_cache(table_name)
480
+ end
481
+
482
+ def remove_column(table_name, column_name, type = nil, options = {}) # :nodoc:
483
+ execute "ALTER TABLE #{quote_table_name(table_name)} DROP COLUMN #{quote_column_name(column_name)} CASCADE CONSTRAINTS"
484
+ ensure
485
+ clear_table_columns_cache(table_name)
486
+ end
487
+
488
+ def remove_columns(table_name, *column_names, type: nil, **options) # :nodoc:
489
+ quoted_column_names = column_names.map { |column_name| quote_column_name(column_name) }.join(", ")
490
+
491
+ execute "ALTER TABLE #{quote_table_name(table_name)} DROP (#{quoted_column_names}) CASCADE CONSTRAINTS"
492
+ ensure
493
+ clear_table_columns_cache(table_name)
494
+ end
495
+
496
+ def change_table_comment(table_name, comment_or_changes)
497
+ clear_cache!
498
+ comment = extract_new_comment_value(comment_or_changes)
499
+ if comment.nil?
500
+ execute "COMMENT ON TABLE #{quote_table_name(table_name)} IS ''"
501
+ else
502
+ execute "COMMENT ON TABLE #{quote_table_name(table_name)} IS #{quote(comment)}"
503
+ end
504
+ end
505
+
506
+ def change_column_comment(table_name, column_name, comment_or_changes)
507
+ clear_cache!
508
+ comment = extract_new_comment_value(comment_or_changes)
509
+ execute "COMMENT ON COLUMN #{quote_table_name(table_name)}.#{quote_column_name(column_name)} IS '#{comment}'"
510
+ end
511
+
512
+ def table_comment(table_name) # :nodoc:
513
+ # TODO
514
+ (_owner, table_name) = _connection.describe(table_name)
515
+ select_value(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table_name)])
516
+ SELECT comments FROM all_tab_comments
517
+ WHERE owner = SYS_CONTEXT('userenv', 'current_schema')
518
+ AND table_name = :table_name
519
+ SQL
520
+ end
521
+
522
+ def table_options(table_name) # :nodoc:
523
+ if comment = table_comment(table_name)
524
+ { comment: comment }
525
+ end
526
+ end
527
+
528
+ def column_comment(table_name, column_name) # :nodoc:
529
+ # TODO: it does not exist in Abstract adapter
530
+ (_owner, table_name) = _connection.describe(table_name)
531
+ select_value(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table_name), bind_string("column_name", column_name.upcase)])
532
+ SELECT comments FROM all_col_comments
533
+ WHERE owner = SYS_CONTEXT('userenv', 'current_schema')
534
+ AND table_name = :table_name
535
+ AND column_name = :column_name
536
+ SQL
537
+ end
538
+
539
+ # Maps logical Rails types to Oracle-specific data types.
540
+ def type_to_sql(type, limit: nil, precision: nil, scale: nil, **) # :nodoc:
541
+ # Ignore options for :text, :ntext and :binary columns
542
+ return super(type) if ["text", "ntext", "binary"].include?(type.to_s)
543
+
544
+ super
545
+ end
546
+
547
+ def tablespace(table_name)
548
+ select_value(<<~SQL.squish, "SCHEMA")
549
+ SELECT tablespace_name
550
+ FROM all_tables
551
+ WHERE table_name='#{table_name.to_s.upcase}'
552
+ AND owner = SYS_CONTEXT('userenv', 'current_schema')
553
+ SQL
554
+ end
555
+
556
+ # get table foreign keys for schema dump
557
+ def foreign_keys(table_name) # :nodoc:
558
+ (_owner, desc_table_name) = _connection.describe(table_name)
559
+
560
+ fk_info = select_all(<<~SQL.squish, "SCHEMA", [bind_string("desc_table_name", desc_table_name)])
561
+ SELECT r.table_name to_table
562
+ ,rc.column_name references_column
563
+ ,cc.column_name
564
+ ,c.constraint_name name
565
+ ,c.delete_rule
566
+ FROM all_constraints c, all_cons_columns cc,
567
+ all_constraints r, all_cons_columns rc
568
+ WHERE c.owner = SYS_CONTEXT('userenv', 'current_schema')
569
+ AND c.table_name = :desc_table_name
570
+ AND c.constraint_type = 'R'
571
+ AND cc.owner = c.owner
572
+ AND cc.constraint_name = c.constraint_name
573
+ AND r.constraint_name = c.r_constraint_name
574
+ AND r.owner = c.owner
575
+ AND rc.owner = r.owner
576
+ AND rc.constraint_name = r.constraint_name
577
+ AND rc.position = cc.position
578
+ ORDER BY name, to_table, column_name, references_column
579
+ SQL
580
+
581
+ fk_info.map do |row|
582
+ options = {
583
+ column: oracle_downcase(row["column_name"]),
584
+ name: oracle_downcase(row["name"]),
585
+ primary_key: oracle_downcase(row["references_column"])
586
+ }
587
+ options[:on_delete] = extract_foreign_key_action(row["delete_rule"])
588
+ ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(oracle_downcase(table_name), oracle_downcase(row["to_table"]), options)
589
+ end
590
+ end
591
+
592
+ def extract_foreign_key_action(specifier) # :nodoc:
593
+ case specifier
594
+ when "CASCADE"; :cascade
595
+ when "SET NULL"; :nullify
596
+ end
597
+ end
598
+
599
+ # REFERENTIAL INTEGRITY ====================================
600
+
601
+ def disable_referential_integrity(&block) # :nodoc:
602
+ old_constraints = select_all(<<~SQL.squish, "SCHEMA")
603
+ SELECT constraint_name, owner, table_name
604
+ FROM all_constraints
605
+ WHERE constraint_type = 'R'
606
+ AND status = 'ENABLED'
607
+ AND owner = SYS_CONTEXT('userenv', 'current_schema')
608
+ SQL
609
+ begin
610
+ old_constraints.each do |constraint|
611
+ execute "ALTER TABLE #{quote_table_name(constraint["table_name"])} DISABLE CONSTRAINT #{quote_table_name(constraint["constraint_name"])}"
612
+ end
613
+ yield
614
+ ensure
615
+ old_constraints.each do |constraint|
616
+ execute "ALTER TABLE #{quote_table_name(constraint["table_name"])} ENABLE CONSTRAINT #{quote_table_name(constraint["constraint_name"])}"
617
+ end
618
+ end
619
+ end
620
+
621
+ def create_alter_table(name)
622
+ OracleEnhanced::AlterTable.new create_table_definition(name)
623
+ end
624
+
625
+ def add_timestamps(table_name, **options)
626
+ fragments = add_timestamps_for_alter(table_name, **options)
627
+ fragments.each do |fragment|
628
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{fragment}"
629
+ end
630
+ end
631
+
632
+ def update_table_definition(table_name, base)
633
+ OracleEnhanced::Table.new(table_name, base)
634
+ end
635
+
636
+ def create_schema_dumper(options)
637
+ OracleEnhanced::SchemaDumper.create(self, options)
638
+ end
639
+
640
+ private
641
+ def schema_creation
642
+ OracleEnhanced::SchemaCreation.new self
643
+ end
644
+
645
+ def create_table_definition(name, **options)
646
+ OracleEnhanced::TableDefinition.new(self, name, **options)
647
+ end
648
+
649
+ def new_column_from_field(table_name, field, definitions)
650
+ limit, scale = field["limit"], field["scale"]
651
+ if limit || scale
652
+ field["sql_type"] += "(#{(limit || 38).to_i}" + ((scale = scale.to_i) > 0 ? ",#{scale})" : ")")
653
+ end
654
+
655
+ if field["sql_type_owner"]
656
+ field["sql_type"] = field["sql_type_owner"] + "." + field["sql_type"]
657
+ end
658
+
659
+ is_virtual = field["virtual_column"] == "YES"
660
+
661
+ # clean up odd default spacing from Oracle
662
+ if field["data_default"] && !is_virtual
663
+ field["data_default"].sub!(/^(.*?)\s*$/, '\1')
664
+
665
+ # If a default contains a newline these cleanup regexes need to
666
+ # match newlines.
667
+ field["data_default"].sub!(/^'(.*)'$/m, '\1')
668
+ field["data_default"] = nil if /^(null|empty_[bc]lob\(\))$/i.match?(field["data_default"])
669
+ # TODO: Needs better fix to fallback "N" to false
670
+ field["data_default"] = false if field["data_default"] == "N" && OracleEnhancedAdapter.emulate_booleans_from_strings
671
+ end
672
+
673
+ type_metadata = fetch_type_metadata(field["sql_type"], is_virtual)
674
+ default_value = extract_value_from_default(field["data_default"])
675
+ default_value = nil if is_virtual
676
+ OracleEnhanced::Column.new(oracle_downcase(field["name"]),
677
+ lookup_cast_type(field["sql_type"]),
678
+ default_value,
679
+ type_metadata,
680
+ field["nullable"] == "Y",
681
+ comment: field["column_comment"]
682
+ )
683
+ end
684
+
685
+ def fetch_type_metadata(sql_type, virtual = nil)
686
+ OracleEnhanced::TypeMetadata.new(super(sql_type), virtual: virtual)
687
+ end
688
+
689
+ def tablespace_for(obj_type, tablespace_option, table_name = nil, column_name = nil)
690
+ tablespace_sql = +""
691
+ if tablespace = (tablespace_option || default_tablespace_for(obj_type))
692
+ if [:blob, :clob, :nclob].include?(obj_type.to_sym)
693
+ tablespace_sql << " LOB (#{quote_column_name(column_name)}) STORE AS #{column_name.to_s[0..10]}_#{table_name.to_s[0..14]}_ls (TABLESPACE #{tablespace})"
694
+ else
695
+ tablespace_sql << " TABLESPACE #{tablespace}"
696
+ end
697
+ end
698
+ tablespace_sql
699
+ end
700
+
701
+ def default_tablespace_for(type)
702
+ default_tablespaces[type]
703
+ end
704
+
705
+ def column_for(table_name, column_name)
706
+ unless column = columns(table_name).find { |c| c.name == column_name.to_s }
707
+ raise "No such column: #{table_name}.#{column_name}"
708
+ end
709
+ column
710
+ end
711
+
712
+ def create_sequence_and_trigger(table_name, options)
713
+ # TODO: Needs rename since no triggers created
714
+ # This method will be removed since sequence will not be created separately
715
+ seq_name = options[:sequence_name] || default_sequence_name(table_name)
716
+ seq_start_value = options[:sequence_start_value] || default_sequence_start_value
717
+ execute "CREATE SEQUENCE #{quote_table_name(seq_name)} START WITH #{seq_start_value}"
718
+ end
719
+
720
+ def rebuild_primary_key_index_to_default_tablespace(table_name, options)
721
+ tablespace = default_tablespace_for(:index)
722
+
723
+ return unless tablespace
724
+
725
+ index_name = select_value(<<~SQL.squish, "Index name for primary key", [bind_string("table_name", table_name.upcase)])
726
+ SELECT index_name FROM all_constraints
727
+ WHERE table_name = :table_name
728
+ AND constraint_type = 'P'
729
+ AND owner = SYS_CONTEXT('userenv', 'current_schema')
730
+ SQL
731
+
732
+ return unless index_name
733
+
734
+ execute("ALTER INDEX #{quote_column_name(index_name)} REBUILD TABLESPACE #{tablespace}")
735
+ end
736
+ end
737
+ end
738
+ end
739
+ end