How to insert a row from JSON in Postgres

All we need is an easy explanation of the problem, so here it is.

I have 2 tables recording company market data.
A table recording stock market symbols

CREATE TABLE appl.symbols (
   id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
   symbol VARCHAR(10) NOT NULL,
   exchange VARCHAR(10) NOT NULL,
   date_added DATE NOT NULL DEFAULT CURRENT_DATE,
   active BOOLEAN NOT NULL DEFAULT true,
)

And a table with many columns (about 50) recording details about the company.

CREATE TABLE appl.fundamentals_overview (
    overview_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    symbol_id INT,
    CONSTRAINT fk_symbol
      FOREIGN KEY(symbol_id) 
        REFERENCES appl.symbols(id),
    assettype VARCHAR(255),
    name VARCHAR(255),
    description VARCHAR(1500),
    cik VARCHAR(10),
    currency VARCHAR(10),
    country VARCHAR(10),
    sector VARCHAR(50),
    industry VARCHAR(100),
    address VARCHAR(100),
    fiscalyearend VARCHAR(10),
    latestquarter DATE,
    marketcapitalization BIGINT,
    ebitda BIGINT,
    peratio NUMERIC(20, 4),
    pegratio NUMERIC(20, 4),
    bookvalue NUMERIC(20, 4),
    dividendpershare NUMERIC(20, 4),
    dividendyield NUMERIC(20, 4),
    eps NUMERIC(20, 4),
    revenuepersharettm NUMERIC(20, 4),
    profitmargin NUMERIC(20, 4),
    operatingmarginttm NUMERIC(20, 4)
    ... many more columns
)

I am trying to allow client code to create a row without needing to know unique IDs, and not needing to give a value for all columns. Something like the following needs to be valid (as long as they know the symbol name):

 {"assettype": "Common Stock", 
  "name": "AcmeINC", 
  "cik": "1555752", 
  "currency": "USD", 
  "country": "USA", 
  "fiscalyearend": "December",
  "peratio": 235.56
}

I created a function, but I can not work out how to get the column names, in the right order, and then specify a variable for the column names. This is as far as I got

CREATE OR REPLACE FUNCTION appl.insert_fund_overview(sym TEXT, ex TEXT, js TEXT) RETURNS VOID AS
$func$
DECLARE
    symID BIGINT := 0;
    fullJSON JSON;
    tempJSON JSON;
    colNames TEXT;
BEGIN 
    symID := (SELECT appl.symbols.id FROM appl.symbols WHERE symbol = sym AND exchange = ex);
    tempJSON := json_build_object('symbol_id', symID);
    fullJSON := (js::jsonb) || (tempJSON::jsonb);
    colNames := (SELECT string_agg(elem, ',') FROM json_object_keys(fullJSON) elem);
    --The value of colNames contains the JSON key names, but in wrong order

    -- This does not work
    INSERT INTO appl.fundamentals_overview (colNames)  
    SELECT *  
    FROM json_populate_record(NULL::appl.fundamentals_overview, fullJSON::json);
END
$func$  LANGUAGE plpgsql;
ERROR:  column "colnames" of relation "fundamentals_overview" does not exist

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 1

As has been commented, you don’t have to list target columns for json(b)_populate_record(). While using the same row type, columns are guaranteed to match.

Plus, I would simplify / optimize a bit:

CREATE OR REPLACE FUNCTION appl.insert_fund_overview(_sym text, _ex text, _js jsonb)  -- ②
  RETURNS void
  LANGUAGE plpgsql AS
$func$
DECLARE
   _sym_id jsonb;
BEGIN
   SELECT INTO _sym_id  to_jsonb(t.*)
   FROM  (SELECT id AS symbol_id FROM appl.symbols
          WHERE symbol = _sym AND exchange = _ex) t;
   
   IF NOT FOUND THEN  -- ③
      RAISE EXCEPTION 'Symbol not found in table appl.symbols!';
   END IF;

-- This works:
   INSERT INTO appl.fundamentals_overview OVERRIDING SYSTEM VALUE  -- ①
   SELECT *
   FROM   jsonb_populate_record(NULL::appl.fundamentals_overview, _js || _sym_id);  -- ②
END
$func$;

① You seem to be providing a value for overview_id, which is an IDENTITY column with GENERATED ALWAYS. You can force input values as demonstrated.
The manual about OVERRIDING SYSTEM VALUE:

If this clause is specified, then any values supplied for identity columns will override the default sequence-generated values.

Alternatively, use OVERRIDING USER VALUE:

If this clause is specified, then any values supplied for identity columns are ignored and the default sequence-generated values are applied.

With OVERRIDING SYSTEM VALUE the underlying SEQUENCE will be out of sync, and you may want to get back in sync. See:

Related:

② Going with jsonb since that allows the simple concatenation jsonb || jsonb (as opposed to json).

to_jsonb(t.*) FROM ( ... ) t is slightly shorter and faster than json_build_object(). See:

③ And I raise an exception if the symbol is not found. You may or may not want that. But if you remove it, you need to special-case _sym_id IS NULL as _js || NULL yields NULL. Like:

_js || COALESCE(_sym_id, '{}')

Or, more verbose but cheaper:

CASE WHEN _sym_id IS NULL THEN _js ELSE _js || _sym_id) END

Alternatively, you might insert a new row into appl.symbols if it does not exist yet. A case of SELECT or INSERT. And you may want that safe under concurrent write load. See:

Related:

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply