diff --git a/ext/oci8/bind.c b/ext/oci8/bind.c index 624f0d7c..824eb46b 100644 --- a/ext/oci8/bind.c +++ b/ext/oci8/bind.c @@ -633,20 +633,11 @@ static VALUE oci8_bind_get(VALUE self) return data_type->get(obind, (void*)((size_t)obind->valuep + obind->alloc_sz * idx), null_structp); } -static VALUE oci8_bind_get_data(int argc, VALUE *argv, VALUE self) +static VALUE oci8_bind_get_data(VALUE self) { oci8_bind_t *obind = TO_BIND(self); - VALUE index; - rb_scan_args(argc, argv, "01", &index); - if (!NIL_P(index)) { - ub4 idx = NUM2UINT(index); - if (idx >= obind->maxar_sz) { - rb_raise(rb_eRuntimeError, "data index is too big. (%u for %u)", idx, obind->maxar_sz); - } - obind->curar_idx = idx; - return rb_funcall(self, oci8_id_get, 0); - } else if (obind->maxar_sz == 0) { + if (obind->maxar_sz == 0) { obind->curar_idx = 0; return rb_funcall(self, oci8_id_get, 0); } else { @@ -817,7 +808,7 @@ void Init_oci8_bind(VALUE klass) rb_define_method(cOCI8BindTypeBase, "initialize", oci8_bind_initialize, 4); rb_define_method(cOCI8BindTypeBase, "get", oci8_bind_get, 0); rb_define_method(cOCI8BindTypeBase, "set", oci8_bind_set, 1); - rb_define_private_method(cOCI8BindTypeBase, "get_data", oci8_bind_get_data, -1); + rb_define_private_method(cOCI8BindTypeBase, "get_data", oci8_bind_get_data, 0); rb_define_private_method(cOCI8BindTypeBase, "set_data", oci8_bind_set_data, 1); rb_define_singleton_method(klass, "initial_chunk_size", get_initial_chunk_size, 0); diff --git a/ext/oci8/lob.c b/ext/oci8/lob.c index 00be3f7d..4af174d0 100644 --- a/ext/oci8/lob.c +++ b/ext/oci8/lob.c @@ -639,9 +639,10 @@ static VALUE oci8_lob_read(int argc, VALUE *argv, VALUE self) { oci8_lob_t *lob = TO_LOB(self); oci8_svcctx_t *svcctx = check_svcctx(lob); + ub8 lob_length; + ub8 read_len; ub8 pos = lob->pos; - long strbufsiz = 512; - ub8 sz; + long strbufsiz; ub8 byte_amt; ub8 char_amt; sword rv; @@ -651,21 +652,36 @@ static VALUE oci8_lob_read(int argc, VALUE *argv, VALUE self) ub1 piece = OCI_FIRST_PIECE; rb_scan_args(argc, argv, "01", &size); + lob_length = oci8_lob_get_length(lob); + if (lob_length == 0 && NIL_P(size)) { + return rb_usascii_str_new("", 0); + } + if (lob_length <= pos) /* EOF */ + return Qnil; if (NIL_P(size)) { - sz = UB4MAXVAL; + read_len = lob_length - pos; } else { - sz = NUM2ULL(size); - } - if (lob->state == S_BFILE_CLOSE) { - open_bfile(svcctx, lob, errhp); + ub8 sz = NUM2ULL(size); + read_len = MIN(sz, lob_length - pos); } -read_more_data: if (lob->lobtype == OCI_TEMP_CLOB) { byte_amt = 0; - char_amt = sz; + char_amt = read_len; + if (oci8_nls_ratio == 1) { + strbufsiz = MIN(read_len, ULONG_MAX); + } else { + strbufsiz = MIN(read_len + read_len / 8, ULONG_MAX); + } + if (strbufsiz <= 10) { + strbufsiz = 10; + } } else { - byte_amt = sz; + byte_amt = read_len; char_amt = 0; + strbufsiz = MIN(read_len, ULONG_MAX); + } + if (lob->state == S_BFILE_CLOSE) { + open_bfile(svcctx, lob, errhp); } do { VALUE strbuf = rb_str_buf_new(strbufsiz); @@ -695,24 +711,15 @@ static VALUE oci8_lob_read(int argc, VALUE *argv, VALUE self) } rb_str_set_len(strbuf, byte_amt); rb_ary_push(v, strbuf); - if (strbufsiz < 128 * 1024 * 1024) { - strbufsiz *= 2; - } } while (rv == OCI_NEED_DATA); - if (NIL_P(size) && pos - lob->pos == sz) { - lob->pos = pos; - piece = OCI_FIRST_PIECE; - goto read_more_data; + if (pos >= lob_length) { + bfile_close(lob); } lob->pos = pos; switch (RARRAY_LEN(v)) { case 0: - if (NIL_P(size) && pos == 0) { - return rb_usascii_str_new("", 0); - } else { - return Qnil; - } + return Qnil; case 1: v = RARRAY_AREF(v, 0); break; diff --git a/ext/oci8/object.c b/ext/oci8/object.c index c752e62e..79c70cd8 100644 --- a/ext/oci8/object.c +++ b/ext/oci8/object.c @@ -680,53 +680,43 @@ static VALUE oci8_named_collection_alloc(VALUE klass) return oci8_allocate_typeddata(klass, &oci8_named_collection_data_type); } -typedef struct { - oci8_bind_t bind; - VALUE *obj; -} bind_named_type_t; - static void bind_named_type_mark(oci8_base_t *base) { - bind_named_type_t *bnt = (bind_named_type_t *)base; + oci8_bind_t *obind = (oci8_bind_t *)base; + oci8_hp_obj_t *oho = (oci8_hp_obj_t *)obind->valuep; - if (bnt->obj != NULL) { + if (oho != NULL) { ub4 idx = 0; do { - rb_gc_mark(bnt->obj[idx]); - } while (++idx < bnt->bind.maxar_sz); + rb_gc_mark(oho[idx].obj); + } while (++idx < obind->maxar_sz); } - rb_gc_mark(bnt->bind.tdo); + rb_gc_mark(obind->tdo); } static void bind_named_type_free(oci8_base_t *base) { - bind_named_type_t *bnt = (bind_named_type_t *)base; - void **hp = (void **)bnt->bind.valuep; + oci8_bind_t *obind = (oci8_bind_t *)base; + oci8_hp_obj_t *oho = (oci8_hp_obj_t *)obind->valuep; - if (hp != NULL) { + if (oho != NULL) { ub4 idx = 0; do { - if (hp[idx] != NULL) { - OCIObjectFree(oci8_envhp, oci8_errhp, hp[idx], OCI_DEFAULT); - hp[idx] = NULL; + if (oho[idx].hp != NULL) { + OCIObjectFree(oci8_envhp, oci8_errhp, oho[idx].hp, OCI_DEFAULT); + oho[idx].hp = NULL; } - } while (++idx < bnt->bind.maxar_sz); - } - if (bnt->obj != NULL) { - xfree(bnt->obj); - bnt->obj = NULL; + } while (++idx < obind->maxar_sz); } oci8_bind_free(base); } static VALUE bind_named_type_get(oci8_bind_t *obind, void *data, void *null_struct) { - bind_named_type_t *bnt = (bind_named_type_t *)obind; - ub4 idx = obind->curar_idx; - - return bnt->obj[idx]; + oci8_hp_obj_t *oho = (oci8_hp_obj_t *)data; + return oho->obj; } NORETURN(static void bind_named_type_set(oci8_bind_t *obind, void *data, void **null_structp, VALUE val)); @@ -738,12 +728,10 @@ static void bind_named_type_set(oci8_bind_t *obind, void *data, void **null_stru static void bind_named_type_init(oci8_bind_t *obind, VALUE svc, VALUE val, VALUE length) { - bind_named_type_t *bnt = (bind_named_type_t *)obind; VALUE tdo_obj = length; obind->value_sz = sizeof(void*); - obind->alloc_sz = sizeof(void*); - bnt->obj = xcalloc(sizeof(VALUE), obind->maxar_sz ? obind->maxar_sz : 1); + obind->alloc_sz = sizeof(oci8_hp_obj_t); CHECK_TDO(tdo_obj); RB_OBJ_WRITE(obind->base.self, &obind->tdo, tdo_obj); @@ -751,8 +739,7 @@ static void bind_named_type_init(oci8_bind_t *obind, VALUE svc, VALUE val, VALUE static void bind_named_type_init_elem(oci8_bind_t *obind, VALUE svc) { - bind_named_type_t *bnt = (bind_named_type_t *)obind; - void **hp = (void **)obind->valuep; + oci8_hp_obj_t *oho = (oci8_hp_obj_t *)obind->valuep; oci8_base_t *tdo = DATA_PTR(obind->tdo); OCITypeCode tc = OCITypeTypeCode(oci8_envhp, oci8_errhp, tdo->hp.tdo); VALUE klass = Qnil; @@ -770,14 +757,14 @@ static void bind_named_type_init_elem(oci8_bind_t *obind, VALUE svc) } svcctx = oci8_get_svcctx(svc); do { - bnt->obj[idx] = rb_class_new_instance(0, NULL, klass); - RB_OBJ_WRITTEN(obind->base.self, Qundef, bnt->obj[idx]); - obj = DATA_PTR(bnt->obj[idx]); - RB_OBJ_WRITE(bnt->obj[idx], &obj->tdo, obind->tdo); - obj->instancep = (char**)&hp[idx]; + oho[idx].obj = rb_class_new_instance(0, NULL, klass); + RB_OBJ_WRITTEN(obind->base.self, Qundef, oho[idx].obj); + obj = DATA_PTR(oho[idx].obj); + RB_OBJ_WRITE(oho[idx].obj, &obj->tdo, obind->tdo); + obj->instancep = (char**)&oho[idx].hp; obj->null_structp = (char**)&obind->u.null_structs[idx]; oci8_link_to_parent(&obj->base, &obind->base); - RB_OBJ_WRITTEN(bnt->obj[idx], Qundef, obind->base.self); + RB_OBJ_WRITTEN(oho[idx].obj, Qundef, obind->base.self); chker2(OCIObjectNew(oci8_envhp, oci8_errhp, svcctx->base.hp.svc, tc, tdo->hp.tdo, NULL, OCI_DURATION_SESSION, TRUE, (dvoid**)obj->instancep), &svcctx->base); @@ -819,7 +806,7 @@ static const oci8_bind_data_type_t bind_named_type_data_type = { #endif }, bind_named_type_free, - sizeof(bind_named_type_t) + sizeof(oci8_bind_t) }, bind_named_type_get, bind_named_type_set, diff --git a/ext/oci8/stmt.c b/ext/oci8/stmt.c index 00f1b60a..dbe9e011 100644 --- a/ext/oci8/stmt.c +++ b/ext/oci8/stmt.c @@ -15,8 +15,7 @@ static VALUE cOCIStmt; typedef struct { oci8_base_t base; VALUE svc; - char use_stmt_release; - char end_of_fetch; + int use_stmt_release; } oci8_stmt_t; static void oci8_stmt_mark(oci8_base_t *base) @@ -261,7 +260,6 @@ static VALUE oci8_stmt_execute(VALUE self, VALUE iteration_count) oci8_stmt_t *stmt = TO_STMT(self); oci8_svcctx_t *svcctx = oci8_get_svcctx(stmt->svc); - stmt->end_of_fetch = 0; chker3(oci8_call_stmt_execute(svcctx, stmt, NUM2UINT(iteration_count), svcctx->is_autocommit ? OCI_COMMIT_ON_SUCCESS : OCI_DEFAULT), &stmt->base, stmt->base.hp.stmt); @@ -269,7 +267,7 @@ static VALUE oci8_stmt_execute(VALUE self, VALUE iteration_count) } /* - * @overload __fetch(connection, max_rows) + * @overload __fetch(connection) * * Fetches one row and set the result to @define_handles. * This is called by private methods of OCI8::Cursor. @@ -279,18 +277,13 @@ static VALUE oci8_stmt_execute(VALUE self, VALUE iteration_count) * * @private */ -static VALUE oci8_stmt_fetch(VALUE self, VALUE svc, VALUE max_rows) +static VALUE oci8_stmt_fetch(VALUE self, VALUE svc) { oci8_stmt_t *stmt = TO_STMT(self); oci8_svcctx_t *svcctx = oci8_get_svcctx(svc); sword rv; oci8_bind_t *obind; const oci8_bind_data_type_t *data_type; - ub4 nrows = NUM2UINT(max_rows); - - if (stmt->end_of_fetch) { - return Qnil; - } if (stmt->base.children != NULL) { obind = (oci8_bind_t *)stmt->base.children; @@ -300,22 +293,16 @@ static VALUE oci8_stmt_fetch(VALUE self, VALUE svc, VALUE max_rows) if (data_type->pre_fetch_hook != NULL) { data_type->pre_fetch_hook(obind, stmt->svc); } - if (nrows > 1 && nrows != obind->maxar_sz) { - rb_raise(rb_eRuntimeError, "fetch size (%u) != define-handle size %u", nrows, obind->maxar_sz); - } } obind = (oci8_bind_t *)obind->base.next; } while (obind != (oci8_bind_t*)stmt->base.children); } - rv = OCIStmtFetch_nb(svcctx, stmt->base.hp.stmt, oci8_errhp, nrows, OCI_FETCH_NEXT, OCI_DEFAULT); + rv = OCIStmtFetch_nb(svcctx, stmt->base.hp.stmt, oci8_errhp, 1, OCI_FETCH_NEXT, OCI_DEFAULT); if (rv == OCI_NO_DATA) { - stmt->end_of_fetch = 1; - } else { - chker3(rv, &svcctx->base, stmt->base.hp.stmt); + return Qfalse; } - chker2(OCIAttrGet(stmt->base.hp.stmt, OCI_HTYPE_STMT, &nrows, 0, OCI_ATTR_ROWS_FETCHED, oci8_errhp), - &svcctx->base); - return nrows ? UINT2NUM(nrows) : Qnil; + chker3(rv, &svcctx->base, stmt->base.hp.stmt); + return Qtrue; } /* @@ -440,7 +427,7 @@ void Init_oci8_stmt(VALUE cOCI8) rb_define_private_method(cOCIStmt, "__define", oci8_define_by_pos, 2); rb_define_private_method(cOCIStmt, "__bind", oci8_bind, 2); rb_define_private_method(cOCIStmt, "__execute", oci8_stmt_execute, 1); - rb_define_private_method(cOCIStmt, "__fetch", oci8_stmt_fetch, 2); + rb_define_private_method(cOCIStmt, "__fetch", oci8_stmt_fetch, 1); rb_define_private_method(cOCIStmt, "__paramGet", oci8_stmt_get_param, 1); rb_define_method(cOCIStmt, "rowid", oci8_stmt_get_rowid, 0); diff --git a/lib/oci8/bindtype.rb b/lib/oci8/bindtype.rb index 6151c9db..e86bb96e 100644 --- a/lib/oci8/bindtype.rb +++ b/lib/oci8/bindtype.rb @@ -245,13 +245,18 @@ def self.create(con, val, param, max_array_size) # datatype type size prec scale # ------------------------------------------------- # CLOB SQLT_CLOB 4000 0 0 -OCI8::BindType::Mapping[:clob] = OCI8::BindType::CLOB -OCI8::BindType::Mapping[:nclob] = OCI8::BindType::NCLOB +# NCLOB SQLT_CLOB 4000 0 0 +# Default: Fetch as String using SQLT_CHR (fast, max 2GB) +# See: OCI8::lob_fetch_mode +OCI8::BindType::Mapping[:clob] = OCI8::BindType::Long +OCI8::BindType::Mapping[:nclob] = OCI8::BindType::Long # datatype type size prec scale # ------------------------------------------------- # BLOB SQLT_BLOB 4000 0 0 -OCI8::BindType::Mapping[:blob] = OCI8::BindType::BLOB +# Default: Fetch as binary String using SQLT_CHR (fast, max 2GB) +# See: OCI8::lob_fetch_mode +OCI8::BindType::Mapping[:blob] = OCI8::BindType::LongRaw # datatype type size prec scale # ------------------------------------------------- diff --git a/lib/oci8/cursor.rb b/lib/oci8/cursor.rb index 966c65e1..66092ec8 100644 --- a/lib/oci8/cursor.rb +++ b/lib/oci8/cursor.rb @@ -25,11 +25,7 @@ def initialize(conn, sql = nil) @names = nil @con = conn @max_array_size = nil - @fetch_array_size = nil - @rowbuf_size = 0 - @rowbuf_index = 0 __initialize(conn, sql) # Initialize the internal C structure. - self.prefetch_rows = conn.instance_variable_get(:@prefetch_rows) end # explicitly indicate the date type of fetched value. run this @@ -42,7 +38,7 @@ def initialize(conn, sql = nil) # cursor.define(2, Time) # fetch the second column as Time. # cursor.exec() def define(pos, type, length = nil) - bindobj = make_bind_object({:type => type, :length => length}, @fetch_array_size || 1) + bindobj = make_bind_object(:type => type, :length => length) __define(pos, bindobj) if old = @define_handles[pos - 1] old.send(:free) @@ -130,8 +126,6 @@ def exec(*bindvars) when :select_stmt __execute(0) define_columns() if @column_metadata.size == 0 - @rowbuf_size = 0 - @rowbuf_index = 0 @column_metadata.size else __execute(1) @@ -390,7 +384,6 @@ def keys # @param [Integer] rows The number of rows to be prefetched def prefetch_rows=(rows) attr_set_ub4(11, rows) # OCI_ATTR_PREFETCH_ROWS(11) - @prefetch_rows = rows end if OCI8::oracle_client_version >= ORAVER_12_1 @@ -475,7 +468,7 @@ def type private - def make_bind_object(param, fetch_array_size = nil) + def make_bind_object(param) case param when Hash key = param[:type] @@ -517,37 +510,22 @@ def make_bind_object(param, fetch_array_size = nil) OCI8::BindType::Mapping[key] = bindclass if bindclass end raise "unsupported datatype: #{key}" if bindclass.nil? - bindclass.create(@con, val, param, fetch_array_size || max_array_size) + bindclass.create(@con, val, param, max_array_size) end - @@use_array_fetch = false - def define_columns # http://docs.oracle.com/cd/E11882_01/appdev.112/e10646/ociaahan.htm#sthref5494 num_cols = attr_get_ub4(18) # OCI_ATTR_PARAM_COUNT(18) - @column_metadata = 1.upto(num_cols).collect do |i| - __paramGet(i) - end - if @define_handles.size == 0 - use_array_fetch = @@use_array_fetch - @column_metadata.each do |md| - case md.data_type - when :clob, :blob, :bfile - # Rows prefetching doesn't work for CLOB, BLOB and BFILE. - # Use array fetching to get more than one row in a network round trip. - use_array_fetch = true - end - end - @fetch_array_size = @prefetch_rows if use_array_fetch - end - @column_metadata.each_with_index do |md, i| - define_one_column(i + 1, md) unless @define_handles[i] + 1.upto(num_cols) do |i| + parm = __paramGet(i) + define_one_column(i, parm) unless @define_handles[i - 1] + @column_metadata[i - 1] = parm end num_cols end def define_one_column(pos, param) - bindobj = make_bind_object(param, @fetch_array_size || 1) + bindobj = make_bind_object(param) __define(pos, bindobj) @define_handles[pos - 1] = bindobj end @@ -562,33 +540,22 @@ def bind_params(*bindvars) end end - def fetch_row_internal - if @rowbuf_size && @rowbuf_size == @rowbuf_index - @rowbuf_size = __fetch(@con, @fetch_array_size || 1) - @rowbuf_index = 0 - end - @rowbuf_size - end - def fetch_one_row_as_array - if fetch_row_internal - ret = @define_handles.collect do |handle| - handle.send(:get_data, @rowbuf_index) + if __fetch(@con) + @define_handles.collect do |handle| + handle.send(:get_data) end - @rowbuf_index += 1 - ret else nil end end def fetch_one_row_as_hash - if fetch_row_internal + if __fetch(@con) ret = {} get_col_names.each_with_index do |name, idx| - ret[name] = @define_handles[idx].send(:get_data, @rowbuf_index) + ret[name] = @define_handles[idx].send(:get_data) end - @rowbuf_index += 1 ret else nil diff --git a/lib/oci8/oci8.rb b/lib/oci8/oci8.rb index c7b9bb86..076237c8 100644 --- a/lib/oci8/oci8.rb +++ b/lib/oci8/oci8.rb @@ -152,6 +152,7 @@ def initialize(*args) end @prefetch_rows = 100 + @lob_prefetch_size = 0 # 0 means use Oracle default (disabled) @username = nil end @@ -169,6 +170,7 @@ def parse(sql) # @private def parse_internal(sql) cursor = OCI8::Cursor.new(self, sql) + cursor.prefetch_rows = @prefetch_rows if @prefetch_rows cursor end @@ -304,7 +306,6 @@ def exec_internal(sql, *bindvars) # @return [Array] an array of first row. def select_one(sql, *bindvars) cursor = self.parse(sql) - cursor.prefetch_rows = 1 begin cursor.exec(*bindvars) row = cursor.fetch @@ -333,6 +334,27 @@ def prefetch_rows=(num) @prefetch_rows = num end + # Sets the LOB prefetch size in bytes. Only used when lob_fetch_mode is :locator. + # i.e. sets OCI_ATTR_DEFAULT_LOBPREFETCH_SIZE on the session handle. + # The default value is 0 (disabled). + # + # When set to a non-zero value (e.g., 65536 for 64KB), Oracle prefetches + # LOB data along with the row if the LOB size is <= this value. + # This reduces network round trips when fetching small to medium LOBs. + # + # This is a session-wide setting that applies to all LOB columns fetched + # from this connection. + # + # It has no effect when lob_fetch_mode is :long_as_string (the default). + # + # @param [Integer] size prefetch size in bytes (0 to disable) + # @see OCI8::lob_fetch_mode= + # @note Requires Oracle 11g or later + def lob_prefetch_size=(size) + @lob_prefetch_size = size + @session_handle.send(:attr_set_ub4, 438, size) + end + # @private def inspect "#" @@ -370,6 +392,48 @@ def self.client_charset_name @@client_charset_name end + # Returns the current LOB fetch mode. + # + # @return [Symbol] :long_as_string or :locator + # @see lob_fetch_mode= + def self.lob_fetch_mode + @@lob_fetch_mode ||= :long_as_string + end + + # Sets the LOB fetch mode. + # + # - +:long_as_string+ (default): Fetch LOBs as Strings using + # Runtime Data Allocation and Piecewise Operations in OCI. + # Fastest and most efficient fetch but limited to 2GB LOBs. + # + # - +:locator+: Fetch LOB locators (OCI8::CLOB/BLOB objects). + # Calls OCILobRead2() on the lob fields individually. + # Required for LOBs > 2GB, random access, or read/write operations. + # @conn.lob_prefetch_size may reduce network roundtrips but my + # unscientific testing only showed performance degradation. + # + # @param [Symbol] mode :long_as_string or :locator + # @return [Symbol] the mode that was set + # @see https://github.com/oracle/odpi/issues/163 + def self.lob_fetch_mode=(mode) + unless [:long_as_string, :locator].include?(mode) + raise ArgumentError, "lob_fetch_mode must be :long_as_string or :locator" + end + + case mode + when :long_as_string + OCI8::BindType::Mapping[:clob] = OCI8::BindType::Long + OCI8::BindType::Mapping[:nclob] = OCI8::BindType::Long + OCI8::BindType::Mapping[:blob] = OCI8::BindType::LongRaw + when :locator + OCI8::BindType::Mapping[:clob] = OCI8::BindType::CLOB + OCI8::BindType::Mapping[:nclob] = OCI8::BindType::NCLOB + OCI8::BindType::Mapping[:blob] = OCI8::BindType::BLOB + end + + @@lob_fetch_mode = mode + end + if OCI8.oracle_client_version >= OCI8::ORAVER_11_1 # Returns send timeout in seconds. # Zero means no timeout. diff --git a/test/test_clob.rb b/test/test_clob.rb index 748d4b86..13385ffd 100644 --- a/test/test_clob.rb +++ b/test/test_clob.rb @@ -6,10 +6,17 @@ class TestCLob < Minitest::Test def setup @conn = get_oci8_connection + # This test needs LOB locators for read/write operations + OCI8.lob_fetch_mode = :locator drop_table('test_table') @conn.exec('CREATE TABLE test_table (filename VARCHAR2(40), content CLOB)') end + def teardown + # Restore default mode + OCI8.lob_fetch_mode = :long_as_string if @conn + end + def test_insert filename = File.basename($lobfile) @conn.exec("DELETE FROM test_table WHERE filename = :1", filename) diff --git a/test/test_dbi_clob.rb b/test/test_dbi_clob.rb index b2c7aa26..fe092087 100644 --- a/test/test_dbi_clob.rb +++ b/test/test_dbi_clob.rb @@ -6,10 +6,17 @@ class TestDbiCLob < Minitest::Test def setup @dbh = get_dbi_connection() + # This test needs LOB locators for read/write operations + OCI8.lob_fetch_mode = :locator drop_table('test_table') @dbh.execute('CREATE TABLE test_table (filename VARCHAR2(40), content CLOB)') end + def teardown + # Restore default mode + OCI8.lob_fetch_mode = :long_as_string if @dbh + end + def test_insert filename = File.basename($lobfile) diff --git a/test/test_encoding.rb b/test/test_encoding.rb index 888d96ab..4195f155 100644 --- a/test/test_encoding.rb +++ b/test/test_encoding.rb @@ -4,6 +4,13 @@ class TestEncoding < Minitest::Test def setup @conn = get_oci8_connection + # This test needs LOB locators for read/write operations + OCI8.lob_fetch_mode = :locator + end + + def teardown + # Restore default mode + OCI8.lob_fetch_mode = :long_as_string if @conn end def test_select diff --git a/test/test_large_lob.rb b/test/test_large_lob.rb new file mode 100644 index 00000000..6dfd2ea4 --- /dev/null +++ b/test/test_large_lob.rb @@ -0,0 +1,99 @@ +#!/usr/bin/env ruby +# +# Test to verify that 512KB LOBs can be fetched using both :long_as_string and :locator modes +# + +require 'oci8' +require_relative 'config' + +class TestLargeLob < Minitest::Test + def setup + @conn = get_oci8_connection + @saved_lob_fetch_mode = OCI8.lob_fetch_mode + drop_table('test_large_lob') + @conn.exec(<<-SQL) + CREATE TABLE test_large_lob ( + id NUMBER, + clob_data CLOB, + blob_data BLOB + ) + SQL + end + + def teardown + return unless @conn + + OCI8.lob_fetch_mode = @saved_lob_fetch_mode + drop_table('test_large_lob') + @conn.logoff + end + + def test_512kb_lob_roundtrip_with_long_interface + size_kb = 512 + clob_data = generate_text_data(size_kb) + blob_data = generate_binary_data(size_kb) + insert_lob_row(1, clob_data, blob_data) + + # Fetch using :long_as_string mode (LONG interface) + OCI8.lob_fetch_mode = :long_as_string + cursor = @conn.exec("SELECT id, clob_data, blob_data FROM test_large_lob WHERE id = 1") + row = cursor.fetch + cursor.close + + # Verify we got String objects with correct data + assert_equal 1, row[0] + assert_instance_of String, row[1], "CLOB should be fetched as String" + assert_instance_of String, row[2], "BLOB should be fetched as String" + assert_equal size_kb * 1024, row[1].size, "CLOB size should match" + assert_equal size_kb * 1024, row[2].size, "BLOB size should match" + assert_equal clob_data, row[1], "CLOB data should match (verifies chunk order)" + assert_equal blob_data, row[2], "BLOB data should match (verifies chunk order)" + end + + def test_512kb_lob_roundtrip_with_locator_mode + size_kb = 512 + clob_data = generate_text_data(size_kb) + blob_data = generate_binary_data(size_kb) + insert_lob_row(2, clob_data, blob_data) + + # Fetch using :locator mode (LOB locators) + OCI8.lob_fetch_mode = :locator + cursor = @conn.exec("SELECT id, clob_data, blob_data FROM test_large_lob WHERE id = 2") + row = cursor.fetch + cursor.close + + # Verify we got LOB locator objects + assert_equal 2, row[0] + assert_instance_of OCI8::CLOB, row[1], "CLOB should be fetched as locator" + assert_instance_of OCI8::BLOB, row[2], "BLOB should be fetched as locator" + + # Read from locators and verify data + fetched_clob_data = row[1].read + fetched_blob_data = row[2].read + assert_equal size_kb * 1024, fetched_clob_data.size, "CLOB size should match" + assert_equal size_kb * 1024, fetched_blob_data.size, "BLOB size should match" + assert_equal clob_data, fetched_clob_data, "CLOB data should match (verifies chunk order)" + assert_equal blob_data, fetched_blob_data, "BLOB data should match (verifies chunk order)" + end + + private + + def generate_text_data(size_kb, seed = 42) + # hex encoding: 1 byte -> 2 hex characters + binary_size_kb = (size_kb / 2.0).ceil + binary_data = generate_binary_data(binary_size_kb, seed) + hex_data = binary_data.unpack1('H*') + hex_data[0, size_bytes] # truncate to exact requested size + end + + def generate_binary_data(size_kb, seed = 42) + Random.new(seed).bytes(size_kb * 1024) + end + + def insert_lob_row(id, clob_data, blob_data) + cursor = @conn.parse("INSERT INTO test_large_lob VALUES (:1, :2, :3)") + cursor.exec(id, OCI8::CLOB.new(@conn, clob_data), OCI8::BLOB.new(@conn, blob_data)) + cursor.close + @conn.commit + end +end diff --git a/test/test_object.rb b/test/test_object.rb index b29ba7b8..6e648cae 100644 --- a/test/test_object.rb +++ b/test/test_object.rb @@ -59,10 +59,14 @@ class TestObj1 < Minitest::Test def setup @conn = get_oci8_connection + # This test needs LOB locators for read operations on object attributes + OCI8.lob_fetch_mode = :locator RbTestObj.default_connection = @conn end def teardown + # Restore default mode + OCI8.lob_fetch_mode = :long_as_string @conn.logoff end diff --git a/test/test_oci8.rb b/test/test_oci8.rb index 6c7d6f0b..55f52a6f 100644 --- a/test/test_oci8.rb +++ b/test/test_oci8.rb @@ -41,8 +41,6 @@ def test_rename ] def test_long_type - clob_bind_type = OCI8::BindType::Mapping[:clob] - blob_bind_type = OCI8::BindType::Mapping[:blob] initial_cunk_size = OCI8::BindType::Base.initial_chunk_size begin OCI8::BindType::Base.initial_chunk_size = 5 @@ -50,6 +48,8 @@ def test_long_type drop_table('test_table') ascii_enc = Encoding.find('US-ASCII') 0.upto(1) do |i| + # First part of test uses LOB locators + OCI8.lob_fetch_mode = :locator if i == 0 @conn.exec("CREATE TABLE test_table (id number(38), long_column long, clob_column clob)") cursor = @conn.parse('insert into test_table values (:1, :2, :3)') @@ -108,39 +108,35 @@ def test_long_type assert_nil(cursor.fetch) cursor.close - begin - OCI8::BindType::Mapping[:clob] = OCI8::BindType::Long - OCI8::BindType::Mapping[:blob] = OCI8::BindType::LongRaw - cursor = @conn.parse('SELECT * from test_table order by id') - cursor.exec - LONG_TEST_DATA.each_with_index do |data, index| - row = cursor.fetch - assert_equal(index, row[0]) - if data.nil? - assert_nil(row[1]) - assert_nil(row[2]) - elsif data.empty? - # '' is inserted to the long or long raw column as null. - assert_nil(row[1]) - # '' is inserted to the clob or blob column as an empty clob. - # However it is fetched as nil. - assert_nil(row[2]) - else - assert_equal(data, row[1]) - assert_equal(data, row[2]) - assert_equal(enc, row[1].encoding) - assert_equal(enc, row[2].encoding) - end + # Second part of test uses Long bind type (fetch as strings) + OCI8.lob_fetch_mode = :long_as_string + cursor = @conn.parse('SELECT * from test_table order by id') + cursor.exec + LONG_TEST_DATA.each_with_index do |data, index| + row = cursor.fetch + assert_equal(index, row[0]) + if data.nil? + assert_nil(row[1]) + assert_nil(row[2]) + elsif data.empty? + # '' is inserted to the long or long raw column as null. + assert_nil(row[1]) + # '' is inserted to the clob or blob column as an empty clob. + # However it is fetched as nil. + assert_nil(row[2]) + else + assert_equal(data, row[1]) + assert_equal(data, row[2]) + assert_equal(enc, row[1].encoding) + assert_equal(enc, row[2].encoding) end - assert_nil(cursor.fetch) - cursor.close - ensure - OCI8::BindType::Mapping[:clob] = clob_bind_type - OCI8::BindType::Mapping[:blob] = blob_bind_type end + assert_nil(cursor.fetch) + cursor.close drop_table('test_table') end ensure + OCI8.lob_fetch_mode = :long_as_string OCI8::BindType::Base.initial_chunk_size = initial_cunk_size end drop_table('test_table') @@ -396,6 +392,8 @@ def test_binary_float def test_clob_nclob_and_blob return if OCI8::oracle_client_version < OCI8::ORAVER_8_1 + # This test needs LOB locators + OCI8.lob_fetch_mode = :locator drop_table('test_table') sql = <<-EOS CREATE TABLE test_table (id number(5), C CLOB, NC NCLOB, B BLOB) @@ -435,6 +433,8 @@ def test_clob_nclob_and_blob assert_nil(cursor.fetch) cursor.close drop_table('test_table') + ensure + OCI8.lob_fetch_mode = :long_as_string end def test_select_number @@ -539,7 +539,6 @@ def test_last_error assert_nil(@conn.last_error) @conn.last_error = 'dummy' cursor = @conn.parse('select col1, max(col2) from (select 1 as col1, null as col2 from dual) group by col1') - cursor.prefetch_rows = 1 assert_nil(@conn.last_error) # When an OCI function returns OCI_SUCCESS_WITH_INFO, OCI8#last_error is set.