sqlx icon indicating copy to clipboard operation
sqlx copied to clipboard

Support multiple databases in the same crate (compile-time macros)

Open mehcode opened this issue 4 years ago • 23 comments

# .env
FOO_DATABASE_URL="postgres://..."

Option 1

Inspired from the log crate and log!(target: _).

let rows = sqlx::query!(database: "foo", "SELECT * FROM bar WHERE x = ?val", val = 10)
    .fetch_all(&mut conn)?;

Option 2

#[sqlx::database(url = "env:FOO_DATABASE_URL")]
mod foo;

// [...]

let rows = foo::query!("SELECT * FROM bar WHERE x = ?val", val = 10)
    .fetch_all(&mut conn)?;

Option 3

mod foo {
    sqlx::database!(url = "env:FOO_DATABASE_URL");
}

// [...]

let rows = foo::query!("SELECT * FROM bar WHERE x = ?val", val = 10)
    .fetch_all(&mut conn)?;

That's all I can think of at the moment. I prefer Option 2.

This is a solution to #121 and #224.

Thoughts?

mehcode avatar Dec 21 '20 08:12 mehcode

From a UX perspective, 3 is the friendliest to IDEs because it can be done with macro_rules! macros (can even be built on top of 1). Number 2 would not work at all with code-completion in IDEA. It's also non-local to the module file which I don't really like, although it could be the inner form #![sqlx::database(...)] but that would get lost above the imports in the kind of module that's typical of the projects we've been working on.

abonander avatar Dec 21 '20 09:12 abonander

Option 4

sqlx::database!(name = "foo", url = "env:FOO_DATABASE_URL");

// [...]

let rows = foo::query!("SELECT * FROM bar WHERE x = ?val", val = 10)
    .fetch_all(&mut conn)?;

To keep it 1-line (if that's important), we could inline the module creation within "Option 3".

mehcode avatar Dec 21 '20 09:12 mehcode

I would think name = should be to say "use all the parameters from DATABASE_URL but change the database name to this".

What about

sqlx::database!(mod foo, name = "bar_test_db")

which declares a module foo and redefines the query macros to talk to bar_test_db with the same authority and params as DATABASE_URL.

abonander avatar Dec 21 '20 09:12 abonander

I like the idea of name being "use the same URL but change the name to this".

I don't like !(mod foo, as syntax.

I'd probably prefer the 3-line mod foo { ... } version over that.

# .env
DATABASE_URL="postgres://postgres@localhost/foo"
OTHER_DATABASE_URL="postgres://postgres@other/foo"
mod bar { sqlx::database!(name = "bar"); } // url is "postgres://postgres@localhost/bar"
mod other_foo { sqlx::database!(url = "env:OTHER_DATABASE_URL"); } // url is "postgres://postgres@other/foo"

mehcode avatar Dec 21 '20 09:12 mehcode

name is really ambiguous anyway.

What about

sqlx::database!(mod = "foo", db = "bar")

abonander avatar Dec 21 '20 11:12 abonander

What else is it going to produce besides a mod, presumably pub(crate)?

sqlx::database!(foo, url = "env:FOO_DATABASE_URL");

Although if the user wants to put additional items in that module or adjust the visibility, the above macro could delegate to another one with both exposed:

mod foo { sqlx::database_impl!(url = "env:FOO_DATABASE_URL"); }

andrewwhitehead avatar Dec 21 '20 17:12 andrewwhitehead

What else is it going to produce besides a mod, presumably pub(crate)?

That's a really good reason for me to not generate the rust module at all. I hadn't considered visibility.

mehcode avatar Dec 21 '20 18:12 mehcode

Yeah, that could get messy.

abonander avatar Dec 23 '20 04:12 abonander

@mehcode I just realized we were already talking about doing this a long time ago, potentially with allowing other configuration to the macros such as specifying type overrides https://github.com/launchbadge/sqlx/issues/121

We also have talked about a sqlx.toml but I'm still somewhat of the persuasion that we shouldn't have an external config file that changes the behavior of the macros as that's a non-local effect.

abonander avatar Dec 27 '20 16:12 abonander

In talking about it, @mehcode and I settled on sqlx::macros!(), short and sweet.

abonander avatar Jan 08 '21 03:01 abonander

Unfortunately, the whole idea with using sqlx::macros!() in two different modules so you could do, e.g. foo::query!() and bar::query!() isn't gonna work with macro_rules! as it exists in this day and age.

Even in the 2018 edition you still can't invoke a macro in the same crate via its fully-qualified path; you have to use #[macro_use] to import all macros from a submodule into the parent module and all child modules following the annotated submodule in declaration order. Presumably declarative macros 2.0 would let this work as expected as it actually participates in normal item resolution but I have no idea when that's going to be stabilized.

There's still some utility with sqlx::macros!() but using it with two separate databases at once would require either invoking it in separate crates or making sqlx::macros!() a proc-macro so it can append a user-defined prefix to the macro names, which as mentioned above would kill autocomplete for the macros at least with IntelliJ-Rust.

We could make it work like this, though:

sqlx::macros!(
    db_url[foo] = "env::DATABASE_FOO",
    db_url[bar] = "env::DATABASE_BAR",
    types[bar] = {
        "BAZ": crate::model::Baz,
    },
    // top-level option which applies to both `foo` and `bar`:
    types = {
        "CITEXT": String,
    }
);

// this matches how arguments are passed to the macros internally but can be changed
query!(db = foo, "select * from ...")

I'm thinking any configuration options we plan to add, we can have them optionally accept a database name as subscript, and then they only apply to that database. Otherwise, they apply to all databases.

It could also look like this, maybe (again, with top-level options applying to all databases):

sqlx::macros! (
    db.foo = {
        url: "env::DATABASE_FOO",
    },
    db.bar = {
        url: "env::DATABASE_BAR",
        types: {
            "BAZ": crate::model::Baz,
        }
    },
    types = {
        "CITEXT": String,

    }
);

Or combine both and make the subscript db[foo] = { ... }.

abonander avatar Feb 03 '21 11:02 abonander

However, I'm also recalling some issues with macros-defining-macros where you want to use parameters from the outer macro invocation in the inner macro... things get weird.

Maybe it's time to dust off the sqlx.toml proposal.

abonander avatar Feb 03 '21 11:02 abonander

Even in the 2018 edition you still can't invoke a macro in the same crate via its fully-qualified path; you have to use #[macro_use] to import all macros from a submodule into the parent module and all child modules following the annotated submodule in declaration order. Presumably declarative macros 2.0 would let this work as expected as it actually participates in normal item resolution but I have no idea when that's going to be stabilized.

Actually there is some movement towards integrating macro_rules! with regular import resolution; see https://github.com/rust-lang/rust/pull/78166.

Wanted to link to that earlier but apparently it's virtually impossible to find with regular search engines, I only found it again now because I got a notification about a new comment.

jplatte avatar Feb 03 '21 18:02 jplatte

It's been put to an RFC now so it's still going to take a while to get merged and then stabilized, but it's nice to see that it's possible. It sounds like they want to make it part of the 2021 Edition though.

As for this:

However, I'm also recalling some issues with macros-defining-macros where you want to use parameters from the outer macro invocation in the inner macro... things get weird.

Apparently it just works now so I'm not sure what funkiness I was remembering: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=4de9983692e5894f174caf52b6c49191

abonander avatar Feb 03 '21 21:02 abonander

I think to reduce the syntax surface area I would be much more in favor of:

// all do the same, just bikeshedding
sqlx::macros!(foo, url = "env:DATABASE_URL");
sqlx::macros!(name: foo, url = "env:DATABASE_URL");
sqlx::macros!(prefix: foo, url = "env:DATABASE_URL");
sqlx::macros!(prefix = foo, url = "env:DATABASE_URL");

and used as:

foo_query!("SELECT ...");
foo_query_as!(Bar, "SELECT ...");

Yes this requires the macros to be in lib.rs or #[macro_use]'d near the top of the file, but I don't like a limitation like that will matter much to people that want this feature.

mehcode avatar Feb 04 '21 00:02 mehcode

Hello

I'm facing the issue where I've multiple DB connections in the same crate. I would like to work on a solution, can we fixe one of the propose option as a base to iterate?

Freyskeyd avatar Sep 06 '21 11:09 Freyskeyd

Sadly the pub_macro_rules feature was removed in https://github.com/rust-lang/rust/pull/83713 so we're likely back to just supporting prefixes for the new macros as depicted in @mehcode's comment above.

abonander avatar Jul 06 '22 03:07 abonander

While this may not be a proper solution, it's a workaround that works in my case (as I have no duplicate table names, and my database schemas are not updated often), so maybe it will be of use to someone else.

Essentially I created a new database (C) that contains the schema of tables in databases A and B. The connection string to database C is then set in the .env file. Now all query! calls will check against the schema of C.

It's then up to me to make sure that the correct database worker pool (i.e. database A or B) is used when calling query!.

aaronmussig avatar May 17 '23 04:05 aaronmussig

I'm having this issue as well. wondering if there's any progress

nthnd avatar Feb 08 '24 12:02 nthnd