PageRenderTime 53ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/dm-core/adapters/oracle_adapter.rb

https://github.com/antw/dm-core
Ruby | 229 lines | 149 code | 37 blank | 43 comment | 38 complexity | 5fb69f52393ce0dd343a80e0280f2303 MD5 | raw file
  1. require DataMapper.root / 'lib' / 'dm-core' / 'adapters' / 'data_objects_adapter'
  2. require 'do_oracle'
  3. module DataMapper
  4. class Property
  5. # for custom sequence names
  6. OPTIONS << :sequence
  7. end
  8. module Adapters
  9. class OracleAdapter < DataObjectsAdapter
  10. module SQL #:nodoc:
  11. IDENTIFIER_MAX_LENGTH = 30
  12. private
  13. # Constructs INSERT statement for given query,
  14. #
  15. # @return [String] INSERT statement as a string
  16. #
  17. # @api private
  18. def insert_statement(model, properties, serial)
  19. statement = "INSERT INTO #{quote_name(model.storage_name(name))} "
  20. no_properties = properties.empty?
  21. custom_sequence = serial && serial.options[:sequence]
  22. serial_field = serial && quote_name(serial.field)
  23. if supports_default_values? && no_properties && !custom_sequence
  24. statement << "(#{serial_field}) " if serial
  25. statement << default_values_clause
  26. else
  27. # do not use custom sequence if identity field was assigned a value
  28. if custom_sequence && properties.include?(serial)
  29. custom_sequence = nil
  30. end
  31. statement << "("
  32. if custom_sequence
  33. statement << "#{serial_field}"
  34. statement << ", " unless no_properties
  35. end
  36. statement << "#{properties.map { |property| quote_name(property.field) }.join(', ')}) "
  37. statement << "VALUES ("
  38. if custom_sequence
  39. statement << "#{quote_name(custom_sequence)}.NEXTVAL"
  40. statement << ", " unless no_properties
  41. end
  42. statement << "#{(['?'] * properties.size).join(', ')})"
  43. end
  44. if supports_returning? && serial
  45. statement << returning_clause(serial)
  46. end
  47. statement
  48. end
  49. # Oracle syntax for inserting default values
  50. def default_values_clause
  51. 'VALUES (DEFAULT)'
  52. end
  53. # @api private
  54. def supports_returning?
  55. true
  56. end
  57. # INTO :insert_id is recognized by Oracle DataObjects driver
  58. def returning_clause(serial)
  59. " RETURNING #{quote_name(serial.field)} INTO :insert_id"
  60. end
  61. # Constructs SELECT statement for given query,
  62. # Overrides DataObjects adapter implementation with using subquery instead of GROUP BY to get unique records
  63. #
  64. # @return [String] SELECT statement as a string
  65. #
  66. # @api private
  67. def select_statement(query)
  68. name = self.name
  69. model = query.model
  70. fields = query.fields
  71. conditions = query.conditions
  72. limit = query.limit
  73. offset = query.offset
  74. order = query.order
  75. group_by = nil
  76. # FIXME: using a boolean for qualify does not work in some cases,
  77. # such as when you have a self-referrential many to many association.
  78. # if you don't qualfiy the columns with a unique alias, then the
  79. # SQL query will fail. This may mean though, that it might not
  80. # be enough to pass in a Property, but we may need to know the
  81. # table and the alias we should use for the column.
  82. qualify = query.links.any?
  83. if query.unique?
  84. group_by = fields.select { |property| property.kind_of?(Property) }
  85. end
  86. # create subquery to find all valid keys and then use these keys to retrive all other columns
  87. use_subquery = qualify
  88. no_group_by = group_by.blank?
  89. no_order = order.blank?
  90. # when we can include ROWNUM condition in main WHERE clause
  91. use_simple_rownum_limit = limit && (offset||0 == 0) && no_group_by && no_order
  92. unless (limit && limit > 1) || offset > 0 || qualify
  93. # TODO: move this method to Query, so that it walks the conditions
  94. # and finds an OR operator
  95. # TODO: handle cases where two or more properties need to be
  96. # used together to be unique
  97. # if a unique property is used, and there is no OR operator, then an ORDER
  98. # and LIMIT are unecessary because it should only return a single row
  99. if conditions.respond_to?(:slug) && conditions.slug == :and &&
  100. conditions.any? { |operand| operand.respond_to?(:slug) && operand.slug == :eql && operand.subject.respond_to?(:unique?) && operand.subject.unique? } &&
  101. !conditions.any? { |operand| operand.respond_to?(:slug) && operand.slug == :or }
  102. order = nil
  103. no_order = true
  104. limit = nil
  105. end
  106. end
  107. conditions_statement, bind_values = conditions_statement(conditions, qualify)
  108. model_key_column = columns_statement(model.key(name), qualify)
  109. from_statement = " FROM #{quote_name(model.storage_name(name))}"
  110. statement = "SELECT #{columns_statement(fields, qualify)}"
  111. if use_subquery
  112. statement << from_statement
  113. statement << " WHERE (#{model_key_column}) IN"
  114. statement << " (SELECT DISTINCT #{model_key_column}"
  115. # do not need to do group by for uniqueness as just one row per primary key will be returned
  116. no_group_by = true
  117. end
  118. statement << from_statement
  119. statement << join_statement(query, bind_values, qualify) if qualify
  120. statement << " WHERE (#{conditions_statement})" unless conditions_statement.blank?
  121. if use_subquery
  122. statement << ")"
  123. end
  124. if use_simple_rownum_limit
  125. statement << " AND rownum <= ?"
  126. bind_values << limit
  127. end
  128. statement << " GROUP BY #{columns_statement(group_by, qualify)}" unless no_group_by
  129. statement << " ORDER BY #{order_statement(order, qualify)}" unless no_order
  130. add_limit_offset!(statement, limit, offset, bind_values) unless use_simple_rownum_limit
  131. return statement, bind_values
  132. end
  133. # Oracle does not support LIMIT and OFFSET
  134. # Functionality is mimiced through the use of nested selects.
  135. # See http://asktom.oracle.com/pls/ask/f?p=4950:8:::::F4950_P8_DISPLAYID:127412348064
  136. def add_limit_offset!(statement, limit, offset, bind_values)
  137. positive_offset = offset > 0
  138. if limit && positive_offset
  139. statement.replace "select * from (select raw_sql_.*, rownum raw_rnum_ from (#{statement}) raw_sql_ where rownum <= ?) where raw_rnum_ > ?"
  140. bind_values << offset + limit << offset
  141. elsif limit
  142. statement.replace "select raw_sql_.* from (#{statement}) raw_sql_ where rownum <= ?"
  143. bind_values << limit
  144. elsif positive_offset
  145. statement.replace "select * from (select raw_sql_.*, rownum raw_rnum_ from (#{statement}) raw_sql_) where raw_rnum_ > ?"
  146. bind_values << offset
  147. end
  148. end
  149. # @api private
  150. # Oracle does not allow " in table or column names therefore substitute them with underscore
  151. def quote_name(name)
  152. "\"#{oracle_upcase(name)[0, self.class::IDENTIFIER_MAX_LENGTH].gsub('"', '_')}\""
  153. end
  154. # If table or column name contains just lowercase characters then do uppercase
  155. # as uppercase version will be used in Oracle data dictionary tables
  156. def oracle_upcase(name)
  157. name =~ /[A-Z]/ ? name : name.upcase
  158. end
  159. # CLOB value should be compared using DBMS_LOB.SUBSTR function
  160. # NOTE: just first 32767 bytes will be compared!
  161. # @api private
  162. def equality_operator(property, operand)
  163. if operand.nil?
  164. 'IS'
  165. elsif property.type == Types::Text
  166. 'DBMS_LOB.SUBSTR(%s) = ?'
  167. else
  168. '='
  169. end
  170. end
  171. # @api private
  172. def include_operator(property, operand)
  173. operator = case operand
  174. when Array then 'IN'
  175. when Range then 'BETWEEN'
  176. end
  177. if property.type == Types::Text
  178. "DBMS_LOB.SUBSTR(%s) #{operator} ?"
  179. else
  180. operator
  181. end
  182. end
  183. # @api private
  184. def regexp_operator(operand)
  185. 'REGEXP_LIKE(%s, ?)'
  186. end
  187. end #module SQL
  188. include SQL
  189. end # class PostgresAdapter
  190. const_added(:OracleAdapter)
  191. end # module Adapters
  192. end # module DataMapper