Skip to content

Commit

Permalink
make sure the quack extension is loaded in the type test, add support…
Browse files Browse the repository at this point in the history
… for DECIMALs
  • Loading branch information
Tishj committed May 13, 2024
1 parent 50ba4f8 commit 5e7513f
Show file tree
Hide file tree
Showing 3 changed files with 303 additions and 5 deletions.
111 changes: 111 additions & 0 deletions include/quack/types/decimal.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@
(num)->choice.n_short.n_data : (num)->choice.n_long.n_data)
#define NUMERIC_NDIGITS(num) \
((VARSIZE(num) - NUMERIC_HEADER_SIZE(num)) / sizeof(NumericDigit))
#define NUMERIC_CAN_BE_SHORT(scale,weight) \
((scale) <= NUMERIC_SHORT_DSCALE_MAX && \
(weight) <= NUMERIC_SHORT_WEIGHT_MAX && \
(weight) >= NUMERIC_SHORT_WEIGHT_MIN)

#include "duckdb.hpp"

Expand Down Expand Up @@ -174,6 +178,113 @@ NumericVar FromNumeric(Numeric num)
return dest;
}

/*
* make_result_opt_error() -
*
* Create the packed db numeric format in palloc()'d memory from
* a variable. This will handle NaN and Infinity cases.
*
* If "have_error" isn't NULL, on overflow *have_error is set to true and
* NULL is returned. This is helpful when caller needs to handle errors.
*/
Numeric CreateNumeric(const NumericVar &var, bool *have_error)
{
Numeric result;
NumericDigit *digits = var.digits;
int weight = var.weight;
int sign = var.sign;
int n;
Size len;

if (have_error)
*have_error = false;

if ((sign & NUMERIC_SIGN_MASK) == NUMERIC_SPECIAL)
{
/*
* Verify valid special value. This could be just an Assert, perhaps,
* but it seems worthwhile to expend a few cycles to ensure that we
* never write any nonzero reserved bits to disk.
*/
if (!(sign == NUMERIC_NAN ||
sign == NUMERIC_PINF ||
sign == NUMERIC_NINF))
elog(ERROR, "invalid numeric sign value 0x%x", sign);

result = (Numeric) palloc(NUMERIC_HDRSZ_SHORT);

SET_VARSIZE(result, NUMERIC_HDRSZ_SHORT);
result->choice.n_header = sign;
/* the header word is all we need */
return result;
}

n = var.ndigits;

/* truncate leading zeroes */
while (n > 0 && *digits == 0)
{
digits++;
weight--;
n--;
}
/* truncate trailing zeroes */
while (n > 0 && digits[n - 1] == 0)
n--;

/* If zero result, force to weight=0 and positive sign */
if (n == 0)
{
weight = 0;
sign = NUMERIC_POS;
}

/* Build the result */
if (NUMERIC_CAN_BE_SHORT(var.dscale, weight))
{
len = NUMERIC_HDRSZ_SHORT + n * sizeof(NumericDigit);
result = (Numeric) palloc(len);
SET_VARSIZE(result, len);
result->choice.n_short.n_header =
(sign == NUMERIC_NEG ? (NUMERIC_SHORT | NUMERIC_SHORT_SIGN_MASK)
: NUMERIC_SHORT)
| (var.dscale << NUMERIC_SHORT_DSCALE_SHIFT)
| (weight < 0 ? NUMERIC_SHORT_WEIGHT_SIGN_MASK : 0)
| (weight & NUMERIC_SHORT_WEIGHT_MASK);
}
else
{
len = NUMERIC_HDRSZ + n * sizeof(NumericDigit);
result = (Numeric) palloc(len);
SET_VARSIZE(result, len);
result->choice.n_long.n_sign_dscale =
sign | (var.dscale & NUMERIC_DSCALE_MASK);
result->choice.n_long.n_weight = weight;
}

Assert(NUMERIC_NDIGITS(result) == n);
if (n > 0)
memcpy(NUMERIC_DIGITS(result), digits, n * sizeof(NumericDigit));

/* Check for overflow of int16 fields */
if (NUMERIC_WEIGHT(result) != weight ||
NUMERIC_DSCALE(result) != var.dscale)
{
if (have_error)
{
*have_error = true;
return NULL;
}
else
{
ereport(ERROR,
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
errmsg("value overflows numeric format")));
}
}
return result;
}

struct DecimalConversionInteger {
static int64_t GetPowerOfTen(idx_t index) {
static const int64_t POWERS_OF_TEN[] {1,
Expand Down
50 changes: 48 additions & 2 deletions sql/type_support.sql
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
drop extension if exists quack;
create extension quack;

-- CHAR
CREATE TABLE chr(a CHAR);
Expand Down Expand Up @@ -36,9 +38,14 @@ SELECT * FROM date_tbl;

-- TIMESTAMP
CREATE TABLE timestamp_tbl(a TIMESTAMP);
INSERT INTO timestamp_tbl SELECT CAST(a AS TIMESTAMP) FROM (VALUES ('2022-04-29 10:15:30'::TIMESTAMP), (NULL), ('2023-05-15 12:30:45'::TIMESTAMP)) t(a);
INSERT INTO timestamp_tbl SELECT CAST(a AS TIMESTAMP) FROM (VALUES
('2022-04-29 10:15:30'::TIMESTAMP),
(NULL),
('2023-05-15 12:30:45'::TIMESTAMP)
) t(a);
SELECT * FROM timestamp_tbl;

-- FIXME: currently broken
-- FLOAT4
CREATE TABLE float4_tbl(a FLOAT4);
INSERT INTO float4_tbl SELECT CAST(a AS FLOAT4) FROM (VALUES
Expand All @@ -64,9 +71,44 @@ INSERT INTO numeric_as_double SELECT a FROM (VALUES
(NULL),
(458234502034234234234.000012)
) t(a);
select pg_typeof(a) from numeric_as_double;
SELECT * FROM numeric_as_double;

-- NUMERIC with a physical type of SMALLINT
CREATE TABLE smallint_numeric(a NUMERIC(4, 2));
INSERT INTO smallint_numeric SELECT a FROM (VALUES
(0.23),
(NULL),
(45.12)
) t(a);
SELECT * FROM smallint_numeric;

-- NUMERIC with a physical type of INTEGER
CREATE TABLE integer_numeric(a NUMERIC(9, 6));
INSERT INTO integer_numeric SELECT a FROM (VALUES
(243.345035::NUMERIC(9,6)),
(NULL),
(45.000012::NUMERIC(9,6))
) t(a);
SELECT * FROM integer_numeric;

-- NUMERIC with a physical type of BIGINT
CREATE TABLE bigint_numeric(a NUMERIC(18, 12));
INSERT INTO bigint_numeric SELECT a FROM (VALUES
(856324.1111222233334444::NUMERIC(18,12)),
(NULL),
(12.0000000000000001::NUMERIC(18,12))
) t(a);
SELECT * FROM bigint_numeric;

-- NUMERIC with a physical type of HUGEINT
CREATE TABLE hugeint_numeric(a NUMERIC(38, 24));
INSERT INTO hugeint_numeric SELECT a FROM (VALUES
(3294234856324.111122223333444455::NUMERIC(38,24)),
(NULL),
(123456789.000000000000000001::NUMERIC(38,24)) -- FIXME: does not convert correctly currently
) t(a);
SELECT * FROM hugeint_numeric;

DROP TABLE chr;
DROP TABLE small;
DROP TABLE intgr;
Expand All @@ -77,3 +119,7 @@ DROP TABLE timestamp_tbl;
DROP TABLE float4_tbl;
DROP TABLE float8_tbl;
DROP TABLE numeric_as_double;
DROP TABLE smallint_numeric;
DROP TABLE integer_numeric;
DROP TABLE bigint_numeric;
DROP TABLE hugeint_numeric;
147 changes: 144 additions & 3 deletions src/quack_types.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,76 @@ static void ConvertDouble(TupleTableSlot *slot, double value, idx_t col) {
memcpy(&slot->tts_values[col], (char *)&value, sizeof(double));
}

template <class T, class OP = DecimalConversionInteger>
NumericVar ConvertNumeric(T value, idx_t scale) {
NumericVar result;
auto &sign = result.sign;
result.dscale = scale;
auto &weight = result.weight;
auto &ndigits = result.ndigits;

constexpr idx_t MAX_DIGITS = sizeof(T) * 4;
if (value < 0) {
value = -value;
sign = NUMERIC_NEG;
} else {
sign = NUMERIC_POS;
}
// divide the decimal into the integer part (before the decimal point) and fractional part (after the point)
T integer_part;
T fractional_part;
if (scale == 0) {
integer_part = value;
fractional_part = 0;
} else {
integer_part = value / OP::GetPowerOfTen(scale);
fractional_part = value % OP::GetPowerOfTen(scale);
}

uint16_t integral_digits[MAX_DIGITS];
uint16_t fractional_digits[MAX_DIGITS];
int32_t integral_ndigits;

// split the integral part into parts of up to NBASE (4 digits => 0..9999)
integral_ndigits = 0;
while (integer_part > 0) {
integral_digits[integral_ndigits++] = uint16_t(integer_part % T(NBASE));
integer_part /= T(NBASE);
}
weight = integral_ndigits - 1;
// split the fractional part into parts of up to NBASE (4 digits => 0..9999)
// count the amount of digits required for the fractional part
// note that while it is technically possible to leave out zeros here this adds even more complications
// so we just always write digits for the full "scale", even if not strictly required
int32_t fractional_ndigits = (scale + DEC_DIGITS - 1) / DEC_DIGITS;
// fractional digits are LEFT aligned (for some unknown reason)
// that means if we write ".12" with a scale of 2 we actually need to write "1200", instead of "12"
// this means we need to "correct" the number 12 by multiplying by 100 in this case
// this correction factor is the "number of digits to the next full number"
int32_t correction = fractional_ndigits * DEC_DIGITS - scale;
fractional_part *= OP::GetPowerOfTen(correction);
for (idx_t i = 0; i < fractional_ndigits; i++) {
fractional_digits[i] = uint16_t(fractional_part % NBASE);
fractional_part /= NBASE;
}

ndigits = integral_ndigits + fractional_ndigits;

result.buf = (NumericDigit *)palloc(ndigits * sizeof(NumericDigit));
result.digits = result.buf;
auto &digits = result.digits;

idx_t digits_idx = 0;
for (idx_t i = integral_ndigits; i > 0; i--) {
digits[digits_idx++] = integral_digits[i - 1];
}
for (idx_t i = fractional_ndigits; i > 0; i--) {
digits[digits_idx++] = fractional_digits[i - 1];
}

return result;
}

void
ConvertDuckToPostgresValue(TupleTableSlot *slot, duckdb::Value &value, idx_t col) {
Oid oid = slot->tts_tupleDescriptor->attrs[col].atttypid;
Expand Down Expand Up @@ -67,6 +137,13 @@ ConvertDuckToPostgresValue(TupleTableSlot *slot, duckdb::Value &value, idx_t col
slot->tts_values[col] = timestamp.micros - QUACK_DUCK_TIMESTAMP_OFFSET;
break;
}
case FLOAT4OID: {
double result_float = value.GetValue<float>();
slot->tts_tupleDescriptor->attrs[col].atttypid = FLOAT4OID;
slot->tts_tupleDescriptor->attrs[col].attbyval = true;
memcpy(&slot->tts_values[col], (char *)&result_float, sizeof(float));
break;
}
case FLOAT8OID: {
double result_double = value.GetValue<double>();
ConvertDouble(slot, result_double, col);
Expand All @@ -78,7 +155,39 @@ ConvertDuckToPostgresValue(TupleTableSlot *slot, duckdb::Value &value, idx_t col
ConvertDouble(slot, result_double, col);
break;
}
elog(ERROR, "Unsupported NUMERIC type: %d", oid);
NumericVar numeric_var;
D_ASSERT(value.type().id() == duckdb::LogicalTypeId::DECIMAL);
auto physical_type = value.type().InternalType();
auto scale = duckdb::DecimalType::GetScale(value.type());
switch (physical_type) {
case duckdb::PhysicalType::INT16: {
elog(INFO, "SMALLINT");
numeric_var = ConvertNumeric<int16_t>(value.GetValueUnsafe<int16_t>(), scale);
break;
}
case duckdb::PhysicalType::INT32: {
elog(INFO, "INTEGER");
numeric_var = ConvertNumeric<int32_t>(value.GetValueUnsafe<int32_t>(), scale);
break;
}
case duckdb::PhysicalType::INT64: {
elog(INFO, "BIGINT");
numeric_var = ConvertNumeric<int64_t>(value.GetValueUnsafe<int64_t>(), scale);
break;
}
case duckdb::PhysicalType::INT128: {
elog(INFO, "HUGEINT");
numeric_var = ConvertNumeric<hugeint_t, DecimalConversionHugeint>(value.GetValueUnsafe<hugeint_t>(), scale);
break;
}
default: {
elog(ERROR, "Unrecognized physical type for DECIMAL value");
break;
}
}
auto numeric = CreateNumeric(numeric_var, NULL);
auto datum = NumericGetDatum(numeric);
slot->tts_values[col] = datum;
break;
}
default:
Expand Down Expand Up @@ -120,6 +229,8 @@ ConvertPostgresToDuckColumnType(Oid type, int32_t typmod) {
return duckdb::LogicalTypeId::DATE;
case TIMESTAMPOID:
return duckdb::LogicalTypeId::TIMESTAMP;
case FLOAT4OID:
return duckdb::LogicalTypeId::FLOAT;
case FLOAT8OID:
return duckdb::LogicalTypeId::DOUBLE;
case NUMERICOID: {
Expand Down Expand Up @@ -247,6 +358,10 @@ ConvertPostgresToDuckValue(Datum value, duckdb::Vector &result, idx_t offset) {
Append<duckdb::dtime_t>(result, duckdb::dtime_t(static_cast<int64_t>(value + QUACK_DUCK_TIMESTAMP_OFFSET)),
offset);
break;
case duckdb::LogicalTypeId::FLOAT: {
Append<float>(result, DatumGetFloat4(value), offset);
break;
}
case duckdb::LogicalTypeId::DOUBLE: {
auto aux_info = type.GetAuxInfoShrPtr();
if (aux_info && dynamic_cast<NumericAsDouble *>(aux_info.get())) {
Expand All @@ -260,8 +375,34 @@ ConvertPostgresToDuckValue(Datum value, duckdb::Vector &result, idx_t offset) {
}
break;
}
case duckdb::LogicalTypeId::DECIMAL:
elog(ERROR, "DECIMAL TYPE");
case duckdb::LogicalTypeId::DECIMAL: {
auto physical_type = type.InternalType();
auto numeric = DatumGetNumeric(value);
auto numeric_var = FromNumeric(numeric);
switch (physical_type) {
case duckdb::PhysicalType::INT16: {
Append(result, ConvertDecimal<int16_t>(numeric_var), offset);
break;
}
case duckdb::PhysicalType::INT32: {
Append(result, ConvertDecimal<int32_t>(numeric_var), offset);
break;
}
case duckdb::PhysicalType::INT64: {
Append(result, ConvertDecimal<int64_t>(numeric_var), offset);
break;
}
case duckdb::PhysicalType::INT128: {
Append(result, ConvertDecimal<hugeint_t, DecimalConversionHugeint>(numeric_var), offset);
break;
}
default: {
elog(ERROR, "Unrecognized physical type for DECIMAL value");
break;
}
}
break;
}
default:
elog(ERROR, "Unsupported quack (DuckDB) type: %d", static_cast<int>(result.GetType().id()));
break;
Expand Down

0 comments on commit 5e7513f

Please sign in to comment.