Remix.run Logo
Paul-E 4 hours ago

I want to address this one point:

> Similar thing can be said about writing SQL. I was really happy with using sqlx, which is a crate for compile-time checked SQL queries. By relying on macros in Rust, sqlx would execute the query against a real database instance in order to make sure that your query is valid, and the mappings are correct. However, writing dynamic queries with sqlx is a PITA, as you can’t build a dynamic string and make sure it’s checked during compilation, so you have to resort to using non-checked SQL queries. And honestly, with kysely in Node.js, I can get a similar result, without the need to have a connection to the DB, while having ergonomic query builder to build dynamic queries, without the overhead of compilation time.

I've used sqlx, and its alright, but I've found things much easier after switching to sea-orm. Sea-orm has a wonderful query builder that makes it feel like you are writing SQL. Whereas with sqlx you end up writing Rust that generates SQL strings, ie re-inventing query builders.

You also get type checking; define your table schema as a struct, and sea-orm knows what types your columns are. No active connection required. This approach lets you use Rust types for fields, eg Email from the email crate or Url from the url crate, which lets you constrain fields even further than what is easy to do at the DB layer.

ORMs tend to get a bad reputation for how some ORMs implement the active record pattern. For example, you might forget something is an active record and write something like "len(posts)" in sqlalchemy and suddenly you are counting records by pulling them from the DB in one by one. I haven't had this issue with sea-orm, because it is very clear about what is an active record and what is not, and it is very clear when you are making a request out to the DB. For me, it turns out 90% of the value of an ORM is the query builder.

cogman10 3 hours ago | parent [-]

sqlx doesn't build queries, or at least it minimally builds them. Which I think is the thing the OP is complaining about.

And, IMO, making dynamic queries harder is preferable. Dynamic queries are inherently unsafe. Sometimes necessary, however you have to start considering things like sql injection attacks with dynamic queries.

This isn't to poo poo sea-orm. I'm just saying that sqlx's design choice to make dynamic queries hard is a logical choice from a safety standpoint.

spoiler 3 hours ago | parent | next [-]

They didn't make them hard by design, I think, it's just the limitations of the current API and prioritisation. Dynamic queries are possible, just not trivial

cogman10 3 hours ago | parent [-]

Nope, it really was part of the design [1]

[1] https://github.com/launchbadge/sqlx/issues/333#issuecomment-...

Paul-E 38 minutes ago | parent | prev [-]

> And, IMO, making dynamic queries harder is preferable. Dynamic queries are inherently unsafe. Sometimes necessary, however you have to start considering things like sql injection attacks with dynamic queries.

Depends on what you mean by "dynamic query". You are dealing with injection attacks as soon as you start taking user input. Most useful user facing applications take user input.

In a simple case it might be "SELECT * FROM posts WHERE title LIKE '%hello world%', where "hello world" is a user specified string. This is easy with sqlx. Where things get more difficult is if you want to optionally add filters for things like date posted, score of the post, author, etc... That makes the query dynamic in a way that can't be solved by simply including a bind.

That's where sea-orm shines over sqlx IMO. sqlx will force you to do something like

```

let mut my_query = "SELECT * FROM posts WHERE title LIKE '%' + $1 + '%'";

let mut my_binds = vec![args.keyword];

if let Some(date) = args.date {

  my_query = format("{my_query} AND date = $2");

  my_binds.push(date);
}

...

```

Your building a string and tracking binds. It gets messy. A good query builder like seaorm has lets you do something this:

```

let mut query = Posts::find().filter(Column::title::like(args.keyword));

if let Some(date) = args.date {

  query = query.filter(column::Date::eq(date));
}

```

This pays off as your queries get more complicated. It pushes the string manipulation and bookkeeping into a library, which can be more thoroughly tested.

It also lets you pass around typed partial queries, eg in the example above query might be returned from a function, which helps you build more modular code.

cogman10 19 minutes ago | parent [-]

I agree with what you are saying, this is exactly what I was thinking when I said it was sometimes necessary. It's just not preferable IMO.

For this specific example, the better way is something like this

    let result = if let Some(date) = args.date {
      sqlx::query("SELECT * FROM posts WHERE title LIKE '%' + $1 + '%' AND date = $2")
        .bind(args.keyword)
        .bind(date)
        .fetch()
    } else {
      sqlx::query("SELECT * FROM posts WHERE title LIKE '%' + $1 + '%")
        .bind(args.keyword)
        .fetch()
    }
But I get how this would be untenable if as the number of query param combos goes up. In that case dynamic SQL really is the only sane way to handle something like that.