On Sun, 20 Jan 2019 21:51:19 +0000
<andrew.g...@l3t.com> wrote:

> > insert into t
> > select :pid, nrows, N
> > from (select 1 as N union select 2 union select 3) as cardinals
> > cross join (select :pid, count(*) as nrows from t) as how_many;
> >
> > By using a single SQL statement, you avoid a user-defined
> > transaction and any proprietary transaction qualifiers.
> 
> There's too much back-and-forth between the database and my logic to
> put it all into a single statement.  Thus, transactions are
> necessary.  Transactions exist to allow multiple statements to become
> an atomic unit, so eschewing them is basically the same thing as
> admitting they don't work.  

I don't suggest you eschew user-defined transactions, and I don't admit
they don't work.  I suggest not relying on proprietary features.  BEGIN
IMMEDIATE may do what you want, but at the cost of not learning
how to accomplish the same thing using standard SQL.  

In general, the formulation

        select ...
        do stuff
        insert ...

will get you in trouble in most SQL setups, because selected data are
subject to change, whether or not the logic is in a user-defined
transaction.  If the DBMS is set up with serialized isolation,
concurrency will be compromised because you're holding a transaction
open across application logic.  That's especially true when it would be
most convenient: when "do stuff" involves user interaction, and said
user might choose that moment for a coffee break.  

The most common solution is to check your assumptions at the door,
otherwise known as "optimistic concurrency". At time of insert,
make sure the previously obtained data still obtain, i.e., that nothing
has changed meanwhile. If it has, the precondition for the update has
been violated, and you start over.  If it hasn't -- and, in practice,
usually it hasn't -- you're good to go.  

In your case, you might be able to do something like 

        select count(*) as nrows from t
        do stuff
        begin transaction
        insert into t select ... where nrows = (select count(*) from t)
        verify rows affected == 1
        insert into t select ... where nrows = (select count(*) from t)
        verify rows affected == 1
        insert into t select ... where nrows = (select count(*) from t)
        verify rows affected == 1
        commit

putting whatever the relevant test is in the WHERE clause.  The test
can be simplified in SQLite to just the first insert, because with
SQLite's single-writer design, they're redundant once the transaction
has begun to take effect (ie, once other writers really are blocked).  

User-defined transactions weren't invented to deal with
read-write-write errors.  They were invented to enforce referential
integrity.  SQL has no syntax to update several related tables at the
same time.  If updates to different tables would leave the database in
an inconsistent state, some mechanism is needed to convert those
several SQL statements into a single all-or-nothing update.  Thus were
born user-defined transactions.  

--jkl

_______________________________________________
sqlite-users mailing list
sqlite-users@mailinglists.sqlite.org
http://mailinglists.sqlite.org/cgi-bin/mailman/listinfo/sqlite-users

Reply via email to