├── .github ├── dependabot.yml ├── issue_template.md ├── stale.yml └── workflows │ ├── linting.yml │ ├── rubocop.yml │ ├── test.yml │ └── test_11g.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .travis.yml ├── .travis ├── oracle │ ├── download.sh │ └── install.sh └── setup_accounts.sh ├── .yamllint.yml ├── Gemfile ├── History.md ├── License.txt ├── README.md ├── RUNNING_TESTS.md ├── Rakefile ├── VERSION ├── activerecord-oracle_enhanced-adapter.gemspec ├── ci ├── network │ └── admin │ │ └── tnsnames.ora └── setup_accounts.sh ├── guides └── bug_report_templates │ ├── README.md │ ├── active_record_gem.rb │ └── active_record_gem_spec.rb ├── lib ├── active_record │ ├── connection_adapters │ │ ├── emulation │ │ │ └── oracle_adapter.rb │ │ ├── oracle_enhanced │ │ │ ├── column.rb │ │ │ ├── connection.rb │ │ │ ├── context_index.rb │ │ │ ├── database_limits.rb │ │ │ ├── database_statements.rb │ │ │ ├── database_tasks.rb │ │ │ ├── dbms_output.rb │ │ │ ├── jdbc_connection.rb │ │ │ ├── jdbc_quoting.rb │ │ │ ├── lob.rb │ │ │ ├── oci_connection.rb │ │ │ ├── oci_quoting.rb │ │ │ ├── procedures.rb │ │ │ ├── quoting.rb │ │ │ ├── schema_creation.rb │ │ │ ├── schema_definitions.rb │ │ │ ├── schema_dumper.rb │ │ │ ├── schema_statements.rb │ │ │ ├── structure_dump.rb │ │ │ ├── type_metadata.rb │ │ │ └── version.rb │ │ └── oracle_enhanced_adapter.rb │ └── type │ │ └── oracle_enhanced │ │ ├── boolean.rb │ │ ├── character_string.rb │ │ ├── integer.rb │ │ ├── json.rb │ │ ├── national_character_string.rb │ │ ├── national_character_text.rb │ │ ├── raw.rb │ │ ├── string.rb │ │ ├── text.rb │ │ ├── timestampltz.rb │ │ └── timestamptz.rb ├── activerecord-oracle_enhanced-adapter.rb └── arel │ └── visitors │ ├── oracle.rb │ ├── oracle12.rb │ └── oracle_common.rb ├── spec ├── active_record │ ├── connection_adapters │ │ ├── emulation │ │ │ └── oracle_adapter_spec.rb │ │ ├── oracle_enhanced │ │ │ ├── connection_spec.rb │ │ │ ├── context_index_spec.rb │ │ │ ├── database_tasks_spec.rb │ │ │ ├── dbms_output_spec.rb │ │ │ ├── procedures_spec.rb │ │ │ ├── quoting_spec.rb │ │ │ ├── schema_dumper_spec.rb │ │ │ ├── schema_statements_spec.rb │ │ │ └── structure_dump_spec.rb │ │ ├── oracle_enhanced_adapter_spec.rb │ │ └── oracle_enhanced_data_types_spec.rb │ └── oracle_enhanced │ │ └── type │ │ ├── binary_spec.rb │ │ ├── boolean_spec.rb │ │ ├── character_string_spec.rb │ │ ├── custom_spec.rb │ │ ├── decimal_spec.rb │ │ ├── dirty_spec.rb │ │ ├── float_spec.rb │ │ ├── integer_spec.rb │ │ ├── json_spec.rb │ │ ├── national_character_string_spec.rb │ │ ├── national_character_text_spec.rb │ │ ├── raw_spec.rb │ │ ├── text_spec.rb │ │ └── timestamp_spec.rb ├── spec_config.yaml.template ├── spec_helper.rb └── support │ ├── alter_system_set_open_cursors.sql │ ├── alter_system_user_password.sql │ └── create_oracle_enhanced_users.sql └── test └── cases └── arel └── visitors ├── oracle12_test.rb └── oracle_test.rb /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | # Check for updates to GitHub Actions every weekday 8 | interval: "daily" 9 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ### Steps to reproduce 2 | Please refer https://github.com/rsim/oracle-enhanced/tree/master/guides/bug_report_templates 3 | to create an executable test case 4 | 5 | ### Expected behavior 6 | Tell us what should happen 7 | 8 | ### Actual behavior 9 | Tell us what happens instead 10 | 11 | ### System configuration 12 | **Rails version**: 13 | 14 | **Oracle enhanced adapter version**: 15 | 16 | **Ruby version**: 17 | 18 | **Oracle Database version**: 19 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | # Number of days of inactivity before an Issue or Pull Request becomes stale 4 | daysUntilStale: 60 5 | # Number of days of inactivity before a stale Issue or Pull Request is closed 6 | daysUntilClose: 7 7 | # Issues or Pull Requests with these labels will never be considered stale 8 | exemptLabels: 9 | - pinned 10 | - security 11 | - rails6 12 | - upstream 13 | # Label to use when marking as stale 14 | staleLabel: wontfix 15 | # Comment to post when marking as stale. Set to `false` to disable 16 | markComment: > 17 | This issue has been automatically marked as stale because it has not had 18 | recent activity. It will be closed if no further activity occurs. Thank you 19 | for your contributions. 20 | # Comment to post when closing a stale Issue or Pull Request. Set to `false` to disable 21 | closeComment: false 22 | # Limit to only `issues` or `pulls` 23 | # only: issues 24 | -------------------------------------------------------------------------------- /.github/workflows/linting.yml: -------------------------------------------------------------------------------- 1 | name: Linting 2 | on: 3 | - pull_request 4 | jobs: 5 | yamllint: 6 | name: Yamllint 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - name: Yamllint 11 | uses: karancode/yamllint-github-action@master 12 | with: 13 | yamllint_comment: true 14 | env: 15 | GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/rubocop.yml: -------------------------------------------------------------------------------- 1 | name: RuboCop 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * *" 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | env: 15 | LD_LIBRARY_PATH: /opt/oracle/instantclient_23_6 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Set up Ruby 3.3 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | ruby-version: 3.3 23 | - name: Download Oracle instant client 24 | run: | 25 | wget -q https://download.oracle.com/otn_software/linux/instantclient/2360000/instantclient-basic-linux.x64-23.6.0.24.10.zip 26 | wget -q https://download.oracle.com/otn_software/linux/instantclient/2360000/instantclient-sdk-linux.x64-23.6.0.24.10.zip 27 | wget -q https://download.oracle.com/otn_software/linux/instantclient/2360000/instantclient-sqlplus-linux.x64-23.6.0.24.10.zip 28 | - name: Install Oracle instant client 29 | run: | 30 | sudo unzip instantclient-basic-linux.x64-23.6.0.24.10.zip -d /opt/oracle/ 31 | sudo unzip -o instantclient-sdk-linux.x64-23.6.0.24.10.zip -d /opt/oracle/ 32 | sudo unzip -o instantclient-sqlplus-linux.x64-23.6.0.24.10.zip -d /opt/oracle/ 33 | echo "/opt/oracle/instantclient_23_6" >> $GITHUB_PATH 34 | - name: Build and run RuboCop 35 | run: | 36 | bundle install --jobs 4 --retry 3 37 | bundle exec rubocop 38 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * *" 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-22.04 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | # Rails 7.0 requires Ruby 2.7 or higeher. 17 | # CI pending the following matrix until JRuby 9.4 that supports Ruby 2.7 will be released. 18 | # https://github.com/jruby/jruby/issues/6464 19 | # - jruby, 20 | # - jruby-head 21 | ruby: [ 22 | '3.3', 23 | '3.2', 24 | '3.1', 25 | '3.0', 26 | '2.7' 27 | ] 28 | env: 29 | ORACLE_HOME: /opt/oracle/instantclient_23_6 30 | LD_LIBRARY_PATH: /opt/oracle/instantclient_23_6 31 | NLS_LANG: AMERICAN_AMERICA.AL32UTF8 32 | TNS_ADMIN: ./ci/network/admin 33 | DATABASE_NAME: FREEPDB1 34 | TZ: Europe/Riga 35 | DATABASE_SYS_PASSWORD: Oracle18 36 | DATABASE_HOST: localhost 37 | DATABASE_PORT: 1521 38 | 39 | services: 40 | oracle: 41 | image: gvenzl/oracle-free:latest 42 | ports: 43 | - 1521:1521 44 | env: 45 | TZ: Europe/Riga 46 | ORACLE_PASSWORD: Oracle18 47 | options: >- 48 | --health-cmd healthcheck.sh 49 | --health-interval 10s 50 | --health-timeout 5s 51 | --health-retries 10 52 | steps: 53 | - uses: actions/checkout@v4 54 | - name: Set up Ruby 55 | uses: ruby/setup-ruby@v1 56 | with: 57 | ruby-version: ${{ matrix.ruby }} 58 | - name: Download Oracle instant client 59 | run: | 60 | wget -q https://download.oracle.com/otn_software/linux/instantclient/2360000/instantclient-basic-linux.x64-23.6.0.24.10.zip 61 | wget -q https://download.oracle.com/otn_software/linux/instantclient/2360000/instantclient-sdk-linux.x64-23.6.0.24.10.zip 62 | wget -q https://download.oracle.com/otn_software/linux/instantclient/2360000/instantclient-sqlplus-linux.x64-23.6.0.24.10.zip 63 | - name: Install Oracle instant client 64 | run: | 65 | sudo unzip instantclient-basic-linux.x64-23.6.0.24.10.zip -d /opt/oracle/ 66 | sudo unzip -o instantclient-sdk-linux.x64-23.6.0.24.10.zip -d /opt/oracle/ 67 | sudo unzip -o instantclient-sqlplus-linux.x64-23.6.0.24.10.zip -d /opt/oracle/ 68 | echo "/opt/oracle/instantclient_23_6" >> $GITHUB_PATH 69 | - name: Install JDBC Driver 70 | run: | 71 | wget -q https://download.oracle.com/otn-pub/otn_software/jdbc/211/ojdbc11.jar -O ./lib/ojdbc11.jar 72 | - name: Create database user 73 | run: | 74 | ./ci/setup_accounts.sh 75 | - name: Update RubyGems 76 | run: | 77 | gem update --system || gem update --system 3.4.22 78 | - name: Bundle install 79 | run: | 80 | bundle install --jobs 4 --retry 3 81 | - name: Run RSpec 82 | run: | 83 | bundle exec rspec 84 | - name: Run bug report templates 85 | if: "false" 86 | run: | 87 | cd guides/bug_report_templates 88 | ruby active_record_gem.rb 89 | ruby active_record_gem_spec.rb 90 | -------------------------------------------------------------------------------- /.github/workflows/test_11g.yml: -------------------------------------------------------------------------------- 1 | name: test_11g 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * *" 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-22.04 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | # Rails 7.0 requires Ruby 2.7 or higeher. 17 | # CI pending the following matrix until JRuby 9.4 that supports Ruby 2.7 will be released. 18 | # https://github.com/jruby/jruby/issues/6464 19 | # - jruby, 20 | # - jruby-head 21 | ruby: [ 22 | '3.3', 23 | '3.2', 24 | '3.1', 25 | '3.0', 26 | '2.7' 27 | ] 28 | env: 29 | ORACLE_HOME: /opt/oracle/instantclient_21_15 30 | LD_LIBRARY_PATH: /opt/oracle/instantclient_21_15 31 | NLS_LANG: AMERICAN_AMERICA.AL32UTF8 32 | TNS_ADMIN: ./ci/network/admin 33 | DATABASE_NAME: XE 34 | TZ: Europe/Riga 35 | DATABASE_SYS_PASSWORD: Oracle18 36 | DATABASE_HOST: localhost 37 | DATABASE_PORT: 1521 38 | DATABASE_VERSION: 11.2.0.2 39 | DATABASE_SERVER_AND_CLIENT_VERSION_DO_NOT_MATCH: true 40 | 41 | services: 42 | oracle: 43 | image: gvenzl/oracle-xe:11 44 | ports: 45 | - 1521:1521 46 | env: 47 | TZ: Europe/Riga 48 | ORACLE_PASSWORD: Oracle18 49 | options: >- 50 | --health-cmd healthcheck.sh 51 | --health-interval 10s 52 | --health-timeout 5s 53 | --health-retries 10 54 | steps: 55 | - uses: actions/checkout@v4 56 | - name: Set up Ruby 57 | uses: ruby/setup-ruby@v1 58 | with: 59 | ruby-version: ${{ matrix.ruby }} 60 | - name: Download Oracle instant client 61 | run: | 62 | wget -q https://download.oracle.com/otn_software/linux/instantclient/2115000/instantclient-basic-linux.x64-21.15.0.0.0dbru.zip 63 | wget -q https://download.oracle.com/otn_software/linux/instantclient/2115000/instantclient-sqlplus-linux.x64-21.15.0.0.0dbru.zip 64 | wget -q https://download.oracle.com/otn_software/linux/instantclient/2115000/instantclient-sdk-linux.x64-21.15.0.0.0dbru.zip 65 | - name: Install Oracle instant client 66 | run: | 67 | sudo mkdir -p /opt/oracle/ 68 | sudo unzip instantclient-basic-linux.x64-21.15.0.0.0dbru.zip -d /opt/oracle 69 | sudo unzip -o instantclient-sqlplus-linux.x64-21.15.0.0.0dbru.zip -d /opt/oracle 70 | sudo unzip -o instantclient-sdk-linux.x64-21.15.0.0.0dbru.zip -d /opt/oracle 71 | echo "/opt/oracle/instantclient_21_15" >> $GITHUB_PATH 72 | 73 | - name: Install JDBC Driver 74 | run: | 75 | wget -q https://download.oracle.com/otn-pub/otn_software/jdbc/211/ojdbc11.jar -O ./lib/ojdbc11.jar 76 | - name: Create database user 77 | run: | 78 | ./ci/setup_accounts.sh 79 | - name: Update RubyGems 80 | run: | 81 | gem update --system || gem update --system 3.4.22 82 | - name: Bundle install 83 | run: | 84 | bundle install --jobs 4 --retry 3 85 | - name: Run RSpec 86 | run: | 87 | bundle exec rspec 88 | - name: Run bug report templates 89 | if: "false" 90 | run: | 91 | cd guides/bug_report_templates 92 | ruby active_record_gem.rb 93 | ruby active_record_gem_spec.rb 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | .rvmrc 3 | .rbenv-gemsets 4 | .svn 5 | .DS_Store 6 | coverage 7 | doc 8 | pkg 9 | log 10 | tmp 11 | sqlnet.log 12 | Gemfile.lock 13 | /spec/spec_config.yaml 14 | ojdbc*.jar 15 | .byebug_history 16 | debug.log 17 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --backtrace 3 | --require spec_helper 4 | --warnings 5 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - rubocop-performance 3 | - rubocop-rails 4 | - rubocop-rspec 5 | 6 | AllCops: 7 | TargetRubyVersion: 2.7 8 | DisabledByDefault: true 9 | SuggestExtensions: false 10 | 11 | Rails/IndexBy: 12 | Enabled: true 13 | 14 | Rails/IndexWith: 15 | Enabled: true 16 | 17 | # Prefer &&/|| over and/or. 18 | Style/AndOr: 19 | Enabled: true 20 | 21 | # Align `when` with `case`. 22 | Layout/CaseIndentation: 23 | Enabled: true 24 | 25 | Layout/ClosingHeredocIndentation: 26 | Enabled: true 27 | 28 | Layout/ClosingParenthesisIndentation: 29 | Enabled: true 30 | 31 | # Align comments with method definitions. 32 | Layout/CommentIndentation: 33 | Enabled: true 34 | 35 | Layout/DefEndAlignment: 36 | Enabled: true 37 | 38 | Layout/ElseAlignment: 39 | Enabled: true 40 | 41 | Layout/EmptyLineAfterMagicComment: 42 | Enabled: true 43 | 44 | Layout/EmptyLinesAroundAccessModifier: 45 | Enabled: true 46 | EnforcedStyle: only_before 47 | 48 | Layout/EmptyLinesAroundBlockBody: 49 | Enabled: true 50 | 51 | # Align `end` with the matching keyword or starting expression except for 52 | # assignments, where it should be aligned with the LHS. 53 | Layout/EndAlignment: 54 | Enabled: true 55 | EnforcedStyleAlignWith: variable 56 | AutoCorrect: true 57 | 58 | Layout/EndOfLine: 59 | Enabled: true 60 | 61 | # No extra empty lines. 62 | Layout/EmptyLines: 63 | Enabled: true 64 | 65 | # In a regular class definition, no empty lines around the body. 66 | Layout/EmptyLinesAroundClassBody: 67 | Enabled: true 68 | 69 | # In a regular method definition, no empty lines around the body. 70 | Layout/EmptyLinesAroundMethodBody: 71 | Enabled: true 72 | 73 | # In a regular module definition, no empty lines around the body. 74 | Layout/EmptyLinesAroundModuleBody: 75 | Enabled: true 76 | 77 | Lint/SafeNavigationChain: 78 | Enabled: true 79 | 80 | # Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }. 81 | Style/HashSyntax: 82 | Enabled: true 83 | 84 | # Method definitions after `private` or `protected` isolated calls need one 85 | # extra level of indentation. 86 | Layout/IndentationConsistency: 87 | Enabled: true 88 | EnforcedStyle: indented_internal_methods 89 | 90 | # Two spaces, no tabs (for indentation). 91 | Layout/IndentationWidth: 92 | Enabled: true 93 | 94 | Layout/LeadingCommentSpace: 95 | Enabled: true 96 | 97 | Layout/SpaceAfterColon: 98 | Enabled: true 99 | 100 | Layout/SpaceAfterComma: 101 | Enabled: true 102 | 103 | Layout/SpaceAfterSemicolon: 104 | Enabled: true 105 | 106 | Layout/SpaceAroundEqualsInParameterDefault: 107 | Enabled: true 108 | 109 | Layout/SpaceAroundKeyword: 110 | Enabled: true 111 | 112 | Layout/SpaceAroundOperators: 113 | Enabled: true 114 | 115 | # This cop has not been enabled at rails/rails 116 | Layout/SpaceBeforeBrackets: 117 | Enabled: true 118 | 119 | Layout/SpaceBeforeComma: 120 | Enabled: true 121 | 122 | Layout/SpaceBeforeComment: 123 | Enabled: true 124 | 125 | Layout/SpaceBeforeFirstArg: 126 | Enabled: true 127 | 128 | Style/DefWithParentheses: 129 | Enabled: true 130 | 131 | # Defining a method with parameters needs parentheses. 132 | Style/MethodDefParentheses: 133 | Enabled: true 134 | 135 | Style/ExplicitBlockArgument: 136 | Enabled: true 137 | 138 | Style/FrozenStringLiteralComment: 139 | Enabled: true 140 | EnforcedStyle: always 141 | 142 | Style/MapToHash: 143 | Enabled: true 144 | 145 | Style/RedundantFreeze: 146 | Enabled: true 147 | 148 | # Use `foo {}` not `foo{}`. 149 | Layout/SpaceBeforeBlockBraces: 150 | Enabled: true 151 | 152 | # Use `foo { bar }` not `foo {bar}`. 153 | Layout/SpaceInsideBlockBraces: 154 | Enabled: true 155 | 156 | # Use `{ a: 1 }` not `{a:1}`. 157 | Layout/SpaceInsideHashLiteralBraces: 158 | Enabled: true 159 | 160 | Layout/SpaceInsideParens: 161 | Enabled: true 162 | 163 | # Check quotes usage according to lint rule below. 164 | Style/StringLiterals: 165 | Enabled: true 166 | EnforcedStyle: double_quotes 167 | 168 | # Detect hard tabs, no hard tabs. 169 | Layout/IndentationStyle: 170 | Enabled: true 171 | 172 | # Empty lines should not have any spaces. 173 | Layout/TrailingEmptyLines: 174 | Enabled: true 175 | 176 | # No trailing whitespace. 177 | Layout/TrailingWhitespace: 178 | Enabled: true 179 | 180 | # Use quotes for string literals when they are enough. 181 | Style/RedundantPercentQ: 182 | Enabled: true 183 | 184 | Lint/AmbiguousOperator: 185 | Enabled: true 186 | 187 | Lint/AmbiguousRegexpLiteral: 188 | Enabled: true 189 | 190 | Lint/Debugger: 191 | Enabled: true 192 | DebuggerRequires: 193 | - debug 194 | 195 | Lint/DuplicateRequire: 196 | Enabled: true 197 | 198 | Lint/DuplicateMethods: 199 | Enabled: true 200 | 201 | Lint/ErbNewArguments: 202 | Enabled: true 203 | 204 | Lint/EnsureReturn: 205 | Enabled: true 206 | 207 | # Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg. 208 | Lint/RequireParentheses: 209 | Enabled: true 210 | 211 | Lint/RedundantStringCoercion: 212 | Enabled: true 213 | 214 | Lint/UriEscapeUnescape: 215 | Enabled: true 216 | 217 | Lint/UselessAssignment: 218 | Enabled: true 219 | 220 | Lint/DeprecatedClassMethods: 221 | Enabled: true 222 | 223 | Style/ParenthesesAroundCondition: 224 | Enabled: true 225 | 226 | Style/HashTransformKeys: 227 | Enabled: true 228 | 229 | Style/HashTransformValues: 230 | Enabled: true 231 | 232 | Style/RedundantBegin: 233 | Enabled: true 234 | 235 | Style/RedundantReturn: 236 | Enabled: true 237 | AllowMultipleReturnValues: true 238 | 239 | Style/RedundantRegexpEscape: 240 | Enabled: true 241 | 242 | Style/Semicolon: 243 | Enabled: true 244 | AllowAsExpressionSeparator: true 245 | 246 | # Prefer Foo.method over Foo::method 247 | Style/ColonMethodCall: 248 | Enabled: true 249 | 250 | Style/TrivialAccessors: 251 | Enabled: true 252 | 253 | # Prefer a = b || c over a = b ? b : c 254 | Style/RedundantCondition: 255 | Enabled: true 256 | 257 | Performance/BindCall: 258 | Enabled: true 259 | 260 | Performance/FlatMap: 261 | Enabled: true 262 | 263 | Performance/MapCompact: 264 | Enabled: true 265 | 266 | Performance/SelectMap: 267 | Enabled: true 268 | 269 | Performance/RedundantMerge: 270 | Enabled: true 271 | 272 | Performance/StartWith: 273 | Enabled: true 274 | 275 | Performance/EndWith: 276 | Enabled: true 277 | 278 | Performance/RegexpMatch: 279 | Enabled: true 280 | 281 | Performance/ReverseEach: 282 | Enabled: true 283 | 284 | Performance/StringReplacement: 285 | Enabled: true 286 | 287 | # Use unary plus to get an unfrozen string literal. 288 | Performance/UnfreezeString: 289 | Enabled: true 290 | 291 | Performance/DeletePrefix: 292 | Enabled: true 293 | 294 | Performance/DeleteSuffix: 295 | Enabled: true 296 | 297 | Performance/OpenStruct: 298 | Enabled: true 299 | 300 | # RuboCop RSpec cops 301 | RSpec/BeEq: 302 | Enabled: true 303 | 304 | RSpec/BeNil: 305 | Enabled: true 306 | 307 | RSpec/EmptyLineAfterExampleGroup: 308 | Enabled: true 309 | 310 | RSpec/EmptyLineAfterHook: 311 | Enabled: true 312 | 313 | RSpec/EmptyLineAfterSubject: 314 | Enabled: true 315 | 316 | RSpec/ExcessiveDocstringSpacing: 317 | Enabled: true 318 | 319 | RSpec/RepeatedDescription: 320 | Enabled: true 321 | 322 | RSpec/RepeatedExample: 323 | Enabled: true 324 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | dist: jammy 3 | cache: bundler 4 | 5 | env: 6 | global: 7 | - ORACLE_FILE=oracle11g/xe/oracle-xe-11.2.0-1.0.x86_64.rpm.zip 8 | - ORACLE_HOME=/u01/app/oracle/product/11.2.0/xe 9 | - TNS_ADMIN=$ORACLE_HOME/network/admin 10 | - NLS_LANG=AMERICAN_AMERICA.AL32UTF8 11 | - ORACLE_BASE=/u01/app/oracle 12 | - LD_LIBRARY_PATH=$ORACLE_HOME/lib 13 | - PATH=$PATH:$ORACLE_HOME/jdbc/lib 14 | - DATABASE_VERSION=11.2.0.1 15 | - ORACLE_SID=XE 16 | - DATABASE_NAME=XE 17 | - ORA_SDTZ='Europe/Riga' # Needed as a client parameter 18 | - TZ='Europe/Riga' # Needed as a DB Server parameter 19 | 20 | before_install: 21 | - chmod +x .travis/oracle/download.sh 22 | - chmod +x .travis/oracle/install.sh 23 | - chmod +x .travis/setup_accounts.sh 24 | 25 | install: 26 | - .travis/oracle/download.sh 27 | - .travis/oracle/install.sh 28 | - .travis/setup_accounts.sh 29 | - ruby -v 30 | - gem update --system 31 | - bundle install 32 | 33 | script: 34 | - bundle exec rake spec 35 | 36 | language: ruby 37 | rvm: 38 | - 3.3.5 39 | - 3.2.4 40 | - 3.1.6 41 | 42 | notifications: 43 | email: false 44 | -------------------------------------------------------------------------------- /.travis/oracle/download.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd "$(dirname "$(readlink -f "$0")")" 4 | 5 | deb_file=oracle-xe_11.2.0-1.0_amd64.deb 6 | 7 | git clone https://github.com/wnameless/docker-oracle-xe-11g.git 8 | 9 | cd docker-oracle-xe-11g/assets && 10 | cat "${deb_file}aa" "${deb_file}ab" "${deb_file}ac" > "${deb_file}" 11 | 12 | pwd 13 | 14 | ls -lAh "${deb_file}" 15 | -------------------------------------------------------------------------------- /.travis/oracle/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [ -n "$ORACLE_FILE" ] || { echo "Missing ORACLE_FILE environment variable!"; exit 1; } 4 | [ -n "$ORACLE_HOME" ] || { echo "Missing ORACLE_HOME environment variable!"; exit 1; } 5 | 6 | cd "$(dirname "$(readlink -f "$0")")" 7 | 8 | ORACLE_DEB=docker-oracle-xe-11g/assets/oracle-xe_11.2.0-1.0_amd64.deb 9 | 10 | sudo apt-get -qq update 11 | sudo apt-get --no-install-recommends -qq install bc libaio1 12 | 13 | df -B1 /dev/shm | awk 'END { if ($1 != "shmfs" && $1 != "tmpfs" || $2 < 2147483648) exit 1 }' || 14 | ( sudo rm -r /dev/shm && sudo mkdir /dev/shm && sudo mount -t tmpfs shmfs -o size=2G /dev/shm ) 15 | 16 | test -f /sbin/chkconfig || 17 | ( echo '#!/bin/sh' | sudo tee /sbin/chkconfig > /dev/null && sudo chmod u+x /sbin/chkconfig ) 18 | 19 | test -d /var/lock/subsys || sudo mkdir /var/lock/subsys 20 | 21 | sudo dpkg -i "${ORACLE_DEB}" 22 | 23 | echo 'OS_AUTHENT_PREFIX=""' | sudo tee -a "$ORACLE_HOME/config/scripts/init.ora" > /dev/null 24 | echo 'disk_asynch_io=false' | sudo tee -a "$ORACLE_HOME/config/scripts/init.ora" > /dev/null 25 | 26 | sudo usermod -aG dba $USER 27 | 28 | ( echo ; echo ; echo travis ; echo travis ; echo n ) | sudo AWK='/usr/bin/awk' /etc/init.d/oracle-xe configure 29 | 30 | "$ORACLE_HOME/bin/sqlplus" -L -S / AS SYSDBA < e 9 | $stderr.puts e.message 10 | $stderr.puts "Run `bundle install` to install missing gems" 11 | exit e.status_code 12 | end 13 | 14 | require "rake" 15 | 16 | require "rspec/core/rake_task" 17 | RSpec::Core::RakeTask.new(:spec) 18 | 19 | desc "Clear test database" 20 | task :clear do 21 | require "./spec/spec_helper" 22 | ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) 23 | require "active_support/core_ext" 24 | ActiveRecord::Base.connection.execute_structure_dump(ActiveRecord::Base.connection.full_drop) 25 | ActiveRecord::Base.connection.execute("PURGE RECYCLEBIN") rescue nil 26 | end 27 | 28 | # Clear test database before running spec 29 | task spec: :clear 30 | 31 | task default: :spec 32 | 33 | require "rdoc/task" 34 | Rake::RDocTask.new do |rdoc| 35 | version = File.exist?("VERSION") ? File.read("VERSION") : "" 36 | 37 | rdoc.rdoc_dir = "doc" 38 | rdoc.title = "activerecord-oracle_enhanced-adapter #{version}" 39 | rdoc.rdoc_files.include("README*") 40 | rdoc.rdoc_files.include("lib/**/*.rb") 41 | end 42 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 7.1.0.alpha 2 | -------------------------------------------------------------------------------- /activerecord-oracle_enhanced-adapter.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | version = File.read(File.expand_path("../VERSION", __FILE__)).strip 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "activerecord-oracle_enhanced-adapter" 7 | s.version = version 8 | 9 | s.required_rubygems_version = ">= 1.8.11" 10 | s.required_ruby_version = ">= 2.7.0" 11 | s.license = "MIT" 12 | s.authors = ["Raimonds Simanovskis"] 13 | s.description = 'Oracle "enhanced" ActiveRecord adapter contains useful additional methods for working with new and legacy Oracle databases. 14 | This adapter is superset of original ActiveRecord Oracle adapter. 15 | ' 16 | s.email = "raimonds.simanovskis@gmail.com" 17 | s.extra_rdoc_files = [ 18 | "README.md" 19 | ] 20 | s.files = Dir["History.md", "License.txt", "README.md", "VERSION", "lib/**/*"] 21 | s.homepage = "http://github.com/rsim/oracle-enhanced" 22 | s.require_paths = ["lib"] 23 | s.summary = "Oracle enhanced adapter for ActiveRecord" 24 | s.test_files = Dir["spec/**/*"] 25 | s.metadata = { 26 | "rubygems_mfa_required" => "true" 27 | } 28 | 29 | s.add_runtime_dependency("activerecord", ["~> 7.1.0"]) 30 | s.add_runtime_dependency("ruby-plsql", [">= 0.6.0"]) 31 | if /java/.match?(RUBY_PLATFORM) 32 | s.platform = Gem::Platform.new("java") 33 | else 34 | s.add_runtime_dependency("ruby-oci8") 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /ci/network/admin/tnsnames.ora: -------------------------------------------------------------------------------- 1 | FREEPDB1 = 2 | (DESCRIPTION = 3 | (ADDRESS = (PROTOCOL = TCP)(HOST = 127.0.0.1)(PORT = 1521)) 4 | (CONNECT_DATA = 5 | (SERVICE_NAME = FREEPDB1) 6 | ) 7 | ) 8 | 9 | XE = 10 | (DESCRIPTION = 11 | (ADDRESS = (PROTOCOL = TCP)(HOST = 127.0.0.1)(PORT = 1521)) 12 | (CONNECT_DATA = 13 | (SERVICE_NAME = XE) 14 | ) 15 | ) 16 | -------------------------------------------------------------------------------- /ci/setup_accounts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ev 4 | 5 | sqlplus system/${DATABASE_SYS_PASSWORD}@${DATABASE_NAME} <" 18 | $stdin.gets.strip 19 | } 20 | establish_connection(@config.merge(username: "SYSTEM", password: system_password)) 21 | begin 22 | connection.execute "CREATE USER #{@config[:username]} IDENTIFIED BY #{@config[:password]}" 23 | rescue => e 24 | if /ORA-01920/.match?(e.message) # user name conflicts with another user or role name 25 | connection.execute "ALTER USER #{@config[:username]} IDENTIFIED BY #{@config[:password]}" 26 | else 27 | raise e 28 | end 29 | end 30 | 31 | OracleEnhancedAdapter.permissions.each do |permission| 32 | connection.execute "GRANT #{permission} TO #{@config[:username]}" 33 | end 34 | end 35 | 36 | def drop 37 | establish_connection(@config) 38 | connection.execute_structure_dump(connection.full_drop) 39 | end 40 | 41 | def purge 42 | drop 43 | connection.execute("PURGE RECYCLEBIN") rescue nil 44 | end 45 | 46 | def structure_dump(filename, extra_flags) 47 | establish_connection(@config) 48 | File.open(filename, "w:utf-8") { |f| f << connection.structure_dump } 49 | if @config[:structure_dump] == "db_stored_code" 50 | File.open(filename, "a") { |f| f << connection.structure_dump_db_stored_code } 51 | end 52 | end 53 | 54 | def structure_load(filename, extra_flags) 55 | establish_connection(@config) 56 | connection.execute_structure_dump(File.read(filename)) 57 | end 58 | end 59 | end 60 | end 61 | end 62 | 63 | ActiveRecord::Tasks::DatabaseTasks.register_task(/(oci|oracle)/, ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter::DatabaseTasks) 64 | -------------------------------------------------------------------------------- /lib/active_record/connection_adapters/oracle_enhanced/dbms_output.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module ConnectionAdapters 5 | module OracleEnhanced 6 | module DbmsOutput 7 | # DBMS_OUTPUT ============================================= 8 | # 9 | # PL/SQL in Oracle uses dbms_output for logging print statements 10 | # These methods stick that output into the Rails log so Ruby and PL/SQL 11 | # code can can be debugged together in a single application 12 | 13 | # Maximum DBMS_OUTPUT buffer size 14 | DBMS_OUTPUT_BUFFER_SIZE = 10000 # can be 1-1000000 15 | 16 | # Turn DBMS_Output logging on 17 | def enable_dbms_output 18 | set_dbms_output_plsql_connection 19 | @enable_dbms_output = true 20 | plsql(:dbms_output).sys.dbms_output.enable(DBMS_OUTPUT_BUFFER_SIZE) 21 | end 22 | # Turn DBMS_Output logging off 23 | def disable_dbms_output 24 | set_dbms_output_plsql_connection 25 | @enable_dbms_output = false 26 | plsql(:dbms_output).sys.dbms_output.disable 27 | end 28 | # Is DBMS_Output logging enabled? 29 | def dbms_output_enabled? 30 | @enable_dbms_output 31 | end 32 | 33 | private 34 | def log(sql, name = "SQL", binds = [], type_casted_binds = [], statement_name = nil, async: false, &block) 35 | @instrumenter.instrument( 36 | "sql.active_record", 37 | sql: sql, 38 | name: name, 39 | binds: binds, 40 | type_casted_binds: type_casted_binds, 41 | statement_name: statement_name, 42 | async: async, 43 | connection: self, 44 | &block 45 | ) 46 | rescue => e 47 | # FIXME: raise ex.set_query(sql, binds) 48 | raise translate_exception_class(e, sql, binds) 49 | ensure 50 | log_dbms_output if dbms_output_enabled? 51 | end 52 | 53 | def set_dbms_output_plsql_connection 54 | raise OracleEnhanced::ConnectionException, "ruby-plsql gem is required for logging DBMS output" unless self.respond_to?(:plsql) 55 | # do not reset plsql connection if it is the same (as resetting will clear PL/SQL metadata cache) 56 | unless plsql(:dbms_output).connection && plsql(:dbms_output).connection.raw_connection == raw_connection 57 | plsql(:dbms_output).connection = raw_connection 58 | end 59 | end 60 | 61 | def log_dbms_output 62 | while true do 63 | result = plsql(:dbms_output).sys.dbms_output.get_line(line: "", status: 0) 64 | break unless result[:status] == 0 65 | @logger.debug "DBMS_OUTPUT: #{result[:line]}" if @logger 66 | end 67 | end 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/active_record/connection_adapters/oracle_enhanced/jdbc_quoting.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module ConnectionAdapters 5 | module OracleEnhanced 6 | module JDBCQuoting 7 | def type_cast(value) 8 | case value 9 | when ActiveModel::Type::Binary::Data 10 | blob = Java::OracleSql::BLOB.createTemporary(@raw_connection.raw_connection, false, Java::OracleSql::BLOB::DURATION_SESSION) 11 | blob.setBytes(1, value.to_s.to_java_bytes) 12 | blob 13 | when Type::OracleEnhanced::Text::Data 14 | clob = Java::OracleSql::CLOB.createTemporary(@raw_connection.raw_connection, false, Java::OracleSql::CLOB::DURATION_SESSION) 15 | clob.setString(1, value.to_s) 16 | clob 17 | when Type::OracleEnhanced::NationalCharacterText::Data 18 | clob = Java::OracleSql::NCLOB.createTemporary(@raw_connection.raw_connection, false, Java::OracleSql::NCLOB::DURATION_SESSION) 19 | clob.setString(1, value.to_s) 20 | clob 21 | else 22 | super 23 | end 24 | end 25 | end 26 | end 27 | end 28 | end 29 | 30 | module ActiveRecord 31 | module ConnectionAdapters 32 | module OracleEnhanced 33 | module Quoting 34 | prepend JDBCQuoting 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/active_record/connection_adapters/oracle_enhanced/lob.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord # :nodoc: 4 | module ConnectionAdapters # :nodoc: 5 | module OracleEnhanced # :nodoc: 6 | module Lob # :nodoc: 7 | extend ActiveSupport::Concern 8 | 9 | included do 10 | class_attribute :custom_create_method, :custom_update_method, :custom_delete_method 11 | 12 | # After setting large objects to empty, select the OCI8::LOB 13 | # and write back the data. 14 | before_update :record_changed_lobs 15 | after_update :enhanced_write_lobs 16 | end 17 | 18 | module ClassMethods 19 | def lob_columns 20 | columns.select do |column| 21 | column.sql_type_metadata.sql_type.end_with?("LOB") 22 | end 23 | end 24 | end 25 | 26 | private 27 | def enhanced_write_lobs 28 | if self.class.connection.is_a?(ConnectionAdapters::OracleEnhancedAdapter) && 29 | !(self.class.custom_create_method || self.class.custom_update_method) 30 | self.class.connection.write_lobs(self.class.table_name, self.class, attributes, @changed_lob_columns) 31 | end 32 | end 33 | def record_changed_lobs 34 | @changed_lob_columns = self.class.lob_columns.select do |col| 35 | self.will_save_change_to_attribute?(col.name) && !self.class.readonly_attributes.to_a.include?(col.name) 36 | end 37 | end 38 | end 39 | end 40 | end 41 | end 42 | 43 | ActiveSupport.on_load(:active_record) do 44 | ActiveRecord::Base.send(:include, ActiveRecord::ConnectionAdapters::OracleEnhanced::Lob) 45 | end 46 | -------------------------------------------------------------------------------- /lib/active_record/connection_adapters/oracle_enhanced/oci_quoting.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module ConnectionAdapters 5 | module OracleEnhanced 6 | module OCIQuoting 7 | def type_cast(value) 8 | case value 9 | when ActiveModel::Type::Binary::Data 10 | lob_value = value == "" ? " " : value 11 | bind_type = OCI8::BLOB 12 | ora_value = bind_type.new(_connection.raw_oci_connection, lob_value) 13 | ora_value.size = 0 if value == "" 14 | ora_value 15 | when Type::OracleEnhanced::Text::Data 16 | lob_value = value.to_s == "" ? " " : value.to_s 17 | bind_type = OCI8::CLOB 18 | ora_value = bind_type.new(_connection.raw_oci_connection, lob_value) 19 | ora_value.size = 0 if value.to_s == "" 20 | ora_value 21 | when Type::OracleEnhanced::NationalCharacterText::Data 22 | lob_value = value.to_s == "" ? " " : value.to_s 23 | bind_type = OCI8::NCLOB 24 | ora_value = bind_type.new(_connection.raw_oci_connection, lob_value) 25 | ora_value.size = 0 if value.to_s == "" 26 | ora_value 27 | else 28 | super 29 | end 30 | end 31 | end 32 | end 33 | end 34 | end 35 | 36 | module ActiveRecord 37 | module ConnectionAdapters 38 | module OracleEnhanced 39 | module Quoting 40 | prepend OCIQuoting 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/active_record/connection_adapters/oracle_enhanced/procedures.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "active_support" 4 | 5 | module ActiveRecord # :nodoc: 6 | # Custom create, update, delete methods functionality. 7 | # 8 | # Example: 9 | # 10 | # class Employee < ActiveRecord::Base 11 | # include ActiveRecord::OracleEnhancedProcedures 12 | # 13 | # set_create_method do 14 | # plsql.employees_pkg.create_employee( 15 | # :p_first_name => first_name, 16 | # :p_last_name => last_name, 17 | # :p_employee_id => nil 18 | # )[:p_employee_id] 19 | # end 20 | # 21 | # set_update_method do 22 | # plsql.employees_pkg.update_employee( 23 | # :p_employee_id => id, 24 | # :p_first_name => first_name, 25 | # :p_last_name => last_name 26 | # ) 27 | # end 28 | # 29 | # set_delete_method do 30 | # plsql.employees_pkg.delete_employee( 31 | # :p_employee_id => id 32 | # ) 33 | # end 34 | # end 35 | # 36 | module OracleEnhancedProcedures # :nodoc: 37 | module ClassMethods 38 | # Specify custom create method which should be used instead of Rails generated INSERT statement. 39 | # Provided block should return ID of new record. 40 | # Example: 41 | # set_create_method do 42 | # plsql.employees_pkg.create_employee( 43 | # :p_first_name => first_name, 44 | # :p_last_name => last_name, 45 | # :p_employee_id => nil 46 | # )[:p_employee_id] 47 | # end 48 | def set_create_method(&block) 49 | self.custom_create_method = block 50 | end 51 | 52 | # Specify custom update method which should be used instead of Rails generated UPDATE statement. 53 | # Example: 54 | # set_update_method do 55 | # plsql.employees_pkg.update_employee( 56 | # :p_employee_id => id, 57 | # :p_first_name => first_name, 58 | # :p_last_name => last_name 59 | # ) 60 | # end 61 | def set_update_method(&block) 62 | self.custom_update_method = block 63 | end 64 | 65 | # Specify custom delete method which should be used instead of Rails generated DELETE statement. 66 | # Example: 67 | # set_delete_method do 68 | # plsql.employees_pkg.delete_employee( 69 | # :p_employee_id => id 70 | # ) 71 | # end 72 | def set_delete_method(&block) 73 | self.custom_delete_method = block 74 | end 75 | end 76 | 77 | def self.included(base) 78 | base.class_eval do 79 | extend ClassMethods 80 | class_attribute :custom_create_method 81 | class_attribute :custom_update_method 82 | class_attribute :custom_delete_method 83 | end 84 | end 85 | 86 | def destroy # :nodoc: 87 | # check if class has custom delete method 88 | if self.class.custom_delete_method 89 | # wrap destroy in transaction 90 | with_transaction_returning_status do 91 | # run before/after callbacks defined in model 92 | run_callbacks(:destroy) { destroy_using_custom_method } 93 | end 94 | else 95 | super 96 | end 97 | end 98 | 99 | private 100 | # Creates a record with custom create method 101 | # and returns its id. 102 | def _create_record 103 | # check if class has custom create method 104 | if self.class.custom_create_method 105 | # run before/after callbacks defined in model 106 | run_callbacks(:create) do 107 | # timestamp 108 | if self.record_timestamps 109 | current_time = current_time_from_proper_timezone 110 | 111 | all_timestamp_attributes_in_model.each do |column| 112 | if respond_to?(column) && respond_to?("#{column}=") && self.send(column).nil? 113 | write_attribute(column.to_s, current_time) 114 | end 115 | end 116 | end 117 | # run 118 | create_using_custom_method 119 | end 120 | else 121 | super 122 | end 123 | end 124 | 125 | def create_using_custom_method 126 | log_custom_method("custom create method", "#{self.class.name} Create") do 127 | self.id = instance_eval(&self.class.custom_create_method) 128 | end 129 | @new_record = false 130 | # Starting from ActiveRecord 3.0.3 @persisted is used instead of @new_record 131 | @persisted = true 132 | id 133 | end 134 | 135 | # Updates the associated record with custom update method 136 | # Returns the number of affected rows. 137 | def _update_record(attribute_names = @attributes.keys) 138 | # check if class has custom update method 139 | if self.class.custom_update_method 140 | # run before/after callbacks defined in model 141 | run_callbacks(:update) do 142 | # timestamp 143 | if should_record_timestamps? 144 | current_time = current_time_from_proper_timezone 145 | 146 | timestamp_attributes_for_update_in_model.each do |column| 147 | column = column.to_s 148 | next if will_save_change_to_attribute?(column) 149 | write_attribute(column, current_time) 150 | end 151 | end 152 | # update just dirty attributes 153 | if partial_updates? 154 | # Serialized attributes should always be written in case they've been 155 | # changed in place. 156 | update_using_custom_method(changed | (attributes.keys & self.class.columns.select { |column| column.is_a?(Type::Serialized) })) 157 | else 158 | update_using_custom_method(attributes.keys) 159 | end 160 | end 161 | else 162 | super 163 | end 164 | end 165 | 166 | def update_using_custom_method(attribute_names) 167 | return 0 if attribute_names.empty? 168 | log_custom_method("custom update method with #{self.class.primary_key}=#{self.id}", "#{self.class.name} Update") do 169 | instance_eval(&self.class.custom_update_method) 170 | end 171 | 1 172 | end 173 | 174 | # Deletes the record in the database with custom delete method 175 | # and freezes this instance to reflect that no changes should 176 | # be made (since they can't be persisted). 177 | def destroy_using_custom_method 178 | unless new_record? || @destroyed 179 | log_custom_method("custom delete method with #{self.class.primary_key}=#{self.id}", "#{self.class.name} Destroy") do 180 | instance_eval(&self.class.custom_delete_method) 181 | end 182 | end 183 | 184 | @destroyed = true 185 | freeze 186 | end 187 | 188 | def log_custom_method(*args, &block) 189 | self.class.connection.send(:log, *args, &block) 190 | end 191 | 192 | alias_method :update_record, :_update_record if private_method_defined?(:_update_record) 193 | alias_method :create_record, :_create_record if private_method_defined?(:_create_record) 194 | end 195 | end 196 | -------------------------------------------------------------------------------- /lib/active_record/connection_adapters/oracle_enhanced/quoting.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module ConnectionAdapters 5 | module OracleEnhanced 6 | module Quoting 7 | # QUOTING ================================================== 8 | # 9 | # see: abstract/quoting.rb 10 | QUOTED_COLUMN_NAMES = Concurrent::Map.new # :nodoc: 11 | QUOTED_TABLE_NAMES = Concurrent::Map.new # :nodoc: 12 | 13 | def quote_column_name(name) # :nodoc: 14 | name = name.to_s 15 | QUOTED_COLUMN_NAMES[name] ||= if /\A[a-z][a-z_0-9$#]*\Z/.match?(name) 16 | "\"#{name.upcase}\"" 17 | else 18 | # remove double quotes which cannot be used inside quoted identifier 19 | "\"#{name.delete('"')}\"" 20 | end 21 | end 22 | 23 | # This method is used in add_index to identify either column name (which is quoted) 24 | # or function based index (in which case function expression is not quoted) 25 | def quote_column_name_or_expression(name) # :nodoc: 26 | name = name.to_s 27 | case name 28 | # if only valid lowercase column characters in name 29 | when /^[a-z][a-z_0-9$#]*$/ 30 | "\"#{name.upcase}\"" 31 | when /^[a-z][a-z_0-9$#-]*$/i 32 | "\"#{name}\"" 33 | # if other characters present then assume that it is expression 34 | # which should not be quoted 35 | else 36 | name 37 | end 38 | end 39 | 40 | # Names must be from 1 to 30 bytes long with these exceptions: 41 | # * Names of databases are limited to 8 bytes. 42 | # * Names of database links can be as long as 128 bytes. 43 | # 44 | # Nonquoted identifiers cannot be Oracle Database reserved words 45 | # 46 | # Nonquoted identifiers must begin with an alphabetic character from 47 | # your database character set 48 | # 49 | # Nonquoted identifiers can contain only alphanumeric characters from 50 | # your database character set and the underscore (_), dollar sign ($), 51 | # and pound sign (#). 52 | # Oracle strongly discourages you from using $ and # in nonquoted identifiers. 53 | NONQUOTED_OBJECT_NAME = /[[:alpha:]][\w$#]{0,29}/ 54 | VALID_TABLE_NAME = /\A(?:#{NONQUOTED_OBJECT_NAME}\.)?#{NONQUOTED_OBJECT_NAME}?\Z/ 55 | 56 | # unescaped table name should start with letter and 57 | # contain letters, digits, _, $ or # 58 | # can be prefixed with schema name 59 | # CamelCase table names should be quoted 60 | def self.valid_table_name?(name) # :nodoc: 61 | object_name = name.to_s 62 | !!(object_name =~ VALID_TABLE_NAME && !mixed_case?(object_name)) 63 | end 64 | 65 | def self.mixed_case?(name) 66 | object_name = name.include?(".") ? name.split(".").second : name 67 | !!(object_name =~ /[A-Z]/ && object_name =~ /[a-z]/) 68 | end 69 | 70 | def quote_table_name(name) # :nodoc: 71 | name, _link = name.to_s.split("@") 72 | QUOTED_TABLE_NAMES[name] ||= [name.split(".").map { |n| quote_column_name(n) }].join(".") 73 | end 74 | 75 | def quote_string(s) # :nodoc: 76 | s.gsub(/'/, "''") 77 | end 78 | 79 | def quote(value) # :nodoc: 80 | case value 81 | when Type::OracleEnhanced::CharacterString::Data then 82 | "'#{quote_string(value.to_s)}'" 83 | when Type::OracleEnhanced::NationalCharacterString::Data then 84 | +"N" << "'#{quote_string(value.to_s)}'" 85 | when ActiveModel::Type::Binary::Data then 86 | "empty_blob()" 87 | when Type::OracleEnhanced::Text::Data then 88 | "empty_clob()" 89 | when Type::OracleEnhanced::NationalCharacterText::Data then 90 | "empty_nclob()" 91 | else 92 | super 93 | end 94 | end 95 | 96 | def quoted_true # :nodoc: 97 | return "'Y'" if emulate_booleans_from_strings 98 | "1" 99 | end 100 | 101 | def unquoted_true # :nodoc: 102 | return "Y" if emulate_booleans_from_strings 103 | "1" 104 | end 105 | 106 | def quoted_false # :nodoc: 107 | return "'N'" if emulate_booleans_from_strings 108 | "0" 109 | end 110 | 111 | def unquoted_false # :nodoc: 112 | return "N" if emulate_booleans_from_strings 113 | "0" 114 | end 115 | 116 | def type_cast(value) 117 | case value 118 | when Type::OracleEnhanced::TimestampTz::Data, Type::OracleEnhanced::TimestampLtz::Data 119 | if value.acts_like?(:time) 120 | zone_conversion_method = ActiveRecord.default_timezone == :utc ? :getutc : :getlocal 121 | value.respond_to?(zone_conversion_method) ? value.send(zone_conversion_method) : value 122 | else 123 | value 124 | end 125 | when Type::OracleEnhanced::NationalCharacterString::Data 126 | value.to_s 127 | when Type::OracleEnhanced::CharacterString::Data 128 | value 129 | else 130 | super 131 | end 132 | end 133 | 134 | def column_name_matcher 135 | COLUMN_NAME 136 | end 137 | 138 | def column_name_with_order_matcher 139 | COLUMN_NAME_WITH_ORDER 140 | end 141 | 142 | COLUMN_NAME = / 143 | \A 144 | ( 145 | (?: 146 | # "table_name"."column_name" | function(one or no argument) 147 | ((?:\w+\.|"\w+"\.)?(?:\w+|"\w+") | \w+\((?:|\g<2>)\)) 148 | ) 149 | (?:(?:\s+AS)?\s+(?:\w+|"\w+"))? 150 | ) 151 | (?:\s*,\s*\g<1>)* 152 | \z 153 | /ix 154 | 155 | COLUMN_NAME_WITH_ORDER = / 156 | \A 157 | ( 158 | (?: 159 | # "table_name"."column_name" | function(one or no argument) 160 | ((?:\w+\.|"\w+"\.)?(?:\w+|"\w+") | \w+\((?:|\g<2>)\)) 161 | ) 162 | (?:\s+ASC|\s+DESC)? 163 | (?:\s+NULLS\s+(?:FIRST|LAST))? 164 | ) 165 | (?:\s*,\s*\g<1>)* 166 | \z 167 | /ix 168 | private_constant :COLUMN_NAME, :COLUMN_NAME_WITH_ORDER 169 | 170 | private 171 | def oracle_downcase(column_name) 172 | return nil if column_name.nil? 173 | /[a-z]/.match?(column_name) ? column_name : column_name.downcase 174 | end 175 | end 176 | end 177 | end 178 | end 179 | 180 | # if MRI or YARV or TruffleRuby 181 | if !defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby" || RUBY_ENGINE == "truffleruby" 182 | require "active_record/connection_adapters/oracle_enhanced/oci_quoting" 183 | # if JRuby 184 | elsif RUBY_ENGINE == "jruby" 185 | require "active_record/connection_adapters/oracle_enhanced/jdbc_quoting" 186 | else 187 | raise "Unsupported Ruby engine #{RUBY_ENGINE}" 188 | end 189 | -------------------------------------------------------------------------------- /lib/active_record/connection_adapters/oracle_enhanced/schema_creation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module ConnectionAdapters 5 | module OracleEnhanced 6 | class SchemaCreation < SchemaCreation 7 | private 8 | def visit_ColumnDefinition(o) 9 | if [:blob, :clob, :nclob].include?(sql_type = type_to_sql(o.type, **o.options).downcase.to_sym) 10 | if (tablespace = default_tablespace_for(sql_type)) 11 | @lob_tablespaces ||= {} 12 | @lob_tablespaces[o.name] = tablespace 13 | end 14 | end 15 | super 16 | end 17 | 18 | def visit_TableDefinition(o) 19 | create_sql = +"CREATE#{' GLOBAL TEMPORARY' if o.temporary} TABLE #{quote_table_name(o.name)} " 20 | statements = o.columns.map { |c| accept c } 21 | statements << accept(o.primary_keys) if o.primary_keys 22 | 23 | if use_foreign_keys? 24 | statements.concat(o.foreign_keys.map { |fk| accept fk }) 25 | end 26 | 27 | create_sql << "(#{statements.join(', ')})" if statements.present? 28 | 29 | unless o.temporary 30 | @lob_tablespaces.each do |lob_column, tablespace| 31 | create_sql << " LOB (#{quote_column_name(lob_column)}) STORE AS (TABLESPACE #{tablespace}) \n" 32 | end if defined?(@lob_tablespaces) 33 | create_sql << " ORGANIZATION #{o.organization}" if o.organization 34 | if (tablespace = o.tablespace || default_tablespace_for(:table)) 35 | create_sql << " TABLESPACE #{tablespace}" 36 | end 37 | end 38 | add_table_options!(create_sql, o) 39 | create_sql << " AS #{to_sql(o.as)}" if o.as 40 | create_sql 41 | end 42 | 43 | def default_tablespace_for(type) 44 | OracleEnhancedAdapter.default_tablespaces[type] 45 | end 46 | 47 | def add_column_options!(sql, options) 48 | type = options[:type] || ((column = options[:column]) && column.type) 49 | type = type && type.to_sym 50 | # handle case of defaults for CLOB/NCLOB columns, which would otherwise get "quoted" incorrectly 51 | if options_include_default?(options) 52 | if type == :text 53 | sql << " DEFAULT #{@conn.quote(options[:default])}" 54 | elsif type == :ntext 55 | sql << " DEFAULT #{@conn.quote(options[:default])}" 56 | else 57 | sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}" 58 | end 59 | end 60 | # must explicitly add NULL or NOT NULL to allow change_column to work on migrations 61 | if options[:null] == false 62 | sql << " NOT NULL" 63 | elsif options[:null] == true 64 | sql << " NULL" unless type == :primary_key 65 | end 66 | # add AS expression for virtual columns 67 | if options[:as].present? 68 | sql << " AS (#{options[:as]})" 69 | end 70 | if options[:primary_key] == true 71 | sql << " PRIMARY KEY" 72 | end 73 | end 74 | 75 | def action_sql(action, dependency) 76 | if action == "UPDATE" 77 | raise ArgumentError, <<~MSG 78 | '#{action}' is not supported by Oracle 79 | MSG 80 | end 81 | case dependency 82 | when :nullify then "ON #{action} SET NULL" 83 | when :cascade then "ON #{action} CASCADE" 84 | else 85 | raise ArgumentError, <<~MSG 86 | '#{dependency}' is not supported for #{action} 87 | Supported values are: :nullify, :cascade 88 | MSG 89 | end 90 | end 91 | end 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /lib/active_record/connection_adapters/oracle_enhanced/schema_definitions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module ConnectionAdapters 5 | module OracleEnhanced 6 | module ColumnMethods 7 | def primary_key(name, type = :primary_key, **options) 8 | # This is a placeholder for future :auto_increment support 9 | super 10 | end 11 | 12 | [ 13 | :raw, 14 | :timestamptz, 15 | :timestampltz, 16 | :ntext 17 | ].each do |column_type| 18 | module_eval <<-CODE, __FILE__, __LINE__ + 1 19 | def #{column_type}(*args, **options) 20 | args.each { |name| column(name, :#{column_type}, **options) } 21 | end 22 | CODE 23 | end 24 | end 25 | 26 | class ReferenceDefinition < ActiveRecord::ConnectionAdapters::ReferenceDefinition # :nodoc: 27 | def initialize( 28 | name, 29 | polymorphic: false, 30 | index: true, 31 | foreign_key: false, 32 | type: :integer, 33 | **options) 34 | super 35 | end 36 | end 37 | 38 | class SynonymDefinition < Struct.new(:name, :table_owner, :table_name) # :nodoc: 39 | end 40 | 41 | class IndexDefinition < ActiveRecord::ConnectionAdapters::IndexDefinition 42 | attr_accessor :parameters, :statement_parameters, :tablespace 43 | 44 | def initialize(table, name, unique, columns, orders, type, parameters, statement_parameters, tablespace) 45 | @parameters = parameters 46 | @statement_parameters = statement_parameters 47 | @tablespace = tablespace 48 | super(table, name, unique, columns, orders: orders, type: type) 49 | end 50 | end 51 | 52 | class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition 53 | include OracleEnhanced::ColumnMethods 54 | 55 | attr_accessor :tablespace, :organization 56 | def initialize( 57 | conn, 58 | name, 59 | temporary: false, 60 | options: nil, 61 | as: nil, 62 | tablespace: nil, 63 | organization: nil, 64 | comment: nil, 65 | ** 66 | ) 67 | @tablespace = tablespace 68 | @organization = organization 69 | super(conn, name, temporary: temporary, options: options, as: as, comment: comment) 70 | end 71 | 72 | def new_column_definition(name, type, **options) # :nodoc: 73 | if type == :virtual 74 | raise "No virtual column definition found." unless options[:as] 75 | type = options[:type] 76 | end 77 | super 78 | end 79 | 80 | def references(*args, **options) 81 | super(*args, type: :integer, **options) 82 | end 83 | alias :belongs_to :references 84 | 85 | private 86 | def valid_column_definition_options 87 | super + [ :as, :sequence_name, :sequence_start_value, :type ] 88 | end 89 | end 90 | 91 | class AlterTable < ActiveRecord::ConnectionAdapters::AlterTable 92 | end 93 | 94 | class Table < ActiveRecord::ConnectionAdapters::Table 95 | include OracleEnhanced::ColumnMethods 96 | end 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/active_record/connection_adapters/oracle_enhanced/schema_dumper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord # :nodoc: 4 | module ConnectionAdapters # :nodoc: 5 | module OracleEnhanced # :nodoc: 6 | class SchemaDumper < ConnectionAdapters::SchemaDumper # :nodoc: 7 | DEFAULT_PRIMARY_KEY_COLUMN_SPEC = { precision: "38", null: "false" }.freeze 8 | private_constant :DEFAULT_PRIMARY_KEY_COLUMN_SPEC 9 | 10 | private 11 | def column_spec_for_primary_key(column) 12 | spec = super 13 | spec.except!(:precision) if prepare_column_options(column) == DEFAULT_PRIMARY_KEY_COLUMN_SPEC 14 | spec 15 | end 16 | 17 | def tables(stream) 18 | # do not include materialized views in schema dump - they should be created separately after schema creation 19 | sorted_tables = (@connection.tables - @connection.materialized_views).sort 20 | sorted_tables.each do |tbl| 21 | # add table prefix or suffix for schema_migrations 22 | next if ignored? tbl 23 | table(tbl, stream) 24 | end 25 | # following table definitions 26 | # add foreign keys if table has them 27 | sorted_tables.each do |tbl| 28 | next if ignored? tbl 29 | foreign_keys(tbl, stream) 30 | end 31 | 32 | # add synonyms in local schema 33 | synonyms(stream) 34 | end 35 | 36 | def synonyms(stream) 37 | syns = @connection.synonyms 38 | syns.each do |syn| 39 | next if ignored? syn.name 40 | table_name = syn.table_name 41 | table_name = "#{syn.table_owner}.#{table_name}" if syn.table_owner 42 | stream.print " add_synonym #{syn.name.inspect}, #{table_name.inspect}, force: true" 43 | stream.puts 44 | end 45 | stream.puts unless syns.empty? 46 | end 47 | 48 | def _indexes(table, stream) 49 | if (indexes = @connection.indexes(table)).any? 50 | add_index_statements = indexes.filter_map do |index| 51 | case index.type 52 | when nil 53 | # do nothing here. see indexes_in_create 54 | statement_parts = [] 55 | when "CTXSYS.CONTEXT" 56 | if index.statement_parameters 57 | statement_parts = [ ("add_context_index " + remove_prefix_and_suffix(table).inspect) ] 58 | statement_parts << index.statement_parameters 59 | else 60 | statement_parts = [ ("add_context_index " + remove_prefix_and_suffix(table).inspect) ] 61 | statement_parts << index.columns.inspect 62 | statement_parts << ("sync: " + $1.inspect) if index.parameters =~ /SYNC\((.*?)\)/ 63 | statement_parts << ("name: " + index.name.inspect) 64 | end 65 | else 66 | # unrecognized index type 67 | statement_parts = ["# unrecognized index #{index.name.inspect} with type #{index.type.inspect}"] 68 | end 69 | " " + statement_parts.join(", ") unless statement_parts.empty? 70 | end 71 | 72 | return if add_index_statements.empty? 73 | 74 | stream.puts add_index_statements.sort.join("\n") 75 | stream.puts 76 | end 77 | end 78 | 79 | def indexes_in_create(table, stream) 80 | if (indexes = @connection.indexes(table)).any? 81 | index_statements = indexes.map do |index| 82 | " t.index #{index_parts(index).join(', ')}" unless index.type == "CTXSYS.CONTEXT" 83 | end 84 | stream.puts index_statements.compact.sort.join("\n") 85 | end 86 | end 87 | 88 | def index_parts(index) 89 | index_parts = super 90 | index_parts << "tablespace: #{index.tablespace.inspect}" if index.tablespace 91 | index_parts 92 | end 93 | 94 | def table(table, stream) 95 | columns = @connection.columns(table) 96 | begin 97 | self.table_name = table 98 | 99 | tbl = StringIO.new 100 | 101 | # first dump primary key column 102 | if @connection.respond_to?(:primary_keys) 103 | pk = @connection.primary_keys(table) 104 | pk = pk.first unless pk.size > 1 105 | else 106 | pk = @connection.primary_key(table) 107 | end 108 | 109 | tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}" 110 | 111 | # addition to make temporary option work 112 | tbl.print ", temporary: true" if @connection.temporary_table?(table) 113 | 114 | case pk 115 | when String 116 | tbl.print ", primary_key: #{pk.inspect}" unless pk == "id" 117 | pkcol = columns.detect { |c| c.name == pk } 118 | pkcolspec = column_spec_for_primary_key(pkcol) 119 | unless pkcolspec.empty? 120 | if pkcolspec != pkcolspec.slice(:id, :default) 121 | pkcolspec = { id: { type: pkcolspec.delete(:id), **pkcolspec }.compact } 122 | end 123 | tbl.print ", #{format_colspec(pkcolspec)}" 124 | end 125 | when Array 126 | tbl.print ", primary_key: #{pk.inspect}" 127 | else 128 | tbl.print ", id: false" 129 | end 130 | 131 | table_options = @connection.table_options(table) 132 | if table_options.present? 133 | tbl.print ", #{format_options(table_options)}" 134 | end 135 | 136 | tbl.puts ", force: :cascade do |t|" 137 | 138 | # then dump all non-primary key columns 139 | columns.each do |column| 140 | raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type) 141 | next if column.name == pk 142 | type, colspec = column_spec(column) 143 | tbl.print " t.#{type} #{column.name.inspect}" 144 | tbl.print ", #{format_colspec(colspec)}" if colspec.present? 145 | tbl.puts 146 | end 147 | 148 | indexes_in_create(table, tbl) 149 | 150 | tbl.puts " end" 151 | tbl.puts 152 | 153 | _indexes(table, tbl) 154 | 155 | tbl.rewind 156 | stream.print tbl.read 157 | rescue => e 158 | stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}" 159 | stream.puts "# #{e.message}" 160 | stream.puts 161 | ensure 162 | self.table_name = nil 163 | end 164 | end 165 | 166 | def prepare_column_options(column) 167 | spec = super 168 | 169 | if @connection.supports_virtual_columns? && column.virtual? 170 | spec[:as] = extract_expression_for_virtual_column(column) 171 | spec = { type: schema_type(column).inspect }.merge!(spec) unless column.type == :decimal 172 | end 173 | 174 | spec 175 | end 176 | 177 | def default_primary_key?(column) 178 | schema_type(column) == :integer 179 | end 180 | 181 | def extract_expression_for_virtual_column(column) 182 | column_name = column.name 183 | @connection.select_value(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table_name.upcase), bind_string("column_name", column_name.upcase)]).inspect 184 | select data_default from all_tab_columns 185 | where owner = SYS_CONTEXT('userenv', 'current_schema') 186 | and table_name = :table_name 187 | and column_name = :column_name 188 | SQL 189 | end 190 | 191 | def bind_string(name, value) 192 | ActiveRecord::Relation::QueryAttribute.new(name, value, Type::OracleEnhanced::String.new) 193 | end 194 | end 195 | end 196 | end 197 | end 198 | -------------------------------------------------------------------------------- /lib/active_record/connection_adapters/oracle_enhanced/type_metadata.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module ConnectionAdapters # :nodoc: 5 | module OracleEnhanced 6 | class TypeMetadata < DelegateClass(ActiveRecord::ConnectionAdapters::SqlTypeMetadata) # :nodoc: 7 | include Deduplicable 8 | 9 | attr_reader :virtual 10 | 11 | def initialize(type_metadata, virtual: nil) 12 | super(type_metadata) 13 | @type_metadata = type_metadata 14 | @virtual = virtual 15 | end 16 | 17 | def ==(other) 18 | other.is_a?(OracleEnhanced::TypeMetadata) && 19 | attributes_for_hash == other.attributes_for_hash 20 | end 21 | alias eql? == 22 | 23 | def hash 24 | attributes_for_hash.hash 25 | end 26 | 27 | protected 28 | def attributes_for_hash 29 | [self.class, @type_metadata, virtual] 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/active_record/connection_adapters/oracle_enhanced/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter::VERSION = File.read(File.expand_path("../../../../../VERSION", __FILE__)).chomp 4 | -------------------------------------------------------------------------------- /lib/active_record/type/oracle_enhanced/boolean.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module Type 5 | module OracleEnhanced 6 | class Boolean < ActiveModel::Type::Boolean # :nodoc: 7 | private 8 | def cast_value(value) 9 | # Kind of adding 'n' and 'N' to `FALSE_VALUES` 10 | if ["n", "N"].include?(value) 11 | false 12 | else 13 | super 14 | end 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/active_record/type/oracle_enhanced/character_string.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "active_model/type/string" 4 | 5 | module ActiveRecord 6 | module Type 7 | module OracleEnhanced 8 | class CharacterString < ActiveRecord::Type::OracleEnhanced::String # :nodoc: 9 | def serialize(value) 10 | return unless value 11 | Data.new(super, self.limit) 12 | end 13 | 14 | class Data # :nodoc: 15 | def initialize(value, limit) 16 | @value = value 17 | @limit = limit 18 | end 19 | 20 | def to_s 21 | @value 22 | end 23 | 24 | def to_character_str 25 | len = @value.to_s.length 26 | if len < @limit 27 | "%-#{@limit}s" % @value 28 | else 29 | @value 30 | end 31 | end 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/active_record/type/oracle_enhanced/integer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module Type 5 | module OracleEnhanced 6 | class Integer < ActiveModel::Type::Integer # :nodoc: 7 | private 8 | def max_value 9 | ("9" * 38).to_i 10 | end 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/active_record/type/oracle_enhanced/json.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module Type 5 | module OracleEnhanced 6 | class Json < ActiveRecord::Type::Json 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/active_record/type/oracle_enhanced/national_character_string.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "active_model/type/string" 4 | 5 | module ActiveRecord 6 | module Type 7 | module OracleEnhanced 8 | class NationalCharacterString < ActiveRecord::Type::OracleEnhanced::String # :nodoc: 9 | def serialize(value) 10 | return unless value 11 | Data.new(super) 12 | end 13 | 14 | class Data # :nodoc: 15 | def initialize(value) 16 | @value = value 17 | end 18 | 19 | def to_s 20 | @value 21 | end 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/active_record/type/oracle_enhanced/national_character_text.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "active_model/type/string" 4 | 5 | module ActiveRecord 6 | module Type 7 | module OracleEnhanced 8 | class NationalCharacterText < ActiveRecord::Type::Text # :nodoc: 9 | def type 10 | :ntext 11 | end 12 | 13 | def changed_in_place?(raw_old_value, new_value) 14 | # TODO: Needs to find a way not to cast here. 15 | raw_old_value = cast(raw_old_value) 16 | super 17 | end 18 | 19 | def serialize(value) 20 | return unless value 21 | Data.new(super) 22 | end 23 | 24 | class Data # :nodoc: 25 | def initialize(value) 26 | @value = value 27 | end 28 | 29 | def to_s 30 | @value 31 | end 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/active_record/type/oracle_enhanced/raw.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "active_model/type/string" 4 | 5 | module ActiveRecord 6 | module Type 7 | module OracleEnhanced 8 | class Raw < ActiveModel::Type::String # :nodoc: 9 | def type 10 | :raw 11 | end 12 | 13 | def serialize(value) 14 | # Encode a string or byte array as string of hex codes 15 | if value.nil? 16 | super 17 | else 18 | value = value.unpack("C*") 19 | value.map { |x| "%02X" % x }.join 20 | end 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/active_record/type/oracle_enhanced/string.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "active_model/type/string" 4 | 5 | module ActiveRecord 6 | module Type 7 | module OracleEnhanced 8 | class String < ActiveModel::Type::String # :nodoc: 9 | def changed?(old_value, new_value, _new_value_before_type_cast) 10 | if old_value.nil? 11 | new_value = nil if new_value == "" 12 | old_value != new_value 13 | else 14 | super 15 | end 16 | end 17 | 18 | def changed_in_place?(raw_old_value, new_value) 19 | if raw_old_value.nil? 20 | new_value = nil if new_value == "" 21 | raw_old_value != new_value 22 | else 23 | super 24 | end 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/active_record/type/oracle_enhanced/text.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "active_model/type/string" 4 | 5 | module ActiveRecord 6 | module Type 7 | module OracleEnhanced 8 | class Text < ActiveRecord::Type::Text # :nodoc: 9 | def changed_in_place?(raw_old_value, new_value) 10 | # TODO: Needs to find a way not to cast here. 11 | raw_old_value = cast(raw_old_value) 12 | super 13 | end 14 | 15 | def serialize(value) 16 | return unless value 17 | Data.new(super) 18 | end 19 | 20 | class Data # :nodoc: 21 | def initialize(value) 22 | @value = value 23 | end 24 | 25 | def to_s 26 | @value 27 | end 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/active_record/type/oracle_enhanced/timestampltz.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module Type 5 | module OracleEnhanced 6 | class TimestampLtz < ActiveRecord::Type::DateTime 7 | def type 8 | :timestampltz 9 | end 10 | 11 | class Data < DelegateClass(::Time) # :nodoc: 12 | end 13 | 14 | def serialize(value) 15 | case value = super 16 | when ::Time 17 | Data.new(value) 18 | else 19 | value 20 | end 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/active_record/type/oracle_enhanced/timestamptz.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActiveRecord 4 | module Type 5 | module OracleEnhanced 6 | class TimestampTz < ActiveRecord::Type::DateTime 7 | def type 8 | :timestamptz 9 | end 10 | 11 | class Data < DelegateClass(::Time) # :nodoc: 12 | end 13 | 14 | def serialize(value) 15 | case value = super 16 | when ::Time 17 | Data.new(value) 18 | else 19 | value 20 | end 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/activerecord-oracle_enhanced-adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if defined?(Rails) 4 | module ActiveRecord 5 | module ConnectionAdapters 6 | class OracleEnhancedRailtie < ::Rails::Railtie 7 | rake_tasks do 8 | load "active_record/connection_adapters/oracle_enhanced/database_tasks.rb" 9 | end 10 | 11 | ActiveSupport.on_load(:active_record) do 12 | require "active_record/connection_adapters/oracle_enhanced_adapter" 13 | 14 | if ActiveRecord::ConnectionAdapters.respond_to?(:register) 15 | ActiveRecord::ConnectionAdapters.register( 16 | "oracle_enhanced", 17 | "ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter", 18 | "active_record/connection_adapters/oracle_enhanced_adapter" 19 | ) 20 | end 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/arel/visitors/oracle.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "oracle_common" 4 | 5 | module Arel # :nodoc: all 6 | module Visitors 7 | class Oracle < Arel::Visitors::ToSql 8 | include OracleCommon 9 | 10 | private 11 | def visit_Arel_Nodes_SelectStatement(o, collector) 12 | o = order_hacks(o) 13 | 14 | # if need to select first records without ORDER BY and GROUP BY and without DISTINCT 15 | # then can use simple ROWNUM in WHERE clause 16 | if o.limit && o.orders.empty? && o.cores.first.groups.empty? && !o.offset && !o.cores.first.set_quantifier.class.to_s.match?(/Distinct/) 17 | o.cores.last.wheres.push Nodes::LessThanOrEqual.new( 18 | Nodes::SqlLiteral.new("ROWNUM"), o.limit.expr 19 | ) 20 | return super 21 | end 22 | 23 | if o.limit && o.offset 24 | o = o.dup 25 | limit = o.limit.expr 26 | offset = o.offset 27 | o.offset = nil 28 | collector << " 29 | SELECT * FROM ( 30 | SELECT raw_sql_.*, rownum raw_rnum_ 31 | FROM (" 32 | 33 | collector = super(o, collector) 34 | 35 | if offset.expr.type.is_a? ActiveModel::Type::Value 36 | collector << ") raw_sql_ WHERE rownum <= (" 37 | collector = visit offset.expr, collector 38 | collector << " + " 39 | collector = visit limit, collector 40 | collector << ") ) WHERE raw_rnum_ > " 41 | collector = visit offset.expr, collector 42 | return collector 43 | else 44 | collector << ") raw_sql_ 45 | WHERE rownum <= #{offset.expr.value_before_type_cast + limit.value_before_type_cast} 46 | ) 47 | WHERE " 48 | return visit(offset, collector) 49 | end 50 | end 51 | 52 | if o.limit 53 | o = o.dup 54 | limit = o.limit.expr 55 | collector << "SELECT * FROM (" 56 | collector = super(o, collector) 57 | collector << ") WHERE ROWNUM <= " 58 | return visit limit, collector 59 | end 60 | 61 | if o.offset 62 | o = o.dup 63 | offset = o.offset 64 | o.offset = nil 65 | collector << "SELECT * FROM ( 66 | SELECT raw_sql_.*, rownum raw_rnum_ 67 | FROM (" 68 | collector = super(o, collector) 69 | collector << ") raw_sql_ 70 | ) 71 | WHERE " 72 | return visit offset, collector 73 | end 74 | 75 | super 76 | end 77 | 78 | def visit_Arel_Nodes_Limit(o, collector) 79 | collector 80 | end 81 | 82 | def visit_Arel_Nodes_Offset(o, collector) 83 | collector << "raw_rnum_ > " 84 | visit o.expr, collector 85 | end 86 | 87 | def visit_Arel_Nodes_Except(o, collector) 88 | collector << "( " 89 | collector = infix_value o, collector, " MINUS " 90 | collector << " )" 91 | end 92 | 93 | ## 94 | # To avoid ORA-01795: maximum number of expressions in a list is 1000 95 | # tell ActiveRecord to limit us to 1000 ids at a time 96 | def visit_Arel_Nodes_HomogeneousIn(o, collector) 97 | in_clause_length = @connection.in_clause_length 98 | values = o.casted_values.map { |v| @connection.quote(v) } 99 | operator = 100 | if o.type == :in 101 | " IN (" 102 | else 103 | " NOT IN (" 104 | end 105 | 106 | if !Array === values || values.length <= in_clause_length 107 | visit o.left, collector 108 | collector << operator 109 | 110 | expr = 111 | if values.empty? 112 | @connection.quote(nil) 113 | else 114 | values.join(",") 115 | end 116 | 117 | collector << expr 118 | collector << ")" 119 | else 120 | separator = 121 | if o.type == :in 122 | " OR " 123 | else 124 | " AND " 125 | end 126 | collector << "(" 127 | values.each_slice(in_clause_length).each_with_index do |valuez, i| 128 | collector << separator unless i == 0 129 | visit o.left, collector 130 | collector << operator 131 | collector << valuez.join(",") 132 | collector << ")" 133 | end 134 | collector << ")" 135 | end 136 | 137 | collector 138 | end 139 | 140 | def visit_Arel_Nodes_UpdateStatement(o, collector) 141 | # Oracle does not allow ORDER BY/LIMIT in UPDATEs. 142 | if o.orders.any? && o.limit.nil? 143 | # However, there is no harm in silently eating the ORDER BY clause if no LIMIT has been provided, 144 | # otherwise let the user deal with the error 145 | o = o.dup 146 | o.orders = [] 147 | end 148 | 149 | super 150 | end 151 | 152 | ### 153 | # Hacks for the order clauses specific to Oracle 154 | def order_hacks(o) 155 | return o if o.orders.empty? 156 | return o unless o.cores.any? do |core| 157 | core.projections.any? do |projection| 158 | /FIRST_VALUE/ === projection 159 | end 160 | end 161 | # Previous version with join and split broke ORDER BY clause 162 | # if it contained functions with several arguments (separated by ','). 163 | # 164 | # orders = o.orders.map { |x| visit x }.join(', ').split(',') 165 | orders = o.orders.map do |x| 166 | string = visit(x, Arel::Collectors::SQLString.new).value 167 | if string.include?(",") 168 | split_order_string(string) 169 | else 170 | string 171 | end 172 | end.flatten 173 | o.orders = [] 174 | orders.each_with_index do |order, i| 175 | o.orders << 176 | Nodes::SqlLiteral.new("alias_#{i}__#{' DESC' if /\bdesc$/i.match?(order)}") 177 | end 178 | o 179 | end 180 | 181 | # Split string by commas but count opening and closing brackets 182 | # and ignore commas inside brackets. 183 | def split_order_string(string) 184 | array = [] 185 | i = 0 186 | string.split(",").each do |part| 187 | if array[i] 188 | array[i] << "," << part 189 | else 190 | # to ensure that array[i] will be String and not Arel::Nodes::SqlLiteral 191 | array[i] = part.to_s 192 | end 193 | i += 1 if array[i].count("(") == array[i].count(")") 194 | end 195 | array 196 | end 197 | 198 | def visit_ActiveModel_Attribute(o, collector) 199 | collector.add_bind(o) { |i| ":a#{i}" } 200 | end 201 | 202 | def visit_Arel_Nodes_BindParam(o, collector) 203 | collector.add_bind(o.value) { |i| ":a#{i}" } 204 | end 205 | 206 | def is_distinct_from(o, collector) 207 | collector << "DECODE(" 208 | collector = visit [o.left, o.right, 0, 1], collector 209 | collector << ")" 210 | end 211 | 212 | # Oracle will occur an error `ORA-00907: missing right parenthesis` 213 | # when using `ORDER BY` in `UPDATE` or `DELETE`'s subquery. 214 | # 215 | # This method has been overridden based on the following code. 216 | # https://github.com/rails/rails/blob/v6.1.0.rc1/activerecord/lib/arel/visitors/to_sql.rb#L815-L825 217 | def build_subselect(key, o) 218 | stmt = super 219 | stmt.orders = [] # `orders` will never be set to prevent `ORA-00907`. 220 | stmt 221 | end 222 | end 223 | end 224 | end 225 | -------------------------------------------------------------------------------- /lib/arel/visitors/oracle12.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "oracle_common" 4 | 5 | module Arel # :nodoc: all 6 | module Visitors 7 | class Oracle12 < Arel::Visitors::ToSql 8 | include OracleCommon 9 | 10 | private 11 | def visit_Arel_Nodes_SelectStatement(o, collector) 12 | # Oracle does not allow LIMIT clause with select for update 13 | if o.limit && o.lock 14 | raise ArgumentError, <<~MSG 15 | Combination of limit and lock is not supported. Because generated SQL statements 16 | `SELECT FOR UPDATE and FETCH FIRST n ROWS` generates ORA-02014. 17 | MSG 18 | end 19 | super 20 | end 21 | 22 | def visit_Arel_Nodes_SelectOptions(o, collector) 23 | collector = maybe_visit o.offset, collector 24 | collector = maybe_visit o.limit, collector 25 | maybe_visit o.lock, collector 26 | end 27 | 28 | def visit_Arel_Nodes_Limit(o, collector) 29 | collector << "FETCH FIRST " 30 | collector = visit o.expr, collector 31 | collector << " ROWS ONLY" 32 | end 33 | 34 | def visit_Arel_Nodes_Offset(o, collector) 35 | collector << "OFFSET " 36 | visit o.expr, collector 37 | collector << " ROWS" 38 | end 39 | 40 | def visit_Arel_Nodes_Except(o, collector) 41 | collector << "( " 42 | collector = infix_value o, collector, " MINUS " 43 | collector << " )" 44 | end 45 | 46 | ## 47 | # To avoid ORA-01795: maximum number of expressions in a list is 1000 48 | # tell ActiveRecord to limit us to 1000 ids at a time 49 | def visit_Arel_Nodes_HomogeneousIn(o, collector) 50 | in_clause_length = @connection.in_clause_length 51 | values = o.casted_values.map { |v| @connection.quote(v) } 52 | operator = 53 | if o.type == :in 54 | " IN (" 55 | else 56 | " NOT IN (" 57 | end 58 | 59 | if !Array === values || values.length <= in_clause_length 60 | visit o.left, collector 61 | collector << operator 62 | 63 | expr = 64 | if values.empty? 65 | @connection.quote(nil) 66 | else 67 | values.join(",") 68 | end 69 | 70 | collector << expr 71 | collector << ")" 72 | else 73 | separator = 74 | if o.type == :in 75 | " OR " 76 | else 77 | " AND " 78 | end 79 | collector << "(" 80 | values.each_slice(in_clause_length).each_with_index do |valuez, i| 81 | collector << separator unless i == 0 82 | visit o.left, collector 83 | collector << operator 84 | collector << valuez.join(",") 85 | collector << ")" 86 | end 87 | collector << ")" 88 | end 89 | end 90 | 91 | def visit_Arel_Nodes_UpdateStatement(o, collector) 92 | # Oracle does not allow ORDER BY/LIMIT in UPDATEs. 93 | if o.orders.any? && o.limit.nil? 94 | # However, there is no harm in silently eating the ORDER BY clause if no LIMIT has been provided, 95 | # otherwise let the user deal with the error 96 | o = o.dup 97 | o.orders = [] 98 | end 99 | 100 | super 101 | end 102 | 103 | def visit_ActiveModel_Attribute(o, collector) 104 | collector.add_bind(o) { |i| ":a#{i}" } 105 | end 106 | 107 | def visit_Arel_Nodes_BindParam(o, collector) 108 | collector.add_bind(o.value) { |i| ":a#{i}" } 109 | end 110 | 111 | def is_distinct_from(o, collector) 112 | collector << "DECODE(" 113 | collector = visit [o.left, o.right, 0, 1], collector 114 | collector << ")" 115 | end 116 | 117 | # Oracle will occur an error `ORA-00907: missing right parenthesis` 118 | # when using `ORDER BY` in `UPDATE` or `DELETE`'s subquery. 119 | # 120 | # This method has been overridden based on the following code. 121 | # https://github.com/rails/rails/blob/v6.1.0.rc1/activerecord/lib/arel/visitors/to_sql.rb#L815-L825 122 | def build_subselect(key, o) 123 | stmt = super 124 | stmt.orders = [] # `orders` will never be set to prevent `ORA-00907`. 125 | stmt 126 | end 127 | end 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /lib/arel/visitors/oracle_common.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Arel # :nodoc: all 4 | module Visitors 5 | module OracleCommon 6 | private 7 | # Oracle can't compare CLOB columns with standard SQL operators for comparison. 8 | # We need to replace standard equality for text/binary columns to use DBMS_LOB.COMPARE function. 9 | # Fixes ORA-00932: inconsistent datatypes: expected - got CLOB 10 | def visit_Arel_Nodes_Equality(o, collector) 11 | left = o.left 12 | return super unless %i(text binary).include?(cached_column_for(left)&.type) 13 | 14 | # https://docs.oracle.com/cd/B19306_01/appdev.102/b14258/d_lob.htm#i1016668 15 | # returns 0 when the comparison succeeds 16 | comparator = Arel::Nodes::NamedFunction.new("DBMS_LOB.COMPARE", [left, o.right]) 17 | collector = visit comparator, collector 18 | collector << " = 0" 19 | collector 20 | end 21 | 22 | def visit_Arel_Nodes_Matches(o, collector) 23 | if !o.case_sensitive && o.left && o.right 24 | o.left = Arel::Nodes::NamedFunction.new("UPPER", [o.left]) 25 | o.right = Arel::Nodes::NamedFunction.new("UPPER", [o.right]) 26 | end 27 | 28 | super o, collector 29 | end 30 | 31 | def cached_column_for(attr) 32 | return unless Arel::Attributes::Attribute === attr 33 | 34 | table = attr.relation.name 35 | return unless schema_cache.columns_hash?(table) 36 | 37 | column = attr.name.to_s 38 | schema_cache.columns_hash(table)[column] 39 | end 40 | 41 | def schema_cache 42 | @connection.schema_cache 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/active_record/connection_adapters/emulation/oracle_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe "OracleEnhancedAdapter emulate OracleAdapter" do 4 | before(:all) do 5 | @old_oracle_adapter = nil 6 | if defined?(ActiveRecord::ConnectionAdapters::OracleAdapter) 7 | @old_oracle_adapter = ActiveRecord::ConnectionAdapters::OracleAdapter 8 | ActiveRecord::ConnectionAdapters.send(:remove_const, :OracleAdapter) 9 | end 10 | end 11 | 12 | it "should be an OracleAdapter" do 13 | @conn = ActiveRecord::Base.establish_connection(CONNECTION_PARAMS.merge(emulate_oracle_adapter: true)) 14 | expect(ActiveRecord::Base.connection).not_to be_nil 15 | expect(ActiveRecord::Base.connection.is_a?(ActiveRecord::ConnectionAdapters::OracleAdapter)).to be_truthy 16 | end 17 | 18 | after(:all) do 19 | if @old_oracle_adapter 20 | ActiveRecord::ConnectionAdapters.send(:remove_const, :OracleAdapter) 21 | ActiveRecord::ConnectionAdapters::OracleAdapter = @old_oracle_adapter 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/active_record/connection_adapters/oracle_enhanced/database_tasks_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "active_record/connection_adapters/oracle_enhanced/database_tasks" 4 | require "stringio" 5 | require "tempfile" 6 | 7 | describe "Oracle Enhanced adapter database tasks" do 8 | include SchemaSpecHelper 9 | 10 | let(:config) { CONNECTION_PARAMS.with_indifferent_access } 11 | 12 | describe "create" do 13 | let(:new_user_config) { config.merge(username: "oracle_enhanced_test_user") } 14 | before do 15 | fake_terminal(SYSTEM_CONNECTION_PARAMS[:password]) do 16 | ActiveRecord::Tasks::DatabaseTasks.create(new_user_config) 17 | end 18 | end 19 | 20 | it "creates user" do 21 | query = "SELECT COUNT(*) FROM dba_users WHERE UPPER(username) = '#{new_user_config[:username].upcase}'" 22 | expect(ActiveRecord::Base.connection.select_value(query)).to eq(1) 23 | end 24 | it "grants permissions defined by OracleEnhancedAdapter.persmissions" do 25 | query = "SELECT COUNT(*) FROM DBA_SYS_PRIVS WHERE GRANTEE = '#{new_user_config[:username].upcase}'" 26 | permissions_count = ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.permissions.size 27 | expect(ActiveRecord::Base.connection.select_value(query)).to eq(permissions_count) 28 | end 29 | after do 30 | ActiveRecord::Base.connection.execute("DROP USER #{new_user_config[:username]}") 31 | end 32 | 33 | def fake_terminal(input) 34 | $stdin = StringIO.new 35 | $stdout = StringIO.new 36 | $stdin.puts(input) 37 | $stdin.rewind 38 | yield 39 | ensure 40 | $stdin = STDIN 41 | $stdout = STDOUT 42 | end 43 | end 44 | 45 | context "with test table" do 46 | before(:all) do 47 | $stdout, @original_stdout = StringIO.new, $stdout 48 | $stderr, @original_stderr = StringIO.new, $stderr 49 | end 50 | 51 | after(:all) do 52 | $stdout, $stderr = @original_stdout, @original_stderr 53 | end 54 | 55 | before do 56 | ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) 57 | schema_define do 58 | create_table :test_posts, force: true do |t| 59 | t.string :name, limit: 20 60 | end 61 | end 62 | end 63 | 64 | describe "drop" do 65 | before { ActiveRecord::Tasks::DatabaseTasks.drop(config) } 66 | 67 | it "drops all tables" do 68 | expect(ActiveRecord::Base.connection.table_exists?(:test_posts)).to be_falsey 69 | end 70 | end 71 | 72 | describe "purge" do 73 | before { ActiveRecord::Tasks::DatabaseTasks.purge(config) } 74 | 75 | it "drops all tables" do 76 | expect(ActiveRecord::Base.connection.table_exists?(:test_posts)).to be_falsey 77 | expect(ActiveRecord::Base.connection.select_value("SELECT COUNT(*) FROM RECYCLEBIN")).to eq(0) 78 | end 79 | end 80 | 81 | describe "structure" do 82 | let(:temp_file) { Tempfile.create(["oracle_enhanced", ".sql"]).path } 83 | before do 84 | ActiveRecord::Base.connection.schema_migration.create_table 85 | ActiveRecord::Base.connection.execute "INSERT INTO schema_migrations (version) VALUES ('20150101010000')" 86 | end 87 | 88 | describe "structure_dump" do 89 | before { ActiveRecord::Tasks::DatabaseTasks.structure_dump(config, temp_file) } 90 | 91 | it "dumps the database structure to a file without the schema information" do 92 | contents = File.read(temp_file) 93 | expect(contents).to include('CREATE TABLE "TEST_POSTS"') 94 | expect(contents).not_to include("INSERT INTO schema_migrations") 95 | end 96 | end 97 | 98 | describe "structure_load" do 99 | before do 100 | ActiveRecord::Tasks::DatabaseTasks.structure_dump(config, temp_file) 101 | ActiveRecord::Tasks::DatabaseTasks.drop(config) 102 | ActiveRecord::Tasks::DatabaseTasks.structure_load(config, temp_file) 103 | end 104 | 105 | it "loads the database structure from a file" do 106 | expect(ActiveRecord::Base.connection.table_exists?(:test_posts)).to be_truthy 107 | end 108 | end 109 | 110 | after do 111 | File.unlink(temp_file) 112 | ActiveRecord::Base.connection.schema_migration.drop_table 113 | end 114 | end 115 | 116 | after do 117 | schema_define do 118 | drop_table :test_posts, if_exists: true 119 | end 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /spec/active_record/connection_adapters/oracle_enhanced/dbms_output_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe "OracleEnhancedAdapter logging dbms_output from plsql" do 4 | include LoggerSpecHelper 5 | 6 | before(:all) do 7 | ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) 8 | ActiveRecord::Base.connection.execute <<~SQL 9 | CREATE or REPLACE 10 | FUNCTION MORE_THAN_FIVE_CHARACTERS_LONG (some_text VARCHAR2) RETURN INTEGER 11 | AS 12 | longer_than_five INTEGER; 13 | BEGIN 14 | dbms_output.put_line('before the if -' || some_text || '-'); 15 | IF length(some_text) > 5 THEN 16 | dbms_output.put_line('it is longer than 5'); 17 | longer_than_five := 1; 18 | ELSE 19 | dbms_output.put_line('it is 5 or shorter'); 20 | longer_than_five := 0; 21 | END IF; 22 | dbms_output.put_line('about to return: ' || longer_than_five); 23 | RETURN longer_than_five; 24 | END; 25 | SQL 26 | end 27 | 28 | after(:all) do 29 | ActiveRecord::Base.connection.execute "DROP FUNCTION MORE_THAN_FIVE_CHARACTERS_LONG" 30 | end 31 | 32 | before(:each) do 33 | set_logger 34 | ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) 35 | @conn = ActiveRecord::Base.connection 36 | end 37 | 38 | after(:each) do 39 | clear_logger 40 | end 41 | 42 | it "should NOT log dbms output when dbms output is disabled" do 43 | @conn.disable_dbms_output 44 | 45 | expect(@conn.select_all("select more_than_five_characters_long('hi there') is_it_long from dual").to_a).to eq([{ "is_it_long" => 1 }]) 46 | 47 | expect(@logger.output(:debug)).not_to match(/^DBMS_OUTPUT/) 48 | end 49 | 50 | it "should log dbms output longer lines to the rails log" do 51 | @conn.enable_dbms_output 52 | 53 | expect(@conn.select_all("select more_than_five_characters_long('hi there') is_it_long from dual").to_a).to eq([{ "is_it_long" => 1 }]) 54 | 55 | expect(@logger.output(:debug)).to match(/^DBMS_OUTPUT: before the if -hi there-$/) 56 | expect(@logger.output(:debug)).to match(/^DBMS_OUTPUT: it is longer than 5$/) 57 | expect(@logger.output(:debug)).to match(/^DBMS_OUTPUT: about to return: 1$/) 58 | end 59 | 60 | it "should log dbms output shorter lines to the rails log" do 61 | @conn.enable_dbms_output 62 | 63 | expect(@conn.select_all("select more_than_five_characters_long('short') is_it_long from dual").to_a).to eq([{ "is_it_long" => 0 }]) 64 | 65 | expect(@logger.output(:debug)).to match(/^DBMS_OUTPUT: before the if -short-$/) 66 | expect(@logger.output(:debug)).to match(/^DBMS_OUTPUT: it is 5 or shorter$/) 67 | expect(@logger.output(:debug)).to match(/^DBMS_OUTPUT: about to return: 0$/) 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/active_record/connection_adapters/oracle_enhanced/quoting_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe "OracleEnhancedAdapter quoting" do 4 | include LoggerSpecHelper 5 | include SchemaSpecHelper 6 | 7 | before(:all) do 8 | ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) 9 | end 10 | 11 | describe "reserved words column quoting" do 12 | before(:all) do 13 | schema_define do 14 | create_table :test_reserved_words do |t| 15 | t.string :varchar2 16 | t.integer :integer 17 | t.text :comment 18 | end 19 | end 20 | class ::TestReservedWord < ActiveRecord::Base; end 21 | end 22 | 23 | after(:all) do 24 | schema_define do 25 | drop_table :test_reserved_words 26 | end 27 | Object.send(:remove_const, "TestReservedWord") 28 | ActiveRecord::Base.table_name_prefix = nil 29 | ActiveRecord::Base.clear_cache! 30 | end 31 | 32 | before(:each) do 33 | set_logger 34 | end 35 | 36 | after(:each) do 37 | clear_logger 38 | end 39 | 40 | it "should create table" do 41 | [:varchar2, :integer, :comment].each do |attr| 42 | expect(TestReservedWord.columns_hash[attr.to_s].name).to eq(attr.to_s) 43 | end 44 | end 45 | 46 | it "should create record" do 47 | attrs = { 48 | varchar2: "dummy", 49 | integer: 1, 50 | comment: "dummy" 51 | } 52 | record = TestReservedWord.create!(attrs) 53 | record.reload 54 | attrs.each do |k, v| 55 | expect(record.send(k)).to eq(v) 56 | end 57 | end 58 | 59 | it "should remove double quotes in column quoting" do 60 | expect(ActiveRecord::Base.connection.quote_column_name('aaa "bbb" ccc')).to eq('"aaa bbb ccc"') 61 | end 62 | end 63 | 64 | describe "valid table names" do 65 | before(:all) do 66 | @adapter = ActiveRecord::ConnectionAdapters::OracleEnhanced::Quoting 67 | end 68 | 69 | it "should be valid with letters and digits" do 70 | expect(@adapter.valid_table_name?("abc_123")).to be_truthy 71 | end 72 | 73 | it "should be valid with schema name" do 74 | expect(@adapter.valid_table_name?("abc_123.def_456")).to be_truthy 75 | end 76 | 77 | it "should be valid with schema name and object name in different case" do 78 | expect(@adapter.valid_table_name?("TEST_DBA.def_456")).to be_truthy 79 | end 80 | 81 | it "should be valid with $ in name" do 82 | expect(@adapter.valid_table_name?("sys.v$session")).to be_truthy 83 | end 84 | 85 | it "should be valid with upcase schema name" do 86 | expect(@adapter.valid_table_name?("ABC_123.DEF_456")).to be_truthy 87 | end 88 | 89 | it "should not be valid with two dots in name" do 90 | expect(@adapter.valid_table_name?("abc_123.def_456.ghi_789")).to be_falsey 91 | end 92 | 93 | it "should not be valid with invalid characters" do 94 | expect(@adapter.valid_table_name?("warehouse-things")).to be_falsey 95 | end 96 | 97 | it "should not be valid with for camel-case" do 98 | expect(@adapter.valid_table_name?("Abc")).to be_falsey 99 | expect(@adapter.valid_table_name?("aBc")).to be_falsey 100 | expect(@adapter.valid_table_name?("abC")).to be_falsey 101 | end 102 | 103 | it "should not be valid for names > 30 characters" do 104 | expect(@adapter.valid_table_name?("a" * 31)).to be_falsey 105 | end 106 | 107 | it "should not be valid for schema names > 30 characters" do 108 | expect(@adapter.valid_table_name?(("a" * 31) + ".validname")).to be_falsey 109 | end 110 | 111 | it "should not be valid for names that do not begin with alphabetic characters" do 112 | expect(@adapter.valid_table_name?("1abc")).to be_falsey 113 | expect(@adapter.valid_table_name?("_abc")).to be_falsey 114 | expect(@adapter.valid_table_name?("abc.1xyz")).to be_falsey 115 | expect(@adapter.valid_table_name?("abc._xyz")).to be_falsey 116 | end 117 | end 118 | 119 | describe "table quoting" do 120 | def create_warehouse_things_table 121 | ActiveRecord::Schema.define do 122 | suppress_messages do 123 | create_table "warehouse-things" do |t| 124 | t.string :name 125 | t.integer :foo 126 | end 127 | end 128 | end 129 | end 130 | 131 | def create_camel_case_table 132 | ActiveRecord::Schema.define do 133 | suppress_messages do 134 | create_table "CamelCase" do |t| 135 | t.string :name 136 | t.integer :foo 137 | end 138 | end 139 | end 140 | end 141 | 142 | before(:all) do 143 | @conn = ActiveRecord::Base.connection 144 | end 145 | 146 | after(:each) do 147 | ActiveRecord::Schema.define do 148 | suppress_messages do 149 | drop_table "warehouse-things", if_exists: true 150 | drop_table "CamelCase", if_exists: true 151 | end 152 | end 153 | Object.send(:remove_const, "WarehouseThing") rescue nil 154 | Object.send(:remove_const, "CamelCase") rescue nil 155 | end 156 | 157 | it "should allow creation of a table with non alphanumeric characters" do 158 | create_warehouse_things_table 159 | class ::WarehouseThing < ActiveRecord::Base 160 | self.table_name = "warehouse-things" 161 | end 162 | 163 | wh = WarehouseThing.create!(name: "Foo", foo: 2) 164 | expect(wh.id).not_to be_nil 165 | 166 | expect(@conn.tables).to include("warehouse-things") 167 | end 168 | 169 | it "should allow creation of a table with CamelCase name" do 170 | create_camel_case_table 171 | class ::CamelCase < ActiveRecord::Base 172 | self.table_name = "CamelCase" 173 | end 174 | 175 | cc = CamelCase.create!(name: "Foo", foo: 2) 176 | expect(cc.id).not_to be_nil 177 | 178 | expect(@conn.tables).to include("CamelCase") 179 | end 180 | end 181 | end 182 | -------------------------------------------------------------------------------- /spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe "OracleEnhancedAdapter date and datetime type detection based on attribute settings" do 4 | before(:all) do 5 | ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) 6 | @conn = ActiveRecord::Base.connection 7 | @conn.execute "DROP TABLE test_employees" rescue nil 8 | @conn.execute "DROP SEQUENCE test_employees_seq" rescue nil 9 | @conn.execute <<~SQL 10 | CREATE TABLE test_employees ( 11 | employee_id NUMBER(6,0) PRIMARY KEY, 12 | first_name VARCHAR2(20), 13 | last_name VARCHAR2(25), 14 | email VARCHAR2(25), 15 | phone_number VARCHAR2(20), 16 | hire_date DATE, 17 | job_id NUMBER(6,0), 18 | salary NUMBER(8,2), 19 | commission_pct NUMBER(2,2), 20 | manager_id NUMBER(6,0), 21 | department_id NUMBER(4,0), 22 | created_at DATE, 23 | updated_at DATE 24 | ) 25 | SQL 26 | @conn.execute <<~SQL 27 | CREATE SEQUENCE test_employees_seq MINVALUE 1 28 | INCREMENT BY 1 START WITH 10040 CACHE 20 NOORDER NOCYCLE 29 | SQL 30 | end 31 | 32 | after(:all) do 33 | @conn.execute "DROP TABLE test_employees" 34 | @conn.execute "DROP SEQUENCE test_employees_seq" 35 | end 36 | 37 | describe "/ DATE values from ActiveRecord model" do 38 | before(:each) do 39 | class ::TestEmployee < ActiveRecord::Base 40 | self.primary_key = "employee_id" 41 | end 42 | end 43 | 44 | def create_test_employee(params = {}) 45 | @today = params[:today] || Date.new(2008, 8, 19) 46 | @now = params[:now] || Time.local(2008, 8, 19, 17, 03, 59) 47 | @employee = TestEmployee.create( 48 | first_name: "First", 49 | last_name: "Last", 50 | hire_date: @today, 51 | created_at: @now 52 | ) 53 | @employee.reload 54 | end 55 | 56 | after(:each) do 57 | Object.send(:remove_const, "TestEmployee") 58 | ActiveRecord::Base.clear_cache! 59 | end 60 | 61 | it "should return Date value from DATE column by default" do 62 | create_test_employee 63 | expect(@employee.hire_date.class).to eq(Date) 64 | end 65 | 66 | it "should return Date value from DATE column with old date value by default" do 67 | create_test_employee(today: Date.new(1900, 1, 1)) 68 | expect(@employee.hire_date.class).to eq(Date) 69 | end 70 | 71 | it "should return Time value from DATE column if attribute is set to :datetime" do 72 | class ::TestEmployee < ActiveRecord::Base 73 | attribute :hire_date, :datetime 74 | end 75 | create_test_employee 76 | expect(@employee.hire_date.class).to eq(Time) 77 | # change to current time with hours, minutes and seconds 78 | @employee.hire_date = @now 79 | @employee.save! 80 | @employee.reload 81 | expect(@employee.hire_date.class).to eq(Time) 82 | expect(@employee.hire_date).to eq(@now) 83 | end 84 | end 85 | end 86 | 87 | describe "OracleEnhancedAdapter assign string to :date and :datetime columns" do 88 | include SchemaSpecHelper 89 | 90 | before(:all) do 91 | ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) 92 | @conn = ActiveRecord::Base.connection 93 | schema_define do 94 | create_table :test_employees, force: true do |t| 95 | t.string :first_name, limit: 20 96 | t.string :last_name, limit: 25 97 | t.date :hire_date 98 | t.date :last_login_at 99 | t.datetime :last_login_at_ts 100 | end 101 | end 102 | class ::TestEmployee < ActiveRecord::Base 103 | attribute :last_login_at, :datetime 104 | end 105 | @today = Date.new(2008, 6, 28) 106 | @today_iso = "2008-06-28" 107 | @today_nls = "28.06.2008" 108 | @nls_date_format = "%d.%m.%Y" 109 | @now = Time.local(2008, 6, 28, 13, 34, 33) 110 | @now_iso = "2008-06-28 13:34:33" 111 | @now_nls = "28.06.2008 13:34:33" 112 | @nls_time_format = "%d.%m.%Y %H:%M:%S" 113 | @now_nls_with_tz = "28.06.2008 13:34:33+05:00" 114 | @nls_with_tz_time_format = "%d.%m.%Y %H:%M:%S%Z" 115 | @now_with_tz = Time.parse @now_nls_with_tz 116 | end 117 | 118 | after(:all) do 119 | Object.send(:remove_const, "TestEmployee") 120 | @conn.drop_table :test_employees, if_exists: true 121 | ActiveRecord::Base.clear_cache! 122 | end 123 | 124 | after(:each) do 125 | ActiveRecord.default_timezone = :utc 126 | end 127 | 128 | it "should assign ISO string to date column" do 129 | @employee = TestEmployee.create( 130 | first_name: "First", 131 | last_name: "Last", 132 | hire_date: @today_iso 133 | ) 134 | expect(@employee.hire_date).to eq(@today) 135 | @employee.reload 136 | expect(@employee.hire_date).to eq(@today) 137 | end 138 | 139 | it "should assign NLS string to date column" do 140 | @employee = TestEmployee.create( 141 | first_name: "First", 142 | last_name: "Last", 143 | hire_date: @today_nls 144 | ) 145 | expect(@employee.hire_date).to eq(@today) 146 | @employee.reload 147 | expect(@employee.hire_date).to eq(@today) 148 | end 149 | 150 | it "should assign ISO time string to date column" do 151 | @employee = TestEmployee.create( 152 | first_name: "First", 153 | last_name: "Last", 154 | hire_date: @now_iso 155 | ) 156 | expect(@employee.hire_date).to eq(@today) 157 | @employee.reload 158 | expect(@employee.hire_date).to eq(@today) 159 | end 160 | 161 | it "should assign NLS time string to date column" do 162 | @employee = TestEmployee.create( 163 | first_name: "First", 164 | last_name: "Last", 165 | hire_date: @now_nls 166 | ) 167 | expect(@employee.hire_date).to eq(@today) 168 | @employee.reload 169 | expect(@employee.hire_date).to eq(@today) 170 | end 171 | 172 | it "should assign ISO time string to datetime column" do 173 | ActiveRecord.default_timezone = :local 174 | @employee = TestEmployee.create( 175 | first_name: "First", 176 | last_name: "Last", 177 | last_login_at: @now_iso 178 | ) 179 | expect(@employee.last_login_at).to eq(@now) 180 | @employee.reload 181 | expect(@employee.last_login_at).to eq(@now) 182 | end 183 | 184 | it "should assign NLS time string to datetime column" do 185 | ActiveRecord.default_timezone = :local 186 | @employee = TestEmployee.create( 187 | first_name: "First", 188 | last_name: "Last", 189 | last_login_at: @now_nls 190 | ) 191 | expect(@employee.last_login_at).to eq(@now) 192 | @employee.reload 193 | expect(@employee.last_login_at).to eq(@now) 194 | end 195 | 196 | it "should assign NLS time string with time zone to datetime column" do 197 | @employee = TestEmployee.create( 198 | first_name: "First", 199 | last_name: "Last", 200 | last_login_at: @now_nls_with_tz 201 | ) 202 | expect(@employee.last_login_at).to eq(@now_with_tz) 203 | @employee.reload 204 | expect(@employee.last_login_at).to eq(@now_with_tz) 205 | end 206 | 207 | it "should assign ISO date string to datetime column" do 208 | ActiveRecord.default_timezone = :local 209 | @employee = TestEmployee.create( 210 | first_name: "First", 211 | last_name: "Last", 212 | last_login_at: @today_iso 213 | ) 214 | expect(@employee.last_login_at).to eq(@today.to_time) 215 | @employee.reload 216 | expect(@employee.last_login_at).to eq(@today.to_time) 217 | end 218 | 219 | it "should assign NLS date string to datetime column" do 220 | ActiveRecord.default_timezone = :local 221 | @employee = TestEmployee.create( 222 | first_name: "First", 223 | last_name: "Last", 224 | last_login_at: @today_nls 225 | ) 226 | expect(@employee.last_login_at).to eq(@today.to_time) 227 | @employee.reload 228 | expect(@employee.last_login_at).to eq(@today.to_time) 229 | end 230 | end 231 | -------------------------------------------------------------------------------- /spec/active_record/oracle_enhanced/type/binary_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe "OracleEnhancedAdapter handling of BLOB columns" do 4 | include SchemaSpecHelper 5 | 6 | before(:all) do 7 | ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) 8 | @conn = ActiveRecord::Base.connection 9 | schema_define do 10 | create_table :test_employees, force: true do |t| 11 | t.string :first_name, limit: 20 12 | t.string :last_name, limit: 25 13 | t.binary :binary_data 14 | end 15 | end 16 | class ::TestEmployee < ActiveRecord::Base 17 | end 18 | @binary_data = "\0\1\2\3\4\5\6\7\8\9" * 10000 19 | @binary_data2 = "\1\2\3\4\5\6\7\8\9\0" * 10000 20 | end 21 | 22 | after(:all) do 23 | @conn.drop_table :test_employees, if_exists: true 24 | Object.send(:remove_const, "TestEmployee") 25 | end 26 | 27 | after(:each) do 28 | ActiveRecord::Base.clear_cache! 29 | end 30 | 31 | it "should create record with BLOB data" do 32 | @employee = TestEmployee.create!( 33 | first_name: "First", 34 | last_name: "Last", 35 | binary_data: @binary_data 36 | ) 37 | @employee.reload 38 | expect(@employee.binary_data).to eq(@binary_data) 39 | end 40 | 41 | it "should update record with BLOB data" do 42 | @employee = TestEmployee.create!( 43 | first_name: "First", 44 | last_name: "Last" 45 | ) 46 | @employee.reload 47 | expect(@employee.binary_data).to be_nil 48 | @employee.binary_data = @binary_data 49 | @employee.save! 50 | @employee.reload 51 | expect(@employee.binary_data).to eq(@binary_data) 52 | end 53 | 54 | it "should update record with zero-length BLOB data" do 55 | @employee = TestEmployee.create!( 56 | first_name: "First", 57 | last_name: "Last" 58 | ) 59 | @employee.reload 60 | expect(@employee.binary_data).to be_nil 61 | @employee.binary_data = "" 62 | @employee.save! 63 | @employee.reload 64 | expect(@employee.binary_data).to eq("") 65 | end 66 | 67 | it "should update record that has existing BLOB data with different BLOB data" do 68 | @employee = TestEmployee.create!( 69 | first_name: "First", 70 | last_name: "Last", 71 | binary_data: @binary_data 72 | ) 73 | @employee.reload 74 | @employee.binary_data = @binary_data2 75 | @employee.save! 76 | @employee.reload 77 | expect(@employee.binary_data).to eq(@binary_data2) 78 | end 79 | 80 | it "should update record that has existing BLOB data with nil" do 81 | @employee = TestEmployee.create!( 82 | first_name: "First", 83 | last_name: "Last", 84 | binary_data: @binary_data 85 | ) 86 | @employee.reload 87 | @employee.binary_data = nil 88 | @employee.save! 89 | @employee.reload 90 | expect(@employee.binary_data).to be_nil 91 | end 92 | 93 | it "should update record that has existing BLOB data with zero-length BLOB data" do 94 | @employee = TestEmployee.create!( 95 | first_name: "First", 96 | last_name: "Last", 97 | binary_data: @binary_data 98 | ) 99 | @employee.reload 100 | @employee.binary_data = "" 101 | @employee.save! 102 | @employee.reload 103 | expect(@employee.binary_data).to eq("") 104 | end 105 | 106 | it "should update record that has zero-length BLOB data with non-empty BLOB data" do 107 | @employee = TestEmployee.create!( 108 | first_name: "First", 109 | last_name: "Last", 110 | binary_data: "" 111 | ) 112 | @employee.reload 113 | expect(@employee.binary_data).to eq("") 114 | @employee.binary_data = @binary_data 115 | @employee.save! 116 | @employee.reload 117 | expect(@employee.binary_data).to eq(@binary_data) 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /spec/active_record/oracle_enhanced/type/boolean_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe "OracleEnhancedAdapter boolean type detection based on string column types and names" do 4 | before(:all) do 5 | ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) 6 | @conn = ActiveRecord::Base.connection 7 | @conn.execute <<~SQL 8 | CREATE TABLE test3_employees ( 9 | id NUMBER PRIMARY KEY, 10 | first_name VARCHAR2(20), 11 | last_name VARCHAR2(25), 12 | email VARCHAR2(25), 13 | phone_number VARCHAR2(20), 14 | hire_date DATE, 15 | job_id NUMBER, 16 | salary NUMBER, 17 | commission_pct NUMBER(2,2), 18 | manager_id NUMBER(6), 19 | department_id NUMBER(4,0), 20 | created_at DATE, 21 | has_email CHAR(1), 22 | has_phone VARCHAR2(1) DEFAULT 'Y', 23 | active_flag VARCHAR2(2), 24 | manager_yn VARCHAR2(3) DEFAULT 'N', 25 | test_boolean VARCHAR2(3) 26 | ) 27 | SQL 28 | @conn.execute <<~SQL 29 | CREATE SEQUENCE test3_employees_seq MINVALUE 1 30 | INCREMENT BY 1 START WITH 10040 CACHE 20 NOORDER NOCYCLE 31 | SQL 32 | end 33 | 34 | after(:all) do 35 | @conn.execute "DROP TABLE test3_employees" 36 | @conn.execute "DROP SEQUENCE test3_employees_seq" 37 | end 38 | 39 | before(:each) do 40 | class ::Test3Employee < ActiveRecord::Base 41 | end 42 | end 43 | 44 | after(:each) do 45 | Object.send(:remove_const, "Test3Employee") 46 | ActiveRecord::Base.clear_cache! 47 | end 48 | 49 | describe "default values in new records" do 50 | context "when emulate_booleans_from_strings is false" do 51 | before do 52 | ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_booleans_from_strings = false 53 | end 54 | 55 | it "are Y or N" do 56 | subject = Test3Employee.new 57 | expect(subject.has_phone).to eq("Y") 58 | expect(subject.manager_yn).to eq("N") 59 | end 60 | end 61 | 62 | context "when emulate_booleans_from_strings is true" do 63 | before do 64 | ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_booleans_from_strings = true 65 | end 66 | 67 | it "are True or False" do 68 | class ::Test3Employee < ActiveRecord::Base 69 | attribute :has_phone, :boolean 70 | attribute :manager_yn, :boolean, default: false 71 | end 72 | subject = Test3Employee.new 73 | expect(subject.has_phone).to be_a(TrueClass) 74 | expect(subject.manager_yn).to be_a(FalseClass) 75 | end 76 | end 77 | end 78 | 79 | it "should translate boolean type to NUMBER(1) if emulate_booleans_from_strings is false" do 80 | ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_booleans_from_strings = false 81 | sql_type = ActiveRecord::Base.connection.type_to_sql(:boolean) 82 | expect(sql_type).to eq("NUMBER(1)") 83 | end 84 | 85 | describe "/ VARCHAR2 boolean values from ActiveRecord model" do 86 | before(:each) do 87 | ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_booleans_from_strings = false 88 | end 89 | 90 | after(:each) do 91 | ActiveRecord::Base.clear_cache! 92 | end 93 | 94 | def create_employee3(params = {}) 95 | @employee3 = Test3Employee.create( 96 | { 97 | first_name: "First", 98 | last_name: "Last", 99 | has_email: true, 100 | has_phone: false, 101 | active_flag: true, 102 | manager_yn: false 103 | }.merge(params) 104 | ) 105 | @employee3.reload 106 | end 107 | 108 | it "should return String value from VARCHAR2 boolean column if emulate_booleans_from_strings is false" do 109 | ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_booleans_from_strings = false 110 | create_employee3 111 | %w(has_email has_phone active_flag manager_yn).each do |col| 112 | expect(@employee3.send(col.to_sym).class).to eq(String) 113 | end 114 | end 115 | 116 | it "should return boolean value from VARCHAR2 boolean column if emulate_booleans_from_strings is true" do 117 | ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_booleans_from_strings = true 118 | class ::Test3Employee < ActiveRecord::Base 119 | attribute :has_email, :boolean 120 | attribute :active_flag, :boolean 121 | attribute :has_phone, :boolean, default: false 122 | attribute :manager_yn, :boolean, default: false 123 | end 124 | create_employee3 125 | %w(has_email active_flag).each do |col| 126 | expect(@employee3.send(col.to_sym).class).to eq(TrueClass) 127 | expect(@employee3.send((col + "_before_type_cast").to_sym)).to eq("Y") 128 | end 129 | %w(has_phone manager_yn).each do |col| 130 | expect(@employee3.send(col.to_sym).class).to eq(FalseClass) 131 | expect(@employee3.send((col + "_before_type_cast").to_sym)).to eq("N") 132 | end 133 | end 134 | 135 | it "should return string value from VARCHAR2 column if it is not boolean column and emulate_booleans_from_strings is true" do 136 | ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_booleans_from_strings = true 137 | create_employee3 138 | expect(@employee3.first_name.class).to eq(String) 139 | end 140 | 141 | it "should return boolean value from VARCHAR2 boolean column if attribute is set to :boolean" do 142 | ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_booleans_from_strings = true 143 | class ::Test3Employee < ActiveRecord::Base 144 | attribute :test_boolean, :boolean 145 | end 146 | create_employee3(test_boolean: true) 147 | expect(@employee3.test_boolean.class).to eq(TrueClass) 148 | expect(@employee3.test_boolean_before_type_cast).to eq("Y") 149 | create_employee3(test_boolean: false) 150 | expect(@employee3.test_boolean.class).to eq(FalseClass) 151 | expect(@employee3.test_boolean_before_type_cast).to eq("N") 152 | create_employee3(test_boolean: nil) 153 | expect(@employee3.test_boolean.class).to eq(NilClass) 154 | expect(@employee3.test_boolean_before_type_cast).to be_nil 155 | create_employee3(test_boolean: "") 156 | expect(@employee3.test_boolean.class).to eq(NilClass) 157 | expect(@employee3.test_boolean_before_type_cast).to be_nil 158 | end 159 | 160 | it "should return string value from VARCHAR2 column with boolean column name but attribute is set to :string" do 161 | ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_booleans_from_strings = true 162 | class ::Test3Employee < ActiveRecord::Base 163 | attribute :active_flag, :string 164 | end 165 | create_employee3 166 | expect(@employee3.active_flag.class).to eq(String) 167 | end 168 | end 169 | end 170 | 171 | describe "OracleEnhancedAdapter boolean support when emulate_booleans_from_strings = true" do 172 | include SchemaSpecHelper 173 | 174 | before(:all) do 175 | ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_booleans_from_strings = true 176 | ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) 177 | schema_define do 178 | create_table :posts, force: true do |t| 179 | t.string :name, null: false 180 | t.boolean :is_default, default: false 181 | end 182 | end 183 | end 184 | 185 | after(:all) do 186 | ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_booleans_from_strings = false 187 | end 188 | 189 | before(:each) do 190 | class ::Post < ActiveRecord::Base 191 | attribute :is_default, :boolean 192 | end 193 | end 194 | 195 | after(:each) do 196 | Object.send(:remove_const, "Post") 197 | ActiveRecord::Base.clear_cache! 198 | end 199 | 200 | it "boolean should not change after reload" do 201 | post = Post.create(name: "Test 1", is_default: false) 202 | expect(post.is_default).to be false 203 | post.reload 204 | expect(post.is_default).to be false 205 | end 206 | end 207 | -------------------------------------------------------------------------------- /spec/active_record/oracle_enhanced/type/character_string_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe "OracleEnhancedAdapter processing CHAR column" do 4 | before(:all) do 5 | ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) 6 | @conn = ActiveRecord::Base.connection 7 | @conn.execute <<~SQL 8 | CREATE TABLE test_items ( 9 | id NUMBER(6,0) PRIMARY KEY, 10 | padded CHAR(10) 11 | ) 12 | SQL 13 | @conn.execute "CREATE SEQUENCE test_items_seq" 14 | end 15 | 16 | after(:all) do 17 | @conn.execute "DROP TABLE test_items" 18 | @conn.execute "DROP SEQUENCE test_items_seq" 19 | end 20 | 21 | before(:each) do 22 | class ::TestItem < ActiveRecord::Base 23 | end 24 | end 25 | 26 | after(:each) do 27 | TestItem.delete_all 28 | Object.send(:remove_const, "TestItem") 29 | ActiveRecord::Base.clear_cache! 30 | end 31 | 32 | it "should create and find record" do 33 | str = "ABC" 34 | TestItem.create! 35 | item = TestItem.first 36 | item.padded = str 37 | item.save 38 | 39 | expect(TestItem.where(padded: item.padded).count).to eq(1) 40 | 41 | item_reloaded = TestItem.first 42 | expect(item_reloaded.padded).to eq(str) 43 | end 44 | 45 | it "should support case sensitive matching" do 46 | TestItem.create!( 47 | padded: "First", 48 | ) 49 | TestItem.create!( 50 | padded: "first", 51 | ) 52 | 53 | expect(TestItem.where(TestItem.arel_table[:padded].matches("first%", "\\", true))).to have_attributes(count: 1) 54 | end 55 | 56 | it "should support case insensitive matching" do 57 | TestItem.create!( 58 | padded: "First", 59 | ) 60 | TestItem.create!( 61 | padded: "first", 62 | ) 63 | 64 | expect(TestItem.where(TestItem.arel_table[:padded].matches("first%", "\\", false))).to have_attributes(count: 2) 65 | expect(TestItem.where(TestItem.arel_table[:padded].matches("first%"))).to have_attributes(count: 2) 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/active_record/oracle_enhanced/type/custom_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "base64" 4 | 5 | describe "OracleEnhancedAdapter custom types handling" do 6 | include SchemaSpecHelper 7 | 8 | before(:all) do 9 | ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) 10 | schema_define do 11 | create_table :test_employees, force: true do |t| 12 | t.string :first_name, limit: 20 13 | t.string :last_name, limit: 25 14 | t.text :signature 15 | end 16 | end 17 | 18 | class TestEmployee < ActiveRecord::Base 19 | class AttributeSignature < ActiveRecord::Type::Text 20 | def cast(value) 21 | case value 22 | when Signature 23 | value 24 | when nil 25 | nil 26 | else 27 | Signature.new(Base64.decode64 value) 28 | end 29 | end 30 | 31 | def serialize(value) 32 | Base64.encode64 value.raw 33 | end 34 | 35 | def changed_in_place?(raw_old_value, new_value) 36 | new_value != cast(raw_old_value) 37 | end 38 | end 39 | 40 | class Signature 41 | attr_reader :raw 42 | 43 | def initialize(raw_value) 44 | @raw = raw_value 45 | end 46 | 47 | def to_s 48 | "Signature nice string #{raw[0..5]}" 49 | end 50 | 51 | def ==(object) 52 | raw == object&.raw 53 | end 54 | alias eql? == 55 | end 56 | 57 | attribute :signature, AttributeSignature.new 58 | end 59 | end 60 | 61 | after(:all) do 62 | schema_define do 63 | drop_table :test_employees 64 | end 65 | Object.send(:remove_const, "TestEmployee") 66 | ActiveRecord::Base.clear_cache! 67 | end 68 | 69 | it "should serialize LOBs when creating a record" do 70 | raw_signature = "peter'ssignature" 71 | signature = TestEmployee::Signature.new(raw_signature) 72 | @employee = TestEmployee.create!(first_name: "Peter", last_name: "Doe", signature: signature) 73 | @employee.reload 74 | expect(@employee.signature).to eql(signature) 75 | expect(@employee.signature).to_not be(signature) 76 | expect(TestEmployee.first.read_attribute_before_type_cast(:signature)).to eq(Base64.encode64 raw_signature) 77 | end 78 | 79 | it "should serialize LOBs when updating a record" do 80 | raw_signature = "peter'ssignature" 81 | signature = TestEmployee::Signature.new(raw_signature) 82 | @employee = TestEmployee.create!(first_name: "Peter", last_name: "Doe", signature: TestEmployee::Signature.new("old signature")) 83 | @employee.signature = signature 84 | @employee.save! 85 | @employee.reload 86 | expect(@employee.signature).to eql(signature) 87 | expect(@employee.signature).to_not be(signature) 88 | expect(TestEmployee.first.read_attribute_before_type_cast(:signature)).to eq(Base64.encode64 raw_signature) 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /spec/active_record/oracle_enhanced/type/decimal_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe "OracleEnhancedAdapter handling of DECIMAL columns" do 4 | include SchemaSpecHelper 5 | 6 | before(:all) do 7 | ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) 8 | @conn = ActiveRecord::Base.connection 9 | schema_define do 10 | create_table :test2_employees, force: true do |t| 11 | t.string :first_name, limit: 20 12 | t.string :last_name, limit: 25 13 | t.string :email, limit: 25 14 | t.string :phone_number, limit: 25 15 | t.date :hire_date 16 | t.integer :job_id 17 | t.integer :salary 18 | t.decimal :commission_pct, scale: 2, precision: 2 19 | t.decimal :hourly_rate 20 | t.integer :manager_id, limit: 6 21 | t.integer :is_manager, limit: 1 22 | t.decimal :department_id, scale: 0, precision: 4 23 | t.timestamps 24 | end 25 | end 26 | class ::Test2Employee < ActiveRecord::Base 27 | end 28 | end 29 | 30 | after(:all) do 31 | Object.send(:remove_const, "Test2Employee") 32 | @conn.drop_table :test2_employees, if_exists: true 33 | end 34 | 35 | it "should set DECIMAL column type as decimal" do 36 | columns = @conn.columns("test2_employees") 37 | column = columns.detect { |c| c.name == "hourly_rate" } 38 | expect(column.type).to eq(:decimal) 39 | end 40 | 41 | it "should DECIMAL column type returns an exact value" do 42 | employee = Test2Employee.create(hourly_rate: 4.40125) 43 | 44 | employee.reload 45 | 46 | expect(employee.hourly_rate).to eq(4.40125) 47 | end 48 | 49 | it "should DECIMAL column type rounds if scale is specified and value exceeds scale" do 50 | employee = Test2Employee.create(commission_pct: 0.1575) 51 | 52 | employee.reload 53 | 54 | expect(employee.commission_pct).to eq(0.16) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/active_record/oracle_enhanced/type/dirty_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe "OracleEnhancedAdapter dirty object tracking" do 4 | include SchemaSpecHelper 5 | 6 | before(:all) do 7 | ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) 8 | schema_define do 9 | create_table :test_employees, force: true do |t| 10 | t.string :first_name, limit: 20 11 | t.string :last_name, limit: 25 12 | t.integer :job_id, limit: 6, null: true 13 | t.decimal :salary, precision: 8, scale: 2 14 | t.text :comments 15 | t.date :hire_date 16 | end 17 | end 18 | 19 | class TestEmployee < ActiveRecord::Base 20 | end 21 | end 22 | 23 | after(:all) do 24 | schema_define do 25 | drop_table :test_employees 26 | end 27 | Object.send(:remove_const, "TestEmployee") 28 | ActiveRecord::Base.clear_cache! 29 | end 30 | 31 | it "should not mark empty string (stored as NULL) as changed when reassigning it" do 32 | @employee = TestEmployee.create!(first_name: "") 33 | @employee.first_name = "" 34 | expect(@employee).not_to be_changed 35 | @employee.reload 36 | @employee.first_name = "" 37 | expect(@employee).not_to be_changed 38 | end 39 | 40 | it "should not mark empty integer (stored as NULL) as changed when reassigning it" do 41 | @employee = TestEmployee.create!(job_id: "") 42 | @employee.job_id = "" 43 | expect(@employee).not_to be_changed 44 | @employee.reload 45 | @employee.job_id = "" 46 | expect(@employee).not_to be_changed 47 | end 48 | 49 | it "should not mark empty decimal (stored as NULL) as changed when reassigning it" do 50 | @employee = TestEmployee.create!(salary: "") 51 | @employee.salary = "" 52 | expect(@employee).not_to be_changed 53 | @employee.reload 54 | @employee.salary = "" 55 | expect(@employee).not_to be_changed 56 | end 57 | 58 | it "should not mark empty text (stored as NULL) as changed when reassigning it" do 59 | @employee = TestEmployee.create!(comments: nil) 60 | @employee.comments = nil 61 | expect(@employee).not_to be_changed 62 | @employee.reload 63 | @employee.comments = nil 64 | expect(@employee).not_to be_changed 65 | end 66 | 67 | it "should not mark empty text (stored as empty_clob()) as changed when reassigning it" do 68 | @employee = TestEmployee.create!(comments: "") 69 | @employee.comments = "" 70 | expect(@employee).not_to be_changed 71 | @employee.reload 72 | @employee.comments = "" 73 | expect(@employee).not_to be_changed 74 | end 75 | 76 | it "should mark empty text (stored as empty_clob()) as changed when assigning nil to it" do 77 | @employee = TestEmployee.create!(comments: "") 78 | @employee.comments = nil 79 | expect(@employee).to be_changed 80 | @employee.reload 81 | @employee.comments = nil 82 | expect(@employee).to be_changed 83 | end 84 | 85 | it "should mark empty text (stored as NULL) as changed when assigning '' to it" do 86 | @employee = TestEmployee.create!(comments: nil) 87 | @employee.comments = "" 88 | expect(@employee).to be_changed 89 | @employee.reload 90 | @employee.comments = "" 91 | expect(@employee).to be_changed 92 | end 93 | 94 | it "should not mark empty date (stored as NULL) as changed when reassigning it" do 95 | @employee = TestEmployee.create!(hire_date: "") 96 | @employee.hire_date = "" 97 | expect(@employee).not_to be_changed 98 | @employee.reload 99 | @employee.hire_date = "" 100 | expect(@employee).not_to be_changed 101 | end 102 | 103 | it "should not mark integer as changed when reassigning it" do 104 | @employee = TestEmployee.new 105 | @employee.job_id = 0 106 | expect(@employee.save!).to be_truthy 107 | 108 | expect(@employee).not_to be_changed 109 | 110 | @employee.job_id = "0" 111 | expect(@employee).not_to be_changed 112 | end 113 | 114 | it "should not update unchanged CLOBs" do 115 | @conn = nil 116 | @raw_connection = nil 117 | @employee = TestEmployee.create!( 118 | comments: "initial" 119 | ) 120 | expect(@employee.save!).to be_truthy 121 | @employee.reload 122 | expect(@employee.comments).to eq("initial") 123 | 124 | oci_conn = @conn.instance_variable_get("@raw_connection") 125 | class << oci_conn 126 | def write_lob(lob, value, is_binary = false); raise "don't do this'"; end 127 | end 128 | @employee.comments = +"initial" 129 | expect(@employee.comments_changed?).to be false 130 | expect { @employee.save! }.not_to raise_error 131 | class << oci_conn 132 | remove_method :write_lob 133 | end 134 | end 135 | 136 | it "should be able to handle attributes which are not backed by a column" do 137 | TestEmployee.create!(comments: "initial") 138 | @employee = TestEmployee.select("#{TestEmployee.quoted_table_name}.*, 24 ranking").first 139 | expect { @employee.ranking = 25 }.to_not raise_error 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /spec/active_record/oracle_enhanced/type/float_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe "OracleEnhancedAdapter handling of BINARY_FLOAT columns" do 4 | include SchemaSpecHelper 5 | 6 | before(:all) do 7 | ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) 8 | @conn = ActiveRecord::Base.connection 9 | schema_define do 10 | create_table :test2_employees, force: true do |t| 11 | t.string :first_name, limit: 20 12 | t.string :last_name, limit: 25 13 | t.string :email, limit: 25 14 | t.string :phone_number, limit: 25 15 | t.date :hire_date 16 | t.integer :job_id 17 | t.integer :salary 18 | t.decimal :commission_pct, scale: 2, precision: 2 19 | t.float :hourly_rate 20 | t.integer :manager_id, limit: 6 21 | t.integer :is_manager, limit: 1 22 | t.decimal :department_id, scale: 0, precision: 4 23 | t.timestamps 24 | end 25 | end 26 | class ::Test2Employee < ActiveRecord::Base 27 | end 28 | end 29 | 30 | after(:all) do 31 | Object.send(:remove_const, "Test2Employee") 32 | @conn.drop_table :test2_employees, if_exists: true 33 | end 34 | 35 | it "should set BINARY_FLOAT column type as float" do 36 | columns = @conn.columns("test2_employees") 37 | column = columns.detect { |c| c.name == "hourly_rate" } 38 | expect(column.type).to eq(:float) 39 | end 40 | 41 | it "should BINARY_FLOAT column type returns an approximate value" do 42 | employee = Test2Employee.create(hourly_rate: 4.4) 43 | 44 | employee.reload 45 | 46 | expect(employee.hourly_rate).to eq(4.400000095367432) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/active_record/oracle_enhanced/type/integer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe "OracleEnhancedAdapter integer type detection based on attribute settings" do 4 | before(:all) do 5 | ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) 6 | @conn = ActiveRecord::Base.connection 7 | @conn.execute "DROP TABLE test2_employees" rescue nil 8 | @conn.execute <<~SQL 9 | CREATE TABLE test2_employees ( 10 | id NUMBER PRIMARY KEY, 11 | first_name VARCHAR2(20), 12 | last_name VARCHAR2(25), 13 | email VARCHAR2(25), 14 | phone_number VARCHAR2(20), 15 | hire_date DATE, 16 | job_id NUMBER, 17 | salary NUMBER, 18 | commission_pct NUMBER(2,2), 19 | manager_id NUMBER(6), 20 | is_manager NUMBER(1), 21 | department_id NUMBER(4,0), 22 | created_at DATE 23 | ) 24 | SQL 25 | @conn.execute "DROP SEQUENCE test2_employees_seq" rescue nil 26 | @conn.execute <<~SQL 27 | CREATE SEQUENCE test2_employees_seq MINVALUE 1 28 | INCREMENT BY 1 START WITH 10040 CACHE 20 NOORDER NOCYCLE 29 | SQL 30 | end 31 | 32 | after(:all) do 33 | @conn.execute "DROP TABLE test2_employees" 34 | @conn.execute "DROP SEQUENCE test2_employees_seq" 35 | end 36 | 37 | describe "/ NUMBER values from ActiveRecord model" do 38 | before(:each) do 39 | class ::Test2Employee < ActiveRecord::Base 40 | end 41 | end 42 | 43 | after(:each) do 44 | Object.send(:remove_const, "Test2Employee") 45 | ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_booleans = true 46 | ActiveRecord::Base.clear_cache! 47 | end 48 | 49 | def create_employee2 50 | @employee2 = Test2Employee.create( 51 | first_name: "First", 52 | last_name: "Last", 53 | job_id: 1, 54 | is_manager: 1, 55 | salary: 1000 56 | ) 57 | @employee2.reload 58 | end 59 | 60 | it "should return BigDecimal value from NUMBER column if by default" do 61 | create_employee2 62 | expect(@employee2.job_id.class).to eq(BigDecimal) 63 | end 64 | 65 | it "should return Integer value from NUMBER column if attribute is set to integer" do 66 | class ::Test2Employee < ActiveRecord::Base 67 | attribute :job_id, :integer 68 | end 69 | create_employee2 70 | expect(@employee2.job_id).to be_a(Integer) 71 | end 72 | 73 | it "should return Integer value from NUMBER column with integer value using _before_type_cast method" do 74 | create_employee2 75 | expect(@employee2.job_id_before_type_cast).to be_a(Integer) 76 | end 77 | 78 | it "should return Boolean value from NUMBER(1) column if emulate booleans is used" do 79 | create_employee2 80 | expect(@employee2.is_manager.class).to eq(TrueClass) 81 | end 82 | 83 | it "should return Integer value from NUMBER(1) column if attribute is set to integer" do 84 | class ::Test2Employee < ActiveRecord::Base 85 | attribute :is_manager, :integer 86 | end 87 | create_employee2 88 | expect(@employee2.is_manager).to be_a(Integer) 89 | end 90 | 91 | it "should return Integer value from NUMBER(1) column if emulate_booleans is set to false" do 92 | ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_booleans = false 93 | ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.clear_type_map! 94 | ActiveRecord::Base.clear_cache! 95 | create_employee2 96 | expect(@employee2.is_manager).to be_a(Integer) 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /spec/active_record/oracle_enhanced/type/json_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe "OracleEnhancedAdapter attribute API support for JSON type" do 4 | include SchemaSpecHelper 5 | 6 | before(:all) do 7 | ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) 8 | @conn = ActiveRecord::Base.connection 9 | @oracle12c_or_higher = !! @conn.select_value( 10 | "select * from product_component_version where product like 'Oracle%' and to_number(substr(version,1,2)) >= 12") 11 | skip "Not supported in this database version" unless @oracle12c_or_higher 12 | ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) 13 | schema_define do 14 | create_table :test_posts, force: true do |t| 15 | t.string :title 16 | t.text :article 17 | end 18 | execute "alter table test_posts add constraint test_posts_title_is_json check (title is json)" 19 | execute "alter table test_posts add constraint test_posts_article_is_json check (article is json)" 20 | end 21 | 22 | class ::TestPost < ActiveRecord::Base 23 | attribute :title, :json 24 | attribute :article, :json 25 | end 26 | end 27 | 28 | after(:all) do 29 | ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) 30 | schema_define do 31 | drop_table :test_posts, if_exists: true 32 | end 33 | end 34 | 35 | before(:each) do 36 | TestPost.delete_all 37 | end 38 | 39 | it "should support attribute api for JSON" do 40 | post = TestPost.create!(title: { "publish" => true, "foo" => "bar" }, article: { "bar" => "baz" }) 41 | post.reload 42 | expect(post.title).to eq ({ "publish" => true, "foo" => "bar" }) 43 | expect(post.article).to eq ({ "bar" => "baz" }) 44 | post.title = ({ "publish" => false, "foo" => "bar2" }) 45 | post.save 46 | expect(post.reload.title).to eq ({ "publish" => false, "foo" => "bar2" }) 47 | end 48 | 49 | it "should support IS JSON" do 50 | TestPost.create!(title: { "publish" => true, "foo" => "bar" }) 51 | count_json = TestPost.where("title is json") 52 | expect(count_json.size).to eq 1 53 | count_non_json = TestPost.where("title is not json") 54 | expect(count_non_json.size).to eq 0 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/active_record/oracle_enhanced/type/national_character_string_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe "OracleEnhancedAdapter quoting of NCHAR and NVARCHAR2 columns" do 4 | before(:all) do 5 | ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) 6 | @conn = ActiveRecord::Base.connection 7 | @conn.execute <<~SQL 8 | CREATE TABLE test_items ( 9 | id NUMBER(6,0) PRIMARY KEY, 10 | nchar_column NCHAR(20), 11 | nvarchar2_column NVARCHAR2(20), 12 | char_column CHAR(20), 13 | varchar2_column VARCHAR2(20) 14 | ) 15 | SQL 16 | @conn.execute "CREATE SEQUENCE test_items_seq" 17 | end 18 | 19 | after(:all) do 20 | @conn.execute "DROP TABLE test_items" 21 | @conn.execute "DROP SEQUENCE test_items_seq" 22 | end 23 | 24 | before(:each) do 25 | class ::TestItem < ActiveRecord::Base 26 | end 27 | end 28 | 29 | after(:each) do 30 | Object.send(:remove_const, "TestItem") 31 | ActiveRecord::Base.clear_cache! 32 | end 33 | 34 | it "should quote with N prefix" do 35 | columns = @conn.columns("test_items") 36 | %w(nchar_column nvarchar2_column char_column varchar2_column).each do |col| 37 | column = columns.detect { |c| c.name == col } 38 | type = @conn.lookup_cast_type_from_column(column) 39 | value = type.serialize("abc") 40 | expect(@conn.quote(value)).to eq(column.sql_type[0, 1] == "N" ? "N'abc'" : "'abc'") 41 | type = @conn.lookup_cast_type_from_column(column) 42 | nilvalue = type.serialize(nil) 43 | expect(@conn.quote(nilvalue)).to eq("NULL") 44 | end 45 | end 46 | 47 | it "should create record" do 48 | nchar_data = "āčē" 49 | item = TestItem.create( 50 | nchar_column: nchar_data, 51 | nvarchar2_column: nchar_data 52 | ).reload 53 | expect(item.nchar_column).to eq(nchar_data) 54 | expect(item.nvarchar2_column).to eq(nchar_data) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/active_record/oracle_enhanced/type/national_character_text_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe "OracleEnhancedAdapter handling of NCLOB columns" do 4 | include SchemaSpecHelper 5 | 6 | before(:all) do 7 | ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) 8 | @conn = ActiveRecord::Base.connection 9 | schema_define do 10 | create_table :test_employees, force: true do |t| 11 | t.string :first_name, limit: 20 12 | t.string :last_name, limit: 25 13 | t.ntext :comments 14 | end 15 | create_table :test2_employees, force: true do |t| 16 | t.string :first_name, limit: 20 17 | t.string :last_name, limit: 25 18 | t.ntext :comments 19 | end 20 | create_table :test_serialize_employees, force: true do |t| 21 | t.string :first_name, limit: 20 22 | t.string :last_name, limit: 25 23 | end 24 | add_column :test_serialize_employees, :comments, :ntext 25 | end 26 | 27 | # Some random multibyte characters. They say Hello (Kon'nichiwa) World (Sekai) in Japanese. 28 | @nclob_data = "こんにちは" 29 | @nclob_data2 = "世界" 30 | 31 | class ::TestEmployee < ActiveRecord::Base; end 32 | class ::Test2Employee < ActiveRecord::Base 33 | serialize :comments 34 | end 35 | class ::TestEmployeeReadOnlyNClob < ActiveRecord::Base 36 | self.table_name = "test_employees" 37 | attr_readonly :comments 38 | end 39 | class ::TestSerializeEmployee < ActiveRecord::Base 40 | serialize :comments 41 | attr_readonly :comments 42 | end 43 | end 44 | 45 | after(:all) do 46 | @conn.drop_table :test_employees, if_exists: true 47 | @conn.drop_table :test2_employees, if_exists: true 48 | @conn.drop_table :test_serialize_employees, if_exists: true 49 | Object.send(:remove_const, "TestEmployee") 50 | Object.send(:remove_const, "Test2Employee") 51 | Object.send(:remove_const, "TestEmployeeReadOnlyNClob") 52 | Object.send(:remove_const, "TestSerializeEmployee") 53 | ActiveRecord::Base.clear_cache! 54 | end 55 | 56 | it "should create record without NCLOB data when attribute is serialized" do 57 | @employee = Test2Employee.create!( 58 | first_name: "First", 59 | last_name: "Last" 60 | ) 61 | expect(@employee).to be_valid 62 | @employee.reload 63 | expect(@employee.comments).to be_nil 64 | end 65 | 66 | it "should accept Symbol value for NCLOB column" do 67 | @employee = TestEmployee.create!( 68 | comments: :test_comment 69 | ) 70 | expect(@employee).to be_valid 71 | end 72 | 73 | it "should respect attr_readonly setting for NCLOB column" do 74 | @employee = TestEmployeeReadOnlyNClob.create!( 75 | first_name: "First", 76 | comments: @nclob_data 77 | ) 78 | expect(@employee).to be_valid 79 | @employee.reload 80 | expect(@employee.comments).to eq(@nclob_data) 81 | @employee.comments = @nclob_data2 82 | expect(@employee.save).to be(true) 83 | @employee.reload 84 | expect(@employee.comments).to eq(@nclob_data) 85 | end 86 | 87 | it "should work for serialized readonly NCLOB columns", serialized: true do 88 | @employee = TestSerializeEmployee.new( 89 | first_name: "First", 90 | comments: nil 91 | ) 92 | expect(@employee.comments).to be_nil 93 | expect(@employee.save).to be(true) 94 | expect(@employee).to be_valid 95 | @employee.reload 96 | expect(@employee.comments).to be_nil 97 | @employee.comments = {} 98 | expect(@employee.save).to be(true) 99 | @employee.reload 100 | # should not set readonly 101 | expect(@employee.comments).to be_nil 102 | end 103 | 104 | it "should create record with NCLOB data" do 105 | @employee = TestEmployee.create!( 106 | first_name: "First", 107 | last_name: "Last", 108 | comments: @nclob_data 109 | ) 110 | @employee.reload 111 | expect(@employee.comments).to eq(@nclob_data) 112 | end 113 | 114 | it "should update record with NCLOB data" do 115 | @employee = TestEmployee.create!( 116 | first_name: "First", 117 | last_name: "Last" 118 | ) 119 | @employee.reload 120 | expect(@employee.comments).to be_nil 121 | @employee.comments = @nclob_data 122 | @employee.save! 123 | @employee.reload 124 | expect(@employee.comments).to eq(@nclob_data) 125 | end 126 | 127 | it "should update record with zero-length NCLOB data" do 128 | @employee = TestEmployee.create!( 129 | first_name: "First", 130 | last_name: "Last" 131 | ) 132 | @employee.reload 133 | expect(@employee.comments).to be_nil 134 | @employee.comments = "" 135 | @employee.save! 136 | @employee.reload 137 | expect(@employee.comments).to eq("") 138 | end 139 | 140 | it "should update record that has existing NCLOB data with different NCLOB data" do 141 | @employee = TestEmployee.create!( 142 | first_name: "First", 143 | last_name: "Last", 144 | comments: @nclob_data 145 | ) 146 | @employee.reload 147 | @employee.comments = @nclob_data2 148 | @employee.save! 149 | @employee.reload 150 | expect(@employee.comments).to eq(@nclob_data2) 151 | end 152 | 153 | it "should update record that has existing NCLOB data with nil" do 154 | @employee = TestEmployee.create!( 155 | first_name: "First", 156 | last_name: "Last", 157 | comments: @nclob_data 158 | ) 159 | @employee.reload 160 | @employee.comments = nil 161 | @employee.save! 162 | @employee.reload 163 | expect(@employee.comments).to be_nil 164 | end 165 | 166 | it "should update record that has existing NCLOB data with zero-length NCLOB data" do 167 | @employee = TestEmployee.create!( 168 | first_name: "First", 169 | last_name: "Last", 170 | comments: @nclob_data 171 | ) 172 | @employee.reload 173 | @employee.comments = "" 174 | @employee.save! 175 | @employee.reload 176 | expect(@employee.comments).to eq("") 177 | end 178 | 179 | it "should update record that has zero-length NCLOB data with non-empty NCLOB data" do 180 | @employee = TestEmployee.create!( 181 | first_name: "First", 182 | last_name: "Last", 183 | comments: "" 184 | ) 185 | @employee.reload 186 | expect(@employee.comments).to eq("") 187 | @employee.comments = @nclob_data 188 | @employee.save! 189 | @employee.reload 190 | expect(@employee.comments).to eq(@nclob_data) 191 | end 192 | 193 | it "should store serializable ruby data structures" do 194 | ruby_data1 = { "arbitrary1" => ["ruby", :data, 123] } 195 | ruby_data2 = { "arbitrary2" => ["ruby", :data, 123] } 196 | @employee = Test2Employee.create!( 197 | comments: ruby_data1 198 | ) 199 | @employee.reload 200 | expect(@employee.comments).to eq(ruby_data1) 201 | @employee.comments = ruby_data2 202 | @employee.save 203 | @employee.reload 204 | expect(@employee.comments).to eq(ruby_data2) 205 | end 206 | 207 | it "should keep unchanged serialized data when other columns changed" do 208 | @employee = Test2Employee.create!( 209 | first_name: "First", 210 | last_name: "Last", 211 | comments: @nclob_data 212 | ) 213 | @employee.first_name = "Steve" 214 | @employee.save 215 | @employee.reload 216 | expect(@employee.comments).to eq(@nclob_data) 217 | end 218 | 219 | it "should keep serialized data after save" do 220 | @employee = Test2Employee.new 221 | @employee.comments = { length: { is: 1 } } 222 | @employee.save 223 | @employee.reload 224 | expect(@employee.comments).to eq(length: { is: 1 }) 225 | @employee.comments = { length: { is: 2 } } 226 | @employee.save 227 | @employee.reload 228 | expect(@employee.comments).to eq(length: { is: 2 }) 229 | end 230 | end 231 | -------------------------------------------------------------------------------- /spec/active_record/oracle_enhanced/type/raw_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe "OracleEnhancedAdapter handling of RAW columns" do 4 | include SchemaSpecHelper 5 | 6 | before(:all) do 7 | ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) 8 | schema_define do 9 | create_table :test_employees, force: true do |t| 10 | t.string :first_name, limit: 20 11 | t.string :last_name, limit: 25 12 | t.raw :binary_data, limit: 1024 13 | end 14 | end 15 | @binary_data = "\0\1\2\3\4\5\6\7\8\9" * 100 16 | @binary_data2 = "\1\2\3\4\5\6\7\8\9\0" * 100 17 | end 18 | 19 | after(:all) do 20 | schema_define do 21 | drop_table :test_employees 22 | end 23 | end 24 | 25 | before(:each) do 26 | class ::TestEmployee < ActiveRecord::Base 27 | end 28 | end 29 | 30 | after(:each) do 31 | Object.send(:remove_const, "TestEmployee") 32 | ActiveRecord::Base.clear_cache! 33 | end 34 | 35 | it "should create record with RAW data" do 36 | @employee = TestEmployee.create!( 37 | first_name: "First", 38 | last_name: "Last", 39 | binary_data: @binary_data 40 | ) 41 | @employee.reload 42 | expect(@employee.binary_data).to eq(@binary_data) 43 | end 44 | 45 | it "should update record with RAW data" do 46 | @employee = TestEmployee.create!( 47 | first_name: "First", 48 | last_name: "Last" 49 | ) 50 | @employee.reload 51 | expect(@employee.binary_data).to be_nil 52 | @employee.binary_data = @binary_data 53 | @employee.save! 54 | @employee.reload 55 | expect(@employee.binary_data).to eq(@binary_data) 56 | end 57 | 58 | it "should update record with zero-length RAW data" do 59 | @employee = TestEmployee.create!( 60 | first_name: "First", 61 | last_name: "Last" 62 | ) 63 | @employee.reload 64 | expect(@employee.binary_data).to be_nil 65 | @employee.binary_data = "" 66 | @employee.save! 67 | @employee.reload 68 | expect(@employee.binary_data).to be_nil 69 | end 70 | 71 | it "should update record that has existing RAW data with different RAW data" do 72 | @employee = TestEmployee.create!( 73 | first_name: "First", 74 | last_name: "Last", 75 | binary_data: @binary_data 76 | ) 77 | @employee.reload 78 | @employee.binary_data = @binary_data2 79 | @employee.save! 80 | @employee.reload 81 | expect(@employee.binary_data).to eq(@binary_data2) 82 | end 83 | 84 | it "should update record that has existing RAW data with nil" do 85 | @employee = TestEmployee.create!( 86 | first_name: "First", 87 | last_name: "Last", 88 | binary_data: @binary_data 89 | ) 90 | @employee.reload 91 | @employee.binary_data = nil 92 | @employee.save! 93 | @employee.reload 94 | expect(@employee.binary_data).to be_nil 95 | end 96 | 97 | it "should update record that has existing RAW data with zero-length RAW data" do 98 | @employee = TestEmployee.create!( 99 | first_name: "First", 100 | last_name: "Last", 101 | binary_data: @binary_data 102 | ) 103 | @employee.reload 104 | @employee.binary_data = "" 105 | @employee.save! 106 | @employee.reload 107 | expect(@employee.binary_data).to be_nil 108 | end 109 | 110 | it "should update record that has zero-length BLOB data with non-empty RAW data" do 111 | @employee = TestEmployee.create!( 112 | first_name: "First", 113 | last_name: "Last", 114 | binary_data: "" 115 | ) 116 | @employee.reload 117 | @employee.binary_data = @binary_data 118 | @employee.save! 119 | @employee.reload 120 | expect(@employee.binary_data).to eq(@binary_data) 121 | end 122 | 123 | it "should allow equality on select" do 124 | TestEmployee.delete_all 125 | TestEmployee.create!( 126 | first_name: "First", 127 | last_name: "Last", 128 | binary_data: @binary_data, 129 | ) 130 | TestEmployee.create!( 131 | first_name: "First1", 132 | last_name: "Last1", 133 | binary_data: @binary_data2, 134 | ) 135 | expect(TestEmployee.where(binary_data: @binary_data)).to have_attributes(count: 1) 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /spec/active_record/oracle_enhanced/type/text_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe "OracleEnhancedAdapter handling of CLOB columns" do 4 | include SchemaSpecHelper 5 | 6 | before(:all) do 7 | ActiveRecord::Base.establish_connection(CONNECTION_PARAMS) 8 | @conn = ActiveRecord::Base.connection 9 | schema_define do 10 | create_table :test_employees, force: true do |t| 11 | t.string :first_name, limit: 20 12 | t.string :last_name, limit: 25 13 | t.text :comments 14 | end 15 | create_table :test2_employees, force: true do |t| 16 | t.string :first_name, limit: 20 17 | t.string :last_name, limit: 25 18 | t.text :comments 19 | end 20 | create_table :test_serialize_employees, force: true do |t| 21 | t.string :first_name, limit: 20 22 | t.string :last_name, limit: 25 23 | end 24 | add_column :test_serialize_employees, :comments, :text 25 | end 26 | 27 | @char_data = (0..127).to_a.pack("C*") * 800 28 | @char_data2 = ((1..127).to_a.pack("C*") + "\0") * 800 29 | 30 | class ::TestEmployee < ActiveRecord::Base; end 31 | class ::Test2Employee < ActiveRecord::Base 32 | serialize :comments 33 | end 34 | class ::TestEmployeeReadOnlyClob < ActiveRecord::Base 35 | self.table_name = "test_employees" 36 | attr_readonly :comments 37 | end 38 | class ::TestSerializeEmployee < ActiveRecord::Base 39 | serialize :comments 40 | attr_readonly :comments 41 | end 42 | end 43 | 44 | after(:all) do 45 | @conn.drop_table :test_employees, if_exists: true 46 | @conn.drop_table :test2_employees, if_exists: true 47 | @conn.drop_table :test_serialize_employees, if_exists: true 48 | Object.send(:remove_const, "TestEmployee") 49 | Object.send(:remove_const, "Test2Employee") 50 | Object.send(:remove_const, "TestEmployeeReadOnlyClob") 51 | Object.send(:remove_const, "TestSerializeEmployee") 52 | ActiveRecord::Base.clear_cache! 53 | end 54 | 55 | it "should create record without CLOB data when attribute is serialized" do 56 | @employee = Test2Employee.create!( 57 | first_name: "First", 58 | last_name: "Last" 59 | ) 60 | expect(@employee).to be_valid 61 | @employee.reload 62 | expect(@employee.comments).to be_nil 63 | end 64 | 65 | it "should accept Symbol value for CLOB column" do 66 | @employee = TestEmployee.create!( 67 | comments: :test_comment 68 | ) 69 | expect(@employee).to be_valid 70 | end 71 | 72 | it "should respect attr_readonly setting for CLOB column" do 73 | @employee = TestEmployeeReadOnlyClob.create!( 74 | first_name: "First", 75 | comments: "initial" 76 | ) 77 | expect(@employee).to be_valid 78 | @employee.reload 79 | expect(@employee.comments).to eq("initial") 80 | @employee.comments = "changed" 81 | expect(@employee.save).to be(true) 82 | @employee.reload 83 | expect(@employee.comments).to eq("initial") 84 | end 85 | 86 | it "should work for serialized readonly CLOB columns", serialized: true do 87 | @employee = TestSerializeEmployee.new( 88 | first_name: "First", 89 | comments: nil 90 | ) 91 | expect(@employee.comments).to be_nil 92 | expect(@employee.save).to be(true) 93 | expect(@employee).to be_valid 94 | @employee.reload 95 | expect(@employee.comments).to be_nil 96 | @employee.comments = {} 97 | expect(@employee.save).to be(true) 98 | @employee.reload 99 | # should not set readonly 100 | expect(@employee.comments).to be_nil 101 | end 102 | 103 | it "should create record with CLOB data" do 104 | @employee = TestEmployee.create!( 105 | first_name: "First", 106 | last_name: "Last", 107 | comments: @char_data 108 | ) 109 | @employee.reload 110 | expect(@employee.comments).to eq(@char_data) 111 | end 112 | 113 | it "should update record with CLOB data" do 114 | @employee = TestEmployee.create!( 115 | first_name: "First", 116 | last_name: "Last" 117 | ) 118 | @employee.reload 119 | expect(@employee.comments).to be_nil 120 | @employee.comments = @char_data 121 | @employee.save! 122 | @employee.reload 123 | expect(@employee.comments).to eq(@char_data) 124 | end 125 | 126 | it "should update record with zero-length CLOB data" do 127 | @employee = TestEmployee.create!( 128 | first_name: "First", 129 | last_name: "Last" 130 | ) 131 | @employee.reload 132 | expect(@employee.comments).to be_nil 133 | @employee.comments = "" 134 | @employee.save! 135 | @employee.reload 136 | expect(@employee.comments).to eq("") 137 | end 138 | 139 | it "should update record that has existing CLOB data with different CLOB data" do 140 | @employee = TestEmployee.create!( 141 | first_name: "First", 142 | last_name: "Last", 143 | comments: @char_data 144 | ) 145 | @employee.reload 146 | @employee.comments = @char_data2 147 | @employee.save! 148 | @employee.reload 149 | expect(@employee.comments).to eq(@char_data2) 150 | end 151 | 152 | it "should update record that has existing CLOB data with nil" do 153 | @employee = TestEmployee.create!( 154 | first_name: "First", 155 | last_name: "Last", 156 | comments: @char_data 157 | ) 158 | @employee.reload 159 | @employee.comments = nil 160 | @employee.save! 161 | @employee.reload 162 | expect(@employee.comments).to be_nil 163 | end 164 | 165 | it "should update record that has existing CLOB data with zero-length CLOB data" do 166 | @employee = TestEmployee.create!( 167 | first_name: "First", 168 | last_name: "Last", 169 | comments: @char_data 170 | ) 171 | @employee.reload 172 | @employee.comments = "" 173 | @employee.save! 174 | @employee.reload 175 | expect(@employee.comments).to eq("") 176 | end 177 | 178 | it "should update record that has zero-length CLOB data with non-empty CLOB data" do 179 | @employee = TestEmployee.create!( 180 | first_name: "First", 181 | last_name: "Last", 182 | comments: "" 183 | ) 184 | @employee.reload 185 | expect(@employee.comments).to eq("") 186 | @employee.comments = @char_data 187 | @employee.save! 188 | @employee.reload 189 | expect(@employee.comments).to eq(@char_data) 190 | end 191 | 192 | it "should store serializable ruby data structures" do 193 | ruby_data1 = { "arbitrary1" => ["ruby", :data, 123] } 194 | ruby_data2 = { "arbitrary2" => ["ruby", :data, 123] } 195 | @employee = Test2Employee.create!( 196 | comments: ruby_data1 197 | ) 198 | @employee.reload 199 | expect(@employee.comments).to eq(ruby_data1) 200 | @employee.comments = ruby_data2 201 | @employee.save 202 | @employee.reload 203 | expect(@employee.comments).to eq(ruby_data2) 204 | end 205 | 206 | it "should keep unchanged serialized data when other columns changed" do 207 | @employee = Test2Employee.create!( 208 | first_name: "First", 209 | last_name: "Last", 210 | comments: "initial serialized data" 211 | ) 212 | @employee.first_name = "Steve" 213 | @employee.save 214 | @employee.reload 215 | expect(@employee.comments).to eq("initial serialized data") 216 | end 217 | 218 | it "should keep serialized data after save" do 219 | @employee = Test2Employee.new 220 | @employee.comments = { length: { is: 1 } } 221 | @employee.save 222 | @employee.reload 223 | expect(@employee.comments).to eq(length: { is: 1 }) 224 | @employee.comments = { length: { is: 2 } } 225 | @employee.save 226 | @employee.reload 227 | expect(@employee.comments).to eq(length: { is: 2 }) 228 | end 229 | 230 | it "should allow equality on select" do 231 | search_data = "text search CLOB" 232 | Test2Employee.create!( 233 | first_name: "First", 234 | last_name: "Last", 235 | comments: search_data, 236 | ) 237 | Test2Employee.create!( 238 | first_name: "First1", 239 | last_name: "Last1", 240 | comments: "other data", 241 | ) 242 | expect(Test2Employee.where(comments: search_data)).to have_attributes(count: 1) 243 | end 244 | end 245 | -------------------------------------------------------------------------------- /spec/active_record/oracle_enhanced/type/timestamp_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe "OracleEnhancedAdapter timestamp with timezone support" do 4 | include SchemaSpecHelper 5 | 6 | before(:all) do 7 | skip if ENV["DATABASE_SERVER_AND_CLIENT_VERSION_DO_NOT_MATCH"] == "true" 8 | if ENV["DATABASE_VERSION"] == "11.2.0.2" && ENV["ORACLE_HOME"] == "/usr/lib/oracle/21/client64" 9 | skip 10 | end 11 | ActiveRecord.default_timezone = :local 12 | ActiveRecord::Base.establish_connection(CONNECTION_WITH_TIMEZONE_PARAMS) 13 | @conn = ActiveRecord::Base.connection 14 | schema_define do 15 | create_table :test_employees, force: true do |t| 16 | t.string :first_name, limit: 20 17 | t.string :last_name, limit: 25 18 | t.string :email, limit: 25 19 | t.string :phone_number, limit: 20 20 | t.date :hire_date 21 | t.decimal :job_id, scale: 0, precision: 6 22 | t.decimal :salary, scale: 2, precision: 8 23 | t.decimal :commission_pct, scale: 2, precision: 2 24 | t.decimal :manager_id, scale: 0, precision: 6 25 | t.decimal :department_id, scale: 0, precision: 4 26 | t.timestamp :created_at 27 | t.timestamptz :created_at_tz 28 | t.timestampltz :created_at_ltz 29 | end 30 | end 31 | end 32 | 33 | after(:all) do 34 | @conn.drop_table :test_employees, if_exists: true rescue nil 35 | ActiveRecord.default_timezone = :utc 36 | end 37 | 38 | describe "/ TIMESTAMP WITH TIME ZONE values from ActiveRecord model" do 39 | before(:all) do 40 | class ::TestEmployee < ActiveRecord::Base 41 | end 42 | end 43 | 44 | after(:all) do 45 | Object.send(:remove_const, "TestEmployee") 46 | ActiveRecord::Base.clear_cache! 47 | end 48 | 49 | it "should return Time value from TIMESTAMP columns" do 50 | @now = Time.local(2008, 5, 26, 23, 11, 11, 0) 51 | @employee = TestEmployee.create( 52 | created_at: @now, 53 | created_at_tz: @now, 54 | created_at_ltz: @now 55 | ) 56 | @employee.reload 57 | [:created_at, :created_at_tz, :created_at_ltz].each do |c| 58 | expect(@employee.send(c).class).to eq(Time) 59 | expect(@employee.send(c).to_f).to eq(@now.to_f) 60 | end 61 | end 62 | 63 | it "should return Time value with fractional seconds from TIMESTAMP columns" do 64 | @now = Time.local(2008, 5, 26, 23, 11, 11, 10) 65 | @employee = TestEmployee.create( 66 | created_at: @now, 67 | created_at_tz: @now, 68 | created_at_ltz: @now 69 | ) 70 | @employee.reload 71 | [:created_at, :created_at_tz, :created_at_ltz].each do |c| 72 | expect(@employee.send(c).class).to eq(Time) 73 | expect(@employee.send(c).to_f).to eq(@now.to_f) 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/spec_config.yaml.template: -------------------------------------------------------------------------------- 1 | # copy this file to spec/spec_config.yaml and set appropriate values 2 | # you can also use environment variables, see spec_helper.rb 3 | database: 4 | name: 'FREEPDB1' 5 | host: '127.0.0.1' 6 | port: 1521 7 | user: 'oracle_enhanced' 8 | password: 'oracle_enhanced' 9 | sys_password: 'oracle' 10 | non_default_tablespace: 'SYSTEM' 11 | timezone: 'Europe/Riga' 12 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rubygems" 4 | require "bundler" 5 | require "yaml" 6 | Bundler.setup(:default, :development) 7 | 8 | $:.unshift(File.expand_path("../../lib", __FILE__)) 9 | config_path = File.expand_path("../spec_config.yaml", __FILE__) 10 | if File.exist?(config_path) 11 | puts "==> Loading config from #{config_path}" 12 | config = YAML.load_file(config_path) 13 | else 14 | puts "==> Loading config from ENV or use default" 15 | config = { "rails" => {}, "database" => {} } 16 | end 17 | 18 | require "rspec" 19 | 20 | if !defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby" || RUBY_ENGINE == "truffleruby" 21 | puts "==> Running specs with ruby version #{RUBY_VERSION}" 22 | require "oci8" 23 | elsif RUBY_ENGINE == "jruby" 24 | puts "==> Running specs with JRuby version #{JRUBY_VERSION}" 25 | end 26 | 27 | require "active_record" 28 | 29 | require "active_support/core_ext/module/attribute_accessors" 30 | require "active_support/core_ext/class/attribute_accessors" 31 | 32 | require "active_support/log_subscriber" 33 | require "active_record/log_subscriber" 34 | 35 | require "logger" 36 | 37 | require "active_record/connection_adapters/oracle_enhanced_adapter" 38 | require "ruby-plsql" 39 | 40 | puts "==> Effective ActiveRecord version #{ActiveRecord::VERSION::STRING}" 41 | 42 | module LoggerSpecHelper 43 | def set_logger 44 | @logger = MockLogger.new 45 | @old_logger = ActiveRecord::Base.logger 46 | 47 | @notifier = ActiveSupport::Notifications::Fanout.new 48 | 49 | ActiveSupport::LogSubscriber.colorize_logging = false 50 | 51 | ActiveRecord::Base.logger = @logger 52 | @old_notifier = ActiveSupport::Notifications.notifier 53 | ActiveSupport::Notifications.notifier = @notifier 54 | 55 | ActiveRecord::LogSubscriber.attach_to(:active_record) 56 | ActiveSupport::Notifications.subscribe("sql.active_record", ActiveRecord::ExplainSubscriber.new) 57 | end 58 | 59 | class MockLogger 60 | LEVELS = %i[debug info warn error fatal unknown] 61 | 62 | attr_reader :flush_count 63 | 64 | def initialize 65 | @flush_count = 0 66 | @logged = Hash.new { |h, k| h[k] = [] } 67 | end 68 | 69 | # used in ActiveRecord 2.x 70 | def debug? 71 | true 72 | end 73 | 74 | def level 75 | 0 76 | end 77 | 78 | def method_missing(*args) 79 | if LEVELS.include?(args[0]) 80 | level, message = args 81 | @logged[level] << message 82 | else 83 | super 84 | end 85 | end 86 | 87 | def logged(level) 88 | @logged[level].compact.map { |l| l.to_s.strip } 89 | end 90 | 91 | def output(level) 92 | logged(level).join("\n") 93 | end 94 | 95 | def flush 96 | @flush_count += 1 97 | end 98 | 99 | def clear(level) 100 | @logged[level] = [] 101 | end 102 | end 103 | 104 | def clear_logger 105 | ActiveRecord::Base.logger = @old_logger 106 | @logger = nil 107 | 108 | ActiveSupport::Notifications.notifier = @old_notifier 109 | @notifier = nil 110 | end 111 | 112 | # Wait notifications to be published (for Rails 3.0) 113 | # should not be currently used with sync queues in tests 114 | def wait 115 | @notifier.wait if @notifier 116 | end 117 | end 118 | 119 | ActiveRecord::LogSubscriber::IGNORE_PAYLOAD_NAMES.replace(["EXPLAIN"]) 120 | 121 | module SchemaSpecHelper 122 | def schema_define(&block) 123 | ActiveRecord::Schema.define do 124 | suppress_messages do 125 | instance_eval(&block) 126 | end 127 | end 128 | end 129 | end 130 | 131 | module SchemaDumpingHelper 132 | def dump_table_schema(table, connection = ActiveRecord::Base.connection) 133 | old_ignore_tables = ActiveRecord::SchemaDumper.ignore_tables 134 | ActiveRecord::SchemaDumper.ignore_tables = connection.data_sources - [table] 135 | stream = StringIO.new 136 | ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) 137 | stream.string 138 | ensure 139 | ActiveRecord::SchemaDumper.ignore_tables = old_ignore_tables 140 | end 141 | end 142 | 143 | DATABASE_NAME = config["database"]["name"] || ENV["DATABASE_NAME"] || "orcl" 144 | DATABASE_HOST = config["database"]["host"] || ENV["DATABASE_HOST"] || "127.0.0.1" 145 | DATABASE_PORT = config["database"]["port"] || ENV["DATABASE_PORT"] || 1521 146 | DATABASE_USER = config["database"]["user"] || ENV["DATABASE_USER"] || "oracle_enhanced" 147 | DATABASE_PASSWORD = config["database"]["password"] || ENV["DATABASE_PASSWORD"] || "oracle_enhanced" 148 | DATABASE_SCHEMA = config["database"]["schema"] || ENV["DATABASE_SCHEMA"] || "oracle_enhanced_schema" 149 | DATABASE_SYS_PASSWORD = config["database"]["sys_password"] || ENV["DATABASE_SYS_PASSWORD"] || "admin" 150 | 151 | CONNECTION_PARAMS = { 152 | adapter: "oracle_enhanced", 153 | database: DATABASE_NAME, 154 | host: DATABASE_HOST, 155 | port: DATABASE_PORT, 156 | username: DATABASE_USER, 157 | password: DATABASE_PASSWORD 158 | } 159 | 160 | CONNECTION_WITH_SCHEMA_PARAMS = { 161 | adapter: "oracle_enhanced", 162 | database: DATABASE_NAME, 163 | host: DATABASE_HOST, 164 | port: DATABASE_PORT, 165 | username: DATABASE_USER, 166 | password: DATABASE_PASSWORD, 167 | schema: DATABASE_SCHEMA 168 | } 169 | 170 | CONNECTION_WITH_TIMEZONE_PARAMS = { 171 | adapter: "oracle_enhanced", 172 | database: DATABASE_NAME, 173 | host: DATABASE_HOST, 174 | port: DATABASE_PORT, 175 | username: DATABASE_USER, 176 | password: DATABASE_PASSWORD, 177 | time_zone: "Europe/Riga" 178 | } 179 | 180 | SYS_CONNECTION_PARAMS = { 181 | adapter: "oracle_enhanced", 182 | database: DATABASE_NAME, 183 | host: DATABASE_HOST, 184 | port: DATABASE_PORT, 185 | username: "sys", 186 | password: DATABASE_SYS_PASSWORD, 187 | privilege: "SYSDBA" 188 | } 189 | 190 | SYSTEM_CONNECTION_PARAMS = { 191 | adapter: "oracle_enhanced", 192 | database: DATABASE_NAME, 193 | host: DATABASE_HOST, 194 | port: DATABASE_PORT, 195 | username: "system", 196 | password: DATABASE_SYS_PASSWORD 197 | } 198 | 199 | SERVICE_NAME_CONNECTION_PARAMS = { 200 | adapter: "oracle_enhanced", 201 | database: "/#{DATABASE_NAME}", 202 | host: DATABASE_HOST, 203 | port: DATABASE_PORT, 204 | username: DATABASE_USER, 205 | password: DATABASE_PASSWORD 206 | } 207 | 208 | DATABASE_NON_DEFAULT_TABLESPACE = config["database"]["non_default_tablespace"] || ENV["DATABASE_NON_DEFAULT_TABLESPACE"] || "SYSTEM" 209 | 210 | # set default time zone in TZ environment variable 211 | # which will be used to set session time zone 212 | ENV["TZ"] ||= config["timezone"] || "Europe/Riga" 213 | 214 | ActiveRecord::Base.logger = ActiveSupport::Logger.new("debug.log", 0, 100 * 1024 * 1024) 215 | -------------------------------------------------------------------------------- /spec/support/alter_system_set_open_cursors.sql: -------------------------------------------------------------------------------- 1 | alter system set open_cursors = 1200 scope = both; 2 | -------------------------------------------------------------------------------- /spec/support/alter_system_user_password.sql: -------------------------------------------------------------------------------- 1 | alter user sys identified by admin; 2 | alter user system identified by admin; 3 | -------------------------------------------------------------------------------- /spec/support/create_oracle_enhanced_users.sql: -------------------------------------------------------------------------------- 1 | alter database default tablespace USERS; 2 | 3 | CREATE USER oracle_enhanced IDENTIFIED BY oracle_enhanced; 4 | 5 | GRANT unlimited tablespace, create session, create table, create sequence, 6 | create procedure, create trigger, create view, create materialized view, 7 | create database link, create synonym, create type, ctxapp TO oracle_enhanced; 8 | 9 | CREATE USER oracle_enhanced_schema IDENTIFIED BY oracle_enhanced_schema; 10 | 11 | GRANT unlimited tablespace, create session, create table, create sequence, 12 | create procedure, create trigger, create view, create materialized view, 13 | create database link, create synonym, create type, ctxapp TO oracle_enhanced_schema; 14 | 15 | CREATE USER arunit IDENTIFIED BY arunit; 16 | 17 | GRANT unlimited tablespace, create session, create table, create sequence, 18 | create procedure, create trigger, create view, create materialized view, 19 | create database link, create synonym, create type, ctxapp TO arunit; 20 | 21 | CREATE USER arunit2 IDENTIFIED BY arunit2; 22 | 23 | GRANT unlimited tablespace, create session, create table, create sequence, 24 | create procedure, create trigger, create view, create materialized view, 25 | create database link, create synonym, create type, ctxapp TO arunit2; 26 | 27 | CREATE USER ruby IDENTIFIED BY oci8; 28 | GRANT connect, resource, create view,create synonym TO ruby; 29 | GRANT EXECUTE ON dbms_lock TO ruby; 30 | GRANT CREATE VIEW TO ruby; 31 | GRANT unlimited tablespace to ruby; 32 | -------------------------------------------------------------------------------- /test/cases/arel/visitors/oracle12_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../helper" 4 | 5 | module Arel 6 | module Visitors 7 | class Oracle12Test < Arel::Spec 8 | before do 9 | @visitor = Oracle12.new Table.engine.connection 10 | @table = Table.new(:users) 11 | end 12 | 13 | def compile(node) 14 | @visitor.accept(node, Collectors::SQLString.new).value 15 | end 16 | 17 | it "modified except to be minus" do 18 | left = Nodes::SqlLiteral.new("SELECT * FROM users WHERE age > 10") 19 | right = Nodes::SqlLiteral.new("SELECT * FROM users WHERE age > 20") 20 | sql = compile Nodes::Except.new(left, right) 21 | sql.must_be_like %{ 22 | ( SELECT * FROM users WHERE age > 10 MINUS SELECT * FROM users WHERE age > 20 ) 23 | } 24 | end 25 | 26 | it "generates select options offset then limit" do 27 | stmt = Nodes::SelectStatement.new 28 | stmt.offset = Nodes::Offset.new(1) 29 | stmt.limit = Nodes::Limit.new(10) 30 | sql = compile(stmt) 31 | sql.must_be_like "SELECT OFFSET 1 ROWS FETCH FIRST 10 ROWS ONLY" 32 | end 33 | 34 | describe "locking" do 35 | it "generates ArgumentError if limit and lock are used" do 36 | stmt = Nodes::SelectStatement.new 37 | stmt.limit = Nodes::Limit.new(10) 38 | stmt.lock = Nodes::Lock.new(Arel.sql("FOR UPDATE")) 39 | assert_raises ArgumentError do 40 | compile(stmt) 41 | end 42 | end 43 | 44 | it "defaults to FOR UPDATE when locking" do 45 | node = Nodes::Lock.new(Arel.sql("FOR UPDATE")) 46 | compile(node).must_be_like "FOR UPDATE" 47 | end 48 | end 49 | 50 | describe "Nodes::BindParam" do 51 | it "increments each bind param" do 52 | query = @table[:name].eq(Arel::Nodes::BindParam.new(1)) 53 | .and(@table[:id].eq(Arel::Nodes::BindParam.new(1))) 54 | compile(query).must_be_like %{ 55 | "users"."name" = :a1 AND "users"."id" = :a2 56 | } 57 | end 58 | end 59 | 60 | describe "Nodes::IsNotDistinctFrom" do 61 | it "should construct a valid generic SQL statement" do 62 | test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" 63 | compile(test).must_be_like %{ 64 | DECODE("users"."name", 'Aaron Patterson', 0, 1) = 0 65 | } 66 | end 67 | 68 | it "should handle column names on both sides" do 69 | test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] 70 | compile(test).must_be_like %{ 71 | DECODE("users"."first_name", "users"."last_name", 0, 1) = 0 72 | } 73 | end 74 | 75 | it "should handle nil" do 76 | @table = Table.new(:users) 77 | val = Nodes.build_quoted(nil, @table[:active]) 78 | sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) 79 | sql.must_be_like %{ "users"."name" IS NULL } 80 | end 81 | end 82 | 83 | describe "Nodes::IsDistinctFrom" do 84 | it "should handle column names on both sides" do 85 | test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] 86 | compile(test).must_be_like %{ 87 | DECODE("users"."first_name", "users"."last_name", 0, 1) = 1 88 | } 89 | end 90 | 91 | it "should handle nil" do 92 | @table = Table.new(:users) 93 | val = Nodes.build_quoted(nil, @table[:active]) 94 | sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) 95 | sql.must_be_like %{ "users"."name" IS NOT NULL } 96 | end 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /test/cases/arel/visitors/oracle_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../helper" 4 | 5 | module Arel 6 | module Visitors 7 | class OracleTest < Arel::Spec 8 | before do 9 | @visitor = Oracle.new Table.engine.connection 10 | @table = Table.new(:users) 11 | end 12 | 13 | def compile(node) 14 | @visitor.accept(node, Collectors::SQLString.new).value 15 | end 16 | 17 | it "modifies order when there is distinct and first value" do 18 | # *sigh* 19 | select = "DISTINCT foo.id, FIRST_VALUE(projects.name) OVER (foo) AS alias_0__" 20 | stmt = Nodes::SelectStatement.new 21 | stmt.cores.first.projections << Nodes::SqlLiteral.new(select) 22 | stmt.orders << Nodes::SqlLiteral.new("foo") 23 | sql = compile(stmt) 24 | sql.must_be_like %{ 25 | SELECT #{select} ORDER BY alias_0__ 26 | } 27 | end 28 | 29 | it "is idempotent with crazy query" do 30 | # *sigh* 31 | select = "DISTINCT foo.id, FIRST_VALUE(projects.name) OVER (foo) AS alias_0__" 32 | stmt = Nodes::SelectStatement.new 33 | stmt.cores.first.projections << Nodes::SqlLiteral.new(select) 34 | stmt.orders << Nodes::SqlLiteral.new("foo") 35 | 36 | sql = compile(stmt) 37 | sql2 = compile(stmt) 38 | sql.must_equal sql2 39 | end 40 | 41 | it "splits orders with commas" do 42 | # *sigh* 43 | select = "DISTINCT foo.id, FIRST_VALUE(projects.name) OVER (foo) AS alias_0__" 44 | stmt = Nodes::SelectStatement.new 45 | stmt.cores.first.projections << Nodes::SqlLiteral.new(select) 46 | stmt.orders << Nodes::SqlLiteral.new("foo, bar") 47 | sql = compile(stmt) 48 | sql.must_be_like %{ 49 | SELECT #{select} ORDER BY alias_0__, alias_1__ 50 | } 51 | end 52 | 53 | it "splits orders with commas and function calls" do 54 | # *sigh* 55 | select = "DISTINCT foo.id, FIRST_VALUE(projects.name) OVER (foo) AS alias_0__" 56 | stmt = Nodes::SelectStatement.new 57 | stmt.cores.first.projections << Nodes::SqlLiteral.new(select) 58 | stmt.orders << Nodes::SqlLiteral.new("NVL(LOWER(bar, foo), foo) DESC, UPPER(baz)") 59 | sql = compile(stmt) 60 | sql.must_be_like %{ 61 | SELECT #{select} ORDER BY alias_0__ DESC, alias_1__ 62 | } 63 | end 64 | 65 | describe "Nodes::SelectStatement" do 66 | describe "limit" do 67 | it "adds a rownum clause" do 68 | stmt = Nodes::SelectStatement.new 69 | stmt.limit = Nodes::Limit.new(10) 70 | sql = compile stmt 71 | sql.must_be_like %{ SELECT WHERE ROWNUM <= 10 } 72 | end 73 | 74 | it "is idempotent" do 75 | stmt = Nodes::SelectStatement.new 76 | stmt.orders << Nodes::SqlLiteral.new("foo") 77 | stmt.limit = Nodes::Limit.new(10) 78 | sql = compile stmt 79 | sql2 = compile stmt 80 | sql.must_equal sql2 81 | end 82 | 83 | it "creates a subquery when there is order_by" do 84 | stmt = Nodes::SelectStatement.new 85 | stmt.orders << Nodes::SqlLiteral.new("foo") 86 | stmt.limit = Nodes::Limit.new(10) 87 | sql = compile stmt 88 | sql.must_be_like %{ 89 | SELECT * FROM (SELECT ORDER BY foo ) WHERE ROWNUM <= 10 90 | } 91 | end 92 | 93 | it "creates a subquery when there is group by" do 94 | stmt = Nodes::SelectStatement.new 95 | stmt.cores.first.groups << Nodes::SqlLiteral.new("foo") 96 | stmt.limit = Nodes::Limit.new(10) 97 | sql = compile stmt 98 | sql.must_be_like %{ 99 | SELECT * FROM (SELECT GROUP BY foo ) WHERE ROWNUM <= 10 100 | } 101 | end 102 | 103 | it "creates a subquery when there is DISTINCT" do 104 | stmt = Nodes::SelectStatement.new 105 | stmt.cores.first.set_quantifier = Arel::Nodes::Distinct.new 106 | stmt.cores.first.projections << Nodes::SqlLiteral.new("id") 107 | stmt.limit = Arel::Nodes::Limit.new(10) 108 | sql = compile stmt 109 | sql.must_be_like %{ 110 | SELECT * FROM (SELECT DISTINCT id ) WHERE ROWNUM <= 10 111 | } 112 | end 113 | 114 | it "creates a different subquery when there is an offset" do 115 | stmt = Nodes::SelectStatement.new 116 | stmt.limit = Nodes::Limit.new(10) 117 | stmt.offset = Nodes::Offset.new(10) 118 | sql = compile stmt 119 | sql.must_be_like %{ 120 | SELECT * FROM ( 121 | SELECT raw_sql_.*, rownum raw_rnum_ 122 | FROM (SELECT ) raw_sql_ 123 | WHERE rownum <= 20 124 | ) 125 | WHERE raw_rnum_ > 10 126 | } 127 | end 128 | 129 | it "creates a subquery when there is limit and offset with BindParams" do 130 | stmt = Nodes::SelectStatement.new 131 | stmt.limit = Nodes::Limit.new(Nodes::BindParam.new(1)) 132 | stmt.offset = Nodes::Offset.new(Nodes::BindParam.new(1)) 133 | sql = compile stmt 134 | sql.must_be_like %{ 135 | SELECT * FROM ( 136 | SELECT raw_sql_.*, rownum raw_rnum_ 137 | FROM (SELECT ) raw_sql_ 138 | WHERE rownum <= (:a1 + :a2) 139 | ) 140 | WHERE raw_rnum_ > :a3 141 | } 142 | end 143 | 144 | it "is idempotent with different subquery" do 145 | stmt = Nodes::SelectStatement.new 146 | stmt.limit = Nodes::Limit.new(10) 147 | stmt.offset = Nodes::Offset.new(10) 148 | sql = compile stmt 149 | sql2 = compile stmt 150 | sql.must_equal sql2 151 | end 152 | end 153 | 154 | describe "only offset" do 155 | it "creates a select from subquery with rownum condition" do 156 | stmt = Nodes::SelectStatement.new 157 | stmt.offset = Nodes::Offset.new(10) 158 | sql = compile stmt 159 | sql.must_be_like %{ 160 | SELECT * FROM ( 161 | SELECT raw_sql_.*, rownum raw_rnum_ 162 | FROM (SELECT) raw_sql_ 163 | ) 164 | WHERE raw_rnum_ > 10 165 | } 166 | end 167 | end 168 | end 169 | 170 | it "modified except to be minus" do 171 | left = Nodes::SqlLiteral.new("SELECT * FROM users WHERE age > 10") 172 | right = Nodes::SqlLiteral.new("SELECT * FROM users WHERE age > 20") 173 | sql = compile Nodes::Except.new(left, right) 174 | sql.must_be_like %{ 175 | ( SELECT * FROM users WHERE age > 10 MINUS SELECT * FROM users WHERE age > 20 ) 176 | } 177 | end 178 | 179 | describe "locking" do 180 | it "defaults to FOR UPDATE when locking" do 181 | node = Nodes::Lock.new(Arel.sql("FOR UPDATE")) 182 | compile(node).must_be_like "FOR UPDATE" 183 | end 184 | end 185 | 186 | describe "Nodes::BindParam" do 187 | it "increments each bind param" do 188 | query = @table[:name].eq(Arel::Nodes::BindParam.new(1)) 189 | .and(@table[:id].eq(Arel::Nodes::BindParam.new(1))) 190 | compile(query).must_be_like %{ 191 | "users"."name" = :a1 AND "users"."id" = :a2 192 | } 193 | end 194 | end 195 | 196 | describe "Nodes::IsNotDistinctFrom" do 197 | it "should construct a valid generic SQL statement" do 198 | test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" 199 | compile(test).must_be_like %{ 200 | DECODE("users"."name", 'Aaron Patterson', 0, 1) = 0 201 | } 202 | end 203 | 204 | it "should handle column names on both sides" do 205 | test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] 206 | compile(test).must_be_like %{ 207 | DECODE("users"."first_name", "users"."last_name", 0, 1) = 0 208 | } 209 | end 210 | 211 | it "should handle nil" do 212 | @table = Table.new(:users) 213 | val = Nodes.build_quoted(nil, @table[:active]) 214 | sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) 215 | sql.must_be_like %{ "users"."name" IS NULL } 216 | end 217 | end 218 | 219 | describe "Nodes::IsDistinctFrom" do 220 | it "should handle column names on both sides" do 221 | test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] 222 | compile(test).must_be_like %{ 223 | DECODE("users"."first_name", "users"."last_name", 0, 1) = 1 224 | } 225 | end 226 | 227 | it "should handle nil" do 228 | @table = Table.new(:users) 229 | val = Nodes.build_quoted(nil, @table[:active]) 230 | sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) 231 | sql.must_be_like %{ "users"."name" IS NOT NULL } 232 | end 233 | end 234 | end 235 | end 236 | end 237 | --------------------------------------------------------------------------------