sqlpp11 icon indicating copy to clipboard operation
sqlpp11 copied to clipboard

How to clean up & and recreate prepared statements?

Open MacDue opened this issue 5 years ago • 11 comments

How do you tidy up after & fix a dead prepared statement?

You have a statement you reuse a bunch:

thread_local auto prepared = conn->prepare(
      select(player_tbl.name).where(player_tbl.id == parameter(player_tbl.id)));

Then your database is rebooted.

That statement will now throw No result set.

I'd like some way of robustly handling prepared statements where if they're used after the original connection has closed it'll be cleaned up (if needed) & a new one prepared.

Is there any nice way to do this?

MacDue avatar Mar 09 '20 15:03 MacDue

I can't see any ways to check it the handle is still valid or even access the handle.

It looks like the only way to know if it's valid still is to try & run it? Which makes handling it very tricky.

MacDue avatar Mar 09 '20 15:03 MacDue

Good question! I looked briefly at the MySQL documentation and did not find a good way of checking.

Thus, a validity check would be connector-specific (at best). If a specific database allows checks, then it should be possible to add a wrapper to the respective connector.

rbock avatar Mar 09 '20 16:03 rbock

Seems like it. I couldn't figure out a great way of handling this (for postgres) but I've made up a quick hack that might work:

template<typename Query>
class RobustPrepared {
  Query const & preparable_query;
public:
  RobustPrepared(Query const & query) : preparable_query{query} {};
  template<typename Connection>
  auto& prepare(Connection & conn) {
    thread_local auto prepared = conn->prepare(preparable_query);
    return prepared;
  }
  auto operator() (auto & conn) {
    auto& prepared = prepare(conn);
    try {
      return conn(prepared);
    } catch (sqlpp::postgresql::failure const & e) {
      auto old_params = prepared.params;
      prepared = conn->prepare(preparable_query);
      prepared.params = old_params;
      return conn(prepared);
    }
  };
};

Used like:

static RobustPrepared rp(
      select(player_tbl.name).where(player_tbl.id == parameter(player_tbl.id)));
auto& prepared = rp.prepare(conn);
auto results = rp(conn);

MacDue avatar Mar 09 '20 17:03 MacDue

That looks pretty neat to me :-)

Thanks for sharing!

rbock avatar Mar 09 '20 17:03 rbock

Not really getting any luck with it sadly. Still throws the same error & I'm not sure why.

MacDue avatar Mar 09 '20 17:03 MacDue

Well it sort of works but if I run an old prepared statement after reconnecting to the database my app seg faults (sometimes) as the old prepared statement references the now deleted connection (I think). Not sure how to handle that.

MacDue avatar Mar 09 '20 18:03 MacDue

Looks like I'll need to fork the connector to make this work.

Though I'm not sure how to fix it really.

MacDue avatar Mar 09 '20 18:03 MacDue

What does conn->prepare really return? I need to add a method to check if it's connection is current (as the easiest way to start handle things). @rbock

MacDue avatar Mar 09 '20 18:03 MacDue

It depends on the connector. In the case of Postgresql, prepared statements are identified by a name. The prepared statement therefore is mostly a reference to the connection and a name.

You would need to change prepared_statement_t in the postgresql connector library.

rbock avatar Mar 09 '20 18:03 rbock

Thanks, I've got something that seems to work now:

template<typename Query>
class RobustPrepared {
  Query const & preparable_query;
public:
  RobustPrepared(Query const & query) : preparable_query{query} {};
  template<typename Connection>
  auto& prepare(Connection & conn) {
    thread_local auto prepared = conn->prepare(preparable_query);
    return prepared;
  }

  void reprepare(auto & conn) {
    auto& prepared = prepare(conn);
    auto old_params = prepared.params;
    prepared = conn->prepare(preparable_query);
    prepared.params = old_params;
  }

  auto operator() (auto & conn) {
    auto& prepared = prepare(conn);
    if (!prepared._prepared_statement.is_valid_for_connection(*conn)) {
      prepared._prepared_statement.invalidate_connection();
      reprepare(conn);
    }
    try {
      return conn(prepared);
    } catch (sqlpp::exception const & e) {
      reprepare(conn);
      return conn(prepared);
    }
  };
};

I just needed to add in is_valid_for_connection & invalidate_connection that allows me to check & safely dispose of old statements. Feels a little icky but works.

MacDue avatar Mar 09 '20 19:03 MacDue

Cool. Please share with the postgresql connector team :-)

rbock avatar Mar 09 '20 19:03 rbock