Slices don't take db config from parent by default

Hiya!

There’s either a bug in Hanami or an inconsistency in the guides on the subject of slices inheriting the db config from the parent. The guides claim this is the default and can be disabled in the slice config:

However, it appears to be the other way around, I have to explicitly set this config to true for the slice to inherit the db. Here’s a little demo app to illustrate:

In order to fix this: Can anyone tell whether the default in Hanami is wrong or whether it’s just a misinformation in the guides?

Cheers! -sven

Hey @svoop, thanks for sharing this!

I’ve taken a look at your demo app, and it did help me discover one bug: Hanami::DB::Testing.database_url should apply to explicitly configured database URLs, not just ENV-detected URLs · Issue #1482 · hanami/hanami · GitHub. It was this bug that was causing this warning to print when running commands like hanami db migrate:

WARNING: Database svoop_demo_test expects migrations to be located within slices/admin/config/db/migrate/ but that folder does not exist.

The workaround for now here is to remove/comment out the explicit datase_url assignment line from your db provider:

Hanami.app.configure_provider :db do
  config.gateway :default do |gw|
    # gw.database_url = "postgres://postgres@localhost/svoop_demo"
  end
end

With this change, Hanami will then pick up the database URL from the DATABASE_URL ENV var.

Two things related things happen that lead to that warning above:

  1. When a hanami db command is run in the development environment, it also re-runs itself in the test environment, to make sure the test databases are kept in sync.
  2. Our default behaviour is to append a _test string to a database URL when running in the test environment. We currently do this for ENV-based database URLs, but not for explicitly configured ones (hence the issue I linked above). So when the hanami db command executed for your app, it picked up a database URL from both the ENV (since you have it in your .env file as well as your explicitly configured URL. This led Hanami to mistakenly believe you had two databases configured instead of one, because they each had different URLs (one with _test appended and one not).

So by removing the explicitly configured URL for now, the warning no longer prints, because Hanami resolves just a single database URL for your app, and it knows that database is already configured via the config/db/ folder you have:

$ bundle exec hanami db migrate
NOTE: Empty database migrations folder (config/db/migrate/) for svoop_demo
=> svoop_demo structure dumped to config/db/structure.sql in 0.0382s
NOTE: Empty database migrations folder (config/db/migrate/) for svoop_demo_test

But none of this addresses your concerns about config.db.import_from_parent and its default value.

It’s important to clarify something here. There are actually two related settings:

config.db.configure_from_parent # default is true
config.db.import_from_parent    # default is false

configure_from_parent will copy DB config from parent slices to child slices, but allow the child slice to have its own fully independent DB setup thereafter. So the slice will have its own relations, etc.

import_from_parent, on the other hand, will take the entire DB setup from the parent slice and simply register it into the child slice. You would use this setting if you only want a single set of relations for your entire app (e.g. defined in app/relations/), and then have those relations made available in every slice.

Your demo app is fairly minimal, so it’s not clear which of these you actually want.

But here’s how you can verify the two different modes:

config.db.configure_from_parent = true (our default)

To verify this, add some kind of config to the parent slice’s DB provider, for example:

# config/providers/db.rb

Hanami.app.configure_provider :db do
  config.gateway :default do |gw|
    config.adapter :sql do |a|
      a.extension :exclude_or_null
    end
  end
end

Then verify that the config is also set in a child slice. So in your demo app’s case, the admin slice. Via the console:

# Note that :exclude_or_null is here
demo[development]> app["db.gateways.default"].options[:extensions]
=> [:exclude_or_null, :caller_logging, :error_sql, :sql_comments, :pg_array, :pg_enum, :pg_json, :pg_range]

# Same for the admin slice
demo[development]> admin["db.gateways.default"].options[:extensions]
=> [:exclude_or_null, :caller_logging, :error_sql, :sql_comments, :pg_array, :pg_enum, :pg_json, :pg_range]

The configure_from_parent mode makes sure that all possible configs are taken from the parent slice and also configured on the DB setup created in child slices.

In this mode, the config is copied, but the actual ROM containers are distinct to each slice:

demo[development]> app["db.rom"].object_id
=> 84940
demo[development]> admin["db.rom"].object_id
=> 91040

This is what allows you to create your unique relations to each slice. For example, if I updated that create_posts.rb migration to actually create a table, and then ran hanami g relation posts and hanami g relation posts --slice=admin, you’ll see that each slice’s ROM container only sees its own version of the posts relation:

demo[development]> app["db.rom"].relations[:posts]
=> #<Demo::Relations::Posts name=ROM::Relation::Name(posts) dataset=#<Sequel::Postgres::Dataset: "SELECT \"posts\".\"title\" FROM \"posts\"">>

demo[development]> admin["db.rom"].relations[:posts]
=> #<Admin::Relations::Posts name=ROM::Relation::Name(posts) dataset=#<Sequel::Postgres::Dataset: "SELECT \"posts\".\"title\" FROM \"posts\"">>

So in this arrangement:

  • DB config is shared. This so that you only have to set that config in one place, rather than copy it into every slice, which is an important convenience).
  • But everything else afterwards is specific to each slice.

config.db.import_from_parent = true (not the default)

Now if I add this line to either config/app.rb or your config/slices/admin.rb (either place is fine, since slices will inherit their config from their parent), we see some different behaviour. The ROM containers for both the app and slices are identical (the literal same object), and as such, only a single shared set of relations is available in all places.

demo[development]> app["db.rom"].object_id
=> 17380
demo[development]> admin["db.rom"].object_id
=> 17380

demo[development]> app["db.rom"].relations[:posts]
=> #<Demo::Relations::Posts name=ROM::Relation::Name(posts) dataset=#<Sequel::Postgres::Dataset: "SELECT \"posts\".\"title\" FROM \"posts\"">>
demo[development]> admin["db.rom"].relations[:posts]
=> #<Demo::Relations::Posts name=ROM::Relation::Name(posts) dataset=#<Sequel::Postgres::Dataset: "SELECT \"posts\".\"title\" FROM \"posts\"">>

So in this arrangement:

  • DB config is centralised, by virtue of a single ROM setup being used everywhere.
  • Relations are shared across all slices. This is convenient if your app is small and you don’t want the hassle of defining and managing a distinct set of relations for each slice.
  • Slices still have their own repos and structs on top of those shared relations, since repos and structs those aren’t managed via the ROM container.

All of the examples above come from me working directly on your demo app, so I’m quite confident everything is working as it should. We also have tests covering these cases.

I think we could do a lot more to document these different modes. Our DB docs are still a work in progress, and we’ll add more detail when we can.

I’m really grateful you raised this though, because it’s made it clearer the need to document these better, as well as helping me find that bug! Thank you! :heart:

If you have any more questions, or if you think I’ve missed something from your original report, please let me know :slight_smile:

I believe that I had confused configure_from_parent and import_from_parent in that doc page.

Ah, forgot about .env in the demo, I don’t use it on my real app. Glad the issue #1482 bug surfaced by this mishap of mine.

As for configure_ and import_from_parent though, apart from the mixup in the guides, I still have this gut feeling that there’s more.

Please take a look at my slightly revised demo. It’s still a minimalist app which sets the database URL in a provider. This time, I’ve removed the .env beforehand.

As you see, as soon as a slice is generated, dev fails to boot the app, that’s with the defaults you @timriley mentioned:

config.db.configure_from_parent # default is true
config.db.import_from_parent    # default is false

That’s surprising because setting the database URL in the provider is “configure” and should be taken from parent by default, isn’t it?

As soon as I enable import_from_parent to the mix, it works fine, but relations are then shared across all slices. (No biggie for me, I probably want to keep things this way anyway.)


As a side question: For improvements on the guides, do you prefer PRs with the gist of discussions like this one (but maybe not the best of English since I’m no native speaker) or rather GitHub-issues… or neither?

Thanks for continuing to investigate this, @svoop. I definitely want to make sure we get to the bottom of this!

Just so I’m clear, is this what you mean:

  1. I edit config/slices/admin.rb and comment out these lines of config, so we’re using the defaults again:
    module Admin
      class Slice < Hanami::Slice
        # config.db.configure_from_parent = true   # default: true
        # config.db.import_from_parent = true      # default: false
      end
    end
    
  2. Then when I run hanami console and type admin["db.rom"], I see this error?
    demo[development]> admin["db.rom"]
    /Users/tim/.local/share/mise/installs/ruby/3.3.5/lib/ruby/gems/3.3.0/gems/hanami-2.2.1/lib/hanami/providers/db.rb:194:in `block (2 levels) in configure_gateways': A database_url for gateway default is required to start :db. (Hanami::ComponentLoadError)
    

Is that how I should be reproducing the “fails to boot” error you’re describing?

If not, please let me know what I should be doing. Once I’m clear on that, I can go and share with you what is happening and why :slight_smile:

@timriley

Is that how I should be reproducing the “fails to boot” error you’re describing?

Yes, exactly. You get the same error if you do dev instead of hanami console (...):

15:52:19 web.1    | ! Unable to load application: Hanami::ComponentLoadError: A database_url for gateway default is required to start :db.
15:52:19 web.1    | /Users/myself/.gem/ruby/3.3.0/gems/hanami-2.2.1/lib/hanami/providers/db.rb:194:in `block (2 levels) in configure_gateways': A database_url for gateway default is required to start :db. (Hanami::ComponentLoadError)

(Btw: That’s on Ruby 3.3.6, I manage Rubies with direnv which builds gems per minor Ruby, not per patchlevel Ruby, hence the 3.3.0 in the path.)