Статьи

Расширение PostgreSQL: тип данных комплексного числа

Несколько лет назад я обсуждал создание пользовательских типов с использованием PL / java. ( Введение в PostgreSQL PL / Java, часть 4: Определяемые пользователем типы ) Сегодня я прокомментирую комментарий о том, что на практике мы будем использовать стандартное расширение PostgreSQL на языке C.

Что такое расширение?

Расширение PostgreSQL — это не что иное, как объединенные операторы SQL и необязательная собственная библиотека. Расширения только для SQL не содержат ничего, кроме операторов SQL.

Расширения предоставляют два основных преимущества по сравнению с автономными скриптами. Сначала операторы загружаются и выгружаются как единое целое. Невозможно изменить что-либо определенное в расширении. Это большая победа в обслуживании и безопасности. Во-вторых, расширения имеют версии. Это позволяет корректно обновлять артефакты SQL с течением времени, что особенно важно для пользовательских типов, поскольку простое удаление UDT приведет к потере данных.

PostgreSQL имеет четко определенную поддержку расширений. См. Упаковка связанных объектов в расширение .

Загрузка расширения

1
CREATE EXTENSION IF NOT EXISTS pg_complex;

Выгрузка расширения

1
DROP EXTENSION pg_complex;

Важно помнить, что многие, если не большинство провайдеров баз данных как услуга (DAAS), например Amazon RDS, не позволяют устанавливать произвольные расширения. Вы по-прежнему можете загружать расширения только для SQL, выполняя сценарии создания вручную, но расширения C-языка могут вызвать проблемы в будущем.

Что такое пользовательские типы (UDT)?

Пользовательские типы являются расширениями базы данных для поддержки новых типов данных. Пуристы SQL хотят, чтобы в таблицах не было ничего, кроме стандартных примитивов. Данные, имеющие структуру, могут быть записаны в отдельной таблице и во внешнем ключе, например, путем извлечения таблицы адресов из таблицы клиентов.

Многие разработчики признают, что некоторые данные более тесно связаны, чем другие, и для полезных операций обычно требуется более одного примитивного значения. Географическое положение (широта, долгота) и криптографическая информация являются классическими примерами. Эти объекты часто настолько малы, что не имеет смысла иметь для них отдельную таблицу — сравните почтовый адрес с широтно-длинным адресом.

(Могут быть и другие причины для хранения UDT в отдельной таблице, например, для повышения безопасности конфиденциальной информации.)

Другое использование для UDT — добавить безопасность типов в BLOB. Например, разумно иметь пользовательские функции, которые возвращают высоту или ширину изображения или количество страниц документа PDF. Вы можете легко написать функции, которые принимают BLOB (bytea), но вы не можете гарантировать, что значение, переданное функции, имеет соответствующий тип. Определение UDT, например, pdf или jpeg , дает разработчику мощный инструмент.

В заключение следует учитывать UDT, когда 1) объект не имеет смысла, если какой-либо элемент отсутствует, или 2) в противном случае объект был бы BLOB, и вы хотите предоставить безопасные для типов хранимые процедуры и пользовательские функции. В противном случае мы должны придерживаться стандартных примитивов.

Комплексные числа?

Я буду создавать UDT для комплексных чисел ниже. Я не вижу большой неудовлетворенной необходимости хранить комплексные числа в реляционных базах данных, но это хороший выбор для образовательных целей, поскольку требует пользовательских типов, функций, приведений, операторов и агрегатных функций. Не хватает только заказа.

Эта реализация использует составной тип PostgreSQL с комбинацией хранимых процедур SQL и пользовательских функций языка C. Комплексные числа будут показаны как (a, b) вместо обычных a + bi, но последнее возможно при дополнительной работе.

Определения SQL

Это расширение PostgreSQL, поэтому мы должны начать с определения того, что мы ожидаем увидеть в нашей улучшенной базе данных.

Определение комплекса УДТ

Комплекс UDT состоит из двух полей — реального (пере) компонента и мнимого (им) компонента.

1
CREATE TYPE complex AS (re float8, im float8);

Ниже приведена очень простая демонстрация его использования.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
$> CREATE TABLE t (c complex);
CREATE TABLE
 
-- insert a value. Note that we are inserting '(1,2)', not '1,2'.
$> INSERT INTO t VALUES((1,2));
INSERT 0 1
 
-- select full UDT
$> SELECT c FROM t;
   c  
-------
 (1,2)
 
-- select components. Note that we must surround field with parentheses.
$> SELECT (c).re, (c).im FROM t;
 re | im
----+----
  1 |  2

Автопромотирование плавает в комплексные числа

Легко извлечь действительную составляющую комплексного числа, но все еще трудно преобразовать действительное число в комплексное число. PostgreSQL может сделать это прозрачно, если мы определим CAST.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
CREATE OR REPLACE FUNCTION pgx_complex_from_int(int) RETURNS complex AS $$
   SELECT ROW($1::float8, 0)::complex;
$$ LANGUAGE SQL IMMUTABLE STRICT;
 
CREATE OR REPLACE FUNCTION pgx_complex_from_bigint(bigint) RETURNS complex AS $$
   SELECT ROW($1::float8, 0)::complex;
$$ LANGUAGE SQL IMMUTABLE STRICT;
 
CREATE OR REPLACE FUNCTION pgx_complex_from_numeric(numeric) RETURNS complex AS $$
   SELECT ROW($1::float8, 0)::complex;
$$ LANGUAGE SQL IMMUTABLE STRICT;
 
CREATE OR REPLACE FUNCTION pgx_complex_from_numeric(float8) RETURNS complex AS $$
   SELECT ROW($1, 0)::complex;
$$ LANGUAGE SQL IMMUTABLE STRICT;

Определение арифметических операторов

Комплексные числа — это числа, поэтому мы хотим реализовать для них стандартные арифметические операторы. Все имена функций C являются смарфированными, поскольку они должны быть уникальными. Имена функций SQL не обязательно должны быть уникальными, поскольку также учитывается сигнатура функции.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
CREATE OPERATOR = (
   LEFT_ARG = complex,
   RIGHT_ARG = complex,
   PROCEDURE = pgx_complex_eq,
   NEGATOR = <>,
   HASHES,
   MERGES
);
 
CREATE OPERATOR  (
   LEFT_ARG = complex,
   RIGHT_ARG = complex,
   PROCEDURE = pgx_complex_ne,
   NEGATOR = <>,
   HASHES,
   MERGES
);
 
CREATE OPERATOR ~= (
   LEFT_ARG = complex,
   RIGHT_ARG = complex,
   PROCEDURE = pgx_complex_near,
   NEGATOR = <~>
);
 
CREATE OPERATOR  (
   LEFT_ARG = complex,
   RIGHT_ARG = complex,
   PROCEDURE = pgx_complex_not_near,
   NEGATOR = <>
);
 
CREATE OPERATOR - (
   RIGHT_ARG = complex,
   PROCEDURE = pgx_complex_negate,
   NEGATOR = -
);
 
CREATE OPERATOR ~ (
   RIGHT_ARG = complex,
   PROCEDURE = pgx_complex_conjugate,
   NEGATOR = ~
);
 
-- variants mixing 'complex' and 'numeric' types elided
CREATE OPERATOR + (
   LEFT_ARG = complex,
   RIGHT_ARG = complex,
   PROCEDURE = pgx_complex_add
);
 
-- variants mixing 'complex' and 'numeric' types elided
CREATE OPERATOR - (
   LEFT_ARG = complex,
   RIGHT_ARG = complex,
   PROCEDURE = pgxdefine_complex_subtract
);
 
-- variants mixing 'complex' and 'numeric' types elided
CREATE OPERATOR * (
   LEFT_ARG = complex,
   RIGHT_ARG = complex,
   PROCEDURE = pgx_complex_multiply
);
 
-- variants mixing 'complex' and 'numeric' types elided
CREATE OPERATOR / (
   LEFT_ARG = complex,
   RIGHT_ARG = complex,
   PROCEDURE = pgx_complex_divide
);

Определение агрегатных функций

Агрегатные функции являются одним из основных способов поместить интеллект в базу данных вместо того, чтобы рассматривать его как прославленную файловую систему. Это функции, которые работают с коллекцией значений и возвращают совокупное значение некоторого типа. Агрегатные функции могут иметь несколько параметров.

Не предполагайте, что агрегаты могут только потреблять и производить числовые данные. Рассмотрим функцию, которая принимает пары (x, y) и создает график результатов в формате .png.

Агрегатные функции могут дополнительно поддерживать оконные функции ( http://www.postgresql.org/docs/9.4/static/tutorial-window.html ). Это относительно новая функция и невероятно мощная. Реализация концептуально проста — нам нужны только функции для добавления или удаления значения из «состояния» агрегатной функции, но на практике операции могут быть необратимыми. Это имеет место здесь — если мы вычисляем ‘1e20 + 1 — 1e20’, результат должен быть ‘1’, но может быть нулевым из-за ограниченного разрешения.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
--
-- UDT that keeps track of sums and sums-of-squares of a collection
-- of complex values. Tracking all three values allows us to compute
-- a wide variety of statistical values.
--
CREATE TYPE complex_accum AS (
   cnt  int,
   sum  complex,
   sofs complex
);
 
--
-- Calculate sum of a collection of complex values
--
CREATE AGGREGATE sum(complex) (
   sfunc = pg_complex_add,
   stype = complex_accum,
   initcond = '(0, 0)',
   -- msfunc = pg_complex_add,
   -- minvfunc = pg_complex_subtract,
   -- mstype = complex_accum,
   -- minitcond = (0, 0)'
);
 
--
-- Calculate average of a collection of complex values.
--
CREATE AGGREGATE avg(complex) (
   sfunc = pg_complex_accum,
   stype = complex_accum,
   finalfunc = pg_complex_avg
   -- msfunc = pg_complex_accum,
   -- minvfunc = pg_complex_disaccum,
   -- mstype = complex_accum,
);

(См .: http://www.postgresql.org/docs/9.4/static/xaggr.html .)

Определение пользовательских функций

Теперь мы знаем функции и подписи, которые мы должны реализовать. В этом случае мы можем выполнять большинство функций в чистом SQL, но выбираем несколько в C, чтобы продемонстрировать продвинутые методы.

Примечание: в соответствии с принципами TDD мы должны реализовать только достаточно, чтобы тесты могли выполняться. В этом случае функции должны возвращать ноль. Я не делаю этого здесь, так как функции настолько просты, что их можно проверить с первого взгляда. Любая многострочная функция должна следовать принципам TDD и возвращать ноль.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
--
-- create functions implemented in C.
--
CREATE OR REPLACE FUNCTION pgx_complex_near(complex, complex)
RETURNS bool
AS 'pg_complex', 'pgx_complex_near'
LANGUAGE C IMMUTABLE STRICT;
 
CREATE OR REPLACE FUNCTION pgx_complex_divide(complex, complex)
RETURNS complex
AS 'pg_complex', 'pgx_complex_divide'
LANGUAGE C IMMUTABLE STRICT;
 
CREATE OR REPLACE FUNCTION norm(complex)
RETURNS complex
AS 'pg_complex', 'pgx_complex_norm'
LANGUAGE C IMMUTABLE STRICT;
 
--
-- create functions implemented in SQL.
--
CREATE OR REPLACE FUNCTION pgx_complex_from_int(int) RETURNS complex AS $$
   SELECT ROW($1::float8, 0)::complex;
$$ LANGUAGE SQL IMMUTABLE STRICT;
 
CREATE OR REPLACE FUNCTION pgx_complex_from_bigint(bigint) RETURNS complex AS $$
   SELECT ROW($1::float8, 0)::complex;
$$ LANGUAGE SQL IMMUTABLE STRICT;
 
CREATE OR REPLACE FUNCTION pgx_complex_from_numeric(numeric) RETURNS complex AS $$
   SELECT ROW($1::float8, 0)::complex;
$$ LANGUAGE SQL IMMUTABLE STRICT;
 
CREATE OR REPLACE FUNCTION pgx_complex_eq(complex, complex) RETURNS bool AS $$
   SELECT $1.re = $2.re AND $1.im = $2.im;
$$ LANGUAGE SQL IMMUTABLE STRICT;
 
CREATE OR REPLACE FUNCTION pgx_complex_ne(complex, complex) RETURNS bool AS $$
   SELECT $1.re <> $2.re OR $1.im <> $2.im;
$$ LANGUAGE SQL IMMUTABLE STRICT;
 
CREATE OR REPLACE FUNCTION pgx_complex_not_near(complex, complex) RETURNS bool AS $$
   SELECT NOT pgx_complex_near($1, $2);
$$ LANGUAGE SQL IMMUTABLE STRICT;
 
CREATE OR REPLACE FUNCTION pgx_complex_negate(complex) RETURNS complex AS $$
   SELECT ROW(-$1.re, -$1.im)::complex;
$$ LANGUAGE SQL IMMUTABLE STRICT;
 
CREATE OR REPLACE FUNCTION pgx_complex_conjugate(complex) RETURNS complex AS $$
   SELECT ROW($1.re, -$1.im)::complex;
$$ LANGUAGE SQL IMMUTABLE STRICT;
 
CREATE OR REPLACE FUNCTION pgx_complex_add(complex, complex) RETURNS complex AS $$
   SELECT ROW($1.re + $2.re, $1.im + $2.im)::complex;
$$ LANGUAGE SQL IMMUTABLE STRICT;
 
CREATE OR REPLACE FUNCTION pgx_complex_add_f8(float8, complex) RETURNS complex AS $$
   SELECT ROW($1 + $2.re, $2.im)::complex;
$$ LANGUAGE SQL IMMUTABLE STRICT;
 
CREATE OR REPLACE FUNCTION pgx_complex_add_f8(complex, float8) RETURNS complex AS $$
   SELECT ROW($1.re + $2, $1.im)::complex;
$$ LANGUAGE SQL IMMUTABLE STRICT;
 
CREATE OR REPLACE FUNCTION pgx_complex_subtract(complex, complex) RETURNS complex AS $$
   SELECT ROW($1.re - $2.re, $1.im - $2.im)::complex;
$$ LANGUAGE SQL IMMUTABLE STRICT;
 
CREATE OR REPLACE FUNCTION pgx_complex_subtract_f8(float8, complex) RETURNS complex AS $$
   SELECT ROW($1 - $2.re, -$2.im)::complex;
$$ LANGUAGE SQL IMMUTABLE STRICT;
 
CREATE OR REPLACE FUNCTION pgx_complex_subtract_f8(complex, float8) RETURNS complex AS $$
   SELECT ROW($1.re - $2, $1.im)::complex;
$$ LANGUAGE SQL IMMUTABLE STRICT;
 
CREATE OR REPLACE FUNCTION pgx_complex_multiply(complex, complex) RETURNS complex AS $$
   SELECT ROW($1.re * $2.re - $1.im * $2.im, $1.re * $2.im + $1.im * $2.re)::complex;
$$ LANGUAGE SQL IMMUTABLE STRICT;
 
CREATE OR REPLACE FUNCTION pgx_complex_multiply_f8(float8, complex) RETURNS complex AS $$
   SELECT ROW($1 * $2.re, $1 * $2.im)::complex;
$$ LANGUAGE SQL IMMUTABLE STRICT;
 
CREATE OR REPLACE FUNCTION pgx_complex_multiply_f8(complex, float8) RETURNS complex AS $$
   SELECT ROW($1.re * $2, $1.im * $2)::complex;
$$ LANGUAGE SQL IMMUTABLE STRICT;
 
CREATE OR REPLACE FUNCTION magnitude(complex) RETURNS float8 AS $$
   SELECT sqrt($1.re * $1.re + $1.im * $1.im);
$$ LANGUAGE SQL IMMUTABLE STRICT;

Создание расширения скелета

Теперь мы готовы создать расширение скелета. Это расширение следует методам разработки на основе тестирования (TDD) — мы хотим, чтобы минимальный объем кода не удался. В этом случае это означает, что расширение будет загружать и определять пользовательские функции и тип, но все функции и операторы немедленно возвращают NULL.

Самый простой способ сделать это — утилиты PGXN.

Сначала убедитесь, что установлены следующие пакеты:

  • pgxnclient
  • PostgreSQL-сервер DEV-9,4
  • делать
  • Рубин
  • ruby2.1-DEV
  • НКУ

(Это для PostgreSQL 9.4 под Ubuntu. Настройте соответственно.)

Во-вторых , клонируйте репозиторий github guedes / pgxn-utils .

В-третьих , установите эти инструменты.

01
02
03
04
05
06
07
08
09
10
11
$ sudo pgxnclient install pgxn_utils
 
# verify utilities have been installed.
$ pgxn-utils help
PGXN Utils version: 0.1.4
Commands:
  pgxn-utils bundle [extension_name]  # Bundles the extension in a zip file
  pgxn-utils change [extension_name]  # Changes META's attributes in current extension
  pgxn-utils help [COMMAND]           # Describe available commands or one specific command
  pgxn-utils release filename         # Release an extension to PGXN
  pgxn-utils skeleton extension_name  # Creates an extension skeleton in current directory

В-четвертых , создайте каркас для расширения PostgreSQL на основе C, используя наши новые утилиты.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
$ pgxn skeleton -m "Bear Giles <[email protected]>" --template=c pg_complex
      create  pg_complex
      create  pg_complex/pg_complex.control
      create  pg_complex/.gitignore
      create  pg_complex/.template
      create  pg_complex/META.json
      create  pg_complex/Makefile
      create  pg_complex/README.md
      create  pg_complex/doc/pg_complex.md
      create  pg_complex/sql/pg_complex.sql
      create  pg_complex/sql/uninstall_pg_complex.sql
      create  pg_complex/src/pg_complex.c
      create  pg_complex/test/expected/base.out
      create  pg_complex/test/sql/base.sql

В-пятых , отредактируйте файлы META.json, README.md и doc / pg_complex.md, чтобы описать расширение. Это также подходящее время для копирования файла LICENSE в этот каталог, если у вас есть планы выпустить расширение для других. Ваша будущая личность поблагодарит вас за эту документацию.

Файл META.json позволяет нам определять зависимости между расширениями.

В-шестых , создайте фиктивную реализацию каждой функции, которая немедленно возвращает ноль.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include "postgres.h"
#include "fmgr.h"
  
PG_MODULE_MAGIC;
  
/**
 * Are two points "near" each other. This function requires reading
 * composite types.
 */
PG_FUNCTION_INFO_V1(pgx_complex_near);
  
Datum
pgx_complex_near(PG_FUNCTION_ARGS) {
    PG_RETURN_NULL();
}
  
/**
 * Divide one complex number by another. This function requires reading
 * and returning composite types.
 */
PG_FUNCTION_INFO_V1(pgx_complex_divide);
  
Datum
pgx_complex_divide(PG_FUNCTION_ARGS) {
    PG_RETURN_NULL();
}
  
/**
 * Scale a complex number so it on the unit circle. This function requires
 * reading and returning composite types.
 */
PG_FUNCTION_INFO_V1(pgx_complex_norm);
  
Datum
pgx_complex_norm(PG_FUNCTION_ARGS) {
    PG_RETURN_NULL();
}

Наша проблема достаточно проста, чтобы хранимые процедуры SQL реализовали необходимую функциональность. Более сложные хранимые процедуры могут быть реализованы как хранимые процедуры plpsql, которые возвращают ноль.

В-седьмых , построить систему.

1
2
$ make
$ sudo make install

Возможно, вам придется загрузить расширение, прежде чем вы вызовете ‘make install’ в первый раз. Нет необходимости перезагружать его впоследствии.

1
$ sudo pgxn load ./

В-восьмых , запустите тесты.

Стандартный скелет поддерживает регрессионные тесты.

1
$ make installcheck

Регрессионные тесты запускают все сценарии в test/sql и проверяют соответствие результатов соответствующим файлам в test/expected . Фактические результаты сохраняются в results поэтому легко написать тесты, изменить код по мере необходимости, а затем скопировать файл из results в test/expected только будет обнаружено желаемое поведение.

Вы также можете запускать тесты через pgxn.

1
$ pgxn check -d somedb pg_complex

Необязательное расширение pgTAP ( http://pgtap.org/ ) дает нам возможность писать тесты типа xJunit. Эти тесты, вероятно, будут более удобными для разработчиков, чем регрессионные тесты.

Для получения информации об интеграции pgTAP в процесс сборки см. Http://pgtap.org/integration.html и https://gkoenig.wordpress.com/2011/03/04/pgtap-unit-tests-for-postgresql/ .

В-девятых , установите и разверните расширение за пределами тестовой среды.

1
2
$ pgxn install --sudo -- pg_complex
$ pgxn load -d somedb --sudo -- pg_complex

Вы можете отменить и удалить расширение, используя аналогичные команды.

1
2
$ pgxn unload --sudo -- pg_complex
$ pgxn uninstall --sudo -- pg_complex

В-десятых , опубликуйте расширение. Вы, вероятно, не хотите делать это с реализацией скелета, но это естественное место для документирования процесса. Если мы являемся членом PGXN и хотим сделать наше расширение общедоступным, мы начнем с комплектации нашего расширения.

1
$ pgxn bundle

а затем загрузите его на https://manager.pgxn.org/ .

тестирование

Как хорошие разработчики, основанные на тестировании, мы начинаем с написания наших тестов. В нашем случае это прямой SQL или, точнее, файлы, которые можно запустить через psql.

Типичный тестовый скрипт:

тест / SQL / math.sql

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
\set ECHO None
BEGIN;
\i sql/complex.sql
\set ECHO all
 
\set c1 (1,2)::complex
\set c2 (1,1)::complex
\set c3 (3,4)::complex
\set c4 (3,8)::complex
 
SELECT 1::complex AS a, (1::int8)::complex AS b, 1.0::complex AS c;
 
SELECT (1,2)::complex AS a, -(1,2)::complex AS b, ~(1,2)::complex AS c;
 
SELECT :c1 + (3,4)::complex AS a, 3 + :c1 AS b, :c1 + 3 AS c;
 
SELECT :c1 - (3,6)::complex AS a, 3 - :c1 AS b, :c1 - 3 AS c;
 
SELECT :c1 * (3,5)::complex AS a, 3 * :c1 AS b, :c1 * 3 AS c;
SELECT :c1 * (3,5)::complex AS a, 3.0::double precision * :c1 AS b, :c1 * 3.0 AS c;
 
SELECT :c4 / :c1  AS a, (:c4 / :c1) * :c1 = :c4 AS b;
SELECT :c4 / (2,0)::complex AS a, (2,0)::complex * (:c4 / (2,0)::complex)  = :c4 AS b;
SELECT :c4 / (0,2)::complex AS a, (0,2)::complex * (:c4 / (0,2)::complex) = :c4 AS b;
SELECT :c4 / 3 AS a, 3 * (:c4 / 3) = :c4 AS b;
SELECT 3 / :c4 AS a, :c4 * (3 / :c4) = 3::complex AS b;
 
--
-- check magnitude
--
SELECT magnitude(:c1) AS magnitude;
SELECT magnitude(:c2) AS magnitude;
SELECT magnitude(:c3) AS magnitude;
 
ROLLBACK;

Соответствующие ожидаемые результаты:

тест / ожидаемый / math.out

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
\set ECHO None
\set c1 (1,2)::complex
\set c2 (1,1)::complex
\set c3 (3,4)::complex
\set c4 (3,8)::complex
SELECT 1::complex AS a, (1::int8)::complex AS b, 1.0::complex AS c;
   a   |   b   |   c  
-------+-------+-------
 (1,0) | (1,0) | (1,0)
(1 row)
 
SELECT (1,2)::complex AS a, -(1,2)::complex AS b, ~(1,2)::complex AS c;
   a   |    b    |   c   
-------+---------+--------
 (1,2) | (-1,-2) | (1,-2)
(1 row)
 
SELECT :c1 + (3,4)::complex AS a, 3 + :c1 AS b, :c1 + 3 AS c;
   a   |   b   |   c  
-------+-------+-------
 (4,6) | (4,2) | (4,2)
(1 row)
 
SELECT :c1 - (3,6)::complex AS a, 3 - :c1 AS b, :c1 - 3 AS c;
    a    |   b    |   c   
---------+--------+--------
 (-2,-4) | (2,-2) | (-2,2)
(1 row)
 
SELECT :c1 * (3,5)::complex AS a, 3 * :c1 AS b, :c1 * 3 AS c;
    a    |   b   |   c  
---------+-------+-------
 (-7,11) | (3,6) | (3,6)
(1 row)
 
SELECT :c1 * (3,5)::complex AS a, 3.0::double precision * :c1 AS b, :c1 * 3.0 AS c;
    a    |   b   |   c  
---------+-------+-------
 (-7,11) | (3,6) | (3,6)
(1 row)
 
SELECT :c4 / :c1  AS a, (:c4 / :c1) * :c1 = :c4 AS b;
     a     | b
-----------+---
 (3.8,0.4) | t
(1 row)
 
SELECT :c4 / (2,0)::complex AS a, (2,0)::complex * (:c4 / (2,0)::complex)  = :c4 AS b;
    a    | b
---------+---
 (1.5,4) | t
(1 row)
 
SELECT :c4 / (0,2)::complex AS a, (0,2)::complex * (:c4 / (0,2)::complex) = :c4 AS b;
    a     | b
----------+---
 (4,-1.5) | t
(1 row)
 
SELECT :c4 / 3 AS a, 3 * (:c4 / 3) = :c4 AS b;
          a           | b
----------------------+---
 (1,2.66666666666667) | t
(1 row)
 
SELECT 3 / :c4 AS a, :c4 * (3 / :c4) = 3::complex AS b;
                   a                    | b
----------------------------------------+---
 (0.123287671232877,-0.328767123287671) | t
(1 row)
 
--
-- check magnitude
--
SELECT magnitude(:c1) AS magnitude;
    magnitude    
------------------
 2.23606797749979
(1 row)
 
SELECT magnitude(:c2) AS magnitude;
    magnitude   
-----------------
 1.4142135623731
(1 row)
 
SELECT magnitude(:c3) AS magnitude;
 magnitude
-----------
         5
(1 row)
 
ROLLBACK;

Вероятно, проще всего создать «ожидаемый» файл, запустив тест один раз, получив результаты из файла results / math.out и отредактировав этот файл, чтобы показать ожидаемые результаты. В чистой реализации TDD все тесты должны первоначально возвращать ноль, но мы уже определили многие из функций выше.

Реализация

Есть три функции языка SQL для добавления. Детали имеют значение — сумма без значений четко определена как 0 + 0i, но среднее значение без значений не определено (ноль), а не какое-либо конкретное значение.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
--
-- accumulator function is similar to float8_accum. Question: should the result
-- be the product of p * p or the product of p * ~p ?
--
CREATE OR REPLACE FUNCTION pgx_complex_accum(complex_accum, complex) RETURNS complex_accum AS $$
   SELECT CASE WHEN $1 IS NULL THEN 1 ELSE $1.cnt + 1 END,
          CASE WHEN $1 IS NULL THEN $2 ELSE $1.sum + $2 END,
          CASE WHEN $1 IS NULL THEN $2 * ~$2 ELSE $1.sofs + $2 * ~$2 END;
$$ LANGUAGE SQL;
 
--
-- disaccumulator(?) function is similar to pgx_complex_accum. It is required in order
-- to implement windowing functions.
--
CREATE OR REPLACE FUNCTION pgx_complex_disaccum(complex_accum, complex) RETURNS complex_accum AS $$
   SELECT pgx_complex_accum($1, -$2);
$$ LANGUAGE SQL;
 
--
-- average function returns quotient of sum over count.
--
CREATE OR REPLACE FUNCTION pgx_complex_avg(complex_accum) RETURNS complex AS $$
   SELECT CASE WHEN $1 IS NULL THEN NULL
               WHEN $1.cnt = 0 THEN (0,0)::complex
               ELSE $1.sum / $1.cnt END;
$$ LANGUAGE SQL;

Первая функция языка C демонстрирует, как читать составное значение и возвращать примитив. В этом случае мы получаем компоненты ‘re’ и ‘im’ по имени.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
PG_MODULE_MAGIC;
 
/**
 * Test complex numbers for proximity. This avoids the problems with testing floats
 * and doubles but does not guarantee absolute equality.
 */
PG_FUNCTION_INFO_V1(pgx_complex_near);
 
Datum
pgx_complex_near(PG_FUNCTION_ARGS) {
    double re[2];
    double im[2];
    double p, q;
    int i;
 
    // unwrap values.   
    for (i = 0; i < 2; i++) {
        HeapTupleHeader t = PG_GETARG_HEAPTUPLEHEADER(i);
        bool isnull[2];
 
        Datum dr = GetAttributeByName(t, "re", &isnull[0]);
        Datum di = GetAttributeByName(t, "im", &isnull[1]);
 
        // STRICT prevents the 'complex' value from being null but does
        // not prevent its components from being null.       
        if (isnull[0] || isnull[1]) {
            PG_RETURN_NULL();
        }
         
        re[i] = DatumGetFloat8(dr);
        im[i] = DatumGetFloat8(di);
    }
 
    // compute distance between points, distance of points from origin.
    p = hypot(re[0] - re[1], im[0] - im[1]);
    q = hypot(re[0], im[0]) + hypot(re[1], im[1]);
     
    if (q == 0) {
        PG_RETURN_BOOL(1);
    }
     
    // we consider the points 'near' each other if the distance between them is small
    // relative to the size of them.
    PG_RETURN_BOOL(p / q < 1e-8);
}

Второй случай возвращает составное значение. Есть два способа вернуть составные значения. Это более старый способ и требует немного больше работы. Более новый способ требует, чтобы все возвращалось в виде строки — это имеет скромную стоимость с примитивными значениями, но это может быть дорогостоящим для маршалирования и демаршализации пользовательских типов.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
 * Divide complex number by another. We do this by multiplying nominator and denominator
 * by the conjugate of the denominator. The denominator then becomes the scalar square of
 * the magnitude of the number.
 */
PG_FUNCTION_INFO_V1(pgx_complex_divide);
 
Datum
pgx_complex_divide(PG_FUNCTION_ARGS) {
    TupleDesc tupdesc;
    HeapTuple tuple;
    double re[2];
    double im[2];
    int i;
    double q;
    Datum datum[2];
    bool isnull[2];
  
    // build a tuple descriptor for our result type
    if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
        ereport(ERROR,
                (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                 errmsg("function returning record called in context "
                        "that cannot accept type record")));
 
    // unwrap values.   
    for (i = 0; i < 2; i++) {
        HeapTupleHeader t = PG_GETARG_HEAPTUPLEHEADER(i);
        bool isnull[2];
        Datum dr, di;
 
        dr = GetAttributeByName(t, "re", &isnull[0]);
        di = GetAttributeByName(t, "im", &isnull[1]);
 
        // STRICT prevents the 'complex' value from being null but does
        // not prevent its components from being null.       
        if (isnull[0] || isnull[1]) {
            PG_RETURN_NULL();
        }
         
        re[i] = DatumGetFloat8(dr);
        im[i] = DatumGetFloat8(di);
    }
 
    // the denominator is the square of the magnitude of the divisor.
    q = re[1] * re[1] + im[1] * im[1];
     
    // should I throw error instead of returning null?
    if (q == 0.0) {
        PG_RETURN_NULL();
    }
 
    datum[0] = Float8GetDatum((re[0] * re[1] + im[0] * im[1]) / q);
    datum[1] = Float8GetDatum((im[0] * re[1] - im[1] * re[0]) / q);
 
    BlessTupleDesc(tupdesc);
    tuple = heap_form_tuple(tupdesc, datum, isnull);
  
    PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
}

Третий пример также потребляет и производит составное значение.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
 * Calculate the norm of a complex number. This is the complex number on the unit
 * circle so that magnitude(norm(x)) = 1 and magnitude(x) * norm(x) = x.
 */
PG_FUNCTION_INFO_V1(pgx_complex_norm);
 
Datum
pgx_complex_norm(PG_FUNCTION_ARGS) {
    HeapTupleHeader t = PG_GETARG_HEAPTUPLEHEADER(0);
    TupleDesc tupdesc;
    HeapTuple tuple;
    double re;
    double im;
    bool isnull[2];
    Datum datum[2];
    double m;
  
    // build a tuple descriptor for our result type
    if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
        ereport(ERROR,
                (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                 errmsg("function returning record called in context "
                        "that cannot accept type record")));
         
    // unwrap values.   
    datum[0] = GetAttributeByName(t, "re", &isnull[0]);
    datum[1] = GetAttributeByName(t, "im", &isnull[1]);
 
    // STRICT prevents the 'complex' value from being null but does
    // not prevent its components from being null.       
    if (isnull[0] || isnull[1]) {
        PG_RETURN_NULL();
    }
         
    re = DatumGetFloat8(datum[0]);
    im = DatumGetFloat8(datum[1]);
 
    m = hypot(re, im);
    
    // should I throw error instead of returning null?
    if (m == 0.0) {
        PG_RETURN_NULL();
    }
 
    datum[0] = Float8GetDatum(re / m);
    datum[1] = Float8GetDatum(im / m);
 
    BlessTupleDesc(tupdesc);
    tuple = heap_form_tuple(tupdesc, datum, isnull);
  
    PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
}

Заворачивать

Это оборачивает расширение комплексного числа. Большинство расширений будет намного проще, но эта проблема была выбрана именно потому, что она показывает, насколько глубоко вы можете интегрировать расширение.

Исходный код