Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions ext/sqlite3/database.c
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,110 @@ db_filename(VALUE self, VALUE db_name)
return Qnil;
}

#ifdef HAVE_SQLITE3_SERIALIZE
/* call-seq: db.serialize(database_name = "main")
*
* Serialize the database into a binary string.
*
* [database_name] The name of the database to serialize. The default value
* is +main+ which is the main database file. For ATTACHed
* databases, use the name given in the ATTACH statement.
*
* Returns the contents of the database as a binary string. For an ordinary
* on-disk database file, this is just a copy of the disk file. For an
* in-memory database or a "TEMP" database, this is the same sequence of bytes
* which would be written to disk if that database were backed up to disk.
*
* Note: This method will only be defined if SQLite was compiled without
* +SQLITE_OMIT_DESERIALIZE+.
*
* See also: https://www.sqlite.org/c3ref/serialize.html
*/
static VALUE
serialize(int argc, VALUE *argv, VALUE self)
{
sqlite3RubyPtr ctx;
VALUE db_name;
const char *zSchema;
sqlite3_int64 size;
unsigned char *data;
VALUE result;

TypedData_Get_Struct(self, sqlite3Ruby, &database_type, ctx);
REQUIRE_OPEN_DB(ctx);

rb_scan_args(argc, argv, "01", &db_name);

zSchema = NIL_P(db_name) ? NULL : StringValueCStr(db_name);

data = sqlite3_serialize(ctx->db, zSchema, &size, 0);

if (data == NULL && size != 0) {
rb_raise(rb_eNoMemError, "failed to allocate %"PRId64" bytes for serialization", size);
}

result = rb_str_new((const char *)data, (long)size);
sqlite3_free(data);

return result;
}
#endif

#ifdef HAVE_SQLITE3_DESERIALIZE
/* call-seq: db.deserialize(data, database_name = "main")
*
* Deserialize a binary string into the database. The database is replaced with
* the contents of the serialized data.
*
* [data] A binary string containing the serialized database content.
* This is typically obtained from a prior call to #serialize.
*
* [database_name] The name of the database to replace. The default value is
* +main+ which is the main database file. For ATTACHed
* databases, use the name given in the ATTACH statement.
*
* The database connection must not be in a transaction or involved in a backup
* operation when this method is called, or it will raise SQLite3::BusyException.
*
* Note: This method will only be defined if SQLite was compiled without
* +SQLITE_OMIT_DESERIALIZE+.
*
* See also: https://www.sqlite.org/c3ref/deserialize.html
*/
static VALUE
deserialize(int argc, VALUE *argv, VALUE self)
{
sqlite3RubyPtr ctx;
VALUE data, db_name;
const char *zSchema;
unsigned char *pData;
sqlite3_int64 szDb;
int status;

TypedData_Get_Struct(self, sqlite3Ruby, &database_type, ctx);
REQUIRE_OPEN_DB(ctx);

rb_scan_args(argc, argv, "11", &data, &db_name);

StringValue(data);
zSchema = NIL_P(db_name) ? NULL : StringValueCStr(db_name);
szDb = RSTRING_LEN(data);

pData = sqlite3_malloc64(szDb);
if (pData == NULL) {
rb_raise(rb_eNoMemError, "failed to allocate %"PRId64" bytes for deserialization", szDb);
}
memcpy(pData, RSTRING_PTR(data), (size_t)szDb);

status = sqlite3_deserialize(ctx->db, zSchema, pData, szDb, szDb,
SQLITE_DESERIALIZE_FREEONCLOSE | SQLITE_DESERIALIZE_RESIZEABLE);

CHECK(ctx->db, status);

return self;
}
#endif

static VALUE
rb_sqlite3_open16(VALUE self, VALUE file)
{
Expand Down Expand Up @@ -997,6 +1101,13 @@ init_sqlite3_database(void)
#endif
#endif

#ifdef HAVE_SQLITE3_SERIALIZE
rb_define_method(cSqlite3Database, "serialize", serialize, -1);
#endif

#ifdef HAVE_SQLITE3_DESERIALIZE
rb_define_method(cSqlite3Database, "deserialize", deserialize, -1);
#endif

rb_sqlite3_aggregator_init();
}
Expand Down
2 changes: 2 additions & 0 deletions ext/sqlite3/extconf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ def configure_extension
have_func("sqlite3_prepare_v2")
have_func("sqlite3_db_name", "sqlite3.h") # v3.39.0
have_func("sqlite3_error_offset", "sqlite3.h") # v3.38.0
have_func("sqlite3_serialize", "sqlite3.h") # v3.23.0
have_func("sqlite3_deserialize", "sqlite3.h") # v3.23.0

have_type("sqlite3_int64", "sqlite3.h")
have_type("sqlite3_uint64", "sqlite3.h")
Expand Down
112 changes: 112 additions & 0 deletions test/test_database.rb
Original file line number Diff line number Diff line change
Expand Up @@ -869,5 +869,117 @@ def test_dbstat_table_exists

assert_nothing_raised { @db.execute("select * from dbstat") }
end

def test_serialize
skip("serialize not supported") unless @db.respond_to?(:serialize)

@db.execute("create table foo (a integer, b text)")
@db.execute("insert into foo (a, b) values (1, 'hello')")
@db.execute("insert into foo (a, b) values (2, 'world')")

data = @db.serialize
assert_instance_of(String, data)
assert_equal(Encoding::ASCII_8BIT, data.encoding)
refute_empty(data)
assert_equal("SQLite format 3\0", data[0, 16])
end

def test_serialize_with_database_name
skip("serialize not supported") unless @db.respond_to?(:serialize)

@db.execute("create table foo (a integer)")
@db.execute("insert into foo values (42)")

data = @db.serialize("main")
assert_instance_of(String, data)
assert_equal("SQLite format 3\0", data[0, 16])
end

def test_serialize_closed_database
skip("serialize not supported") unless @db.respond_to?(:serialize)

@db.close
assert_raise(SQLite3::Exception) { @db.serialize }
end

def test_deserialize
skip("deserialize not supported") unless @db.respond_to?(:deserialize)

# Create a source database with data
source = SQLite3::Database.new(":memory:")
source.execute("create table foo (a integer, b text)")
source.execute("insert into foo values (1, 'hello')")
source.execute("insert into foo values (2, 'world')")

# Serialize and deserialize into target
data = source.serialize
@db.deserialize(data)

# Verify the data was transferred
rows = @db.execute("select * from foo order by a")
assert_equal [[1, "hello"], [2, "world"]], rows
ensure
source&.close
end

def test_deserialize_round_trip
skip("serialize/deserialize not supported") unless @db.respond_to?(:serialize) && @db.respond_to?(:deserialize)

# Create and populate a database
@db.execute("create table users (id integer primary key, name text, score real)")
@db.execute("insert into users values (1, 'Alice', 95.5)")
@db.execute("insert into users values (2, 'Bob', 87.3)")
@db.execute("insert into users values (3, 'Charlie', 92.1)")

# Serialize
data = @db.serialize

# Create a new database and deserialize into it
db2 = SQLite3::Database.new(":memory:")
db2.deserialize(data)

# Verify the data matches
original_rows = @db.execute("select * from users order by id")
restored_rows = db2.execute("select * from users order by id")
assert_equal original_rows, restored_rows

# Verify we can still modify the restored database
db2.execute("insert into users values (4, 'Diana', 88.8)")
assert_equal 4, db2.execute("select count(*) from users").first.first
ensure
db2&.close
end

def test_deserialize_with_database_name
skip("deserialize not supported") unless @db.respond_to?(:deserialize)

# Create a source database
source = SQLite3::Database.new(":memory:")
source.execute("create table items (x integer)")
source.execute("insert into items values (42)")
data = source.serialize

# Deserialize into main
@db.deserialize(data, "main")
assert_equal [[42]], @db.execute("select x from items")
ensure
source&.close
end

def test_deserialize_closed_database
skip("deserialize not supported") unless @db.respond_to?(:deserialize)

data = "SQLite format 3\0" + ("\0" * 84) # minimal header
@db.close
assert_raise(SQLite3::Exception) { @db.deserialize(data) }
end

def test_deserialize_invalid_data
skip("deserialize not supported") unless @db.respond_to?(:deserialize)

# Deserializing invalid data should raise an error when accessed
@db.deserialize("not a valid database")
assert_raise(SQLite3::Exception) { @db.execute("select 1") }
end
end
end