Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Introduce Oracle enhanced adapter with Rails

Yasuo Honda
September 19, 2014

Introduce Oracle enhanced adapter with Rails

A presentation at RubyKaigi 2014

Yasuo Honda

September 19, 2014
Tweet

More Decks by Yasuo Honda

Other Decks in Technology

Transcript

  1. Who am I • Yasuo Honda @yahonda • Oracle enhanced

    adapter maintener since Nov 2011 • Rails Contributor since September 2011
  2. What is Oracle enhanced adapter? • 3rd party ActiveRecord adapter

    • https://github.com/rsim/oracle-enhanced • Supports Rails 2.3* to Rails 4.1 • Rails 4 support started on the same day Rails 4.0.1 released • Rails 4.2 support is in progress, 79 failures/errors to fix • Want to release the same day Rails 4.2 released
  3. What is Oracle enhanced adapter? (cont) • Built in foreign

    key support • Data type difference mapping • “Legacy schemas” support
  4. "Legacy schemas" class Employee < ActiveRecord::Base # specify schema and

    table name self.table_name "hr.hr_employees" # specify primary key name self.primary_key "employee_id" # specify sequence name self.sequence_name "hr.hr_employee_s" # set which DATE columns should be converted to Ruby Date set_date_columns :hired_on, :birth_date_on # set which DATE columns should be converted to Ruby Time set_datetime_columns :last_login_time # set which VARCHAR2 columns should be converted to true and false set_boolean_columns :manager, :active # set which columns should be ignored in ActiveRecord ignore_table_columns :attribute1, :attribute2 end
  5. Optimizer and prepared statements • Prepared statement can • Save

    your CPU time to parse sql statements • Make use of shared pool area • Prevent SQL injection • ActiveRecord 3.1 implemented prepared statements
  6. Optimizer and prepared statement(cont) • Usually a prepared statement chooses

    only one "generic" execution plan • 11g - Adaptive cursor sharing The adaptive cursor sharing feature enables a single statement that contains bind variables to use multiple execution plans. - Oracle® Database Performance Tuning Guide 11g Release 2 (11.2)
  7. How does it work with ActiveRecord? • ActiveRecord supports prepared

    statements since Rails 3.1, but not every method • Every sql statement will be modified to prepared statement when you use oracle enhanced adapter • Adaptive cursor sharing may address side effects of prepared statements by having multiple execution plans for one sql statement based on bind values
  8. Example based on database usage survey $ rails generate model

    User database:string $ rails generate migration AddIndexToUser class AddIndexToUser < ActiveRecord::Migration def change add_index :users, :database end end $ rake db:migrate
  9. Generate database usage data insert into users values (users_seq.nextval,'Oracle',sysdate,sysdate); begin

    for i in 1..50000 loop insert into users values (users_seq.nextval,dbms_random.string('X',10), sysdate,sysdate); end loop; end; / begin for i in 1..50000 loop insert into users values (users_seq.nextval,'PostgreSQL',sysdate, sysdate); end loop; end; /
  10. User.where(:database => 'Oracle').last.id • ActiveRecord SELECT "USERS".* FROM "USERS" WHERE

    "USERS"."DATABASE" = 'Oracle' ORDER BY "USERS"."ID" DESC FETCH FIRST 1 ROWS ONLY • Oracle modifies sql statement SELECT "USERS".* FROM "USERS" WHERE "USERS"."DATABASE" = :"SYS_B_0" ORDER BY "USERS"."ID" DESC FETCH FIRST :"SYS_B_1" ROWS ONLY • Execution plan using adaptive cursor sharing TABLE ACCESS BY INDEX ROWID BATCHED USERS INDEX RANGE SCAN INDEX_USERS_ON_DATABASE
  11. User.where(:database => 'PostgreSQL').last.id • ActiveRecord SELECT "USERS".* FROM "USERS" WHERE

    "USERS"."DATABASE" = 'PostgreSQL' ORDER BY "USERS"."ID" DESC FETCH FIRST 1 ROWS ONLY • Oracle modifies sql statement SELECT "USERS".* FROM "USERS" WHERE "USERS"."DATABASE" = :"SYS_B_0" ORDER BY "USERS"."ID" DESC FETCH FIRST :"SYS_B_1" ROWS ONLY • Execution plan using adaptive cursor sharing TABLE ACCESS FULL USERS
  12. Any incompatibility with Rails? • 4-1-stable test results $ rake

    test_oracle 3965 runs, 11064 assertions, 2 failures, 4 errors, 0 skips
  13. Known Restrictions • Empty string as NULL • Identifier length

    <= 30 byte • "id" needs to set explicitly • No limit in sub queries • No release savepoint • CLOB/BLOB require specific implementation • Absence of LIMIT, OFFSET
  14. Empty string as NULL $ ARCONN=oracle ruby -Itest test/cases/calculations_test.rb -n

    test_pluck_without_column_names 1) Failure: CalculationsTest#test_pluck_without_column_names [test/cases/calculations_test.rb:494]: --- expected +++ actual @@ -1 +1 @@ -[[1, "Firm", 1, nil, "37signals", nil, 1, nil, ""]] +[[1, "Firm", 1, nil, "37signals", nil, 1, nil, nil]] 1 runs, 1 assertions, 1 failures, 0 errors, 0 skips • "" expected but nil returned
  15. "Oracle Database currently treats a character value with a length

    of zero as null. However, this may not continue to be true in future releases" - Oracle® Database SQL Language Reference 12c Release 1 (12.1), July 2014
  16. “Oracle7 currently treats a character value with a length of

    zero as null. However, this may not continue to be true in future versions of Oracle7.” –Oracle7 Server SQL Reference Release 7.3, February 1996
  17. Identifier length <= 30 byte $ ARCONN=oracle ruby -Itest test/cases/associations/has_and_belongs_to_many_associations_test.rb

    \ -n test_join_table_alias 1) Error: HasAndBelongsToManyAssociationsTest#test_join_table_alias: ActiveRecord::StatementInvalid: OCIError: ORA-00972: identifier is too long: SELECT "DEVELOPERS"."ID" AS t0_r0, "DEVELOPERS"."NAME" AS t0_r1, .. WHERE (projects_developers_projects_join.joined_on IS NOT NULL) • irb(main):001:0> 'projects_developers_projects_join'.size => 33
  18. 128 byte? No, not yet. SQL> desc all_objects Name Null?

    Type ----------------------------------------- -------- ---------------------------- OWNER NOT NULL VARCHAR2(128) OBJECT_NAME NOT NULL VARCHAR2(128) SUBOBJECT_NAME VARCHAR2(128) OBJECT_ID NOT NULL NUMBER … • Only data dictionary length changed from 30 to 128 in 12c
  19. "id" needs to set explicitly $ ARCONN=oracle ruby -Itest test/cases/serialized_attribute_test.rb

    \ -n test_json_db_null 1) Error: SerializedAttributeTest#test_json_db_null: ActiveRecord::StatementInvalid: OCIError: ORA-01400: cannot insert NULL into ("ARUNIT"."TOPICS"."ID"): INSERT INTO topics (content) VALUES(NULL) • Needs to change as follows INSERT INTO topics (id, content) VALUES(topics_seq.nextval, NULL)
  20. No limit in sub queries $ ARCONN=oracle ruby -Itest test/cases/associations/eager_test.rb

    \ -n test_including_association_based_on_sql_condition_and_no_database_column 1) Error: EagerAssociationTest#test_including_association_based_on_sql_condition_and_no_database_column: ActiveRecord::StatementInvalid: OCIError: ORA-00907: missing right parenthesis: SELECT * FROM (SELECT owners.*, ( select p.pet_id from pets p where p.owner_id = owners.owner_id order by p.name desc limit 1 ) as last_pet_id FROM "OWNERS" ORDER BY "OWNERS"."OWNER_ID" ASC) WHERE ROWNUM <= 1 stmt.c:230:in oci8lib_210.so • This error message is kind of misleading...
  21. No release savepoint $ ARCONN=oracle ruby -Itest test/cases/transactions_test.rb \ -n

    test_releasing_named_savepoints 1) Failure: TransactionTest#test_releasing_named_savepoints [test/cases/transactions_test.rb:408]: ActiveRecord::StatementInvalid expected but nothing was raised. • Do you need release savepoint?
  22. Rails 4.0 and 4.1 • AbstractAdapter::SchemaCreation behavior changed between beta

    and RC. • columns_for_distinct(columns, orders) • rails/rails#6792 • rails/arel#211 • I've learned Visitor patterns and how to use debugger
  23. Rails 4.2 • Adequate Record • Removed BindSubstitution Class •

    Refactoring Columns • initialize_type_map • "Legacy Schema" support... • Foreign Key support • A lot of things to learn
  24. Oracle 12c features for ActiveRecord • Better Top-N query support

    • VARCHAR2 length <= 32k • IDENTITY data type • JSON data type
  25. Better Top-N query support • Mandatory features for pagination •

    Experimental implementation based on arel 5-0-stable • https://github.com/yahonda/arel/tree/sandbox_limit • Removed order_hacks from Arel • Will open a pull request to rails/arel
  26. Current Arel visitors/oracle.rb def visit_Arel_Nodes_SelectStatement o, collector o = order_hacks(o)

    …… if o.limit o = o.dup limit = o.limit.expr collector << "SELECT * FROM (" collector = super(o, collector) collector << ") WHERE ROWNUM <= " return visit limit, collector end …… super end • Since ROWNUM is set before order by executed
  27. Updated Arel visitors/oracle.rb def visit_Arel_Nodes_SelectStatement o, a if o.limit &&

    o.lock o = o.dup o.limit = [] end super end • select for update statement not compatible with LIMIT
  28. Current test visitors/test_oracle.rb it 'modifies order when there is distinct

    and first value' do # *sigh* select = "DISTINCT foo.id, FIRST_VALUE(projects.name) OVER (foo) AS alias_0__" stmt = Nodes::SelectStatement.new stmt.cores.first.projections << Nodes::SqlLiteral.new(select) stmt.orders << Nodes::SqlLiteral.new('foo') sql = @visitor.accept(stmt) sql.must_be_like %{ SELECT #{select} ORDER BY alias_0__ } end
  29. Entrant.order("id ASC").limit(2) • Current Oracle SELECT * FROM (SELECT "ENTRANTS".*

    FROM "ENTRANTS" ORDER BY id ASC) WHERE ROWNUM <= 2 • Current PostgreSQL SELECT "entrants".* FROM "entrants" ORDER BY id ASC LIMIT 2 • Oracle with better Top-N support SELECT "ENTRANTS".* FROM "ENTRANTS" ORDER BY id ASC FETCH FIRST 2 ROWS ONLY
  30. Ready to support • Absence of LIMIT, OFFSET by better

    Top-N query • "id" needs to set explicitly by IDENTITY datatype • CLOB/BLOB require specific implementation by 32k VARCHAR2
  31. Wish list • Identifier length <= 128 byte • Empty

    string should not be NULL • Release savepoint support
  32. rails-dev-box-runs-oracle • Fork of rails-dev-box • http://github.com/yahonda/rails-dev-box-runs-oracle • Add support

    to install Oracle 11g XE using Puppet • Work with x86-64 Vagrant and Virtualbox