Home > Back-end >  How to order generated JSON column in PostgreSQL?
How to order generated JSON column in PostgreSQL?

Time:12-10

This is my database schema:

Database schema

I am trying to query products to display on my website's front page. Although, I need every product row to come with information about it's availability. Also, I need to ability to order these rows so that products that are not in stock come last.

This is what I am looking to recieve as json in my front end website, if the product is available, list the sizes in the "inventory" property, if not, inventory should be null:

[
    {
        id: 1,
        name: "product 1",
        sku: "dslfgsd",
        brand_slug: "some-brand",
        inventory: [
            { "price": 400, "size": "41½" },
            { "price": 500, "size": "42" }
        ]
    },
    {
        id: 2,
        name: "product 2",
        sku: "djsfkj",
        brand_slug: "some-other-brand",
        inventory: null
    }
]

My first attempt was to run two different queries then matching the available sizes to the products. So, I run the first query to get products, get the product ids, then search for them in the junction table. Then I match them to the products, sort them so that the products that are available rank first. I don't like the following attempt because it uses two queries and won't work well with pagination since I am only ordering the 12 products received from the first query.

const productsQuery = `
    SELECT products.id,
        products.name,
        products.sku,
        brands.slug as brand_slug
    FROM products
    INNER JOIN brands
        ON products.brand = brands.id
        AND products.public IS true
    LIMIT 12
`
 
const productsResult = await database.query(productsQuery)

let products = productsResult.rows.map(p => ({ ...p, inventory: [] }))

if (products.length === 0) return { props: { initialProducts: products } }

const productIds = products.map(r => r.id)

const inventoryQuery = `
    SELECT products_sizes_suppliers.product_id,
        products_sizes_suppliers.price,
        sizes.display_text as size
    FROM products_sizes_suppliers
    INNER JOIN sizes
        ON products_sizes_suppliers.size_id = sizes.id
        AND products_sizes_suppliers.product_id IN (${productIds.join(",")})
        AND cost IS NOT NULL
        AND price IS NOT NULL
    ORDER BY sizes.numerical_value
`

const inventoryResult = await database.query(inventoryQuery)

for (let inventory of inventoryResult.rows) {
    for (let index = 0; index < products.length; index  ) {
            if (products[index].id === inventory.product_id) products[index].inventory = [...products[index].inventory, inventory]
    }
}

products = products
    .sort((a, b) => {
        if (a.inventory.length === 0 && b.inventory.length > 0) return 1
        if (a.inventory.length > 0 && b.inventory.length === 0) return -1
        if ((a.inventory.length === 0 && b.inventory.length === 0)) return 0
    })
    .map(product => {
        const newProduct = { ...product }
        if (newProduct.inventory.length === 0) newProduct.inventory = null
        return newProduct
    })

My second attempt was to do everything in one query. I used array_to_json, array_agg and json_build_object. But I am unable to order this query.

SELECT products_a.id,
    products_a.name,
    products_a.sku,
    brands_a.slug as brand_slug,
    (
        SELECT array_to_json(array_agg(json_build_object('price', products_sizes_suppliers.price, 'size', sizes.display_text))) AS inventory
        FROM products_sizes_suppliers
        INNER JOIN sizes
        ON products_sizes_suppliers.size_id = sizes.id
            AND products_sizes_suppliers.product_id = products_a.id
            AND products_sizes_suppliers.cost IS NOT NULL
            AND products_sizes_suppliers.price IS NOT NULL
    )
FROM products as products_a
INNER JOIN brands as brands_a
    ON brands_a.id = products_a.brand
    AND products_a.public IS true

Query result

I am unable to order this query using ORDER BY inventory DESC NULLS LAST which results in the error: "could not identify an ordering operator for type json". Other attempts I have made result in the error: "column "inventory" does not exist".

I feel as though I am missing a core concept here, and that there is a proper way of doing this as I know SQL is a very powerful language.

Thanks in advance.

CodePudding user response:

To determine the order in which rows are aggreated, use

jsonb_agg(element ORDER BY expression)

CodePudding user response:

@Lauren Albe Pointed me in the right direction, his suggestion was to use jsonb_agg(element ORDER BY expression). I had to use the ORDER BY clause outside of the function and I had to find something to order it with do I just chose the price and the date the product was added. Also this article came in very handy.

EDIT: The first query didn't really solve the problem because it returned duplicates. Here is the one that worked:

SELECT products.id,
    products.activity,
    products.colors,
    products.gender,
    products.images,
    products.name,
    products.sku,
    brands.slug as brand_slug,
    jsonb_agg(to_jsonb(variants) - 'product_id' - 'numerical_value' ORDER BY numerical_value) AS inventory
FROM products
INNER JOIN brands
    ON products.brand = brands.id
LEFT JOIN (
    SELECT products_sizes_suppliers.product_id,
        products_sizes_suppliers.price,
        sizes.display_text as size,
        sizes.numerical_value
    FROM products_sizes_suppliers
    INNER JOIN sizes
        ON products_sizes_suppliers.size_id = sizes.id
) AS variants
    ON products.id = variants.product_id
GROUP BY products.id, brands.slug, variants.product_id
HAVING products.public IS true
ORDER BY variants.product_id, products.added_on DESC NULLS LAST
LIMIT 12

The old query:

SELECT products.id,
    products.sku,
    jsonb_agg(
        to_jsonb(something) - 'product_id'
    ) AS inventory
FROM products
LEFT JOIN (
    SELECT products_sizes_suppliers.product_id,
        products_sizes_suppliers.price,
        sizes.display_text
    FROM products_sizes_suppliers
    INNER JOIN sizes
        ON products_sizes_suppliers.size_id = sizes.id
) AS something ON products.id = something.product_id
GROUP BY products.id, something.price
ORDER BY price, products.added_on DESC NULLS LAST
  • Related