Dan, I tried the above patch and it resolves the issue! Also you were right about use of "*" in attach. I misinterpreted the output of my test in that case.
Thank you so much for taking the time to fix this! When can I expect this patch to make it into a release? Adam Levy On Thu, Feb 27, 2020 at 8:23 AM Dan Kennedy <danielk1...@gmail.com> wrote: > > > On 27/2/63 05:04, Adam Levy wrote: > > Hi all > > > > When I have a database connection with a temp table of the same name > > as a table in main, I am getting what I feel is unexpected behavior > > from the session extension. > > > > Most succinctly, I start a session on a connection on the main db with > > a table with an INTEGER PRIMARY KEY, attach it to all tables by > > passing NULL, make a change such as a single INSERT in aforementioned > > table, and then record a changeset. Then I invert this changeset, and > > apply that inverse to the same connection, to reverse the changes just > > made. > > > > Normally this works flawlessly. However, if a table by the same name > > exists in the temp database on that connection, I run into odd > > behavrior. > > > > If the temp table has different rows than the main table, a very odd > > changeset conflict occurs. The conflict type is SQLITE_CHANGESET_DATA > > but when I examine the changeset iter the values for the columns, they > > match the conflicting values. The inverse of an INSERT is a DELETE so > > this conflict type indicates that one of the non-primary key column > > values doesn't match, but in every examination of the database and the > > changeset I have performed they do match. The primary key exists and > > matches as well. > > > > If the temp table has the same rows at the main table (what the > > changeset expects), the changeset_apply does not raise a conflict, but > > it takes no affect on the main table. > > Thanks for reporting this. As far as I can figure, it's a bug in > sqlite3changeset_apply(), not sqlite3session_attach(). Now fixed here: > > https://sqlite.org/src/info/f71a13d072398c9f > > If you get the chance, please check if this fixes your test script and > let us know if it does not. > > > This can all be avoided by being explicit about the database when > > calling sqlite3session_attach. > > For example passing "*" or "main.*" or "main.<table>" avoids the > > issue. > > I think that's just because sqlite3session_attach() doesn't understand > any of those shorthands. If you pass "*", it searches for a table named > "*", not a table that matches the glob pattern "*". > > Thanks, > > Dan. > > > > > > > But it occurs if NULL is passed. This is not obvious and > > arguably not expected given that the documentation about the Session > > extension makes it seem like it should be already scoped to a single > > database on a connection. After all, sqlite3session_create accepts the > > database as an argument. Also, sqlite3_changeset_apply appears to be > > specific to the "main" database. So it shouldn't care about whats in > > the temp database at all. > > > > From all of the docs I have read on the Session extension this seems > > like misbehavior at the level of sqlite3session_attach being passed > > NULL for the zTab argument. At the minimum perhaps a note in the > > documentation of sqlite3session_attach would be appropriate. > > > > Below is a very simple example that can reproduce this behavior. It > > has a number of lines commented out that describe how the bug can be > > avoided to caused. > > > > Its written in Golang, but the library it uses is just a very thin > > wrapper around the C interface. It is using the latest 3.31.1 > > amalgamation. I help maintain the SQLite library that it uses and can > > confirm that everything being called here is a very thin wrapper > > around the C interface. I could write a C example but it would take me > > more work. > > > > The library it calls to print the SQL of the changeset is a utility I > > wrote that is just using a thin wrapper around the changeset_iter to > > parse the changeset into human readable SQL. It of course is not aware > > of database names so it doesn't print "main" but this SQL is just for > > displaying to the user. The values returned by > > sqlite3changeset_conflict are formatted into the comments of that > > printed SQL. > > > > The code can be more easily downloaded here: > > https://github.com/AdamSLevy/sqlite-session-bug > > > > // main.go > > > > package main > > > > import ( > > "bytes" > > "fmt" > > "io" > > "log" > > > > "crawshaw.io/sqlite" > > "crawshaw.io/sqlite/sqlitex" > > "github.com/AdamSLevy/sqlitechangeset" > > ) > > > > func run() error { > > conn, err := sqlite.OpenConn(":memory:", 0) > > if err != nil { > > return fmt.Errorf("sqlite open: %w", err) > > } > > defer conn.Close() > > > > fmt.Println("Creating tables...") > > err = sqlitex.ExecScript(conn, ` > > CREATE TABLE main.t(id INTEGER PRIMARY KEY, a,b,c); > > CREATE TABLE temp.t(id INTEGER PRIMARY KEY, a,b,c) --- remove this > > line or rename the table to avoid issue; > > `) > > if err != nil { > > return err > > } > > fmt.Println("Starting session on main...") > > sess, err := conn.CreateSession("main") > > if err != nil { > > return err > > } > > defer sess.Delete() > > // An empty string will pass NULL to sqlite3session_attach and allow > > // the bug. > > var attach string > > // Any of these prevent the bug. > > //attach = "*" > > //attach = "main.*" > > //attach = "main.t" > > fmt.Printf("Attaching to %s ...\n", attach) > > if err := sess.Attach(attach); err != nil { > > return err > > } > > > > fmt.Println("Inserting into main.t ...") > > commit := sqlitex.Save(conn) > > err = sqlitex.ExecScript(conn, ` > > INSERT INTO main.t(a,b,c) VALUES (1,2,3); > > INSERT INTO temp.t(a,b,c) VALUES (1,2,3) --- This line avoids the > > conflict, but the changeset doesn't apply to main.t like expected; > > `) > > if err != nil { > > return err > > } > > commit(&err) > > > > sql, err := sqlitechangeset.SessionToSQL(conn, sess) > > if err != nil { > > return err > > } > > fmt.Println("changeset:", sql) > > > > chgset := bytes.NewBuffer(nil) > > if err := sess.Changeset(chgset); err != nil { > > return err > > } > > invrt := bytes.NewBuffer(nil) > > invrtCp := bytes.NewBuffer(nil) > > if err := sqlite.ChangesetInvert( > > io.MultiWriter(invrt, invrtCp), chgset); err != nil { > > return err > > } > > > > sql, err = sqlitechangeset.ToSQL(conn, invrtCp) > > if err != nil { > > return err > > } > > fmt.Println("inverted changeset:", sql) > > > > /*// Dropping the temp table avoids the issue... > > fmt.Println("Dropping temp table...") > > err = sqlitex.ExecScript(conn, ` > > DROP TABLE temp.t; > > `) > > if err != nil { > > return err > > } > > */ > > > > fmt.Println("applying inverted changeset...") > > conflictFn := func(cType sqlite.ConflictType, > > iter sqlite.ChangesetIter) sqlite.ConflictAction { > > fmt.Println("ConflictType:", cType) > > sql, err := sqlitechangeset.ConflictChangesetIterToSQL(conn, iter) > > if err != nil { > > fmt.Println(err) > > } > > fmt.Println("Conflict:", sql) > > return sqlite.SQLITE_CHANGESET_ABORT > > } > > > > if err := conn.ChangesetApply(invrt, filterFn, conflictFn); err != nil > > { > > return err > > } > > fmt.Println("done.") > > > > sql, err = sqlitechangeset.SessionToSQL(conn, sess) > > if err != nil { > > return err > > } > > if len(sql) > 0 { > > return fmt.Errorf("Error: current session changeset not empty: > > %v", sql) > > } > > fmt.Println("success") > > > > return nil > > } > > > > func filterFn(string) bool { return true } > > > > func main() { > > if err := run(); err != nil { > > log.Fatal(err) > > } > > } > > _______________________________________________ > > sqlite-users mailing list > > sqlite-users@mailinglists.sqlite.org > > http://mailinglists.sqlite.org/cgi-bin/mailman/listinfo/sqlite-users > _______________________________________________ > sqlite-users mailing list > sqlite-users@mailinglists.sqlite.org > http://mailinglists.sqlite.org/cgi-bin/mailman/listinfo/sqlite-users _______________________________________________ sqlite-users mailing list sqlite-users@mailinglists.sqlite.org http://mailinglists.sqlite.org/cgi-bin/mailman/listinfo/sqlite-users