# Adding jsonb columns for custom fields

**URL:** https://meta.discourse.org/t/adding-jsonb-columns-for-custom-fields/93418
**Category:** Development
**Created:** [July 29, 2018, 6:55pm UTC](https://meta.discourse.org/t/adding-jsonb-columns-for-custom-fields/93418 "2018-07-29T18:55:48Z")
**Posts on this page:** 18
**Page:** 1

<div class="post-metadata">

### Author: ![gdpelican](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/gdpelican/32/81308_2.png) [@gdpelican](https://meta.discourse.org/u/gdpelican)
#### Post date: [July 29, 2018, 6:55pm UTC](https://meta.discourse.org/t/adding-jsonb-columns-for-custom-fields/93418/1 "2018-07-29T18:55:48Z")

</div>

Often times when developing plugins, I’ve had to use the CustomField or PluginRowStore tables to store arbitrary data related to the plugin (for example, [retort](https://meta.discourse.org/t/retort-a-reaction-style-plugin-for-discourse/35903) stores a string representation of reactions to a particular post as a PostCustomField)

This works well for simple data, but for anything a bit more complex, I’ve found myself wanting access to more than a postgres `text`-valued column can provide. Postgres has supported the jsonb column type for several versions now ([since 9.4](https://www.postgresql.org/docs/9.4/static/datatype-json.html)), which provides some super-great features like finding and filtering by nested json fields, as well as indexing.

I wonder what the feasibility would be of adding a jsonb column to PluginStoreRow, or possibly converting the existing `value` column to jsonb to allow for more complicated models to be stored there?

---

<div class="post-metadata">

### Author: ![gdpelican](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/gdpelican/32/81308_2.png) [@gdpelican](https://meta.discourse.org/u/gdpelican)
#### Post date: [July 29, 2018, 6:56pm UTC](https://meta.discourse.org/t/adding-jsonb-columns-for-custom-fields/93418/2 "2018-07-29T18:56:13Z")

</div>

The use case I’m working with at the moment is a real-time collaborative editor, which involves storing a series of Changesets to a document; each Changeset holds a post\_id, author\_id, document length, and string representation of the changes. It would be feasible to store this information as JSON like so:

```plaintext
{
  post_id: 1,
  author_id: 2,
  length: 10,
  changes: ['1-5', 'world']
}

```

but putting it into a `text` representation

```plaintext
"1|2|10|['1-5', 'world']"

```

, or simply storing that JSON as text in the DB, doesn’t really work because I need to be able to search for rows by post\_id:

```plaintext
PluginStoreRow.where(plugin_name: :my_plugin, key: :changeset).where("value ->>'post_id' = 1")

```

---

<div class="post-metadata">

### Author: ![david](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/david/32/157490_2.png) [@david](https://meta.discourse.org/u/david)
#### Post date: [July 29, 2018, 7:16pm UTC](https://meta.discourse.org/t/adding-jsonb-columns-for-custom-fields/93418/3 "2018-07-29T19:16:31Z")

</div>

A method I’ve used before is:

```plaintext
PluginStoreRow.where("value::json->>'provider'=?", provider)

```

There is no need for the database column to actually be set to json for that to work. ([see here](https://www.postgresql.org/docs/9.3/static/datatype-json.html)).

The other thing I’ve used to help with JSON in plugins is `ActiveRecord::Store`. It allows storing data in json, while keeping all the activerecord validation/serialization magic.

> <https://github.com/discourse/discourse-chat-integration/blob/main/app/models/channel.rb#L3>

Switching to jsonb would certainly be more performant for looking up values - I like the idea of adding an additional column, so that we don’t break existing plugins. If we add jsonb, it looks like activerecord has good support for it as well 🙂

---

<div class="post-metadata">

### Author: ![gdpelican](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/gdpelican/32/81308_2.png) [@gdpelican](https://meta.discourse.org/u/gdpelican)
#### Post date: [July 29, 2018, 7:20pm UTC](https://meta.discourse.org/t/adding-jsonb-columns-for-custom-fields/93418/4 "2018-07-29T19:20:18Z")

</div>

Yes, I’m happy to start with that suggestion, although with this particular use case lookup performance is fairly critical (y’know, real-time and all that), and without an index on the value column it’s likely to be Not Quite Good Enough.

---

<div class="post-metadata">

### Author: ![david](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/david/32/157490_2.png) [@david](https://meta.discourse.org/u/david)
#### Post date: [July 29, 2018, 7:28pm UTC](https://meta.discourse.org/t/adding-jsonb-columns-for-custom-fields/93418/5 "2018-07-29T19:28:56Z")

</div>

There is an index on the `key` column of `PluginStoreRow`, so if your lookups are always against post\_id, you could utilise that.

Setting the key to `post_12_<random id>`

means you could use a lookup like:

```ruby
post_id = 12
PluginStoreRow.where("key LIKE ?", "post_#{post_id}_%")

```

That breaks down as soon as you want to ‘index’ against more than one value, but for a lot of applications it is enough.

---

<div class="post-metadata">

### Author: ![sam](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/sam/32/102149_2.png) [@sam](https://meta.discourse.org/u/sam)
#### Post date: [August 15, 2018, 1:50am UTC](https://meta.discourse.org/t/adding-jsonb-columns-for-custom-fields/93418/6 "2018-08-15T01:50:04Z")

</div>

As time goes by I become less and less of a fan of plugin store.

Can you just use a migration here and add a proper table, it gives proper data ownership to the plugin and is way more easy to clean up, data is easier to access and so on.

I get we want to make something dynamic here, but feel it is way cleaner just to have plugins stage the tables they need with proper models and so on.

Longer term we can even have a “registry” file committed to core the lists what plugin owns what tables.

---

<div class="post-metadata">

### Author: ![gdpelican](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/gdpelican/32/81308_2.png) [@gdpelican](https://meta.discourse.org/u/gdpelican)
#### Post date: [August 15, 2018, 2:07am UTC](https://meta.discourse.org/t/adding-jsonb-columns-for-custom-fields/93418/7 "2018-08-15T02:07:17Z")

</div>

I ended up doing this thing:

> [@New columns in directory (/users)](https://meta.discourse.org/t/new-columns-in-directory-users/77814/5):
>
> FWIW I’ve decided to go this route with a plugin of mine, where PluginStoreRow and CustomFields didn’t qqquite cut it on their own. warning Caveat emptor warning You should be trying really hard not to do this - It’s not recommended behaviour to modify the database from within a plugin, but it can be done unobtrusively in small, additive doses (I’d say you never for any reason want to remove or change existing columns) Here’s how I did it: Write a migration in the plugin which modifies th…

Which is adding (and allowing the removal of) a column from within the plugin (using this format to add and remove tables would be certainly possible as well).

The removal bit’s pretty manual atm, but maybe the registry you mention could handle removing related tables if the plugin is taken out of app.yml [it would be bad to wipe the database of all the plugin’s data just because the admin deactivated it] EDIT: No wait erasing data like that is probably a real nasty side effect and it probably wouldn’t be the worst thing in the world to have an orphan table or column that you could get rid of with a little work.

Code that might be cool to write:

```plaintext
# plugin.rb
register_plugin_table :my_plugin, :wingbats do |t|
  # this block is passed through to create_table
  t.integer :id 
  t.timestamps  
end

register_plugin_column :my_plugin, :post_custom_fields, :my_column, :jsonb, options: {
  default: {},
  index: true
}

```

---

<div class="post-metadata">

### Author: ![sam](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/sam/32/102149_2.png) [@sam](https://meta.discourse.org/u/sam)
#### Post date: [August 15, 2018, 2:21am UTC](https://meta.discourse.org/t/adding-jsonb-columns-for-custom-fields/93418/8 "2018-08-15T02:21:06Z")

</div>

My pref here today is just to lean on:

> <https://github.com/discourse/discourse/blob/06f82a7d72f79f1419b4ea8c1d264476dd794d87/lib/plugin/instance.rb#L456-L457>

So as long as your migrations are all there we will run them on db:migrate. Only caveat is we need to run them on the test database (which we should fix so it is done by default)

---

<div class="post-metadata">

### Author: ![david](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/david/32/157490_2.png) [@david](https://meta.discourse.org/u/david)
#### Post date: [August 15, 2018, 10:00am UTC](https://meta.discourse.org/t/adding-jsonb-columns-for-custom-fields/93418/9 "2018-08-15T10:00:08Z")

</div>

Ok, I will try with migrations inside the plugin and see how it goes. My main concern with plugins shipping migrations is support requests. Right now, uninstalling a plugin removes pretty much all traces of it. If we start encouraging migrations inside plugins, uninstalling the plugin won’t necessarily fix the problem.

As long as plugin developers don’t modify any core tables, it should be reasonably safe 🤞.

---

<div class="post-metadata">

### Author: ![j.jaffeux](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/j.jaffeux/32/60297_2.png) [@j.jaffeux](https://meta.discourse.org/u/j.jaffeux)
#### Post date: [August 15, 2018, 10:21am UTC](https://meta.discourse.org/t/adding-jsonb-columns-for-custom-fields/93418/10 "2018-08-15T10:21:36Z")

</div>

Couldn’t we fix the limitations of plugin store? or you just think it’s broken by design?

---

<div class="post-metadata">

### Author: ![david](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/david/32/157490_2.png) [@david](https://meta.discourse.org/u/david)
#### Post date: [August 15, 2018, 10:40am UTC](https://meta.discourse.org/t/adding-jsonb-columns-for-custom-fields/93418/11 "2018-08-15T10:40:55Z")

</div>

To provide some context, Sam’s post was in response to this PR:

[https://github.com/discourse/discourse/pull/6269](https://github.com/discourse/discourse/pull/6269)

That change would help the problem of storing structured data, but it’s never going to be as efficient or flexible as using separate tables. 🤷‍♂️

---

<div class="post-metadata">

### Author: ![j.jaffeux](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/j.jaffeux/32/60297_2.png) [@j.jaffeux](https://meta.discourse.org/u/j.jaffeux)
#### Post date: [August 15, 2018, 4:12pm UTC](https://meta.discourse.org/t/adding-jsonb-columns-for-custom-fields/93418/12 "2018-08-15T16:12:47Z")

</div>

Maybe we should provide some guidelines on when we think pluginstore is appropriate and when we think it’s not and people should use migrations.

---

<div class="post-metadata">

### Author: ![sam](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/sam/32/102149_2.png) [@sam](https://meta.discourse.org/u/sam)
#### Post date: [August 15, 2018, 11:51pm UTC](https://meta.discourse.org/t/adding-jsonb-columns-for-custom-fields/93418/13 "2018-08-15T23:51:38Z")

</div>

> [@j.jaffeux](#):
>
> Maybe we should provide some guidelines on when we think pluginstore is appropriate and when we think it’s not and people should use migrations.

The general guidelines for using plugin store would be for

- Tiny amounts of data
- You do not think you will need to query it

Overall, actual use cases for PluginStore should be very very small. For example even our existing cases where we use it are misuses imo cause tidy tables here would help a lot.

- staff notes should be a table
- [data explorer](https://meta.discourse.org/t/32566?silent=true) is really messy in the way it uses it and should use a table
- oauth2 basic belongs in the new tables @david is suggesting
- canned replies should be in a table

Regarding the “but I really really want to remove all traces of a plugin, problem”

Firstly, we are not cleaning up plugin store properly anyway. Secondly, if you make the migrations reversible we can just run them in reverse. The big risk around “adding extra tables” is that 2 plugins may fight for 1, which is why I suggested a central registry in core.

---

<div class="post-metadata">

### Author: ![LeoMcA](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/leomca/32/87233_2.png) [@LeoMcA](https://meta.discourse.org/u/LeoMcA)
#### Post date: [August 16, 2018, 1:20pm UTC](https://meta.discourse.org/t/adding-jsonb-columns-for-custom-fields/93418/14 "2018-08-16T13:20:08Z")

</div>

That’s how I ended up using it in [`discourse-mozilla-iam`](https://github.com/mozilla/discourse-mozilla-iam), PluginStore for storing a couple of global variables which we need to persist across restarts. Full blown models & tables for everything else.

> [@sam](#):
>
> The big risk around “adding extra tables” is that 2 plugins may fight for 1, which is why I suggested a central registry in core.

I got around this by setting up an engine in my plugin, and placing the model under that namespace. If any other plugin decides to touch a `mozilla_iam_group_mappings` table (or `MozillaIAM::GroupMappings` model), I would be 😲

> [@sam](#):
>
> Only caveat is we need to run them on the test database (which we should fix so it is done by default)

That would be 👌, I should’ve thought to report it when implementing this slight monstrosity:

> <https://github.com/mozilla/discourse-mozilla-iam/blob/master/spec/iam_helper.rb#L20-L21>

---

<div class="post-metadata">

### Author: ![david](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/david/32/157490_2.png) [@david](https://meta.discourse.org/u/david)
#### Post date: [August 16, 2018, 1:24pm UTC](https://meta.discourse.org/t/adding-jsonb-columns-for-custom-fields/93418/15 "2018-08-16T13:24:57Z")

</div>

> [@sam](#):
>
> Only caveat is we need to run them on the test database (which we should fix so it is done by default)

It is currently possible by doing `RAILS_ENV=test LOAD_PLUGINS=1 rake db:migrate`. I think that’s probably fine, given that plugins aren’t loaded for any test environment by default.

---

<div class="post-metadata">

### Author: ![LeoMcA](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/leomca/32/87233_2.png) [@LeoMcA](https://meta.discourse.org/u/LeoMcA)
#### Post date: [August 16, 2018, 1:56pm UTC](https://meta.discourse.org/t/adding-jsonb-columns-for-custom-fields/93418/16 "2018-08-16T13:56:19Z")

</div>

What about `RAILS_ENV=test rake plugin:spec[foobar]`? That command won’t work unless you remember to run the migrations (which I think is a bit unexpected).

---

<div class="post-metadata">

### Author: ![david](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/david/32/157490_2.png) [@david](https://meta.discourse.org/u/david)
#### Post date: [August 16, 2018, 1:59pm UTC](https://meta.discourse.org/t/adding-jsonb-columns-for-custom-fields/93418/17 "2018-08-16T13:59:51Z")

</div>

> [@LeoMcA](#):
>
> That command won’t work unless you remember to run the migrations (which I think is a bit unexpected).

`rake spec` doesn’t run core migrations, so I think it would be inconsistent for `rake plugin:spec` to run migrations 🤔. (not necessarily opposed to them both running migrations though)

---

<div class="post-metadata">

### Author: ![LeoMcA](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/leomca/32/87233_2.png) [@LeoMcA](https://meta.discourse.org/u/LeoMcA)
#### Post date: [August 16, 2018, 2:07pm UTC](https://meta.discourse.org/t/adding-jsonb-columns-for-custom-fields/93418/18 "2018-08-16T14:07:30Z")

</div>

There’s also [discourse/lib/tasks/docker.rake at main · discourse/discourse · GitHub](https://github.com/discourse/discourse/blob/master/lib/tasks/docker.rake) which doesn’t seem to run plugin migrations (unless you’ve set `LOAD_PLUGINS` in the env somewhere prior to running it, which would seem to be redundant if you’ve already set `SINGLE_PLUGIN`).
