Updating Data

Record Creation

  • ❌ Cannot be cached or prefetched
  • ❌ Not idempotent

To create a row in a database table post a JSON object whose keys are the names of the columns you would like to create. Missing keys will be set to default values when applicable.

POST /table_name
{ "col1": "value1", "col2": "value2" }

The response will include a Location header describing where to find the new object. If you would like to get the full object back in the response to your request, include the header Prefer: return=representation. That way you won’t have to make another HTTP call to discover properties that may have been filled in on the server side.

Record Update

To update a single existing record, you will need to make a PATCH request, and use filtering via parameters to specify which record to update.

PATCH /table_name?myPKey=eq.thePKey
{ "col1":"update-value1", "col2":"update-value2" }

Bulk Insertion

  • ❌ Cannot be cached or prefetched
  • ❌ Not idempotent

You can POST a JSON array or CSV to insert multiple rows in a single HTTP request. Note that using CSV requires less parsing on the server and is much faster.

Example of CSV bulk insert. Simply post to a table route with Content-Type: text/csv and include the names of the columns as the first row. For instance

POST /people
name,age,height
J Doe,62,70
Jonas,10,55

An empty field (,,) is coerced to an empty string and the reserved word NULL is mapped to the SQL null value. Note that there should be no spaces between the column names and commas.

Example of JSON bulk insert. Send an array:

POST /people
[
  { "name": "J Doe", "age": 62, "height": 70 },
  { "name": "Janus", "age": 10, "height": 55 }
]

If you would like to get the full object back in the response to your request, include the header Prefer: return=representation. Chances are you only want certain information back, though, like created ids. You can pass a select parameter to affect the shape of the response (further documented in the reading page). For instance

POST /people?select=id
[...]

returns something like

[ { "id": 1 }, { "id": 2 } ]

Multiple Tables Insertion or Update

The cleanest way to insert or update data into multiple tables using only one POST/PATCH request is to create a view that will join all target tables and present a single endpoint. In our example let’s assume one users table and one companies table. In this case, we want a signup endpoint to create the first user within a company. And for this endpoint we want to insert with one request both user and company.

CREATE TABLE companies (
    id serial primary key,
    name text unique
);

CREATE TABLE users (
    id serial primary key,
    name text not null,
    pass text,
    company_id integer not null references companies
);

Having both tables created we create a view that joins them to be used as a /signup endpoint.

CREATE VIEW signup AS
    SELECT
        c.name AS company_name,
        u.name AS user_name,
        u.pass
    FROM
        public.users u
        JOIN public.companies c ON c.id = u.company_id;

After the signup view creation, we can issue GET requests to read data from users and companies, but any attempt to POST or PATCH data will fail. PostgreSQL won’t allow any data change on views that have a JOIN clause in their FROM without a proper INSTEAD OF trigger. So in the example bellow we create a trigger to allow insertion of data in the signup view. The trigger is a simple PL/pgSQL function that first inserts into the companies table and uses the newly create company_id to create its first user.

CREATE FUNCTION signup()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
  vcompany_id int;
BEGIN
  INSERT INTO companies (name) VALUES (new.company_name) RETURNING id INTO vcompany_id;
  INSERT INTO users (name, pass, company_id) VALUES (new.user_name, new.pass, vcompany_id);
RETURN new;
END;
$$;

CREATE TRIGGER signup
INSTEAD OF INSERT ON signup
FOR EACH ROW
EXECUTE PROCEDURE signup();

After the trigger creation we can issue a normal POST request to our signup endpoint:

POST /signup
{ "company_name": "foo", "user_name": "bar" }

For an endpoint such as signup its usually not desirable to have a PATCH route for updates, and we will skip this example for the sake of brevity. But it would be implemented in a very similar way to our POST example.

Design Consideration

It's advisable to create a separate trigger for UPDATE and INSERT avoiding conditionals that decide which is the trigger current operation. This makes it easier to change code for (or even disable) one operation without interfering with others while improving readability.

Bulk Updates

  • ❌ Cannot be cached or prefetched
  • ❌ Not idempotent

To change parts of a resource or resources use the PATCH verb. For instance, here is how to mark all young people as children.

PATCH /people?age=lt.13
{
  "person_type": "child"
}

This affects any rows matched by the url param filters, overwrites any fields specified in in the payload JSON and leaves the other fields unaffected. Note that although the payload is not in the JSON patch format specified by RFC6902, HTTP does not specify which patch format to use. Our format is more pleasant, meant for basic field replacements, and not at all “incorrect.”

Deletion

  • ❌ Cannot be cached or prefetched
  • ✅ Idempotent

Simply use the DELETE verb. All records that match your filter will be removed. For instance deleting inactive users:

DELETE /user?active=is.false

Protecting Dangerous Actions

Notice that it is very easy to delete or update many records at once. In fact forgetting a filter will affect an entire table!

Invitation to Contribute

We would like to investigate nginx rules to guard dangerous actions, perhaps requiring a confirmation header or query param to perform the action.

You're invited to research this option and contribute to this documentation.