Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

All notable changes to this project will be documented in this file.
# Unreleased
- support Enum type in `DuckDB::LogicalType` class.
- `DuckDB::LogicalType#internal_type`, `DuckDB::LogicalType#dictionary_size`,
`DuckDB::LogicalType#dictionary_value_at`, and `DuckDB::LogicalType#each_dictionary_value` are
available.

# 1.2.1.0 - 2025-03-30
- bump duckdb v1.2.1 on CI.
Expand Down
72 changes: 72 additions & 0 deletions ext/duckdb/logical_type.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ static VALUE duckdb_logical_type_value_type(VALUE self);
static VALUE duckdb_logical_type_member_count(VALUE self);
static VALUE duckdb_logical_type_member_name_at(VALUE self, VALUE midx);
static VALUE duckdb_logical_type_member_type_at(VALUE self, VALUE midx);
static VALUE duckdb_logical_type__internal_type(VALUE self);
static VALUE duckdb_logical_type_dictionary_size(VALUE self);
static VALUE duckdb_logical_type_dictionary_value_at(VALUE self, VALUE didx);

static const rb_data_type_t logical_type_data_type = {
"DuckDB/LogicalType",
Expand Down Expand Up @@ -288,6 +291,72 @@ static VALUE duckdb_logical_type_member_type_at(VALUE self, VALUE midx) {
return rbduckdb_create_logical_type(union_member_type);
}

/*
* call-seq:
* enum_col.logical_type.internal_type -> Symbol
*
* Returns the logical type's internal type.
*
*/
static VALUE duckdb_logical_type__internal_type(VALUE self) {
rubyDuckDBLogicalType *ctx;
duckdb_type type_id;
duckdb_type internal_type_id;

TypedData_Get_Struct(self, rubyDuckDBLogicalType, &logical_type_data_type, ctx);

type_id = duckdb_get_type_id(ctx->logical_type);
switch (type_id) {
case DUCKDB_TYPE_DECIMAL:
internal_type_id = duckdb_decimal_internal_type(ctx->logical_type);
break;
case DUCKDB_TYPE_ENUM:
internal_type_id = duckdb_enum_internal_type(ctx->logical_type);
break;
default:
internal_type_id = DUCKDB_TYPE_INVALID;
}

return INT2FIX(internal_type_id);
}

/*
* call-seq:
* enum_col.logical_type.dictionary_size -> Integer
*
* Returns the dictionary size of the enum type.
*
*/
static VALUE duckdb_logical_type_dictionary_size(VALUE self) {
rubyDuckDBLogicalType *ctx;
TypedData_Get_Struct(self, rubyDuckDBLogicalType, &logical_type_data_type, ctx);
return INT2FIX(duckdb_enum_dictionary_size(ctx->logical_type));
}

/*
* call-seq:
* enum_col.logical_type.dictionary_value_at(index) -> String
*
* Returns the dictionary value at the specified index.
*
*/
static VALUE duckdb_logical_type_dictionary_value_at(VALUE self, VALUE didx) {
rubyDuckDBLogicalType *ctx;
VALUE dvalue;
const char *dict_value;
idx_t idx = NUM2ULL(didx);

TypedData_Get_Struct(self, rubyDuckDBLogicalType, &logical_type_data_type, ctx);

dict_value = duckdb_enum_dictionary_value(ctx->logical_type, idx);
if (dict_value == NULL) {
rb_raise(eDuckDBError, "fail to get dictionary value of %llu", (unsigned long long)idx);
}
dvalue = rb_utf8_str_new_cstr(dict_value);
duckdb_free((void *)dict_value);
return dvalue;
}

VALUE rbduckdb_create_logical_type(duckdb_logical_type logical_type) {
VALUE obj;
rubyDuckDBLogicalType *ctx;
Expand Down Expand Up @@ -320,4 +389,7 @@ void rbduckdb_init_duckdb_logical_type(void) {
rb_define_method(cDuckDBLogicalType, "member_count", duckdb_logical_type_member_count, 0);
rb_define_method(cDuckDBLogicalType, "member_name_at", duckdb_logical_type_member_name_at, 1);
rb_define_method(cDuckDBLogicalType, "member_type_at", duckdb_logical_type_member_type_at, 1);
rb_define_method(cDuckDBLogicalType, "_internal_type", duckdb_logical_type__internal_type, 0);
rb_define_method(cDuckDBLogicalType, "dictionary_size", duckdb_logical_type_dictionary_size, 0);
rb_define_method(cDuckDBLogicalType, "dictionary_value_at", duckdb_logical_type_dictionary_value_at, 1);
}
40 changes: 40 additions & 0 deletions lib/duckdb/logical_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,24 @@ def type
DuckDB::Converter::IntToSym.type_to_sym(type_id)
end

# returns logical type's internal type symbol for Decimal or Enum types
# `:unknown` means that the logical type's type is unknown/unsupported by ruby-duckdb.
# `:invalid` means that the logical type's type is invalid in duckdb.
#
# require 'duckdb'
# db = DuckDB::Database.open
# con = db.connect
# con.query("CREATE TYPE mood AS ENUM ('happy', 'sad')")
# con.query("CREATE TABLE emotions (id INTEGER, enum_col mood)")
#
# users = con.query('SELECT * FROM emotions')
# ernum_col = users.columns.find { |col| col.name == 'enum_col' }
# enum_col.logical_type.internal_type #=> :utinyint
def internal_type
type_id = _internal_type
DuckDB::Converter::IntToSym.type_to_sym(type_id)
end

# Iterates over each union member name.
#
# When a block is provided, this method yields each union member name in
Expand Down Expand Up @@ -106,5 +124,27 @@ def each_child_type
yield child_type_at(i)
end
end

# Iterates over each enum dictionary value.
#
# When a block is provided, this method yields each enum dictionary value
# in order. It also returns the total number of dictionary values yielded.
#
# enum_logical_type.each_value do |value|
# puts "Enum value: #{value}"
# end
#
# If no block is given, an Enumerator is returned, which can be used to
# retrieve all enum dictionary values.
#
# values = enum_logical_type.each_value.to_a
# # => ["happy", "sad"]
def each_dictionary_value
return to_enum(__method__) {dictionary_size} unless block_given?

dictionary_size.times do |i|
yield dictionary_value_at(i)
end
end
end
end
22 changes: 22 additions & 0 deletions test/duckdb_test/logical_type_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ def test_type
assert_equal(EXPECTED_TYPES, logical_types.map(&:type))
end

def test_decimal_internal_type
decimal_column = @columns.find { |column| column.type == :decimal }
assert_equal(:integer, decimal_column.logical_type.internal_type)
end

def test_decimal_width
decimal_column = @columns.find { |column| column.type == :decimal }
assert_equal(9, decimal_column.logical_type.width)
Expand Down Expand Up @@ -215,6 +220,23 @@ def test_struct_each_child_type
assert_equal([:varchar, :integer], child_types.map(&:type))
end

def test_enum_internal_type
enum_column = @columns.find { |column| column.type == :enum }
assert_equal(:utinyint, enum_column.logical_type.internal_type)
end

def test_enum_dictionary_size
enum_column = @columns.find { |column| column.type == :enum }
assert_equal(4, enum_column.logical_type.dictionary_size)
end

def test_enum_each_dictionary_value
enum_column = @columns.find { |column| column.type == :enum }
enum_logical_type = enum_column.logical_type
dictionary_values = enum_logical_type.each_dictionary_value.to_a
assert_equal(["sad", "ok", "happy", "𝘾𝝾օɭ 😎"], dictionary_values)
end

private

def create_data(con)
Expand Down