Invariant Properties

  • rss
  • Home

Proactive Database Defenses Using Triggers

Bear Giles | January 15, 2017

I’m sure I’ve discussed this a number of years ago but a question came up after the recent Boulder Linux User Group meeting and I decided this would be a good time to revisit it.

The question is how do you protected sensitive information from illicit insertion or modification when the attacker has full SQL access as the website user?

Important: I am focused on approaches we can use in the database itself, not our application, since the former will protect our data even if an attacker has full access to the database. These approaches are invisible to our database frameworks, e.g., JPA, once we have created the tables.

An Approach Without Triggers

At a minimum we can ensure that the database was properly configured with multiple users:

app_owner – owns the schema and tables. Often does not have INSERT/UPDATE/DELETE (or even SELECT) privileges on the tables.

app_user – owns the data but cannot modify the schema, tables, etc.

We can make this much more secure by splitting app_user into two users, app_reader and app_writer. The former user only has SELECT privileges on the tables. This is the only account used by user-facing code. The app_writer user adds INSERT/UPDATE/DELETE privileges and is only used by the methods that actually need to modify the data. Data is typically read so much more often that it is written that it often makes sense to view an application as actually two (or more) separate but related applications. In fact they may be – you can improve security by handling any data manipulation via microservices only visible to the application.

There is a big downside to this – modern database frameworks, e.g., JPA or Hibernate, make heavy use of caching to improve performance. You need to ensure that the the cache is properly updated in the app_reader cache whenever the corresponding record(s) are updated in the app_writer account.

Security Defense

This is highly database specific – does the database maintain logs that show when a user attempts to perform a non-permitted action? If so you can watch the logs on the app_reader account. Any attempt to insert or update data is a strong indication of an attacker.

Triggers Based On Related Information

A 3NF (or higher) database requires that each column be independent. In practice we often perform partial denormalization for performance reasons, e.g., adding a column for the day of the week in addition to the full date. We can easily compute the former from the latter but it takes time and can’t be indexed.

There’s a risk that a bug or intruder will introduce inconsistencies. One common solution is to use an INSERT OR UPDATE trigger that calculates the value at the time the data is inserted into the database. E.g.,

  1. CREATE FUNCTION calculate_day_of_week() ....
  2.  
  3. CREATE TABLE date_with_dow (
  4.     date text,
  5.     dow  text
  6. );
  7.  
  8. CREATE FUNCTION set_day_of_week() RETURNS trigger AS $$
  9.     BEGIN
  10.         NEW.date = OLD.date;
  11.         NEW.dow = calculate_day_of_week(OLD.date);
  12.         RETURN NEW;
  13.     END;
  14. $$ LANGUAGE plpgsql;
  15.  
  16. CREATE TRIGGER set_day_of_week BEFORE INSERT OR UPDATE ON date_with_dow
  17.    FOR EACH ROW EXECUTE PROCEDURE set_day_of_week();
CREATE FUNCTION calculate_day_of_week() ....

CREATE TABLE date_with_dow (
    date text,
    dow  text
);

CREATE FUNCTION set_day_of_week() RETURNS trigger AS $$
    BEGIN
        NEW.date = OLD.date;
        NEW.dow = calculate_day_of_week(OLD.date);
        RETURN NEW;
    END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER set_day_of_week BEFORE INSERT OR UPDATE ON date_with_dow
   FOR EACH ROW EXECUTE PROCEDURE set_day_of_week();

This ensures that the day of week is properly set. A software bug, or attacker, can try specifying an invalid value but they’ll fail.

Of course we don’t really care (much) if the day of the week is incorrect. However there are other times when we care a great deal, e.g., cached attributes from digital certificates. If someone can insert a certificate with mismatched cached values, esp. if it they can replace an existing table entry, then they can do a lot of damage if the code doesn’t assume that the database could be corrupted and thus perform its own validation checks on everything it gets back. (First rule of security: never trust anything.) Even with tests we’ll only know that the data has been corrupted, not when and not how broadly.

Security Defense

Developers are information packrats. Can we learn anything from the provided day of week value?

Yes. It’s a huge red flag if the provided value doesn’t match the calculated value (modulo planned exceptions, e.g., passing null or a sentinel value to indicate that the application is deferring to the database). It’s easy to add a quick test:

  1. CREATE FUNCTION calculate_day_of_week() ....
  2.  
  3. -- user-defined function that can do anything from adding an entry into
  4. -- a table to sending out an email, SMS, etc., alert
  5. CREATE FUNCTION security_alert() ....
  6.  
  7. CREATE TABLE date_with_dow (
  8.     date text,
  9.     dow  text
  10. );
  11.  
  12. CREATE FUNCTION set_day_of_week() RETURNS rigger AS $$
  13.     DECLARE
  14.         calculated_dow text;
  15.     BEGIN
  16.         NEW.date = OLD.date;
  17.         NEW.dow = calculate_day_of_week(OLD.date);
  18.         IF (NEW.dow  OLD.date) THEN
  19.             security_alert("bad dow value!");
  20.             RETURN null;
  21.         END IF;
  22.         RETURN NEW;
  23.     END;
  24. $$ LANGUAGE plpgsql;
  25.  
  26. CREATE TRIGGER set_day_of_week BEFORE INSERT OR UPDATE ON date_with_dow
  27.     FOR EACH ROW EXECUTE PROCEDURE set_day_of_week();
CREATE FUNCTION calculate_day_of_week() ....

-- user-defined function that can do anything from adding an entry into
-- a table to sending out an email, SMS, etc., alert
CREATE FUNCTION security_alert() ....

CREATE TABLE date_with_dow (
    date text,
    dow  text
);

CREATE FUNCTION set_day_of_week() RETURNS rigger AS $$
    DECLARE
        calculated_dow text;
    BEGIN
        NEW.date = OLD.date;
        NEW.dow = calculate_day_of_week(OLD.date);
        IF (NEW.dow  OLD.date) THEN
            security_alert("bad dow value!");
            RETURN null;
        END IF;
        RETURN NEW;
    END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER set_day_of_week BEFORE INSERT OR UPDATE ON date_with_dow
    FOR EACH ROW EXECUTE PROCEDURE set_day_of_week();

Sidenote: check out your database documentation for more ideas. For instance many applications use @PrePersist annotations to autofill creationDate and lastUpdateDate. It’s easy to do this via a trigger – and using a trigger ensures that the data updated even if an attacker does it via SQL injection or direct access. More impressively you can write audit information to a separate table, perhaps even in a separate schema that the app_user only has INSERT privileges for in order to prevent an attacker from learning what the system has learned about them, much less altering or deleting that information.

I’ve written triggers that generate XML representations of the OLD and NEW values and write them to an audit table together with date, etc. On INSERT the OLD data is null, on DELETE the NEW data is null. Using XML allows us to use a common audit table (table name is just a field) and potentially allows you to add transaction id, etc.

It is then easy to use a bit of simple XML diff code to see exactly what changed when by reviewing the audit table.

Resources:

  • PostgreSQL
  • MySQL
  • Oracle

Triggers Based On Secrets

What about tables where there’s no “related” columns? Can we use a trigger to detect an illicit attempt to INSERT or UPDATE a record?

Yes!

In this case we want to add an extra column to the table. It can be anything – the sole purpose is to create a way to pass a validation token to the trigger.

What are validation tokens?

A validation token can be anything you want. A few examples are:

A constant – this is the easiest but will be powerful as long as you can keep it secret. An example is ’42’. An obvious variant is the sum of several of the other columns of the table. This value should not be written to the database or it will be exposed to anyone with SELECT privileges.

A time-based value – your webserver and database will have closely synced clocks so you can use a time-based protocol such as Time-based One-time Password (TOTP) Algorithm. If both the database and application servers use NTP you can keep the window as small as a few seconds. Just remember to include one tick on either side when validating the token – NTP keeps the clocks synchronized but there can still be a very small skew plus network latency to consider.

Note: TOTP requires a shared secret and is independent of the contents of the INSERT or UPDATE statement.

You can save a time-based value but it is meaningless without a timestamp – and some algorithms can be cracked if you have a series of values and the starting time.

An HMAC value – most people will be familiar with standard cryptographic hashes such as MD-5 or SHA-1 (both considered cracked) or SHA-256. They’re powerful tools – but in part because everyone will compute the same values given the same input.

In our case we want an HMAC – it is a cryptographically strong message digest that also requires an encryption key. An attacker cannot generate their own HMAC but anyone with the corresponding digital certificate can verify one. An HMAC value requires something in the message to be processed and it needs to be intrinsic to the value of the record. For instance a digital certificate, a PDF document, even a hashed password. Don’t use it to hash the primary key or any value that can be readily reused.

You can freely save an HMAC value.

Subsequent validation

We would like to know that values haven’t been corrupted, e.g., by an attacker knowledgeable enough to disable the trigger, insert bad values, and then restore the trigger. The last step is important since we can / should run periodic scans to ensure all security-related features like these database triggers are still in place. Can we use these techniques to validate the records after the fact?

Constant value: no.

Time-base value: only if we record a timestamp as well, and if we do then we have to assume that the secret has been compromised. So… no.

HMAC value: yes.

Backups and Restorations

Backups and restorations have the same problems as subsequent validations. You can’t allow any magic values to be backed up (or an attacker could learn it by stealing the backup media) and you can’t allow the time-based values plus timestamps to be backed up (or an attacker could learn the shared secret by stealing the backup media). That means you would need to disable the trigger when restoring data to the database and you can’t verify that it’s properly validated afterwards. Remember: you can’t trust backup media!

The exception is HMAC tokens. They can be safely backed up and restored even if the triggers are in place.

Security Defense

You can add a token column to any table. As always it’s a balance between security and convenience and the less powerful techniques may be Good Enough for your needs. But for highly sensitive records, esp. those that are inserted or updated relatively infrequently, an HMAC token may be a good investment.

Implementation-wise: on the application side you can write a @PrePersist method that handles the creation of the TOTP or HMAC token. It’s a standard calculation and the biggest issue, as always, is key management. On the database side you’ll need to have a crypto library that supports whatever token method you choose.

Shadow Tables

Finally, there are two concerns with the last approach. First, it requires you to have crypto libraries available in your database. That may not be the case. Second if a value is inserted it’s impossible know for sure that it was your application that inserted it.

There’s a solution to this which is entirely app-side. It might not give you immediate notification of a problem but it still gives you some strong protection when you read the data.

You start as above – add a @PrePersist method that calculates an HMAC code. Only now you edit the domain bean so that the HMAC column uses a @SecondaryTable instead of the main table. (I think you can even specify a different schema if you want even higher security.) From the data perspective this is just two tables with a 1:1 relationship but from the source code perspective it’s still a single object.

Putting this into a separate table, if not a separate schema as well, means that a casual attacker will not know that it is there. They might succeed in inserting or modifying data but not realize that the changes will be detected even if audit triggers are disabled.

The final step is adding a @PostLoad method that verifies the HMAC code. If it’s good you can have confidence the data hasn’t been corrupted. If it’s incorrect or missing you know there’s a problem and you shouldn’t trust it.

For advanced users the developer won’t even know that the extra data is present – you can do a lot with AOP and some teams are organized so that the developers write unsecured code and a security team – which is focused entirely on security, not features – is responsible for adding security entirely through AOP code interwoven into the existing code. But that’s a topic for a future blog….

Comments
No Comments »
Categories
CEU, PostgreSQL, security
Comments rss Comments rss
Trackback Trackback

Introduction to Kerberos – PostgreSQL (part 2)

Bear Giles | April 21, 2016

The the first part of this introduction I discussed setting up the Kerberos infrastructure and creating users. In this part I will demonstrate how to use it to connect to a PostgreSQL server.

Note: GSSAPI is a generic security interface that can use different protocols on the backend. Kerberos is one of many protocols that can be used with GSSAPI. Using it allows developers to write flexible code while still allowing the system administrator to have the final word on which authentication method(s) will be accepted. When given a choice you should enable GSSAPI instead of just Kerberos.

Configuring the Server

The default servicename for PostgreSQL is ‘postgres’. There is one notable exception – working with Active Directory requires a servicename of ‘POSTGRES’ and that requires recompiling the server. The full principal for a PostgreSQL server is ‘postgres/fqdn@realm’ although in a some situations you’ll need to specify postgres@fqdn.

We start by creating a keytab file for the server:

  1. $ kadmin
  2. kadmin% ank -randkey postgres/db.invariantproperties.com
  3. kadmin% ktadd -k krb5.keytab postgres/db.invariantproperties.com
$ kadmin
kadmin% ank -randkey postgres/db.invariantproperties.com
kadmin% ktadd -k krb5.keytab postgres/db.invariantproperties.com

Note that the value of the fully-qualified domain name (fqdn) is critical. The server and client must both recognize it as the name of the server – the server uses it to identify which keytab entry to use as it comes up and the client uses it when it constructs its query to the KDC. Ideally the name will be fully supported by DNS – including reverse lookups – but it’s often enough to put entries into the /etc/hosts files if you’re only working with a few systems.

Once you have your keytab file you should copy it to /etc/postgresql/9.4/main/krb5.keytab and change the file’s ownership. You should also make sure that the file can only be read by the server.

  1. $ sudo chown postgres:postgres krb5.keytabe
  2. $ sudo chmod 0600 krb5.keytab
$ sudo chown postgres:postgres krb5.keytabe
$ sudo chmod 0600 krb5.keytab

We now need to tell the server the location of the keytab file and to listen to all network interfaces. Kerberos can only be used on TCP connections.

/etc/postgresql/9.4/main/postgresql.conf

  1. ...
  2. krb_server_keyfile = '/etc/postgresql/9.4/main/krb5.keytab'
  3. listen_addresses = '*'
...
krb_server_keyfile = '/etc/postgresql/9.4/main/krb5.keytab'
listen_addresses = '*'

We must also tell the server which databases can be accessed via Kerberos. Security note: in production we never want to use a wildcard (‘all’) for both database and user on a single line.

/etc/postgresql/9.4/main/pg_hba.conf

  1. # TYPE  DATABASE        USER            ADDRESS                 METHOD       OPTIONS
  2. host    all             all             52.34.69.195/32         gss          include_realm=1 map=gss krb_realm=INVARIANTPROPERTIES.COM
# TYPE  DATABASE        USER            ADDRESS                 METHOD       OPTIONS
host    all             all             52.34.69.195/32         gss          include_realm=1 map=gss krb_realm=INVARIANTPROPERTIES.COM

We will always want to retain the Kerberos realm so we don’t confuse ‘bob@FOO.COM’ with ‘bob@BAZ.COM’ but this requires us to use the pg_ident mapping file. The name of the mapping is arbitrary – I’m using ‘gss’ for convenience.

In this case we’re being even stricter and requiring that the Kerberos domain match ‘INVARIANTPROPERTIES.COM’ specifically. This isn’t always possible but you can repeat the line if you only need to support a few realms.

We must add entries to the server’s identity lookup file. The easiest approach is to add authorized users directly:

/etc/postgresql/9.4/main/pg_ident.conf

  1. # MAPNAME    SYSTEM-USERNAME                    PG-USERNAME
  2. gss          bgiles@INVARIANTPROPERTIES.COM     bgiles
# MAPNAME    SYSTEM-USERNAME                    PG-USERNAME
gss          bgiles@INVARIANTPROPERTIES.COM     bgiles

This is not a good long-term solution since you must manually restart the database, or at least reload the configuration files, every time you need to add or remove an authorized user. It’s much better to qualify the username, e.g., with ‘/postgres’, and then use regular expression matching in the pg_ident file.

/etc/postgresql/9.4/main/pg_ident.conf

  1. # MAPNAME    SYSTEM-USERNAME                                    PG-USERNAME
  2. gss           /^([^/]+)\/postgres@INVARIANTPROPERTIES\.COM$     \1
# MAPNAME    SYSTEM-USERNAME                                    PG-USERNAME
gss           /^([^/]+)\/postgres@INVARIANTPROPERTIES\.COM$     \1

Important: do not share Kerberos credentials. Either map multiple Kerberos users to the same PostgreSQL identity or map them to different PostgreSQL identities and use the standard grants and roles to control access within the database.

We’re now ready to restart the PostgreSQL server.

  1. $ sudo service postgresql restart
$ sudo service postgresql restart

Example

The following is an example dialogue as I attempt to log into the server.

  1. # try to log in without any Kerberos tickets
  2. bgiles@snowflake:~$ psql -h kpg
  3. psql: GSSAPI continuation error: Unspecified GSS failure.  Minor code may provide more information
  4. GSSAPI continuation error: No Kerberos credentials available
  5.  
  6. # log in as regular user, try to connect. The error is a little confusing but we do not get access.
  7. bgiles@snowflake:~$ kinit bgiles
  8. Password for bgiles@INVARIANTPROPERTIES.COM:
  9.  
  10. bgiles@snowflake:~$ psql -h kpg
  11. psql: duplicate GSS authentication request
  12.  
  13. # log in as database user, try to connect.
  14. bgiles@snowflake:~$ kinit bgiles/postgres
  15. Password for bgiles/postgres@INVARIANTPROPERTIES.COM:
  16.  
  17. bgiles@snowflake:~$ psql -h kpg
  18. psql (9.4.7, server 9.4.6)
  19. SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
  20. Type "help" for help.
  21.  
  22. bgiles=#
# try to log in without any Kerberos tickets
bgiles@snowflake:~$ psql -h kpg
psql: GSSAPI continuation error: Unspecified GSS failure.  Minor code may provide more information
GSSAPI continuation error: No Kerberos credentials available

# log in as regular user, try to connect. The error is a little confusing but we do not get access.
bgiles@snowflake:~$ kinit bgiles
Password for bgiles@INVARIANTPROPERTIES.COM: 

bgiles@snowflake:~$ psql -h kpg
psql: duplicate GSS authentication request

# log in as database user, try to connect.
bgiles@snowflake:~$ kinit bgiles/postgres
Password for bgiles/postgres@INVARIANTPROPERTIES.COM: 

bgiles@snowflake:~$ psql -h kpg
psql (9.4.7, server 9.4.6)
SSL connection (protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384, bits: 256, compression: off)
Type "help" for help.

bgiles=# 

Establishing a JDBC Connection

Connecting to the database via the CLI demonstrates that we have properly set up Kerberos authentication but in practice we will want to access the database programmatically. That means JDBC connections.

Is it possible?

The answer is yes and no. I’ll cover it in further detail in the next part but it’s easy to establish the connection but there’s an unexplained problem when mapping the GSS/Kerberos identity to the PostgreSQL identity. Investigation continues…

PostgreSQL currently supports Kerberos authentication if you use a simple Kerberos principal (user) and password as your connection properties. It does not support a compound Kerberos principal as I discussed above, nor does it support the use of keytab files.

I plan to submit a patch to support keytabs in the next few weeks. I don’t know how long review will take and of course it’s unlikely to be applied retroactively. Write me if you want a copy.

What About Other Databases?

Other databases also support Kerberos.

Oracle: Oracle has supported Kerberos for a long time with an optional security extension. In 2013 Oracle made it free to use for all versions of its database. I haven’t had a chance to see if I can use it with Oracle 11 XE for Linux. It was cut at about the same time but it can be tricky to set up Oracle XE on Linux. (I had it working on Ubuntu 14.04 LTS but then a routine package update broke it.)

Microsoft SQLServer: SQLServer has also supported Kerberos for a long time, at least in its Active Directory wolf’s clothing. I would expect MSSQL XE to also support AD/Kerberos.

MySQL/MariaDB: MySQL does not support Kerberos. MariaDB does via a plugin but I haven’t been able to bring my MariaDB server up to verify this.

Cassandra: Cassandra supports Kerberos. See Datastax.

MongoDB: MongoDB supports Kerberos.

Neo4j: No information.

Apache Hive: Hive supports Kerberos.

H2/Derby/embedded databases: They do not appear to support Kerberos but I can’t say this with any certainty.

(Note: if you’re not familiar with Oracle XE and MSSQL XE they’re the ‘first hit is free’ versions of the respective databases. You can use them on your website or application as long as the server is limited to a single CPU and you do not have more than 10 GB (iirc) of data. They’re usually one rev back from the commercial product. A Personal Use license lets you use the latest versions of the software but there are extremely tight restrictions – it’s basically only useful when learning how to use the respective database.)

Next Time

Next time I will discuss establishing low-level connections using Kerberos authentication.

Comments
No Comments »
Categories
CEU, linux, PostgreSQL, security
Comments rss Comments rss
Trackback Trackback

Storing X.509 Digital Certificates (And Other Messy Things)

Bear Giles | August 16, 2015

We often need to store structured binary data in our database – images, pdf documents, etc., but also have a need to search by, or index on, attributes of that data. E.g., we might store the height and width of an image, or the OCR text from a PDF document (for full text searches).

The normal solution is to store the object as a BLOB, programmatically extract the required information, and store it as additional columns. In nearly all cases that’s easy to do and good enough to solve our problem.

Release the kraken!

That’s “nearly” all cases. There are times when we need to exercise more care. Times when kraken, I mean lawyers, might be involved. Times when you might be asked if you took all reasonable steps to ensure that the data was not illicitly modified without you realizing it.

In these cases it’s not enough to just cache a value. We need to enlist the database to help us.

Threat model

There are two threats. First, the contents of the BLOB could be modified while all extracted values are left unchanged. This would mean that extra matches are found during searches. On the bright side we can verify the results actually matched and be alerted there’s a problem.

The second threat is more subtle. Extra results for some queries will be matched by fewer results in other queries. This means we can make records “disappear” even if there are measures in place to detect or prevent the deletion of records.

As a concrete example the first threat could be that an arrest record summary is changed from a felony to a misdemeanor. The second threat is that the search for prior arrests comes up empty.

X.509 digital certificates

For the rest of this article I’ll use X.509 digital certificates as a specific example. There are three reasons for this. First, they’re non-trivial and require a C-language extension to implement the user-defined functions and types – these are the types of things that are usually handled entirely at the application level. Second, they’re a real world example of the need to search and index values by cached values since it’s too expensive to recalculate them for each use. Finally, and not least, I’m familiar with them.

This article uses my PostgreSQL cert extension. This is a highly unstable extension (hence the 0.1.0 version as I write this) but it contains the minimum functions to store a digital certificate as a ‘cert’ type and to extract useful information from it. The details are not important – we just need an example of extracting “interesting” values from a BLOB. If this is distracting just replace any “cert” with a “blob” in the examples below.

Hardening the database

The database almost certainly does not have the functions we need to extract information from our BLOB. If it did it would probably be a first-class type, not a BLOB. This means we have to define our own functions. We might even define our own user-defined type (UDT) so we have some measure of type safety since we can put object validation into the methods that convert the object between internal and external formats.

Constraints

We can start by adding constraints on our table that ensure the cached values match the computed values.

  1. --
  2. -- Create a table containing digital certificates and cached values.
  3. --
  4. create table certs (
  5.     cert cert,
  6.     serial_number bignum,
  7.     not_before timestamp,
  8.     not_after timestamp,
  9.     issuer text,
  10.     subject text,
  11.  
  12.     -- define constraints
  13.     CONSTRAINT cert_serial CHECK (serial_number = get_serial_number(cert)),
  14.     CONSTRAINT cert_not_before CHECK (not_before = get_not_before(cert)),
  15.     CONSTRAINT cert_not_after CHECK (not_after = get_not_after(cert)),
  16.     CONSTRAINT cert_issuer CHECK (issuer = get_issuer(cert)),
  17.     CONSTRAINT cert_subject CHECK (subject = get_subject(cert))
  18. );
--
-- Create a table containing digital certificates and cached values.
--
create table certs (
    cert cert,
    serial_number bignum,
    not_before timestamp,
    not_after timestamp,
    issuer text,
    subject text,

    -- define constraints
    CONSTRAINT cert_serial CHECK (serial_number = get_serial_number(cert)),
    CONSTRAINT cert_not_before CHECK (not_before = get_not_before(cert)),
    CONSTRAINT cert_not_after CHECK (not_after = get_not_after(cert)),
    CONSTRAINT cert_issuer CHECK (issuer = get_issuer(cert)),
    CONSTRAINT cert_subject CHECK (subject = get_subject(cert))
);

The constraints make it impossible to insert or update invalid cached values. This gives us false confidence though – if an attacker has sufficient privileges it is possible to drop constraints, insert bad data, and then restore the constraints. This works since existing rows are not checked when constraints are added.

A good countermeasure to this is to periodically validate the constraints.

  1. ALTER TABLE certs VALIDATE CONSTRAINT check_serial;
  2. ALTER TABLE certs VALIDATE CONSTRAINT check_not_before;
  3. ALTER TABLE certs VALIDATE CONSTRAINT check_not_after;
  4. ALTER TABLE certs VALIDATE CONSTRAINT check_subject;
  5. ALTER TABLE certs VALIDATE CONSTRAINT check_issuer;
ALTER TABLE certs VALIDATE CONSTRAINT check_serial;
ALTER TABLE certs VALIDATE CONSTRAINT check_not_before;
ALTER TABLE certs VALIDATE CONSTRAINT check_not_after;
ALTER TABLE certs VALIDATE CONSTRAINT check_subject;
ALTER TABLE certs VALIDATE CONSTRAINT check_issuer;

Triggers

Another drawback to using constraints is that you must determine the correct values to insert. This either requires maintaining code in two places – the database and the application or writing much more complex INSERT and UPDATE statements. Fortunately it is easy write a trigger that sets the fields to the correct value upon any INSERT or UPDATE.

  1. --
  2. -- Create a stored procedure that sets several columns by calling the
  3. -- appropriate function instead of accepting the value provided to the
  4. -- INSERT or UPDATE call.
  5. --
  6. CREATE FUNCTION cert_trigger_proc() RETURNS trigger AS $$
  7. BEGIN
  8.     NEW.serial_number = get_serial_number(NEW.cert);
  9.     NEW.not_before = get_not_before(NEW.cert);
  10.     NEW.not_after = get_not_after(NEW.cert);
  11.     NEW.issuer = get_issuer(NEW.cert);
  12.     NEW.subject = get_subject(NEW.cert);
  13.  
  14.     RETURN NEW;
  15. END
  16. $$ LANGUAGE plpgsql;
  17.  
  18. --
  19. -- Define a trigger on the 'certs' table that will be called whenever
  20. -- a row is inserted or updated.
  21. --
  22. CREATE TRIGGER cert_trigger BEFORE INSERT OR UPDATE ON certs
  23.     FOR EACH ROW EXECUTE PROCEDURE cert_trigger_proc();
--
-- Create a stored procedure that sets several columns by calling the
-- appropriate function instead of accepting the value provided to the
-- INSERT or UPDATE call.
-- 
CREATE FUNCTION cert_trigger_proc() RETURNS trigger AS $$
BEGIN
    NEW.serial_number = get_serial_number(NEW.cert);
    NEW.not_before = get_not_before(NEW.cert);
    NEW.not_after = get_not_after(NEW.cert);
    NEW.issuer = get_issuer(NEW.cert);
    NEW.subject = get_subject(NEW.cert);

    RETURN NEW;
END
$$ LANGUAGE plpgsql;

--
-- Define a trigger on the 'certs' table that will be called whenever
-- a row is inserted or updated.
--
CREATE TRIGGER cert_trigger BEFORE INSERT OR UPDATE ON certs
    FOR EACH ROW EXECUTE PROCEDURE cert_trigger_proc();

Example

Here is an example of the results of inserting two values with a trigger in place.

  1. insert into certs values (
  2. cert('-----BEGIN CERTIFICATE-----
  3. MIIDEzCCAnygAwIBAgIBATANBgkqhkiG9w0BAQQFADCBxDELMAkGA1UEBhMCWkEx
  4. FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD
  5. VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv
  6. biBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEm
  7. MCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wHhcNOTYwODAx
  8. MDAwMDAwWhcNMjAxMjMxMjM1OTU5WjCBxDELMAkGA1UEBhMCWkExFTATBgNVBAgT
  9. DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3
  10. dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNl
  11. cyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3
  12. DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQAD
  13. gY0AMIGJAoGBANOkUG7I/1Zr5s9dtuoMaHVHoqrC2oQl/Kj0R1HahbUgdJSGHg91
  14. yekIYfUGbTBuFRkC6VLAYttNmZ7iagxEOM3+vuNkCXDF/rFrKbYvScg71CcEJRCX
  15. L+eQbcAoQpnXTEPew/UhbVSfXcNY4cDk2VuwuNy0e982OsK1ZiIS1ocNAgMBAAGj
  16. EzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAB/pMaVz7lcxG
  17. 7oWDTSEwjsrZqG9JGubaUeNgcGyEYRGhGshIPllDfU+VPaGLtwtimHp1it2ITk6e
  18. QNuozDJ0uW8NxuOzRAvZim+aKZuZGCg70eNAKJpaPNW15yAbi8qkq43pUdniTCxZ
  19. qdq5snUb9kLy78fyGPmJvKP/iiMucEc=
  20. -----END CERTIFICATE-----')),
  21. (cert('-----BEGIN CERTIFICATE-----
  22. MIICPDCCAaUCED9pHoGc8JpK83P/uUii5N0wDQYJKoZIhvcNAQEFBQAwXzELMAkG
  23. A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz
  24. cyAxIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2
  25. MDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV
  26. BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAxIFB1YmxpYyBQcmlt
  27. YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN
  28. ADCBiQKBgQDlGb9to1ZhLZlIcfZn3rmN67eehoAKkQ76OCWvRoiC5XOooJskXQ0f
  29. zGVuDLDQVoQYh5oGmxChc9+0WDlrbsH2FdWoqD+qEgaNMax/sDTXjzRniAnNFBHi
  30. TkVWaR94AoDa3EeRKbs2yWNcxeDXLYd7obcysHswuiovMaruo2fa2wIDAQABMA0G
  31. CSqGSIb3DQEBBQUAA4GBAFgVKTk8d6PaXCUDfGD67gmZPCcQcMgMCeazh88K4hiW
  32. NWLMv5sneYlfycQJ9M61Hd8qveXbhpxoJeUwfLaJFf5n0a3hUKw8fGJLj7qE1xIV
  33. Gx/KXQ/BUpQqEZnae88MNhPVNdwQGVnqlMEAv3WP2fr9dgTbYruQagPZRjXZ+Hxb
  34. -----END CERTIFICATE-----'));
  35. select * from certs;
  36.                                cert                               |             serial_number              |        not_before        |        not_after         |         issuer         |        subject        
  37. ------------------------------------------------------------------+----------------------------------------+--------------------------+--------------------------+------------------------+------------------------
  38.  -----BEGIN CERTIFICATE-----                                     +| 1                                      | Thu Aug 01 00:00:00 1996 | Thu Dec 31 23:59:59 2020 | C=ZA/ST=Western Cape/L | C=ZA/ST=Western Cape/L
  39.  MIIDEzCCAnygAwIBAgIBATANBgkqhkiG9w0BAQQFADCBxDELMAkGA1UEBhMCWkEx+|                                        |                          |                          |                        |
  40.  FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD+|                                        |                          |                          |                        |
  41.  VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv+|                                        |                          |                          |                        |
  42.  biBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEm+|                                        |                          |                          |                        |
  43.  MCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wHhcNOTYwODAx+|                                        |                          |                          |                        |
  44.  MDAwMDAwWhcNMjAxMjMxMjM1OTU5WjCBxDELMAkGA1UEBhMCWkExFTATBgNVBAgT+|                                        |                          |                          |                        |
  45.  DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3+|                                        |                          |                          |                        |
  46.  dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNl+|                                        |                          |                          |                        |
  47.  cyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3+|                                        |                          |                          |                        |
  48.  DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQAD+|                                        |                          |                          |                        |
  49.  gY0AMIGJAoGBANOkUG7I/1Zr5s9dtuoMaHVHoqrC2oQl/Kj0R1HahbUgdJSGHg91+|                                        |                          |                          |                        |
  50.  yekIYfUGbTBuFRkC6VLAYttNmZ7iagxEOM3+vuNkCXDF/rFrKbYvScg71CcEJRCX+|                                        |                          |                          |                        |
  51.  L+eQbcAoQpnXTEPew/UhbVSfXcNY4cDk2VuwuNy0e982OsK1ZiIS1ocNAgMBAAGj+|                                        |                          |                          |                        |
  52.  EzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAB/pMaVz7lcxG+|                                        |                          |                          |                        |
  53.  7oWDTSEwjsrZqG9JGubaUeNgcGyEYRGhGshIPllDfU+VPaGLtwtimHp1it2ITk6e+|                                        |                          |                          |                        |
  54.  QNuozDJ0uW8NxuOzRAvZim+aKZuZGCg70eNAKJpaPNW15yAbi8qkq43pUdniTCxZ+|                                        |                          |                          |                        |
  55.  qdq5snUb9kLy78fyGPmJvKP/iiMucEc=                                +|                                        |                          |                          |                        |
  56.  -----END CERTIFICATE-----                                       +|                                        |                          |                          |                        |
  57.                                                                   |                                        |                          |                          |                        |
  58.  -----BEGIN CERTIFICATE-----                                     +| 84287173645887463140025226144593929437 | Mon Jan 29 00:00:00 1996 | Wed Aug 02 23:59:59 2028 | C=US/O=VeriSign, Inc./ | C=US/O=VeriSign, Inc./
  59.  MIICPDCCAaUCED9pHoGc8JpK83P/uUii5N0wDQYJKoZIhvcNAQEFBQAwXzELMAkG+|                                        |                          |                          |                        |
  60.  A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz+|                                        |                          |                          |                        |
  61.  cyAxIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2+|                                        |                          |                          |                        |
  62.  MDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV+|                                        |                          |                          |                        |
  63.  BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAxIFB1YmxpYyBQcmlt+|                                        |                          |                          |                        |
  64.  YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN+|                                        |                          |                          |                        |
  65.  ADCBiQKBgQDlGb9to1ZhLZlIcfZn3rmN67eehoAKkQ76OCWvRoiC5XOooJskXQ0f+|                                        |                          |                          |                        |
  66.  zGVuDLDQVoQYh5oGmxChc9+0WDlrbsH2FdWoqD+qEgaNMax/sDTXjzRniAnNFBHi+|                                        |                          |                          |                        |
  67.  TkVWaR94AoDa3EeRKbs2yWNcxeDXLYd7obcysHswuiovMaruo2fa2wIDAQABMA0G+|                                        |                          |                          |                        |
  68.  CSqGSIb3DQEBBQUAA4GBAFgVKTk8d6PaXCUDfGD67gmZPCcQcMgMCeazh88K4hiW+|                                        |                          |                          |                        |
  69.  NWLMv5sneYlfycQJ9M61Hd8qveXbhpxoJeUwfLaJFf5n0a3hUKw8fGJLj7qE1xIV+|                                        |                          |                          |                        |
  70.  Gx/KXQ/BUpQqEZnae88MNhPVNdwQGVnqlMEAv3WP2fr9dgTbYruQagPZRjXZ+Hxb+|                                        |                          |                          |                        |
  71.  -----END CERTIFICATE-----                                       +|                                        |                          |                          |                        |
  72.                                                                   |                                        |                          |                          |                        |
  73. (2 rows)
insert into certs values (
cert('-----BEGIN CERTIFICATE-----
MIIDEzCCAnygAwIBAgIBATANBgkqhkiG9w0BAQQFADCBxDELMAkGA1UEBhMCWkEx
FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD
VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv
biBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEm
MCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wHhcNOTYwODAx
MDAwMDAwWhcNMjAxMjMxMjM1OTU5WjCBxDELMAkGA1UEBhMCWkExFTATBgNVBAgT
DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3
dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNl
cyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3
DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQAD
gY0AMIGJAoGBANOkUG7I/1Zr5s9dtuoMaHVHoqrC2oQl/Kj0R1HahbUgdJSGHg91
yekIYfUGbTBuFRkC6VLAYttNmZ7iagxEOM3+vuNkCXDF/rFrKbYvScg71CcEJRCX
L+eQbcAoQpnXTEPew/UhbVSfXcNY4cDk2VuwuNy0e982OsK1ZiIS1ocNAgMBAAGj
EzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAB/pMaVz7lcxG
7oWDTSEwjsrZqG9JGubaUeNgcGyEYRGhGshIPllDfU+VPaGLtwtimHp1it2ITk6e
QNuozDJ0uW8NxuOzRAvZim+aKZuZGCg70eNAKJpaPNW15yAbi8qkq43pUdniTCxZ
qdq5snUb9kLy78fyGPmJvKP/iiMucEc=
-----END CERTIFICATE-----')),
(cert('-----BEGIN CERTIFICATE-----
MIICPDCCAaUCED9pHoGc8JpK83P/uUii5N0wDQYJKoZIhvcNAQEFBQAwXzELMAkG
A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz
cyAxIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2
MDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV
BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAxIFB1YmxpYyBQcmlt
YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN
ADCBiQKBgQDlGb9to1ZhLZlIcfZn3rmN67eehoAKkQ76OCWvRoiC5XOooJskXQ0f
zGVuDLDQVoQYh5oGmxChc9+0WDlrbsH2FdWoqD+qEgaNMax/sDTXjzRniAnNFBHi
TkVWaR94AoDa3EeRKbs2yWNcxeDXLYd7obcysHswuiovMaruo2fa2wIDAQABMA0G
CSqGSIb3DQEBBQUAA4GBAFgVKTk8d6PaXCUDfGD67gmZPCcQcMgMCeazh88K4hiW
NWLMv5sneYlfycQJ9M61Hd8qveXbhpxoJeUwfLaJFf5n0a3hUKw8fGJLj7qE1xIV
Gx/KXQ/BUpQqEZnae88MNhPVNdwQGVnqlMEAv3WP2fr9dgTbYruQagPZRjXZ+Hxb
-----END CERTIFICATE-----'));
select * from certs;
                               cert                               |             serial_number              |        not_before        |        not_after         |         issuer         |        subject         
------------------------------------------------------------------+----------------------------------------+--------------------------+--------------------------+------------------------+------------------------
 -----BEGIN CERTIFICATE-----                                     +| 1                                      | Thu Aug 01 00:00:00 1996 | Thu Dec 31 23:59:59 2020 | C=ZA/ST=Western Cape/L | C=ZA/ST=Western Cape/L
 MIIDEzCCAnygAwIBAgIBATANBgkqhkiG9w0BAQQFADCBxDELMAkGA1UEBhMCWkEx+|                                        |                          |                          |                        | 
 FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD+|                                        |                          |                          |                        | 
 VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv+|                                        |                          |                          |                        | 
 biBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEm+|                                        |                          |                          |                        | 
 MCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wHhcNOTYwODAx+|                                        |                          |                          |                        | 
 MDAwMDAwWhcNMjAxMjMxMjM1OTU5WjCBxDELMAkGA1UEBhMCWkExFTATBgNVBAgT+|                                        |                          |                          |                        | 
 DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3+|                                        |                          |                          |                        | 
 dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNl+|                                        |                          |                          |                        | 
 cyBEaXZpc2lvbjEZMBcGA1UEAxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3+|                                        |                          |                          |                        | 
 DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQAD+|                                        |                          |                          |                        | 
 gY0AMIGJAoGBANOkUG7I/1Zr5s9dtuoMaHVHoqrC2oQl/Kj0R1HahbUgdJSGHg91+|                                        |                          |                          |                        | 
 yekIYfUGbTBuFRkC6VLAYttNmZ7iagxEOM3+vuNkCXDF/rFrKbYvScg71CcEJRCX+|                                        |                          |                          |                        | 
 L+eQbcAoQpnXTEPew/UhbVSfXcNY4cDk2VuwuNy0e982OsK1ZiIS1ocNAgMBAAGj+|                                        |                          |                          |                        | 
 EzARMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAB/pMaVz7lcxG+|                                        |                          |                          |                        | 
 7oWDTSEwjsrZqG9JGubaUeNgcGyEYRGhGshIPllDfU+VPaGLtwtimHp1it2ITk6e+|                                        |                          |                          |                        | 
 QNuozDJ0uW8NxuOzRAvZim+aKZuZGCg70eNAKJpaPNW15yAbi8qkq43pUdniTCxZ+|                                        |                          |                          |                        | 
 qdq5snUb9kLy78fyGPmJvKP/iiMucEc=                                +|                                        |                          |                          |                        | 
 -----END CERTIFICATE-----                                       +|                                        |                          |                          |                        | 
                                                                  |                                        |                          |                          |                        | 
 -----BEGIN CERTIFICATE-----                                     +| 84287173645887463140025226144593929437 | Mon Jan 29 00:00:00 1996 | Wed Aug 02 23:59:59 2028 | C=US/O=VeriSign, Inc./ | C=US/O=VeriSign, Inc./
 MIICPDCCAaUCED9pHoGc8JpK83P/uUii5N0wDQYJKoZIhvcNAQEFBQAwXzELMAkG+|                                        |                          |                          |                        | 
 A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFz+|                                        |                          |                          |                        | 
 cyAxIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2+|                                        |                          |                          |                        | 
 MDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVowXzELMAkGA1UEBhMCVVMxFzAVBgNV+|                                        |                          |                          |                        | 
 BAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAxIFB1YmxpYyBQcmlt+|                                        |                          |                          |                        | 
 YXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUAA4GN+|                                        |                          |                          |                        | 
 ADCBiQKBgQDlGb9to1ZhLZlIcfZn3rmN67eehoAKkQ76OCWvRoiC5XOooJskXQ0f+|                                        |                          |                          |                        | 
 zGVuDLDQVoQYh5oGmxChc9+0WDlrbsH2FdWoqD+qEgaNMax/sDTXjzRniAnNFBHi+|                                        |                          |                          |                        | 
 TkVWaR94AoDa3EeRKbs2yWNcxeDXLYd7obcysHswuiovMaruo2fa2wIDAQABMA0G+|                                        |                          |                          |                        | 
 CSqGSIb3DQEBBQUAA4GBAFgVKTk8d6PaXCUDfGD67gmZPCcQcMgMCeazh88K4hiW+|                                        |                          |                          |                        | 
 NWLMv5sneYlfycQJ9M61Hd8qveXbhpxoJeUwfLaJFf5n0a3hUKw8fGJLj7qE1xIV+|                                        |                          |                          |                        | 
 Gx/KXQ/BUpQqEZnae88MNhPVNdwQGVnqlMEAv3WP2fr9dgTbYruQagPZRjXZ+Hxb+|                                        |                          |                          |                        | 
 -----END CERTIFICATE-----                                       +|                                        |                          |                          |                        | 
                                                                  |                                        |                          |                          |                        | 
(2 rows)

The truncated ‘issuer’ and ‘subject’ fields are a known bug. (Version 0.1.0, remember?) It highlights a key point though – this is not a magic bullet and a buggy function may still let things through. Modifying the functions used to create cached values means you’ll need to drop the constraints, reinitialize all cached values, and then restore the constraints.

Database security

Finally I need to come back to the point of someone dropping a constraint and then reinstating it. None of this works without solid database security. A handful of rules will go a long way.

A dedicated database user should own the schema. This user should not be used for any other purpose – it should only be used when creating and modifying the schema.

No other user should have ALTER privileges. A dedicated user isn’t enough if other users can alter the schema anyway. The owner should be the only user with ALTER privileges by default but it would not be a bad idea to double-check.

Periodically audit the schema. This doesn’t have to be very sophisticated and can be done programmatically. Periodically query the metadata for the database and ensure tables and functions are owned by the correct users, that they have the correct privileges, that there are no unexpected tables in the schema, that there are no unexpected columns in the tables.

Comments
No Comments »
Categories
PostgreSQL, security, stored procedures
Comments rss Comments rss
Trackback Trackback

Installing PostgreSQL PL/Java as a PostgreSQL Extension.

Bear Giles | August 8, 2015

In 2011 I wrote a series of articles on PostgreSQL PL/Java. The basic information is still solid but there is a now a much easier way to install PL/Java from source. This also eliminates the need to depend on third parties to create packages. These notes will be fairly brief since I assume my readers are already familiar with git and maven.

(Note: I’ve passed this information to the PL/Java team so it may already be handled by the time you read this.)

Perform the basic build

  1. Clone the PL/Java repository at https://github.com/tada/pljava.
  2. Run maven not make.
  3. …
  4. Profit!

Of course it’s not that simple. Maven can pull in its own dependencies but we still need several specialized libraries beyond the standard GNU toolchain. On my Ubuntu system I needed:

  • postgresql-server-dev-9.4
  • libpg-dev
  • libpgtypes3
  • libecpg-dev

(I don’t know the corresponding package names for RedHat/Fedora/CentOS.)

It may take a bit of experimentation but it shouldn’t be too hard to identify all of the packages you need. Just remember that you’ll usually want the packages with the “-dev” extension.

There are a large number of compiler warnings and errors but most if not all seem to be related to sign conversions. This warrants further investigation – sign conversion warnings indicate possible attack surfaces by malicious users – but for now we should be fine as long as maven succeeds. We need three files:

  1. ./src/sql/install.sql
  2. ./pljava/target/pljava-0.0.2-SNAPSHOT.jar
  3. ./pljava-so/target/nar/pljava-so-0.0.2-SNAPSHOT-i386-Linux-gpp-shared/lib/i386-Linux-gpp/shared/libpljava-so-0.0.2-SNAPSHOT.so
./src/sql/install.sql
./pljava/target/pljava-0.0.2-SNAPSHOT.jar
./pljava-so/target/nar/pljava-so-0.0.2-SNAPSHOT-i386-Linux-gpp-shared/lib/i386-Linux-gpp/shared/libpljava-so-0.0.2-SNAPSHOT.so

Copying the files

We can now copy the three files to their respective locations.

  1. $ sudo cp ./pljava-so/target/nar/pljava-so-0.0.2-SNAPSHOT-i386-Linux-gpp-shared/lib/i386-Linux-gpp/shared/libpljava-so-0.0.2-SNAPSHOT.so \
  2.   /usr/lib/postgresql/9.4/lib/pljava.so
  3.  
  4. $ sudo cp ./pljava/target/pljava-0.0.2-SNAPSHOT.jar /usr/share/postgresql/9.4/extension/pljava--1.4.4.jar
  5.  
  6. $ sudo cp ./src/sql/install.sql /usr/share/postgresql/9.4/extension/pljava--1.4.4.sql
$ sudo cp ./pljava-so/target/nar/pljava-so-0.0.2-SNAPSHOT-i386-Linux-gpp-shared/lib/i386-Linux-gpp/shared/libpljava-so-0.0.2-SNAPSHOT.so \
  /usr/lib/postgresql/9.4/lib/pljava.so

$ sudo cp ./pljava/target/pljava-0.0.2-SNAPSHOT.jar /usr/share/postgresql/9.4/extension/pljava--1.4.4.jar

$ sudo cp ./src/sql/install.sql /usr/share/postgresql/9.4/extension/pljava--1.4.4.sql

We can learn the correct target directory with the ‘pg_config’ command.

  1. $ pg_config
  2. PKGLIBDIR = /usr/lib/postgresql/9.4/lib
  3. SHAREDIR = /usr/share/postgresql/9.4
  4. ...
$ pg_config
PKGLIBDIR = /usr/lib/postgresql/9.4/lib
SHAREDIR = /usr/share/postgresql/9.4
...

I have changed the version from 0.0.2-SNAPSHOT to 1.4.4 since we want to capture the PL/Java version, not the pom.xml version. I hope these will soon be kept in sync.

Editing pljava–1.4.4.sql

We need to add two lines to the installation sql:

  1. SET PLJAVA.CLASSPATH='/usr/share/postgresql/9.4/extension/pljava--1.4.4.jar';
  2. SET PLJAVA.VMOPTIONS='-Xms64M -Xmx128M';
SET PLJAVA.CLASSPATH='/usr/share/postgresql/9.4/extension/pljava--1.4.4.jar';
SET PLJAVA.VMOPTIONS='-Xms64M -Xmx128M';

It’s important to remember that there is a unique JVM instantiated for each database connection. Memory consumption can become a major concern when you have 20+ simultaneous connections.

Create the pljava.control file

We must tell PostgreSQL about the new extension. This is handled by a control file.

/usr/share/postgresql/9.4/extension/pljava.control

  1. # pljava extension
  2. comment = 'PL/Java bundled as an extension'
  3. default_version = '1.4.4'
  4. relocatable = false
# pljava extension
comment = 'PL/Java bundled as an extension'
default_version = '1.4.4'
relocatable = false

Make libjvm.so visible

We normally specify the location of the java binaries and shared libraries via the JAVA_HOME environment variable. This isn’t an option with the database server.

There are two approaches depending on whether you want to make the java shared library (libjvm.so) visible to all applications or just the database server. I think the former is easiest.

We need to create a single file

/etc/ld.so.conf.d/i386-linux-java.conf

  1. /usr/lib/jvm/java-8-openjdk-i386/jre/lib/i386/server
/usr/lib/jvm/java-8-openjdk-i386/jre/lib/i386/server

where most of the pathname comes from JAVA_HOME. The location may be different on your system. The directory must contain the shared library ‘libjvm.so’.

We must also tell the system to refresh its cache.

  1. $ sudo ldconfig
  2. $ sudo ldconfig -p | grep jvm
  3.     libjvm.so (libc6) => /usr/lib/jvm/java-8-openjdk-i386/jre/lib/i386/server/libjvm.so
  4.     libjsig.so (libc6) => /usr/lib/jvm/java-8-openjdk-i386/jre/lib/i386/server/libjsig.so
$ sudo ldconfig
$ sudo ldconfig -p | grep jvm
	libjvm.so (libc6) => /usr/lib/jvm/java-8-openjdk-i386/jre/lib/i386/server/libjvm.so
	libjsig.so (libc6) => /usr/lib/jvm/java-8-openjdk-i386/jre/lib/i386/server/libjsig.so

Loading the extension

We can now easily load and unload PL/Java.

  1. => CREATE EXTENSION pljava;
  2. CREATE EXTENSION
  3.  
  4. => DROP EXTENSION pljava;
  5. DROP EXTENSION
=> CREATE EXTENSION pljava;
CREATE EXTENSION

=> DROP EXTENSION pljava;
DROP EXTENSION

In case of flakiness…

If the system seems flaky you can move the two ‘set’ commands into the postgresql.conf file.

/etc/postgresql/9.4/main/postgresql.conf

  1. #------------------------------------------------------------------------------
  2. # CUSTOMIZED OPTIONS
  3. #------------------------------------------------------------------------------
  4.  
  5. # Add settings for extensions here
  6.  
  7. PLJAVA.CLASSPATH='/usr/share/postgresql/9.4/extension/pljava--1.4.4.jar'
  8. PLJAVA.VMOPTIONS='-Xms64M -Xmx128M'
#------------------------------------------------------------------------------
# CUSTOMIZED OPTIONS
#------------------------------------------------------------------------------

# Add settings for extensions here

PLJAVA.CLASSPATH='/usr/share/postgresql/9.4/extension/pljava--1.4.4.jar'
PLJAVA.VMOPTIONS='-Xms64M -Xmx128M'
Comments
No Comments »
Categories
java, pl/java, PostgreSQL
Comments rss Comments rss
Trackback Trackback

Extending PostgreSQL: Complex Number Data Type

Bear Giles | July 17, 2015

A few years ago I discussed creating user-defined types using PL/java. (Introduction to PostgreSQL PL/Java, part 4: User Defined Types) Today I follow up on the comment that we would use a standard C-language PostgreSQL extension in practice.

What is an extension?

A PostgreSQL extension is nothing more than bundled SQL statements and an optional native library. SQL-only extensions contain nothing but SQL statements.

Extensions provide two major benefits over the standalone scripts. First, the statements are loaded and unloaded as a unit. It is not possible to alter anything defined in an extension. This is a big win for maintainability and security. Second, extensions are versioned. This allows the SQL artifacts to be cleanly updated over time, something particularly important with user-defined types since simply dropping the UDT will cause the loss of data.

PostgreSQL has well-defined support for extensions. See Packaging Related Objects into an Extension.

Loading an extension

  1. CREATE EXTENSION IF NOT EXISTS pg_complex;
CREATE EXTENSION IF NOT EXISTS pg_complex;

Unloading an extension

  1. DROP EXTENSION pg_complex;
DROP EXTENSION pg_complex;

It is important to remember that many if not most hosted database-as-a-service (DAAS) providers, e.g., Amazon RDS, will not allow you to install arbitrary extensions. You can still load SQL-only extensions by running the creation scripts manually but C-language extensions could cause problems in the future.

What are user-defined types (UDT)?

User-defined types are extensions to the database to support new types of data. SQL purists want tables to contain nothing but the standard primitives. Data that has a structure can be captured in a separate table and a foreign key, e.g., pulling an Address table out of a Customer table.

Many developers recognize that some data is more tightly bound than others and useful operations typically require more than one primitive value. Geographic position (latitude, longitude) and cryptographic information are classic examples. These objects are often so small that it does not make sense to have a separate table for them – compare a mailing address vs. the lat-long of that address.

(There may be other reasons to store the UDTs in a separate table, e.g., for improved security of sensitive information.)

Another use for UDTs is to add type safety to BLOBs. For instance it is reasonable to have user-defined functions that return the height or width of an image or the number of pages of a PDF document. You can easily write functions that accept BLOBs (bytea) but you can’t ensure that the value passed to the function is the appropriate type. Defining a UDT, e.g. pdf or jpeg, gives the developer a powerful tool.

In conclusion UDTs should be considered when 1) the object is meaningless if any element is missing or 2) the object would otherwise be a BLOB and you want to provide type-safe stored procedures and user-defined functions. Otherwise we should stick with the standard primitives.

Complex Numbers?

I will be creating a UDT for complex numbers below. I do not see a great unmet need to store complex numbers in relational databases but it is a good choice for educational purposes since it requires custom types, functions, casts, operators, and aggregate functions. The only thing missing is ordering.

This implementation uses a PostgreSQL composite type with a combination of SQL stored procedures and C user-defined functions. Complex numbers will be shown as (a, b) instead of the conventional a + bi but the latter is possible with additional work.

SQL definitions

This is a PostgreSQL extension so we should start by defining what we expect to see in our improved database.

Defining the complex UDT

The complex UDT consists of two fields – a real (re) component and an imaginary (im) component.

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

A very simple demonstration of its use follows.

  1. $> CREATE TABLE t (c complex);
  2. CREATE TABLE
  3.  
  4. -- insert a value. Note that we are inserting '(1,2)', not '1,2'.
  5. $> INSERT INTO t VALUES((1,2));
  6. INSERT 0 1
  7.  
  8. -- select full UDT
  9. $> SELECT c FROM t;
  10.    c  
  11. -------
  12.  (1,2)
  13.  
  14. -- select components. Note that we must surround field with parentheses.
  15. $> SELECT (c).re, (c).im FROM t;
  16.  re | im
  17. ----+----
  18.   1 |  2
$> 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

Autopromoting floats to a complex numbers

It is easy to extract the real component of a complex number but still a pain to convert a real number to a complex number. PostgreSQL can do this transparently if we define a CAST.

  1. CREATE OR REPLACE FUNCTION pgx_complex_from_int(int) RETURNS complex AS $$
  2.    SELECT ROW($1::float8, 0)::complex;
  3. $$ LANGUAGE SQL IMMUTABLE STRICT;
  4.  
  5. CREATE OR REPLACE FUNCTION pgx_complex_from_bigint(bigint) RETURNS complex AS $$
  6.    SELECT ROW($1::float8, 0)::complex;
  7. $$ LANGUAGE SQL IMMUTABLE STRICT;
  8.  
  9. CREATE OR REPLACE FUNCTION pgx_complex_from_numeric(numeric) RETURNS complex AS $$
  10.    SELECT ROW($1::float8, 0)::complex;
  11. $$ LANGUAGE SQL IMMUTABLE STRICT;
  12.  
  13. CREATE OR REPLACE FUNCTION pgx_complex_from_numeric(float8) RETURNS complex AS $$
  14.    SELECT ROW($1, 0)::complex;
  15. $$ LANGUAGE SQL IMMUTABLE STRICT;
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;

Defining arithmetic operators

Complex numbers are numbers so we want to implement the standard arithmetic operators for them. All of the C function names are smurfed since they must unique. The SQL function names do not have to be unique since the function signature is also considered.

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

Defining aggregate functions

Aggregate functions are one of the main ways to put intelligence into the database instead of treating it like a glorified file system. These are functions that operate on a collection of values and return an aggregate value of some type. Aggregate functions can have multiple parameters.

Don’t assume aggregates can only consume and produce numeric data. Consider a function that takes (x, y) pairs and produces a .png plot of the results.

Aggregate functions can optionally support window functions (http://www.postgresql.org/docs/9.4/static/tutorial-window.html). These are a relatively new feature and incredibly powerful. Implementation is conceptually simple – we only need functions to add or remove a value from the aggregate function’s ‘state’ – but in practice operations may be irreversible. That is the case here – if we compute ‘1e20 + 1 – 1e20’ the results should be ‘1’ but may be zero due to limited resolution.

  1. --
  2. -- UDT that keeps track of sums and sums-of-squares of a collection
  3. -- of complex values. Tracking all three values allows us to compute
  4. -- a wide variety of statistical values.
  5. --
  6. CREATE TYPE complex_accum AS (
  7.    cnt  int,
  8.    sum  complex,
  9.    sofs complex
  10. );
  11.  
  12. --
  13. -- Calculate sum of a collection of complex values
  14. --
  15. CREATE AGGREGATE sum(complex) (
  16.    sfunc = pg_complex_add,
  17.    stype = complex_accum,
  18.    initcond = '(0, 0)',
  19.    -- msfunc = pg_complex_add,
  20.    -- minvfunc = pg_complex_subtract,
  21.    -- mstype = complex_accum,
  22.    -- minitcond = (0, 0)'
  23. );
  24.  
  25. --
  26. -- Calculate average of a collection of complex values.
  27. --
  28. CREATE AGGREGATE avg(complex) (
  29.    sfunc = pg_complex_accum,
  30.    stype = complex_accum,
  31.    finalfunc = pg_complex_avg
  32.    -- msfunc = pg_complex_accum,
  33.    -- minvfunc = pg_complex_disaccum,
  34.    -- mstype = complex_accum,
  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,
);

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

Defining user-defined functions

We now know the functions and signatures that we must implement. In this case we can do most of the functions in pure SQL but choose to do a few in C to demonstrate advanced techniques.

Note: under TDD principles we should only implement enough to allow the tests to run. In this case the functions should return null. I’m not doing that here since the functions are so simple they can be verified at a glance. Any multi-line function should follow TDD principles and return null.

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

Creating a skeleton extension

We’re now ready to create a skeleton extension. This extension follows test-driven development (TDD) practices – we want the minimal amount of code that fails. In this case that means the extension will load and define the user-defined functions and type but all functions and operators immediately return NULL.

The easiest way to do this is the PGXN utilities.

First, make sure that the following packages are installed:

  • pgxnclient
  • postgresql-server-dev-9.4
  • make
  • ruby
  • ruby2.1-dev
  • gcc

(This is for PostgreSQL 9.4 under Ubuntu. Adjust accordingly.)

Second, clone the github repository guedes/pgxn-utils.

Third, install these tools.

  1. $ sudo pgxnclient install pgxn_utils
  2.  
  3. # verify utilities have been installed.
  4. $ pgxn-utils help
  5. PGXN Utils version: 0.1.4
  6. Commands:
  7.   pgxn-utils bundle [extension_name]  # Bundles the extension in a zip file
  8.   pgxn-utils change [extension_name]  # Changes META's attributes in current extension
  9.   pgxn-utils help [COMMAND]           # Describe available commands or one specific command
  10.   pgxn-utils release filename         # Release an extension to PGXN
  11.   pgxn-utils skeleton extension_name  # Creates an extension skeleton in current directory
$ 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

Fourth, create a skeleton for a C-based PostgreSQL extension using our new utilities.

  1. $ pgxn skeleton -m "Bear Giles <bgiles@coyotesong.com>" --template=c pg_complex
  2.       create  pg_complex
  3.       create  pg_complex/pg_complex.control
  4.       create  pg_complex/.gitignore
  5.       create  pg_complex/.template
  6.       create  pg_complex/META.json
  7.       create  pg_complex/Makefile
  8.       create  pg_complex/README.md
  9.       create  pg_complex/doc/pg_complex.md
  10.       create  pg_complex/sql/pg_complex.sql
  11.       create  pg_complex/sql/uninstall_pg_complex.sql
  12.       create  pg_complex/src/pg_complex.c
  13.       create  pg_complex/test/expected/base.out
  14.       create  pg_complex/test/sql/base.sql
$ pgxn skeleton -m "Bear Giles <bgiles@coyotesong.com>" --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

Fifth, edit the META.json, README.md and doc/pg_complex.md files to describe the extension. This would also be a good time to copy a LICENSE file into this directory if you have any plans to release the extension to others. Your future self will thank you for this documentation.

The META.json file allows us to specify dependencies among extensions.

Sixth, create a dummy implementation of each function that immediately returns null.

  1. #include "postgres.h"
  2. #include "fmgr.h"
  3.  
  4. PG_MODULE_MAGIC;
  5.  
  6. /*
  7.  * Are two points "near" each other. This function requires reading
  8.  * composite types.
  9.  */
  10. PG_FUNCTION_INFO_V1(pgx_complex_near);
  11.  
  12. Datum
  13. pgx_complex_near(PG_FUNCTION_ARGS) {
  14.     PG_RETURN_NULL();
  15. }
  16.  
  17. /*
  18.  * Divide one complex number by another. This function requires reading
  19.  * and returning composite types.
  20.  */
  21. PG_FUNCTION_INFO_V1(pgx_complex_divide);
  22.  
  23. Datum
  24. pgx_complex_divide(PG_FUNCTION_ARGS) {
  25.     PG_RETURN_NULL();
  26. }
  27.  
  28. /*
  29.  * Scale a complex number so it on the unit circle. This function requires
  30.  * reading and returning composite types.
  31.  */
  32. PG_FUNCTION_INFO_V1(pgx_complex_norm);
  33.  
  34. Datum
  35. pgx_complex_norm(PG_FUNCTION_ARGS) {
  36.     PG_RETURN_NULL();
  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();
}

Our problem is simple enough that the SQL stored procedures implemented the required functionality. More complex stored procedures could be implemented as plpsql stored procedures that return null.

Seventh, build the system.

  1. $ make
  2. $ sudo make install
$ make
$ sudo make install

You may need to load the extension before you call ‘make install’ the first time. It is not necessary to reload it afterwards.

  1. $ sudo pgxn load ./
$ sudo pgxn load ./

Eigth, run the tests.

The standard skeleton has support for regression tests.

  1. $ make installcheck
$ make installcheck

The regression tests run all scripts under test/sql and verifies the results match the corresponding files in test/expected. The actual results are saved in results so it’s easy to write tests, modify the code as required, and then copy the file from results to test/expected once the desired behavior is seen.

You can also run tests through pgxn.

  1. $ pgxn check -d somedb pg_complex
$ pgxn check -d somedb pg_complex

The optional pgTAP (http://pgtap.org/) extension gives us the ability to write xJunit-type tests. These tests will probably be more comfortable for developers than the regression tests.

For information integrating pgTAP into the build process see http://pgtap.org/integration.html and https://gkoenig.wordpress.com/2011/03/04/pgtap-unit-tests-for-postgresql/.

Ninth, install and deploy the extension outside of the test framework.

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

You can undeploy and uninstall the extension using the analogous commands.

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

Tenth, publish the extension. You probably don’t want to do this with the skeleton implementation but this is a natural place to document the process. If we are a member of PGXN and wish to make our extension public we start by bundling our extension

  1. $ pgxn bundle
$ pgxn bundle

and then upload it to https://manager.pgxn.org/.

Testing

As good test-driven development developers we start by writing our tests. In our case it’s straight SQL, or more precisely files that can be run through psql.

A typical test script is

test/sql/math.sql

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

The corresponding expected results are

test/expected/math.out

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

It is probably easiest to create the ‘expected’ file by running the test once, getting the results from results/math.out, and editing that file to show the expected results. In a pure TDD implementation all of the tests should initially return null but we’ve already defined many of the functions above.

Implementation

There are three SQL-language functions to add. Details matter – the sum of no values is well-defined as 0 + 0i but the average of no values is undefined (null), not any particular value.

  1. --
  2. -- accumulator function is similar to float8_accum. Question: should the result
  3. -- be the product of p * p or the product of p * ~p ?
  4. --
  5. CREATE OR REPLACE FUNCTION pgx_complex_accum(complex_accum, complex) RETURNS complex_accum AS $$
  6.    SELECT CASE WHEN $1 IS NULL THEN 1 ELSE $1.cnt + 1 END,
  7.           CASE WHEN $1 IS NULL THEN $2 ELSE $1.sum + $2 END,
  8.           CASE WHEN $1 IS NULL THEN $2 * ~$2 ELSE $1.sofs + $2 * ~$2 END;
  9. $$ LANGUAGE SQL;
  10.  
  11. --
  12. -- disaccumulator(?) function is similar to pgx_complex_accum. It is required in order
  13. -- to implement windowing functions.
  14. --
  15. CREATE OR REPLACE FUNCTION pgx_complex_disaccum(complex_accum, complex) RETURNS complex_accum AS $$
  16.    SELECT pgx_complex_accum($1, -$2);
  17. $$ LANGUAGE SQL;
  18.  
  19. --
  20. -- average function returns quotient of sum over count.
  21. --
  22. CREATE OR REPLACE FUNCTION pgx_complex_avg(complex_accum) RETURNS complex AS $$
  23.    SELECT CASE WHEN $1 IS NULL THEN NULL
  24.                WHEN $1.cnt = 0 THEN (0,0)::complex
  25.                ELSE $1.sum / $1.cnt END;
  26. $$ LANGUAGE SQL;
--
-- 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;

The first C-language function demonstrates how to read a composite value and return a primitive. In this case we get the ‘re’ and ‘im’ components by name.

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

The second case returns a composite value. There are two ways to return composite values. This is the older way and requires a little more work. The newer way requires everything be returned as a string – this has a modest cost with primitive values but it could be costly to marshal and unmarshal user-defined types.

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

The third example also consumes and produces a composite value.

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

Wrap up

This wraps up the complex number extension. Most extensions will be far more simple but this problem was chosen precisely because it demonstrates how deeply you can integrate an extension.

Source code

http://github.com/beargiles/pg-complex
http://pgxn.org/dist/complex

Additional Resources

PXGN blog
PGXN howto
PGXN META.json specification
PGXN client documentation
PGXN usage
pgTab (testing framework)

Comments
No Comments »
Categories
PostgreSQL, user-defined types
Comments rss Comments rss
Trackback Trackback

Three Interesting PostgreSQL Extensions

Bear Giles | December 31, 2014

While doing other research I cam across three interesting PostgreSQL extensions. I can’t speak to their quality or utility but anyone who reads my blog might find them interesting.

PGAudit

PGAudit (and pgxn.org) is a module that adds improved auditing to PostgreSQL. PostgreSQL already has a logging mechanism but it can only be used in certain situations. This module uses hooks to capture additional events. If nothing else this extension can be used as a model for how to access and use these hooks.

For the security conscious – I’ve already written the authors about adding features to provide secure logging.

sslinfo

sslinfo is an official contrib extension that provides information about the SSL certificate, if any, used by the user when establishing a connection to the database.

To be honest I don’t know how much weight I would give this information. Did PostgreSQL perform proper checks? Did it perform any checks? My gut tells me that I’ll want to get the certificate and do these checks myself. If nothing else this extension tells us where to start looking for that information.

sepgsql

sepgsql is an official contrib extension that brings Security Enhanced Linux (SELinux) labels to the database.

It isn’t clear but I think the subject label (the client of the database) is distinct from the database user. That means that we could see different behavior when accessing the database as a human running an application vs. accessing the database as an application running on a webapp server.

This is normally a Bad Thing but if carefully used it could replace a lot of work involving database users and permissions with a few well-chosen SE Labels. On the other hand that could leave you exposed if SELinux is disabled.

Comments
No Comments »
Categories
PostgreSQL, Uncategorized
Comments rss Comments rss
Trackback Trackback

Adding OpenSSL User-Defined Types to PostgreSQL

Bear Giles | December 28, 2014

PostgreSQL supports user-defined types (UDT). These types can be used to provide type-safety on user-defined functions when we would otherwise be forced to use simple BLOB objects.

This comes at a significant cost. Many databases support UDT but implementation details vary widely so there’s a significant amount of vendor lock-in. In addition C language UDT require deployment via PostgreSQL extensions containing shared libraries and that is rarely available when using SAAS, e.g., the Amazon cloud. This forces us to maintain our own database servers instead of relying on a SAAS provider.

On the other hand user-defined types and functions give us far more flexibility, e.g., a centralized location with dedicated cryptographic hardware.

I use the OpenSSL library for the simple reason that it’s already included by PostgreSQL to support encrypted channels. This eliminates the need to worry about library dependencies or legal restrictions on cryptographic software – I am making the reasonable assumption that this software is legal to use at any location that is already using the OpenSSL library.

Motivation

It is very common for sites to store x509 digital certificates as a blob with a number of additional columns used for indexing and searching.

  1. CREATE TABLE certs (
  2.    cert       BLOB NOT NULL,
  3.    name       VARCHAR[100] NOT NULL,
  4.    not_before TIMESTAMP NOT NULL,
  5.    not_after  TIMESTAMP NOT NULL
  6. );
CREATE TABLE certs (
   cert       BLOB NOT NULL,
   name       VARCHAR[100] NOT NULL,
   not_before TIMESTAMP NOT NULL,
   not_after  TIMESTAMP NOT NULL
);

The problem is that there’s no consistency enforced between the cert and the indexed fields. It’s unlikely for an attacker to slip in a different certificate but it can’t be ruled out and the results could be catastrophic.

A far better solution is to use triggers on insert and update

  1. -- create trigger
  2. CREATE CONSTRAINT TRIGGER cert_update() BEFORE INSERT OR UPDATE
  3.     ON certs NOT DEFERRABLE FOR EACH ROW
  4.     EXECUTE PROCEDURE cert_update_proc ();
  5.  
  6. -- create function that ensures indexed fields reflect cert
  7. CREATE OR REPLACE FUNCTION cert_update_proc RETURNING trigger $$
  8.     BEGIN
  9.         INSERT INTO certs(cert, X509_name(cert), X509_not_before(cert), X509_not_after(cert));
  10.         RETURN NEW;
  11.     END;
  12. $$ LANGUAGE plpgsql;
-- create trigger
CREATE CONSTRAINT TRIGGER cert_update() BEFORE INSERT OR UPDATE
    ON certs NOT DEFERRABLE FOR EACH ROW
    EXECUTE PROCEDURE cert_update_proc ();

-- create function that ensures indexed fields reflect cert
CREATE OR REPLACE FUNCTION cert_update_proc RETURNING trigger $$
    BEGIN
        INSERT INTO certs(cert, X509_name(cert), X509_not_before(cert), X509_not_after(cert));
        RETURN NEW;
    END;
$$ LANGUAGE plpgsql;

This ensures that an attacker can never replace the cert without all of the indexed values being updated. Updates to the indexed fields will be ignored. It’s not 100% – a sophisticated attacker could drop the trigger – but it is far more secure than relying on the user to maintain this information. Remember that the schema, including triggers, should be owned by a different database user than the application database user so a compromised application cannot drop the trigger.

(The table definition or stored procedure can also perform sanity checks, e.g., ensuring that the “not_after” timestamp is strictly later than the “not_before” timestamp.)

We are not forced to use a user-defined type but it allows us to be more precise when defining our helper functions. It is also much more user friendly since they can insert and retrieve standard OpenSSL PEM values instead of dealing with BLOBs.

In the long term we can create user-defined functions that can use the OpenSSL types to perform actual work. We can also apply more sophisticated checks that involve internal queries, e.g., ensuring that any certificate added or modified is signed by another certificate in the database. That is beyond the scope of this blog entry.

OpenSSL engines

It is not widely known that OpenSSL supports cryptographic hardware via the “engine” interface. The default behavior is to use a software implementation but with well-written code it should be straightforward to use dedicated cryptographic hardware instead.

This can result in significant performance gains. The software implementation is adequate for development and small sites but may become a limiting factor at large sites. The hardware solutions that reduce this problem.

A more subtle point is that encryption keys are sensitive information and many organizations will not want them exposed under any conditions. No files, no web services. Hardware implementations have the ability to generate their own encryption keys and there is no mechanism for exposing the keys. (At most there’s a way to clone the keys from one hardware device to a second one.)

Design

The design is based on a simple consideration – I only want to store valid objects. The best way to ensure this is to have the user-defined types convert the external PEM values into internal OpenSSL objects and vice versa. We can perform additional sanity checks but that isn’t required.

This has one unfortunate drawback – traditional keys cannot be encrypted. I don’t consider this a problem since traditional keys shouldn’t be used anyway (IMHO) – they should be stored in containers such as keystores (PKCS8 and PKCS12).

That said the traditional keys are so simple that they’re a great platform for developing our PGXS skills.

Implementation Constraints

The constraints imposed by PostgreSQL are specified in section 35.9.5 of the manual http://www.postgresql.org/docs/9.4/static/xfunc-c.html. Most of them are handled by the tools at PGXN – see my earlier blog entry for details.

One particularly nasty constraint is that PostgreSQL, like most long-running services, has its own memory management library. OpenSSL uses the standard memory management library by default. It is possible to override this with the CRYPTO_set_mem_functions function but it’s dangerous to call this in a server because you don’t know what objects have already been created, e.g., when establishing a secure connection to the database. It could cause a server crash if an object is malloc’ed but then pfree’d.

Instead we must be very careful to always convert and release any object created by the OpenSSL library.

Defining a RSA keypair UDT

We are now ready to create an RSA keypair UDT. As mentioned earlier keys should be stored in a PKCS8 or PKCS12 object instead of a traditional RSA keypair object since the latter must be stored unencrypted.

We start by defining the RSA UDT itself.

  1. --
  2. -- Create shell type.
  3. --
  4. CREATE TYPE RSA;
  5.  
  6. --
  7. -- Create function that converts string to internal format.
  8. --
  9. CREATE OR REPLACE FUNCTION rsa_in(cstring)
  10. RETURNS RSA
  11. AS 'pgopenssltypes', 'rsa_in'
  12. LANGUAGE C IMMUTABLE STRICT;
  13.  
  14. --
  15. -- Create function that converts internal format to string.
  16. --
  17. CREATE OR REPLACE FUNCTION rsa_out(RSA)
  18. RETURNS CSTRING
  19. AS 'pgopenssltypes', 'rsa_out'
  20. LANGUAGE C IMMUTABLE STRICT;
  21.  
  22. --
  23. -- Redefine type with necessary functions.
  24. --
  25. CREATE TYPE RSA (
  26.     INPUT   = rsa_in,
  27.     OUTPUT  = rsa_out
  28. );
--
-- Create shell type.
--
CREATE TYPE RSA;

--
-- Create function that converts string to internal format.
--
CREATE OR REPLACE FUNCTION rsa_in(cstring)
RETURNS RSA
AS 'pgopenssltypes', 'rsa_in'
LANGUAGE C IMMUTABLE STRICT;

--
-- Create function that converts internal format to string.
--
CREATE OR REPLACE FUNCTION rsa_out(RSA)
RETURNS CSTRING
AS 'pgopenssltypes', 'rsa_out'
LANGUAGE C IMMUTABLE STRICT;

--
-- Redefine type with necessary functions.
--
CREATE TYPE RSA (
    INPUT   = rsa_in,
    OUTPUT  = rsa_out
);

A UDT can specify about a dozen C functions (see CREATE TYPE) but the only two mandatory functions are the INPUT and OUTPUT functions that convert the object between a C string and bytea representations.

A plain UDT is pretty boring so let’s also define two user-defined functions:

  1. --
  2. -- Generate RSA keypair. This is an expensive operation
  3. -- so it should not be called casually.
  4. --
  5. CREATE OR REPLACE FUNCTION rsa_generate_keypair(int)
  6. RETURNS RSA
  7. AS 'pgopenssltypes', 'rsa_generate_keypair'
  8. LANGUAGE C IMMUTABLE STRICT;
  9.  
  10. CREATE TYPE RSA_INFO AS (
  11.     BITS int,
  12.     N    BN,
  13.     E    BN,
  14.     D    BN,
  15.     P    BN,
  16.     Q    BN
  17. );
  18.  
  19. --
  20. -- Get details about RSA keypair.
  21. --
  22. CREATE OR REPLACE FUNCTION rsa_get_details(RSA)
  23. RETURNS RSA_INFO
  24. AS 'pgopenssltypes', 'rsa_get_details'
  25. LANGUAGE C IMMUTABLE STRICT;
--
-- Generate RSA keypair. This is an expensive operation
-- so it should not be called casually.
--
CREATE OR REPLACE FUNCTION rsa_generate_keypair(int)
RETURNS RSA
AS 'pgopenssltypes', 'rsa_generate_keypair'
LANGUAGE C IMMUTABLE STRICT;

CREATE TYPE RSA_INFO AS (
    BITS int,
    N    BN,
    E    BN,
    D    BN,
    P    BN,
    Q    BN
);

--
-- Get details about RSA keypair.
--
CREATE OR REPLACE FUNCTION rsa_get_details(RSA)
RETURNS RSA_INFO
AS 'pgopenssltypes', 'rsa_get_details'
LANGUAGE C IMMUTABLE STRICT;

RSA_INFO is a composite type. It is not possible to return multiple columns from a function but sometimes values are intrinsically related and you want to return them as a unit. PostgreSQL supports this with composite types. You can access the fields easily.

  1. bgiles=# SELECT rsa_get_details(rsa_generate_keypair(256)) AS details INTO sample;
  2.  
  3. bgiles=# \d sample
  4.      Table "public.sample"
  5.  Column  |   Type   | Modifiers
  6. ---------+----------+-----------
  7.  details | rsa_info |
  8.  
  9. bgiles=# SELECT (details).P, (details).Q FROM sample;
  10.                     p                    |                    q                    
  11. -----------------------------------------+-----------------------------------------
  12.  331128053999826595053108455708184431513 | 294756634092692440982306957700237950609
  13. (1 row)
bgiles=# SELECT rsa_get_details(rsa_generate_keypair(256)) AS details INTO sample;

bgiles=# \d sample
     Table "public.sample"
 Column  |   Type   | Modifiers 
---------+----------+-----------
 details | rsa_info | 

bgiles=# SELECT (details).P, (details).Q FROM sample;
                    p                    |                    q                    
-----------------------------------------+-----------------------------------------
 331128053999826595053108455708184431513 | 294756634092692440982306957700237950609
(1 row)

In this case there’s no reason why you would want to persist a ROW_INFO object – I can’t imagine using it anywhere except in stored procedures. But it’s a good tool to have in your toolbox.

Implementing a RSA keypair UDT (INPUT/OUTPUT)

The implementation of the basic INPUT and OUTPUT methods is straightforward once you understand the OpenSSL library. The public functions are:

  1. /*
  2.  * Read PEM format.
  3.  */
  4. PG_FUNCTION_INFO_V1(rsa_in);
  5.  
  6. Datum rsa_in(PG_FUNCTION_ARGS) {
  7.     char *txt;
  8.     bytea *result;
  9.     RSA *rsa;
  10.  
  11.     // write RSA keypair into buffer
  12.     txt = PG_GETARG_CSTRING(0);
  13.     rsa = rsa_from_string(txt);
  14.     result = rsa_to_bytea(rsa);
  15.     RSA_free(rsa);
  16.  
  17.     // return bytea
  18.     PG_RETURN_BYTEA_P(result);
  19. }
  20.  
  21. /*
  22.  * Write PEM format.
  23.  */
  24. PG_FUNCTION_INFO_V1(rsa_out);
  25.  
  26. Datum rsa_out(PG_FUNCTION_ARGS) {
  27.     bytea *raw;
  28.     char *result;
  29.     RSA *rsa;
  30.  
  31.     // write RSA keypair into buffer
  32.     raw = PG_GETARG_BYTEA_P(0);
  33.     rsa = rsa_from_bytea(raw);
  34.     result = rsa_to_string(rsa);
  35.     RSA_free(rsa);
  36.  
  37.     PG_RETURN_CSTRING(result);
  38. }
/*
 * Read PEM format.
 */
PG_FUNCTION_INFO_V1(rsa_in);

Datum rsa_in(PG_FUNCTION_ARGS) {
    char *txt;
    bytea *result;
    RSA *rsa;

    // write RSA keypair into buffer
    txt = PG_GETARG_CSTRING(0);
    rsa = rsa_from_string(txt);
    result = rsa_to_bytea(rsa);
    RSA_free(rsa);

    // return bytea
    PG_RETURN_BYTEA_P(result);
}

/*
 * Write PEM format.
 */
PG_FUNCTION_INFO_V1(rsa_out);

Datum rsa_out(PG_FUNCTION_ARGS) {
    bytea *raw;
    char *result;
    RSA *rsa;

    // write RSA keypair into buffer
    raw = PG_GETARG_BYTEA_P(0);
    rsa = rsa_from_bytea(raw);
    result = rsa_to_string(rsa);
    RSA_free(rsa);

    PG_RETURN_CSTRING(result);
}

I didn’t check for a NULL value since the user-defined function was declared STRICT and it clutters the code. In practice it won’t do much harm to always check for a null value. The main takeaway is that I create an OpenSSL object, use it, and then immediately discard it.

This public methods use four static convenience methods:

  1. /*
  2.  * Convert string to RSA.
  3.  */
  4. static RSA * rsa_from_string(const char *txt) {
  5.     BIO *inp;
  6.     RSA *rsa = RSA_new();
  7.  
  8.     inp = BIO_new_mem_buf((char *) txt, strlen(txt));
  9.     PEM_read_bio_RSAPrivateKey(inp, &rsa, 0, NULL);
  10.     BIO_free(inp);
  11.  
  12.     return rsa;
  13. }
  14.  
  15. /*
  16.  * Convert bytea to RSA.
  17.  */
  18. static RSA * rsa_from_bytea(const bytea *raw) {
  19.     BIO *bio;
  20.     RSA *rsa;
  21.  
  22.     // convert into RSA keypair
  23.     bio = BIO_new_mem_buf(VARDATA(raw), VARSIZE(raw) - VARHDRSZ);
  24.     BIO_set_close(bio, BIO_NOCLOSE);
  25.     rsa = RSA_new();
  26.     d2i_RSAPrivateKey_bio(bio, &rsa);
  27.     BIO_free(bio);
  28.  
  29.     if (rsa == NULL) {
  30.         ereport(ERROR,
  31.             (errcode(ERRCODE_DATA_CORRUPTED), errmsg(
  32.                 "unable to decode RSA keypair record")));
  33.     }
  34.  
  35.     return rsa;
  36. }
  37.  
  38. /*
  39.  * Convert RSA to string.
  40.  */
  41. static char * rsa_to_string(const RSA *rsa) {
  42.     BIO *bio;
  43.     int len;
  44.     char *ptr, *result;
  45.  
  46.     // write RSA keypair into buffer
  47.     // arguments: ..., cipher, keyptr, keylen, passwd_cb, passwd_cb_data
  48.     bio = BIO_new(BIO_s_mem());
  49.     PEM_write_bio_RSAPrivateKey(bio, (RSA *) rsa, NULL, NULL, 0, NULL, NULL);
  50.  
  51.     // create results.
  52.     len = BIO_number_written(bio);
  53.     BIO_get_mem_data(bio, &ptr);
  54.     result = palloc(len + 1);
  55.     strncpy(result, ptr, len);
  56.     result[len] = '';
  57.     BIO_free(bio);
  58.  
  59.     return result;
  60. }
  61.  
  62. /*
  63.  * Convert RSA to bytea.
  64.  */
  65. static bytea * rsa_to_bytea(const RSA *rsa) {
  66.     BIO *bio;
  67.     int len;
  68.     bytea *result;
  69.     char *ptr;
  70.  
  71.     // write RSA keypair into buffer
  72.     bio = BIO_new(BIO_s_mem());
  73.     i2d_RSAPrivateKey_bio(bio, (RSA *) rsa);
  74.  
  75.     // create bytea results.
  76.     len = BIO_number_written(bio);
  77.     BIO_get_mem_data(bio, &ptr);
  78.     result = (bytea *) palloc(len + VARHDRSZ);
  79.     memcpy(VARDATA(result), ptr, len);
  80.     SET_VARSIZE(result, len + VARHDRSZ);
  81.     BIO_free(bio);
  82.  
  83.     return result;
  84. }
/*
 * Convert string to RSA.
 */
static RSA * rsa_from_string(const char *txt) {
    BIO *inp;
    RSA *rsa = RSA_new();

    inp = BIO_new_mem_buf((char *) txt, strlen(txt));
    PEM_read_bio_RSAPrivateKey(inp, &rsa, 0, NULL);
    BIO_free(inp);

    return rsa;
}

/*
 * Convert bytea to RSA.
 */
static RSA * rsa_from_bytea(const bytea *raw) {
    BIO *bio;
    RSA *rsa;

    // convert into RSA keypair
    bio = BIO_new_mem_buf(VARDATA(raw), VARSIZE(raw) - VARHDRSZ);
    BIO_set_close(bio, BIO_NOCLOSE);
    rsa = RSA_new();
    d2i_RSAPrivateKey_bio(bio, &rsa);
    BIO_free(bio);

    if (rsa == NULL) {
        ereport(ERROR,
            (errcode(ERRCODE_DATA_CORRUPTED), errmsg(
                "unable to decode RSA keypair record")));
    }

    return rsa;
}

/*
 * Convert RSA to string.
 */
static char * rsa_to_string(const RSA *rsa) {
    BIO *bio;
    int len;
    char *ptr, *result;

    // write RSA keypair into buffer
    // arguments: ..., cipher, keyptr, keylen, passwd_cb, passwd_cb_data
    bio = BIO_new(BIO_s_mem());
    PEM_write_bio_RSAPrivateKey(bio, (RSA *) rsa, NULL, NULL, 0, NULL, NULL);

    // create results.
    len = BIO_number_written(bio);
    BIO_get_mem_data(bio, &ptr);
    result = palloc(len + 1);
    strncpy(result, ptr, len);
    result[len] = '';
    BIO_free(bio);

    return result;
}

/*
 * Convert RSA to bytea.
 */
static bytea * rsa_to_bytea(const RSA *rsa) {
    BIO *bio;
    int len;
    bytea *result;
    char *ptr;

    // write RSA keypair into buffer
    bio = BIO_new(BIO_s_mem());
    i2d_RSAPrivateKey_bio(bio, (RSA *) rsa);

    // create bytea results.
    len = BIO_number_written(bio);
    BIO_get_mem_data(bio, &ptr);
    result = (bytea *) palloc(len + VARHDRSZ);
    memcpy(VARDATA(result), ptr, len);
    SET_VARSIZE(result, len + VARHDRSZ);
    BIO_free(bio);

    return result;
}

The bytea is a PostgreSQL type that contains a variable amount of memory. The first four bytes (VARHDRSZ) are the length, and the data itself is accessed through a convenience macro (VARDATA). Since I’m only storing a single value I’m using the macros directly, more complex objects could define a type and cast it to a bytea object.

This code may be baffling if you’re not familiar with the OpenSSL library but there are two simple observations that will make it a lot clearer. First, all I/O is handled through a “Basic Input/Output (BIO)” abstraction. It’s a pain at first but it allows you to stack data manipulations such as compression and encryption.

Second, objects are read and written using d2i and i2d functions. These are “DER to internal” and “internal to DER”, respectively. PEM and DER formats are identical – PEM is base-64 encoded and has a brief comment describing the contents but there’s no additional information.

The rest you get from experience, man pages and googling for examples.

Generating a new keypair

Onward to the interesting user-defined functions. First up is generating a new keypair. This is not something you’ll do very often but it’s very convenient during development and testing since it’s extremely fast to generate 256-bit RSA keys.

The public method just makes sure we have valid and sane values. A warning is printed if a small key is requested but it’s not prohibited. This would be easy to do – use ERROR instead of INFO.

  1. /**
  2.  * Generate a random keypair
  3.  */
  4. PG_FUNCTION_INFO_V1(rsa_generate_keypair);
  5.  
  6. Datum rsa_generate_keypair(PG_FUNCTION_ARGS) {
  7.     bytea *result;
  8.     int bits;
  9.     RSA *rsa;
  10.  
  11.     bits = PG_GETARG_INT32(0);
  12.     if (bits <= 0) {
  13.         bits = 2048;
  14.     }
  15.  
  16.     if (bits < 2048) {
  17.         // elog(INFO, "RSA keys should be at least 2048 bits.")
  18.         ereport(INFO,
  19.                   (errcode(ERRCODE_CHECK_VIOLATION,
  20.                            errmsg("RSA keys should be at least 2048 bits.")));
  21.     }
  22.  
  23.     rsa = rsa_generate_keypair_internal(bits);
  24.     result = rsa_to_bytea(rsa);
  25.     RSA_free(rsa);
  26.  
  27.     // return bytea
  28.     PG_RETURN_BYTEA_P(result);
  29. }
  30.  
  31. /*
  32.  * actual key generation
  33.  */
  34. RSA * rsa_generate_keypair_internal(int bits) {
  35.     BIGNUM *ep;
  36.     RSA *rsa;
  37.  
  38.     rsa = RSA_new();
  39.     ep = BN_new();
  40.     BN_dec2bn(&ep, "65537");
  41.     RSA_generate_key_ex(rsa, bits, ep, NULL);
  42.     BN_free(ep);
  43.  
  44.     return rsa;
  45. }
/**
 * Generate a random keypair
 */
PG_FUNCTION_INFO_V1(rsa_generate_keypair);

Datum rsa_generate_keypair(PG_FUNCTION_ARGS) {
    bytea *result;
    int bits;
    RSA *rsa;

    bits = PG_GETARG_INT32(0);
    if (bits <= 0) {
        bits = 2048;
    }

    if (bits < 2048) {
        // elog(INFO, "RSA keys should be at least 2048 bits.")
        ereport(INFO,
                  (errcode(ERRCODE_CHECK_VIOLATION,
                           errmsg("RSA keys should be at least 2048 bits.")));
    }

    rsa = rsa_generate_keypair_internal(bits);
    result = rsa_to_bytea(rsa);
    RSA_free(rsa);

    // return bytea
    PG_RETURN_BYTEA_P(result);
}

/*
 * actual key generation
 */
RSA * rsa_generate_keypair_internal(int bits) {
    BIGNUM *ep;
    RSA *rsa;

    rsa = RSA_new();
    ep = BN_new();
    BN_dec2bn(&ep, "65537");
    RSA_generate_key_ex(rsa, bits, ep, NULL);
    BN_free(ep);

    return rsa;
}

The key uses the standard exponent – 65537 (0x10001) – since there’s little value in making it configurable.

Retrieving keypair details

Retrieving keypair details is a bit more complicated and you will definitely want to read the PostgreSQL documentation while looking at this code.

  1. /**
  2.  * Get details about an RSA keypair
  3.  */
  4. PG_FUNCTION_INFO_V1( rsa_get_details);
  5.  
  6. Datum rsa_get_details( PG_FUNCTION_ARGS) {
  7.     bytea *raw;
  8.     RSA *rsa;
  9.     TupleDesc desc;
  10.     HeapTuple tuple;
  11.     Datum *values;
  12.     bool *retNulls;
  13.  
  14.     // check for null value.
  15.     raw = PG_GETARG_BYTEA_P(0);
  16.     if (raw == NULL || VARSIZE(raw) == VARHDRSZ) {
  17.         PG_RETURN_NULL();
  18.     }
  19.  
  20.     // read keypair, verify success.
  21.     rsa = rsa_from_bytea(raw);
  22.     if (rsa == NULL) {
  23.         ereport(ERROR,
  24.                 (errcode(ERRCODE_DATA_CORRUPTED), errmsg(
  25.                          "unable to decode RSA keypair record")));
  26.         PG_RETURN_NULL();
  27.     }
  28.  
  29.     // read details about return value.
  30.     if (get_call_result_type(fcinfo, NULL, &desc) != TYPEFUNC_COMPOSITE) {
  31.         RSA_free(rsa);
  32.         ereport(ERROR,
  33.                 (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg(
  34.                         "function returning record called in context "
  35.                                 "that cannot accept type record")));
  36.     }
  37.     desc = BlessTupleDesc(desc);
  38.  
  39.     // these values are freed by PostgreSQL
  40.     values = (Datum *) palloc(6 * sizeof(Datum));
  41.     retNulls = (bool *) palloc(6 * sizeof(bool));
  42.  
  43.     // set return values
  44.     values[0] = Int32GetDatum(8 * RSA_size(rsa));
  45.     retNulls[0] = false;
  46.  
  47.     if (rsa->n == NULL) {
  48.         retNulls[1] = true;
  49.     } else {
  50.         retNulls[1] = false;
  51.         values[1] = BnGetDatum(rsa->n);
  52.     }
  53.  
  54.     if (rsa->e == NULL) {
  55.         retNulls[2] = true;
  56.     } else {
  57.         retNulls[2] = false;
  58.         values[2] = BnGetDatum(rsa->e);
  59.     }
  60.  
  61.     if (rsa->d == NULL) {
  62.         retNulls[3] = true;
  63.     } else {
  64.         retNulls[3] = false;
  65.         values[3] = BnGetDatum(rsa->d);
  66.     }
  67.  
  68.     if (rsa->p == NULL) {
  69.         retNulls[4] = true;
  70.     } else {
  71.         retNulls[4] = false;
  72.         values[4] = BnGetDatum(rsa->p);
  73.     }
  74.  
  75.     if (rsa->q == NULL) {
  76.         retNulls[5] = true;
  77.     } else {
  78.         retNulls[5] = false;
  79.         values[5] = BnGetDatum(rsa->q);
  80.     }
  81.  
  82.     RSA_free(rsa);
  83.  
  84.     // convert to tuple.
  85.     tuple = heap_form_tuple(desc, values, retNulls);
  86.     FreeTupleDesc(desc);
  87.  
  88.     // return datum.
  89.     PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
  90. }
/**
 * Get details about an RSA keypair
 */
PG_FUNCTION_INFO_V1( rsa_get_details);

Datum rsa_get_details( PG_FUNCTION_ARGS) {
    bytea *raw;
    RSA *rsa;
    TupleDesc desc;
    HeapTuple tuple;
    Datum *values;
    bool *retNulls;

    // check for null value.
    raw = PG_GETARG_BYTEA_P(0);
    if (raw == NULL || VARSIZE(raw) == VARHDRSZ) {
        PG_RETURN_NULL();
    }

    // read keypair, verify success.
    rsa = rsa_from_bytea(raw);
    if (rsa == NULL) {
        ereport(ERROR,
                (errcode(ERRCODE_DATA_CORRUPTED), errmsg(
                         "unable to decode RSA keypair record")));
        PG_RETURN_NULL();
    }

    // read details about return value.
    if (get_call_result_type(fcinfo, NULL, &desc) != TYPEFUNC_COMPOSITE) {
        RSA_free(rsa);
        ereport(ERROR,
                (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg(
                        "function returning record called in context "
                                "that cannot accept type record")));
    }
    desc = BlessTupleDesc(desc);

    // these values are freed by PostgreSQL
    values = (Datum *) palloc(6 * sizeof(Datum));
    retNulls = (bool *) palloc(6 * sizeof(bool));

    // set return values
    values[0] = Int32GetDatum(8 * RSA_size(rsa));
    retNulls[0] = false;

    if (rsa->n == NULL) {
        retNulls[1] = true;
    } else {
        retNulls[1] = false;
        values[1] = BnGetDatum(rsa->n);
    }

    if (rsa->e == NULL) {
        retNulls[2] = true;
    } else {
        retNulls[2] = false;
        values[2] = BnGetDatum(rsa->e);
    }

    if (rsa->d == NULL) {
        retNulls[3] = true;
    } else {
        retNulls[3] = false;
        values[3] = BnGetDatum(rsa->d);
    }

    if (rsa->p == NULL) {
        retNulls[4] = true;
    } else {
        retNulls[4] = false;
        values[4] = BnGetDatum(rsa->p);
    }

    if (rsa->q == NULL) {
        retNulls[5] = true;
    } else {
        retNulls[5] = false;
        values[5] = BnGetDatum(rsa->q);
    }

    RSA_free(rsa);

    // convert to tuple.
    tuple = heap_form_tuple(desc, values, retNulls);
    FreeTupleDesc(desc);

    // return datum.
    PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
}

It’s a long function with unfamiliar calls but it follows the usual pattern. Read data, verify it, do something with it, prepare results, return results.

This function refers to the “BN” UDT. I don’t include that here but it is a wrapper for the OpenSSL “BIGNUM” type. If you’re interested the details are at my git repository.

The results are what we expect.

  1. bgiles=# select * from sample;
  2.                                                                                                                        details                                                                                                                        
  3. ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  4.  (256,97602190650652191344500377647616852664474030847991993174417850011862837141417,65535,49816062791999163016436913587169239410285445043144541244445777903161695571583,331128053999826595053108455708184431513,294756634092692440982306957700237950609)
  5. (1 row)
  6.  
  7. bgiles=# select (details).bits from sample;
  8.  bits
  9. ------
  10.   256
  11. (1 row)
  12.  
  13. bgiles=# select (details).e from sample;
  14.    e  
  15. -------
  16.  65535
  17. (1 row)
  18.  
  19. bgiles=# select (details).d from sample;
  20.                                        d                                      
  21. -------------------------------------------------------------------------------
  22.  49816062791999163016436913587169239410285445043144541244445777903161695571583
  23. (1 row)
  24.  
  25. bgiles=#
bgiles=# select * from sample;
                                                                                                                       details                                                                                                                         
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 (256,97602190650652191344500377647616852664474030847991993174417850011862837141417,65535,49816062791999163016436913587169239410285445043144541244445777903161695571583,331128053999826595053108455708184431513,294756634092692440982306957700237950609)
(1 row)

bgiles=# select (details).bits from sample;
 bits 
------
  256
(1 row)

bgiles=# select (details).e from sample;
   e   
-------
 65535
(1 row)

bgiles=# select (details).d from sample;
                                       d                                       
-------------------------------------------------------------------------------
 49816062791999163016436913587169239410285445043144541244445777903161695571583
(1 row)

bgiles=# 

Source code and other resources

The source code shown above is available at https//github.com/beargiles/pgopenssltypes. This is very much a work in progress but I’m posting it since this idea has been on my blog backlog for over a year and I’ll soon be pulled to other tasks.

Here’s a good article from 2007: http://linuxgazette.net/142/peterson.html.

Comments
No Comments »
Categories
PostgreSQL, security, user-defined types
Comments rss Comments rss
Trackback Trackback

Creating User-Defined C Functions in PostgreSQL

Bear Giles | December 25, 2014

Many experienced database developers are familiar with user-defined functions implemented in SQL or a procedural language. PostgreSQL itself supports PL/pgSQL, PL/Tcl, PL/Perl, PL/Python, and has third-party support for PL/sh, PL/Java, PL/Ruby, PL/PHP and even PL/R.

Recent versions of PostgreSQL also support SQL/MED (foreign data) with foreign data wrappers (FDW). This allows the database user to access foreign data sources – other RDMSes such as Oracle or MySQL, noSQL databases, even services such as twitter, as though they were PostgreSQL tables. Obviously there can be performance issues but if an architect only needs to perform a simple task, e.g., sending a tweet when certain conditions are met, it may be easier to use a FDW and database query than to incorporate native twitter support.

Sometimes this is not enough. We may wish to add support for a new procedural language or foreign data wrapper. We may wish to perform work involving external libraries on the database server instead of the application server for efficiency. We may wish to hide implementation details from the database user.

A subset of the last item is management of cryptographic material. Most webapps manage the cryptographic material themselves but proper key management is difficult. For instance encryption keys should be rotated on a regular basis but many webapps have never changed their database keys because there is no provision for it. Their only option is to shut down the application, run an application that loads every record, decrypts it, reencrypts it, and then updates each record. Key management on the database side is still a difficult problem but the solutions are far more likely to be reusable.

PostgreSQL Extensions

It is possible to create user-defined functions in a sql script:

  1. CREATE OR REPLACE FUNCTION dgst_sha1(text)
  2. RETURNS text
  3. AS 'pgopenssltypes', 'dgst_sha1'
  4. LANGUAGE C IMMUTABLE STRICT
CREATE OR REPLACE FUNCTION dgst_sha1(text)
RETURNS text
AS 'pgopenssltypes', 'dgst_sha1'
LANGUAGE C IMMUTABLE STRICT

and provide the implementation by dropping the appropriate shared library in the $libdir directory. See C-Language Functions for details, or my own Introduction to PL/Java for examples of the SQL definitions for functions, triggers, operators, and indexes.

This is unmaintainable for anything other than the smallest tasks. We want to bundle everything we need into one object that we can load and unload it in a single operation. This ensures there are no oversights or inconsistencies. PostgreSQL already has a solution to this problem: PostgreSQL Extensions.

PostgreSQL Extension Network (PGXN)

Writing a PostgreSQL extension from scratch can be tricky. A better approach is to create a skeleton using the PGXN utils package and then install it with the pgxn-client utility from the PostgreSQL Extension Network.

Source Code

I am not including any source code since there are numerous projects listed at the PGXN site. For instance semver (semantic version data type, git or pgaudit. As always all public code should be viewed critically – you don’t want to learn the wrong lessons by reading bad code. (Do not take my reference to these projects as an endorsement.)

Note: if you want to use a shared library your Makefile should include the libraries in a SHLIB_LINK declaration. The system will automatically pull in any dependencies when the module is loaded.

See Also

PostgreSQL Functions By Example (slide deck)

HowTo – Create PostgreSQL C library functions

Comments
No Comments »
Categories
PostgreSQL, server programming, stored procedures, Uncategorized
Comments rss Comments rss
Trackback Trackback

PL/Java Code Finally Available

Bear Giles | October 4, 2012

About a year ago I published a number of articles on PL/Java:

  • Introduction
  • Working with Lists
  • Triggers
  • User Defined Types
  • Operations and Indices

I had always intended to publish the code but never had the time to clean it up for publication – fleshing out the unit tests, adding the copyright and licensing notices, etc.

No longer – I’ve created a googlecode project for my project. At the moment it only has two user-defined types (Rational and Complex) and the unit tests are far from complete but I’ll flesh it out. Check back in… 10 months! 🙂

Google Code Project: PostgreSQL PL/Java examples.

I should point out that there’s a known bug in both UDTs when performing implicit casts. If the first implicit cast is from a string everything works. If the first implicit cast is from an int then all implicit casts are screwed up. I’m following up on this on the pl/java mailing list.

(Sidenote: there’s also a project containing the code I was using in my discussion on digital certificates. It’s much more ambitious and still needs a lot of work but it’s reached the ‘minimally useful’ threshold. Google Code Project: Otter CA.)

Comments
No Comments »
Categories
java, pl/java, PostgreSQL
Comments rss Comments rss
Trackback Trackback

Introduction to PostgreSQL/PLJava, part 5: Operations and Indexes

Bear Giles | January 4, 2012

This article discusses operators and indexes in PL/Java. Many user-defined types will not need the former but a hash index should be supported for their use in SQL join clauses.

Operators

Operators are normal PL/Java methods that are also marked as operators via the CREATE OPERATOR statement.

Basic arithmetic for rational numbers is supported as

  1.     public static Rational negate(Rational p) throws SQLException {
  2.         if (p == null) {
  3.             return null;
  4.         }
  5.         return new Rational(-p.getNumerator(), p.getDenominator());
  6.     }
  7.  
  8.     public static Rational add(Rational p, Rational q) throws SQLException {
  9.         if ((p == null) || (q == null)) {
  10.             return null;
  11.         }
  12.         BigInteger n = BigInteger.valueOf(p.getNumerator()).multiply(BigInteger.valueOf(q.getDenominator())).add(
  13.                 BigInteger.valueOf(q.getNumerator()).multiply(BigInteger.valueOf(p.getDenominator())));
  14.         BigInteger d = BigInteger.valueOf(p.getDenominator()).multiply(BigInteger.valueOf(q.getDenominator()));
  15.         BigInteger gcd = n.gcd(d);
  16.         n = n.divide(gcd);
  17.         d = d.divide(gcd);
  18.         return new Rational(n.longValue(), d.longValue());
  19.     }
  20.  
  21.     public static Rational subtract(Rational p, Rational q) throws SQLException {
  22.         if ((p == null) || (q == null)) {
  23.             return null;
  24.         }
  25.         BigInteger n = BigInteger.valueOf(p.getNumerator()).multiply(BigInteger.valueOf(q.getDenominator())).subtract(
  26.                 BigInteger.valueOf(q.getNumerator()).multiply(BigInteger.valueOf(p.getDenominator())));
  27.         BigInteger d = BigInteger.valueOf(p.getDenominator()).multiply(BigInteger.valueOf(q.getDenominator()));
  28.         BigInteger gcd = n.gcd(d);
  29.         n = n.divide(gcd);
  30.         d = d.divide(gcd);
  31.         return new Rational(n.longValue(), d.longValue());
  32.     }
  33.    
  34.     public static Rational multiply(Rational p, Rational q) throws SQLException {
  35.         if ((p == null) || (q == null)) {
  36.             return null;
  37.         }
  38.         BigInteger n = BigInteger.valueOf(p.getNumerator()).multiply(BigInteger.valueOf(q.getNumerator()));
  39.         BigInteger d = BigInteger.valueOf(p.getDenominator()).multiply(BigInteger.valueOf(q.getDenominator()));
  40.         BigInteger gcd = n.gcd(d);
  41.         n = n.divide(gcd);
  42.         d = d.divide(gcd);
  43.         return new Rational(n.longValue(), d.longValue());
  44.     }
    public static Rational negate(Rational p) throws SQLException {
        if (p == null) {
            return null;
        }
        return new Rational(-p.getNumerator(), p.getDenominator());
    }

    public static Rational add(Rational p, Rational q) throws SQLException {
        if ((p == null) || (q == null)) {
            return null;
        }
        BigInteger n = BigInteger.valueOf(p.getNumerator()).multiply(BigInteger.valueOf(q.getDenominator())).add(
                BigInteger.valueOf(q.getNumerator()).multiply(BigInteger.valueOf(p.getDenominator())));
        BigInteger d = BigInteger.valueOf(p.getDenominator()).multiply(BigInteger.valueOf(q.getDenominator()));
        BigInteger gcd = n.gcd(d);
        n = n.divide(gcd);
        d = d.divide(gcd);
        return new Rational(n.longValue(), d.longValue());
    }

    public static Rational subtract(Rational p, Rational q) throws SQLException {
        if ((p == null) || (q == null)) {
            return null;
        }
        BigInteger n = BigInteger.valueOf(p.getNumerator()).multiply(BigInteger.valueOf(q.getDenominator())).subtract(
                BigInteger.valueOf(q.getNumerator()).multiply(BigInteger.valueOf(p.getDenominator())));
        BigInteger d = BigInteger.valueOf(p.getDenominator()).multiply(BigInteger.valueOf(q.getDenominator()));
        BigInteger gcd = n.gcd(d);
        n = n.divide(gcd);
        d = d.divide(gcd);
        return new Rational(n.longValue(), d.longValue());
    }
    
    public static Rational multiply(Rational p, Rational q) throws SQLException {
        if ((p == null) || (q == null)) {
            return null;
        }
        BigInteger n = BigInteger.valueOf(p.getNumerator()).multiply(BigInteger.valueOf(q.getNumerator()));
        BigInteger d = BigInteger.valueOf(p.getDenominator()).multiply(BigInteger.valueOf(q.getDenominator()));
        BigInteger gcd = n.gcd(d);
        n = n.divide(gcd);
        d = d.divide(gcd);
        return new Rational(n.longValue(), d.longValue());
    }

and

  1.       CREATE FUNCTION javatest.rational_negate(javatest.rational) RETURNS javatest.rational
  2.           AS 'sandbox.Rational.negate'
  3.           LANGUAGE JAVA IMMUTABLE STRICT;
  4.  
  5.       CREATE FUNCTION javatest.rational_add(javatest.rational, javatest.rational)
  6.           RETURNS javatest.rational
  7.           AS 'sandbox.Rational.add'
  8.           LANGUAGE JAVA IMMUTABLE STRICT;
  9.  
  10.       CREATE FUNCTION javatest.rational_subtract(javatest.rational, javatest.rational)
  11.           RETURNS javatest.rational
  12.           AS 'sandbox.Rational.subtract'
  13.           LANGUAGE JAVA IMMUTABLE STRICT;
  14.          
  15.       CREATE FUNCTION javatest.rational_multiply(javatest.rational, javatest.rational)
  16.           RETURNS javatest.rational
  17.           AS 'sandbox.Rational.multiply'
  18.           LANGUAGE JAVA IMMUTABLE STRICT;
  19.          
  20.       CREATE FUNCTION javatest.rational_divide(javatest.rational, javatest.rational)
  21.           RETURNS javatest.rational
  22.           AS 'sandbox.Rational.divide'
  23.           LANGUAGE JAVA IMMUTABLE STRICT;
  24.  
  25.       CREATE OPERATOR - (
  26.          rightarg = javatest.rational, procedure.rational_negate
  27.       );
  28.  
  29.       CREATE OPERATOR + (
  30.          leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_add,
  31.          commutator = +
  32.       );
  33.  
  34.       CREATE OPERATOR - (
  35.          leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_subtract
  36.       );
  37.  
  38.       CREATE OPERATOR * (
  39.          leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_divide,
  40.          commutator = *
  41.       );
  42.  
  43.       CREATE OPERATOR / (
  44.          leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_divide
  45.       );
      CREATE FUNCTION javatest.rational_negate(javatest.rational) RETURNS javatest.rational
          AS 'sandbox.Rational.negate'
          LANGUAGE JAVA IMMUTABLE STRICT;

      CREATE FUNCTION javatest.rational_add(javatest.rational, javatest.rational)
          RETURNS javatest.rational
          AS 'sandbox.Rational.add'
          LANGUAGE JAVA IMMUTABLE STRICT;

      CREATE FUNCTION javatest.rational_subtract(javatest.rational, javatest.rational)
          RETURNS javatest.rational
          AS 'sandbox.Rational.subtract'
          LANGUAGE JAVA IMMUTABLE STRICT;
          
      CREATE FUNCTION javatest.rational_multiply(javatest.rational, javatest.rational)
          RETURNS javatest.rational
          AS 'sandbox.Rational.multiply'
          LANGUAGE JAVA IMMUTABLE STRICT;
          
      CREATE FUNCTION javatest.rational_divide(javatest.rational, javatest.rational)
          RETURNS javatest.rational
          AS 'sandbox.Rational.divide'
          LANGUAGE JAVA IMMUTABLE STRICT;

      CREATE OPERATOR - (
         rightarg = javatest.rational, procedure.rational_negate
      );

      CREATE OPERATOR + (
         leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_add,
         commutator = + 
      );

      CREATE OPERATOR - (
         leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_subtract
      );

      CREATE OPERATOR * (
         leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_divide,
         commutator = *
      );

      CREATE OPERATOR / (
         leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_divide
      );

The operator characters are one to 63 characters from the set “+ – * / < > = ~ ! @ # % ^ & | ` ?” with a few restrictions to avoid confusion with the start of SQL comments.

The commutator operator is a second operator (possibly the same) that has the same results if the left and right values are swapped. This is used by the optimizer.

The negator operator is one that the opposite results if the left and right values are swapped. It is only valid on procedures that return a boolean value. Again this is used by the optimizer.

Ordering Operators

Many UDTs can be ordered in some manner. This may be something obvious, e.g., ordering rational numbers, or something a bit more arbitrary, e.g., ordering complex numbers.

We can define ordering operations in the same manner as above. N.B., there is no longer anything special about these operators – with an unfamiliar UDT you can’t assume that < really means “less than”. The sole exception is “!=” which is always rewritten as “” by the parser.

  1.     public static int compare(Rational p, Rational q) {
  2.         if (p == null) {
  3.             return 1;
  4.         } else if (q == null) {
  5.             return -1;
  6.         }
  7.         BigInteger l = BigInteger.valueOf(p.getNumerator()).multiply(BigInteger.valueOf(q.getDenominator()));
  8.         BigInteger r = BigInteger.valueOf(q.getNumerator()).multiply(BigInteger.valueOf(p.getDenominator()));
  9.         return l.compareTo(r);
  10.     }
  11.    
  12.     public int compareTo(Rational p) {
  13.         return compare(this, p);
  14.     }
  15.  
  16.     public static int compare(Rational p, double q) {
  17.         if (p == null) {
  18.             return 1;
  19.         }
  20.         double d = p.doubleValue();
  21.         return (d < q) ? -1 : ((d == q) ? 0 : 1);
  22.     }
  23.    
  24.     public int compareTo(double q) {
  25.         return compare(this, q);
  26.     }
  27.  
  28.     public static boolean lessThan(Rational p, Rational q) {
  29.         return compare(p, q) < 0;
  30.     }
  31.    
  32.     public static boolean lessThanOrEquals(Rational p, Rational q) {
  33.         return compare(p, q) <= 0;
  34.     }
  35.        
  36.     public static boolean equals(Rational p, Rational q) {
  37.         return compare(p, q) = 0;
  38.     }
  39.  
  40.     public static boolean greaterThan(Rational p, Rational q) {
  41.         return compare(p, q) > 0;
  42.     }
  43.        
  44.     public static boolean lessThan(Rational p, double q) {
  45.         if (p == null) {
  46.             return false;
  47.         }
  48.         return p.compareTo(q) < 0;
  49.     }
  50.    
  51.     public static boolean lessThanOrEquals(Rational p, double q) {
  52.         if (p == null) {
  53.             return false;
  54.         }
  55.         return p.compareTo(q) = 0;
  56.     }
  57.    
  58.     public static boolean greaterThan(Rational p, double q) {
  59.         if (p == null) {
  60.             return true;
  61.         }
  62.         return p.compareTo(q) > 0;
  63.     }
    public static int compare(Rational p, Rational q) {
        if (p == null) {
            return 1;
        } else if (q == null) {
            return -1;
        }
        BigInteger l = BigInteger.valueOf(p.getNumerator()).multiply(BigInteger.valueOf(q.getDenominator()));
        BigInteger r = BigInteger.valueOf(q.getNumerator()).multiply(BigInteger.valueOf(p.getDenominator()));
        return l.compareTo(r);
    }
    
    public int compareTo(Rational p) {
        return compare(this, p);
    }

    public static int compare(Rational p, double q) {
        if (p == null) {
            return 1;
        }
        double d = p.doubleValue();
        return (d < q) ? -1 : ((d == q) ? 0 : 1);
    }
    
    public int compareTo(double q) {
        return compare(this, q);
    }

    public static boolean lessThan(Rational p, Rational q) {
        return compare(p, q) < 0;
    }
    
    public static boolean lessThanOrEquals(Rational p, Rational q) {
        return compare(p, q) <= 0;
    }
        
    public static boolean equals(Rational p, Rational q) {
        return compare(p, q) = 0;
    }

    public static boolean greaterThan(Rational p, Rational q) {
        return compare(p, q) > 0;
    }
        
    public static boolean lessThan(Rational p, double q) {
        if (p == null) {
            return false;
        }
        return p.compareTo(q) < 0;
    }
    
    public static boolean lessThanOrEquals(Rational p, double q) {
        if (p == null) {
            return false;
        }
        return p.compareTo(q) = 0;
    }
    
    public static boolean greaterThan(Rational p, double q) {
        if (p == null) {
            return true;
        }
        return p.compareTo(q) > 0;
    }

Note that I’ve defined methods to compare either two rational numbers or one rational number and one double number.

  1.       CREATE FUNCTION javatest.rational_lt(javatest.rational, javatest.rational)
  2.           RETURNS bool
  3.           AS 'sandbox.Rational.lessThan'
  4.           LANGUAGE JAVA IMMUTABLE STRICT;
  5.  
  6.       CREATE FUNCTION javatest.rational_le(javatest.rational, javatest.rational)
  7.           RETURNS bool
  8.           AS 'sandbox.Rational.lessThanOrEquals'
  9.           LANGUAGE JAVA IMMUTABLE STRICT;
  10.  
  11.       CREATE FUNCTION javatest.rational_eq(javatest.rational, javatest.rational)
  12.           RETURNS bool
  13.           AS 'sandbox.Rational.equals'
  14.           LANGUAGE JAVA IMMUTABLE STRICT;
  15.      
  16.       CREATE FUNCTION javatest.rational_ge(javatest.rational, javatest.rational)
  17.           RETURNS bool
  18.           AS 'sandbox.Rational.greaterThanOrEquals'
  19.           LANGUAGE JAVA IMMUTABLE STRICT;
  20.      
  21.       CREATE FUNCTION javatest.rational_gt(javatest.rational, javatest.rational)
  22.           RETURNS bool
  23.           AS 'sandbox.Rational.greaterThan'
  24.           LANGUAGE JAVA IMMUTABLE STRICT;
  25.  
  26.       CREATE FUNCTION javatest.rational_cmp(javatest.rational, javatest.rational)
  27.           RETURNS int
  28.           AS 'sandbox.Rational.compare'
  29.           LANGUAGE JAVA IMMUTABLE STRICT;
  30.  
  31.       CREATE FUNCTION javatest.rational_lt(javatest.rational, float8)
  32.           RETURNS bool
  33.           AS 'sandbox.Rational.lessThan'
  34.           LANGUAGE JAVA IMMUTABLE STRICT;
  35.  
  36.       CREATE FUNCTION javatest.rational_le(javatest.rational, float8)
  37.           RETURNS bool
  38.           AS 'sandbox.Rational.lessThanOrEquals'
  39.           LANGUAGE JAVA IMMUTABLE STRICT;
  40.  
  41.       CREATE FUNCTION javatest.rational_eq(javatest.rational, float8)
  42.           RETURNS bool
  43.           AS 'sandbox.Rational.equals'
  44.           LANGUAGE JAVA IMMUTABLE STRICT;
  45.  
  46.       CREATE FUNCTION javatest.rational_ge(javatest.rational, float8)
  47.           RETURNS bool
  48.           AS 'sandbox.Rational.greaterThanOrEquals'
  49.           LANGUAGE JAVA IMMUTABLE STRICT;
  50.  
  51.       CREATE FUNCTION javatest.rational_gt(javatest.rational, float8)
  52.           RETURNS bool
  53.           AS 'sandbox.Rational.greaterThan'
  54.           LANGUAGE JAVA IMMUTABLE STRICT;
  55.  
  56.       CREATE OPERATOR < (
  57.          leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_lt,
  58.          commutator = > , negator = >= ,
  59.          restrict = scalarltsel, join = scalarltjoinsel, merges
  60.       );
  61.  
  62.       CREATE OPERATOR <= (
  63.          leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_le,
  64.          commutator = >= , negator = > ,
  65.          restrict = scalarltsel, join = scalarltjoinsel, merges
  66.       );
  67.  
  68.       CREATE OPERATOR = (
  69.          leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_eq,
  70.          commutator = = , negator = <>, hashes, merges
  71.       );
  72.  
  73.       CREATE OPERATOR >= (
  74.          leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_lt,
  75.          commutator = <= , negator = < ,
  76.          restrict = scalarltsel, join = scalarltjoinsel, merges
  77.       );
  78.  
  79.       CREATE OPERATOR > (
  80.          leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_le,
  81.          commutator = <= , negator = < ,
  82.          restrict = scalargtsel, join = scalargtjoinsel, merges
  83.       );
  84.  
  85.       CREATE OPERATOR < (
  86.          leftarg = javatest.rational, rightarg = float8, procedure = javatest.rational_lt,
  87.          commutator = > , negator = >=
  88.       );
  89.  
  90.       CREATE OPERATOR <= (
  91.          leftarg = javatest.rational, rightarg = float8, procedure = javatest.rational_le,
  92.          commutator = >= , negator = >
  93.       );
  94.  
  95.       CREATE OPERATOR = (
  96.          leftarg = javatest.rational, rightarg = float8, procedure = javatest.rational_eq,
  97.          commutator = = , negator = <>
  98.       );
  99.  
  100.       CREATE OPERATOR >= (
  101.          leftarg = javatest.rational, rightarg = float8, procedure = javatest.rational_ge,
  102.          commutator = <= , negator = <
  103.       );
  104.  
  105.       CREATE OPERATOR > (
  106.          leftarg = javatest.rational, rightarg = float8, procedure = javatest.rational_gt,
  107.          commutator = < , negator = <=
  108.       );
      CREATE FUNCTION javatest.rational_lt(javatest.rational, javatest.rational)
          RETURNS bool
          AS 'sandbox.Rational.lessThan'
          LANGUAGE JAVA IMMUTABLE STRICT;

      CREATE FUNCTION javatest.rational_le(javatest.rational, javatest.rational)
          RETURNS bool
          AS 'sandbox.Rational.lessThanOrEquals'
          LANGUAGE JAVA IMMUTABLE STRICT;

      CREATE FUNCTION javatest.rational_eq(javatest.rational, javatest.rational)
          RETURNS bool
          AS 'sandbox.Rational.equals'
          LANGUAGE JAVA IMMUTABLE STRICT;
      
      CREATE FUNCTION javatest.rational_ge(javatest.rational, javatest.rational)
          RETURNS bool
          AS 'sandbox.Rational.greaterThanOrEquals'
          LANGUAGE JAVA IMMUTABLE STRICT;
      
      CREATE FUNCTION javatest.rational_gt(javatest.rational, javatest.rational)
          RETURNS bool
          AS 'sandbox.Rational.greaterThan'
          LANGUAGE JAVA IMMUTABLE STRICT;

      CREATE FUNCTION javatest.rational_cmp(javatest.rational, javatest.rational)
          RETURNS int
          AS 'sandbox.Rational.compare'
          LANGUAGE JAVA IMMUTABLE STRICT;

      CREATE FUNCTION javatest.rational_lt(javatest.rational, float8)
          RETURNS bool
          AS 'sandbox.Rational.lessThan'
          LANGUAGE JAVA IMMUTABLE STRICT;

      CREATE FUNCTION javatest.rational_le(javatest.rational, float8)
          RETURNS bool
          AS 'sandbox.Rational.lessThanOrEquals'
          LANGUAGE JAVA IMMUTABLE STRICT;

      CREATE FUNCTION javatest.rational_eq(javatest.rational, float8)
          RETURNS bool
          AS 'sandbox.Rational.equals'
          LANGUAGE JAVA IMMUTABLE STRICT;

      CREATE FUNCTION javatest.rational_ge(javatest.rational, float8)
          RETURNS bool
          AS 'sandbox.Rational.greaterThanOrEquals'
          LANGUAGE JAVA IMMUTABLE STRICT;

      CREATE FUNCTION javatest.rational_gt(javatest.rational, float8)
          RETURNS bool
          AS 'sandbox.Rational.greaterThan'
          LANGUAGE JAVA IMMUTABLE STRICT;

      CREATE OPERATOR < (
         leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_lt,
         commutator = > , negator = >= ,
         restrict = scalarltsel, join = scalarltjoinsel, merges
      );

      CREATE OPERATOR <= (
         leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_le,
         commutator = >= , negator = > , 
         restrict = scalarltsel, join = scalarltjoinsel, merges
      );

      CREATE OPERATOR = (
         leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_eq,
         commutator = = , negator = <>, hashes, merges
      );

      CREATE OPERATOR >= (
         leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_lt,
         commutator = <= , negator = < ,
         restrict = scalarltsel, join = scalarltjoinsel, merges
      );

      CREATE OPERATOR > (
         leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_le,
         commutator = <= , negator = < , 
         restrict = scalargtsel, join = scalargtjoinsel, merges
      );

      CREATE OPERATOR < (
         leftarg = javatest.rational, rightarg = float8, procedure = javatest.rational_lt,
         commutator = > , negator = >= 
      );

      CREATE OPERATOR <= (
         leftarg = javatest.rational, rightarg = float8, procedure = javatest.rational_le,
         commutator = >= , negator = > 
      );

      CREATE OPERATOR = (
         leftarg = javatest.rational, rightarg = float8, procedure = javatest.rational_eq,
         commutator = = , negator = <> 
      );

      CREATE OPERATOR >= (
         leftarg = javatest.rational, rightarg = float8, procedure = javatest.rational_ge,
         commutator = <= , negator = <
      );

      CREATE OPERATOR > (
         leftarg = javatest.rational, rightarg = float8, procedure = javatest.rational_gt,
         commutator = < , negator = <=
      );

Restrict is an optimization estimator procedure. It’s usually safe to use the appropriate standard procedure.

Join is an optimization estimator procedure. It’s usually safe to use the appropriate standard procedure.

Hashes indicates that the operator can be used in hash joins.

Merges indicates that the operator can be used in merge joins.

Indexes

Indexes are used in three places – to enforce uniqueness constraints and to speed up WHERE and JOIN clauses.

  1.     -- btree join
  2.   CREATE OPERATOR CLASS rational_ops
  3.       DEFAULT FOR TYPE javatest.rational USING btree AS
  4.         OPERATOR        1       < ,
  5.         OPERATOR        2       <= ,
  6.         OPERATOR        3       = ,
  7.         OPERATOR        4       >= ,
  8.         OPERATOR        5       > ,
  9.         FUNCTION        1       javatest.rational_cmp(javatest.rational, javatest.rational);
  10.  
  11.     -- hash join
  12.    CREATE OPERATOR CLASS rational_ops
  13.       DEFAULT FOR TYPE javatest.rational USING hash AS
  14.         OPERATOR        1       = ,
  15.         FUNCTION        1       javatest.rational_hashCode(javatest.rational);
    -- btree join
  CREATE OPERATOR CLASS rational_ops
      DEFAULT FOR TYPE javatest.rational USING btree AS
        OPERATOR        1       < ,
        OPERATOR        2       <= ,
        OPERATOR        3       = ,
        OPERATOR        4       >= ,
        OPERATOR        5       > ,
        FUNCTION        1       javatest.rational_cmp(javatest.rational, javatest.rational);

    -- hash join
   CREATE OPERATOR CLASS rational_ops
      DEFAULT FOR TYPE javatest.rational USING hash AS
        OPERATOR        1       = ,
        FUNCTION        1       javatest.rational_hashCode(javatest.rational);

Operator Families

Finally, PostgreSQL has the concept of “Operator Families” that group related operator classes under a single umbrella. For instance you might have one family that supports cross-comparison between int2, int4 and int8 values. Each can be specified individually but by creating an operator family you give a few more hints to the PostgreSQL optimizer.

More Information

CREATE OPERATOR (PostgreSQL)

CREATE OPERATOR CLASS (PostgreSQL)

CREATE OPERATOR FAMILY (PostgreSQL)

Operator Optimization (PostgreSQL)

Interfacing Extensions To Indexes (PostreSQL)

Comments
No Comments »
Categories
java, pl/java, PostgreSQL, user-defined types
Comments rss Comments rss
Trackback Trackback

« Previous Entries

Archives

  • May 2020 (1)
  • March 2019 (1)
  • August 2018 (1)
  • May 2018 (1)
  • February 2018 (1)
  • November 2017 (4)
  • January 2017 (3)
  • June 2016 (1)
  • May 2016 (1)
  • April 2016 (2)
  • March 2016 (1)
  • February 2016 (3)
  • January 2016 (6)
  • December 2015 (2)
  • November 2015 (3)
  • October 2015 (2)
  • August 2015 (4)
  • July 2015 (2)
  • June 2015 (2)
  • January 2015 (1)
  • December 2014 (6)
  • October 2014 (1)
  • September 2014 (2)
  • August 2014 (1)
  • July 2014 (1)
  • June 2014 (2)
  • May 2014 (2)
  • April 2014 (1)
  • March 2014 (1)
  • February 2014 (3)
  • January 2014 (6)
  • December 2013 (13)
  • November 2013 (6)
  • October 2013 (3)
  • September 2013 (2)
  • August 2013 (5)
  • June 2013 (1)
  • May 2013 (2)
  • March 2013 (1)
  • November 2012 (1)
  • October 2012 (3)
  • September 2012 (2)
  • May 2012 (6)
  • January 2012 (2)
  • December 2011 (12)
  • July 2011 (1)
  • June 2011 (2)
  • May 2011 (5)
  • April 2011 (6)
  • March 2011 (4)
  • February 2011 (3)
  • October 2010 (6)
  • September 2010 (8)

Recent Posts

  • 8-bit Breadboard Computer: Good Encapsulation!
  • Where are all the posts?
  • Better Ad Blocking Through Pi-Hole and Local Caching
  • The difference between APIs and SPIs
  • Hadoop: User Impersonation with Kerberos Authentication

Meta

  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org

Pages

  • About Me
  • Notebook: Common XML Tasks
  • Notebook: Database/Webapp Security
  • Notebook: Development Tips

Syndication

Java Code Geeks

Know Your Rights

Support Bloggers' Rights
Demand Your dotRIGHTS

Security

  • Dark Reading
  • Krebs On Security Krebs On Security
  • Naked Security Naked Security
  • Schneier on Security Schneier on Security
  • TaoSecurity TaoSecurity

Politics

  • ACLU ACLU
  • EFF EFF

News

  • Ars technica Ars technica
  • Kevin Drum at Mother Jones Kevin Drum at Mother Jones
  • Raw Story Raw Story
  • Tech Dirt Tech Dirt
  • Vice Vice

Spam Blocked

53,793 spam blocked by Akismet
rss Comments rss valid xhtml 1.1 design by jide powered by Wordpress get firefox