diff --git a/src/main/java/org/sqlite/SQLiteErrorCode.java b/src/main/java/org/sqlite/SQLiteErrorCode.java index b017534a0..f7cede643 100644 --- a/src/main/java/org/sqlite/SQLiteErrorCode.java +++ b/src/main/java/org/sqlite/SQLiteErrorCode.java @@ -24,6 +24,8 @@ //-------------------------------------- package org.sqlite; +import java.sql.SQLException; + /** * SQLite3 error code * @@ -63,7 +65,53 @@ public enum SQLiteErrorCode { SQLITE_RANGE(25, " 2nd parameter to sqlite3_bind out of range"), SQLITE_NOTADB(26, " File opened that is not a database file"), SQLITE_ROW(100, " sqlite3_step() has another row ready"), - SQLITE_DONE(101, " sqlite3_step() has finished executing"); + SQLITE_DONE(101, " sqlite3_step() has finished executing"), + /* Beginning of extended error codes */ + SQLITE_BUSY_RECOVERY(261, " Another process is busy recovering a WAL mode database file following a crash"), + SQLITE_LOCKED_SHAREDCACHE(262, " Contention with a different database connection that shares the cache"), + SQLITE_READONLY_RECOVERY(264, " The database file needs to be recovered"), + SQLITE_IOERR_READ(266, " I/O error in the VFS layer while trying to read from a file on disk"), + SQLITE_CORRUPT_VTAB(267, " Content in the virtual table is corrupt"), + SQLITE_CONSTRAINT_CHECK(275, " A CHECK constraint failed"), + SQLITE_ABORT_ROLLBACK(516, " The transaction that was active when the SQL statement first started was rolled back"), + SQLITE_BUSY_SNAPSHOT(517, " Another database connection has already written to the database"), + SQLITE_READONLY_CANTLOCK(520, " The shared-memory file associated with that database is read-only"), + SQLITE_IOERR_SHORT_READ(522, " The VFS layer was unable to obtain as many bytes as was requested"), + SQLITE_CANTOPEN_ISDIR(526, " The file is really a directory"), + SQLITE_CONSTRAINT_COMMITHOOK(531, " A commit hook callback returned non-zero"), + SQLITE_READONLY_ROLLBACK(776, " Hot journal needs to be rolled back"), + SQLITE_IOERR_WRITE(778, " I/O error in the VFS layer while trying to write to a file on disk"), + SQLITE_CANTOPEN_FULLPATH(782, " The operating system was unable to convert the filename into a full pathname"), + SQLITE_CONSTRAINT_FOREIGNKEY(787, " A foreign key constraint failed"), + SQLITE_READONLY_DBMOVED(1032, " The database file has been moved since it was opened"), + SQLITE_IOERR_FSYNC(1034, " I/O error in the VFS layer while trying to flush previously written content"), + SQLITE_CANTOPEN_CONVPATH(1038, " cygwin_conv_path() system call failed while trying to open a file"), + SQLITE_CONSTRAINT_FUNCTION(1043, " Error reported by extension function"), + SQLITE_IOERR_DIR_FSYNC(1290, " I/O error in the VFS layer while trying to invoke fsync() on a directory"), + SQLITE_CONSTRAINT_NOTNULL(1299, " A NOT NULL constraint failed"), + SQLITE_IOERR_TRUNCATE(1546, " I/O error in the VFS layer while trying to truncate a file to a smaller size"), + SQLITE_CONSTRAINT_PRIMARYKEY(1555, " A PRIMARY KEY constraint failed"), + SQLITE_IOERR_FSTAT(1802, " I/O error in the VFS layer while trying to invoke fstat()"), + SQLITE_CONSTRAINT_TRIGGER(1811, " A RAISE function within a trigger fired, causing the SQL statement to abort"), + SQLITE_IOERR_UNLOCK(2058, " I/O error within xUnlock"), + SQLITE_CONSTRAINT_UNIQUE(2067, " A UNIQUE constraint failed"), + SQLITE_IOERR_RDLOCK(2314, " I/O error within xLock"), + SQLITE_CONSTRAINT_VTAB(2323, " Error reported by application-defined virtual table"), + SQLITE_IOERR_DELETE(2570, " I/O error within xDelete"), + SQLITE_CONSTRAINT_ROWID(2579, " rowid is not unique"), + SQLITE_IOERR_NOMEM(3082, " Unable to allocate sufficient memory"), + SQLITE_IOERR_ACCESS(3338, " I/O error within the xAccess"), + SQLITE_IOERR_CHECKRESERVEDLOCK(3594, " I/O error within xCheckReservedLock"), + SQLITE_IOERR_LOCK(3850, " I/O error in the advisory file locking logic"), + SQLITE_IOERR_CLOSE(4106, " I/O error within xClose"), + SQLITE_IOERR_SHMOPEN(4618, " I/O error within xShmMap while trying to open a new shared memory segment"), + SQLITE_IOERR_SHMSIZE(4874, " I/O error within xShmMap while trying to resize an existing shared memory segment"), + SQLITE_IOERR_SHMMAP(5386, " I/O error within xShmMap while trying to map a shared memory segment"), + SQLITE_IOERR_SEEK(5642, " I/O error while trying to seek a file descriptor"), + SQLITE_IOERR_DELETE_NOENT(5898, " The file being deleted does not exist"), + SQLITE_IOERR_MMAP(6154, " I/O error while trying to map or unmap part of the database file"), + SQLITE_IOERR_GETTEMPPATH(6410, " Unable to determine a suitable directory in which to place temporary files"), + SQLITE_IOERR_CONVPATH(6666, " cygwin_conv_path() system call failed"); public final int code; public final String message; diff --git a/src/main/java/org/sqlite/SQLiteException.java b/src/main/java/org/sqlite/SQLiteException.java new file mode 100644 index 000000000..9b1c13f5a --- /dev/null +++ b/src/main/java/org/sqlite/SQLiteException.java @@ -0,0 +1,41 @@ +/*-------------------------------------------------------------------------- + * Copyright 2016 Magnus Reftel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *--------------------------------------------------------------------------*/ +//-------------------------------------- +// sqlite-jdbc Project +// +// SQLiteException.java +// Since: Jun 28, 2016 +// +// $URL$ +// $Author$ +//-------------------------------------- +package org.sqlite; + +import java.sql.SQLException; +import org.sqlite.SQLiteErrorCode; + +public class SQLiteException extends SQLException { + private SQLiteErrorCode resultCode; + + public SQLiteException(String message, SQLiteErrorCode resultCode) { + super(message, null, resultCode.code & 0xff); + this.resultCode = resultCode; + } + + public SQLiteErrorCode getResultCode() { + return resultCode; + } +} diff --git a/src/main/java/org/sqlite/core/CoreStatement.java b/src/main/java/org/sqlite/core/CoreStatement.java index cdcfe9642..e488151b7 100644 --- a/src/main/java/org/sqlite/core/CoreStatement.java +++ b/src/main/java/org/sqlite/core/CoreStatement.java @@ -123,7 +123,7 @@ protected void internalClose() throws SQLException { int resp = db.finalize(this); if (resp != SQLITE_OK && resp != SQLITE_MISUSE) - db.throwex(); + db.throwex(resp); } public abstract ResultSet executeQuery(String sql, boolean closeStmt) throws SQLException; diff --git a/src/main/java/org/sqlite/core/DB.java b/src/main/java/org/sqlite/core/DB.java index 28cff1e76..9705b3bc9 100644 --- a/src/main/java/org/sqlite/core/DB.java +++ b/src/main/java/org/sqlite/core/DB.java @@ -24,6 +24,7 @@ import org.sqlite.Function; import org.sqlite.SQLiteConnection; import org.sqlite.SQLiteErrorCode; +import org.sqlite.SQLiteException; /* * This class is the interface to SQLite. It provides some helper functions @@ -132,14 +133,15 @@ public final synchronized void exec(String sql) throws SQLException { long pointer = 0; try { pointer = prepare(sql); - switch (step(pointer)) { + int rc = step(pointer); + switch (rc) { case SQLITE_DONE: ensureAutoCommit(); return; case SQLITE_ROW: return; default: - throwex(); + throwex(rc); } } finally { @@ -749,8 +751,9 @@ final synchronized int[] executeBatch(long stmt, int count, Object[] vals) throw for (int i = 0; i < count; i++) { reset(stmt); for (int j = 0; j < params; j++) { - if (sqlbind(stmt, j, vals[(i * params) + j]) != SQLITE_OK) { - throwex(); + rc = sqlbind(stmt, j, vals[(i * params) + j]); + if (rc != SQLITE_OK) { + throwex(rc); } } @@ -760,7 +763,7 @@ final synchronized int[] executeBatch(long stmt, int count, Object[] vals) throw if (rc == SQLITE_ROW) { throw new BatchUpdateException("batch entry " + i + ": query returns results", changes); } - throwex(); + throwex(rc); } changes[i] = changes(); @@ -790,8 +793,9 @@ public final synchronized boolean execute(CoreStatement stmt, Object[] vals) thr } for (int i = 0; i < params; i++) { - if (sqlbind(stmt.pointer, i, vals[i]) != SQLITE_OK) { - throwex(); + int rc = sqlbind(stmt.pointer, i, vals[i]); + if (rc != SQLITE_OK) { + throwex(rc); } } } @@ -880,7 +884,7 @@ public final void throwex(int errorCode) throws SQLException { * @param errorMessage Error message to be passed. * @throws SQLException */ - final void throwex(int errorCode, String errorMessage) throws SQLException { + static final void throwex(int errorCode, String errorMessage) throws SQLiteException { throw newSQLException(errorCode, errorMessage); } @@ -891,9 +895,11 @@ final void throwex(int errorCode, String errorMessage) throws SQLException { * @return Formated SQLException with error code and message. * @throws SQLException */ - public static SQLException newSQLException(int errorCode, String errorMessage) throws SQLException { + public static SQLiteException newSQLException(int errorCode, String errorMessage) { SQLiteErrorCode code = SQLiteErrorCode.getErrorCode(errorCode); - SQLException e = new SQLException(String.format("%s (%s)", code, errorMessage), null, code.code); + SQLiteException e = new SQLiteException( + String.format("%s (%s)", code, errorMessage), code + ); return e; } @@ -903,7 +909,7 @@ public static SQLException newSQLException(int errorCode, String errorMessage) t * @return SQLException with error code and message. * @throws SQLException */ - private SQLException newSQLException(int errorCode) throws SQLException { + private SQLiteException newSQLException(int errorCode) throws SQLException { return newSQLException(errorCode, errmsg()); } @@ -956,9 +962,10 @@ final void ensureAutoCommit() throws SQLException { { return; // assume we are in a transaction } - if (step(commit) != SQLITE_DONE) { + int rc = step(commit); + if (rc != SQLITE_DONE) { reset(commit); - throwex(); + throwex(rc); } //throw new SQLException("unable to auto-commit"); } diff --git a/src/main/java/org/sqlite/core/NativeDB.c b/src/main/java/org/sqlite/core/NativeDB.c index 44e540755..8b8a49c13 100644 --- a/src/main/java/org/sqlite/core/NativeDB.c +++ b/src/main/java/org/sqlite/core/NativeDB.c @@ -354,6 +354,9 @@ JNIEXPORT void JNICALL Java_org_sqlite_core_NativeDB__1open( return; } + // Ignore failures, as we can tolerate regular result codes. + (void) sqlite3_extended_result_codes(db, 1); + sethandle(env, this, db); } diff --git a/src/test/java/org/sqlite/AllTests.java b/src/test/java/org/sqlite/AllTests.java index 0835043d2..cd4bae02c 100644 --- a/src/test/java/org/sqlite/AllTests.java +++ b/src/test/java/org/sqlite/AllTests.java @@ -9,6 +9,7 @@ BackupTest.class, ConnectionTest.class, DBMetaDataTest.class, + ErrorMessageTest.class, ExtendedCommandTest.class, ExtensionTest.class, FetchSizeTest.class, diff --git a/src/test/java/org/sqlite/ErrorMessageTest.java b/src/test/java/org/sqlite/ErrorMessageTest.java new file mode 100644 index 000000000..f7451b954 --- /dev/null +++ b/src/test/java/org/sqlite/ErrorMessageTest.java @@ -0,0 +1,136 @@ +package org.sqlite; + +import static org.junit.Assume.assumeTrue; + +import java.io.File; +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Properties; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.junit.Rule; +import org.junit.Test; +import org.junit.matchers.JUnitMatchers; +import org.junit.rules.ExpectedException; + +public class ErrorMessageTest { + @Rule + public ExpectedException thrown = ExpectedException.none(); + + static class VendorCodeMatcher extends BaseMatcher { + final SQLiteErrorCode expected; + + VendorCodeMatcher(SQLiteErrorCode expected) {this.expected = expected;} + + public boolean matches(Object o) { + if (!(o instanceof SQLException)) { + return false; + } + SQLException e = (SQLException)o; + SQLiteErrorCode ec = SQLiteErrorCode.getErrorCode(e.getErrorCode()); + return ec == expected; + } + + public void describeTo(Description description) { + description + .appendText("SQLException with error code ") + .appendText(expected.name()) + .appendText(" (") + .appendValue(expected.code) + .appendText(")"); + } + } + + static class ResultCodeMatcher extends BaseMatcher { + final SQLiteErrorCode expected; + + ResultCodeMatcher(SQLiteErrorCode expected) {this.expected = expected;} + + public boolean matches(Object o) { + if (!(o instanceof SQLiteException)) { + return false; + } + SQLiteException e = (SQLiteException)o; + return e.getResultCode() == expected; + } + + public void describeTo(Description description) { + description + .appendText("SQLiteException with error code ") + .appendText(expected.name()) + .appendText(" (") + .appendValue(expected.code) + .appendText(")"); + } + } + + @Test + public void moved() throws SQLException, IOException { + File from = File.createTempFile("error-message-test-moved-from", ".sqlite"); + from.deleteOnExit(); + + Connection conn = DriverManager.getConnection("jdbc:sqlite:" + from.getAbsolutePath()); + Statement stmt = conn.createStatement(); + stmt.executeUpdate("create table sample(id, name)"); + stmt.executeUpdate("insert into sample values(1, \"foo\")"); + + File to = File.createTempFile("error-message-test-moved-from", ".sqlite"); + assumeTrue(to.delete()); + assumeTrue(from.renameTo(to)); + + thrown.expectMessage(JUnitMatchers.containsString("[SQLITE_READONLY_DBMOVED]")); + stmt.executeUpdate("insert into sample values(2, \"bar\")"); + + stmt.close(); + conn.close(); + } + + @Test + public void writeProtected() throws SQLException, IOException { + File file = File.createTempFile("error-message-test-write-protected", ".sqlite"); + file.deleteOnExit(); + + Connection conn = DriverManager.getConnection("jdbc:sqlite:" + file.getAbsolutePath()); + Statement stmt = conn.createStatement(); + stmt.executeUpdate("create table sample(id, name)"); + stmt.executeUpdate("insert into sample values(1, \"foo\")"); + stmt.close(); + conn.close(); + + assumeTrue(file.setReadOnly()); + + conn = DriverManager.getConnection("jdbc:sqlite:" + file.getAbsolutePath()); + stmt = conn.createStatement(); + thrown.expectMessage(JUnitMatchers.containsString("[SQLITE_READONLY]")); + stmt.executeUpdate("insert into sample values(2, \"bar\")"); + stmt.close(); + conn.close(); + } + + @Test + public void shouldUsePlainErrorCodeAsVendorCodeAndExtendedAsResultCode() throws SQLException, IOException { + File from = File.createTempFile("error-message-test-plain-1", ".sqlite"); + from.deleteOnExit(); + + Connection conn = DriverManager.getConnection("jdbc:sqlite:" + from.getAbsolutePath()); + Statement stmt = conn.createStatement(); + stmt.executeUpdate("create table sample(id, name)"); + stmt.executeUpdate("insert into sample values(1, \"foo\")"); + + File to = File.createTempFile("error-message-test-plain-2", ".sqlite"); + assumeTrue(to.delete()); + assumeTrue(from.renameTo(to)); + + thrown.expectMessage(JUnitMatchers.containsString("[SQLITE_READONLY_DBMOVED]")); + thrown.expect(new VendorCodeMatcher(SQLiteErrorCode.SQLITE_READONLY)); + thrown.expect(new ResultCodeMatcher(SQLiteErrorCode.SQLITE_READONLY_DBMOVED)); + stmt.executeUpdate("insert into sample values(2, \"bar\")"); + + stmt.close(); + conn.close(); + } +}