diff --git a/CHANGELOG.md b/CHANGELOG.md index 59422ec7..1cde8d66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +X.Y.Z Release notes +============================================================= +### Breaking changes +* None + +### Enhancements +* Added `shouldCompactOnLaunch` to configuration (#507). +* Added `Realm.compact()` for manually compacting Realm files. + +### Bug fixes +* None + + 1.10.3 Release notes (2017-8-16) ============================================================= ### Breaking changes diff --git a/docs/realm.js b/docs/realm.js index 0d7b5fca..3f5fbd99 100644 --- a/docs/realm.js +++ b/docs/realm.js @@ -182,6 +182,24 @@ class Realm { * @param {function()} callback */ write(callback) {} + + /** + * Replaces all string columns in this Realm with a string enumeration column and compacts the + * database file. + * + * Cannot be called from a write transaction. + * + * Compaction will not occur if other `Realm` instances exist. + * + * While compaction is in progress, attempts by other threads or processes to open the database will + * wait. + * + * Be warned that resource requirements for compaction is proportional to the amount of live data in + * the database. Compaction works by writing the database contents to a temporary database file and + * then replacing the database with the temporary one. + * @returns {true} if compaction succeeds. + */ + compact() {} } /** @@ -213,6 +231,13 @@ Realm.defaultPath; * This function takes two arguments: * - `oldRealm` - The Realm before migration is performed. * - `newRealm` - The Realm that uses the latest `schema`, which should be modified as necessary. + * @property {callback(number, number)} [shouldCompactOnLaunch] - The function called when opening + * a Realm for the first time during the life of a process to determine if it should be compacted + * before being returned to the user. The function takes two arguments: + * - `totalSize` - The total file size (data + free space) + * - `unusedSize` - The total bytes used by data in the file. + * It returns `true` to indicate that an attempt to compact the file should be made. The compaction + * will be skipped if another process is accessing it. * @property {string} [path={@link Realm.defaultPath}] - The path to the file where the * Realm database should be stored. * @property {boolean} [readOnly=false] - Specifies if this Realm should be opened as read-only. diff --git a/lib/browser/index.js b/lib/browser/index.js index 81b2a804..8f6ef684 100644 --- a/lib/browser/index.js +++ b/lib/browser/index.js @@ -132,6 +132,7 @@ util.createMethods(Realm.prototype, objectTypes.REALM, [ 'delete', 'deleteAll', 'write', + 'compact', ], true); const Sync = { diff --git a/lib/index.d.ts b/lib/index.d.ts index 196b83d5..663fe621 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -77,6 +77,7 @@ declare namespace Realm { interface Configuration { encryptionKey?: ArrayBuffer | ArrayBufferView | Int8Array; migration?: (oldRealm: Realm, newRealm: Realm) => void; + shouldCompactOnLaunch?: (totalBytes: number, usedBytes: number) => boolean; path?: string; readOnly?: boolean; schema?: ObjectClass[] | ObjectSchema[]; @@ -452,6 +453,11 @@ declare class Realm { * @returns void */ write(callback: () => void): void; + + /** + * @returns boolean + */ + compact(): boolean; } declare module 'realm' { diff --git a/src/js_realm.hpp b/src/js_realm.hpp index 2d9ac368..a54dfb7e 100644 --- a/src/js_realm.hpp +++ b/src/js_realm.hpp @@ -176,6 +176,8 @@ public: static void remove_listener(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); static void remove_all_listeners(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); static void close(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); + static void compact(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); + // properties static void get_empty(ContextType, ObjectType, ReturnValue &); @@ -223,6 +225,7 @@ public: {"removeListener", wrap}, {"removeAllListeners", wrap}, {"close", wrap}, + {"compact", wrap}, }; PropertyMap const properties = { @@ -390,6 +393,28 @@ void RealmClass::constructor(ContextType ctx, ObjectType this_object, size_t config.schema_version = 0; } + static const String compact_on_launch_string = "shouldCompactOnLaunch"; + ValueType compact_value = Object::get_property(ctx, object, compact_on_launch_string); + if (!Value::is_undefined(ctx, compact_value)) { + if (config.schema_mode == SchemaMode::ReadOnly) { + throw std::invalid_argument("Cannot set 'shouldCompactOnLaunch' when 'readOnly' is set."); + } + if (config.sync_config) { + throw std::invalid_argument("Cannot set 'shouldCompactOnLaunch' when 'sync' is set."); + } + + FunctionType should_compact_on_launch_function = Value::validated_to_function(ctx, compact_value, "shouldCompactOnLaunch"); + config.should_compact_on_launch_function = [=](uint64_t total_bytes, uint64_t unused_bytes) { + ValueType arguments[2] = { + Value::from_number(ctx, total_bytes), + Value::from_number(ctx, unused_bytes) + }; + + ValueType should_compact = Function::callback(ctx, should_compact_on_launch_function, this_object, 2, arguments); + return Value::to_boolean(ctx, should_compact); + }; + } + static const String migration_string = "migration"; ValueType migration_value = Object::get_property(ctx, object, migration_string); if (!Value::is_undefined(ctx, migration_value)) { @@ -836,5 +861,17 @@ void RealmClass::close(ContextType ctx, FunctionType, ObjectType this_object, realm->close(); } +template +void RealmClass::compact(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { + validate_argument_count(argc, 0); + + SharedRealm realm = *get_internal>(this_object); + if (realm->is_in_transaction()) { + throw std::runtime_error("Cannot compact a Realm within a transaction."); + } + + return_value.set(realm->compact()); +} + } // js } // realm diff --git a/tests/js/realm-tests.js b/tests/js/realm-tests.js index a2c335e0..67749dbd 100644 --- a/tests/js/realm-tests.js +++ b/tests/js/realm-tests.js @@ -943,5 +943,61 @@ module.exports = { realm.write(() => realm.delete(realm.objects('PersonObject'))); TestCase.assertTrue(realm.empty); + }, + + testCompact: function() { + var wasCalled = false; + const count = 1000; + // create compactable Realm + const realm1 = new Realm({schema: [schemas.StringOnly]}); + realm1.write(() => { + realm1.create('StringOnlyObject', { stringCol: 'A' }); + for (var i = 0; i < count; i++) { + realm1.create('StringOnlyObject', { stringCol: 'ABCDEFG' }); + } + realm1.create('StringOnlyObject', { stringCol: 'B' }); + }); + realm1.close(); + + // open Realm and see if it is compacted + var shouldCompact = function(totalBytes, usedBytes) { + wasCalled = true; + const fiveHundredKB = 500*1024; + return (totalBytes > fiveHundredKB) && (usedBytes / totalBytes) < 0.2; + }; + const realm2 = new Realm({schema: [schemas.StringOnly], shouldCompactOnLaunch: shouldCompact}); + TestCase.assertTrue(wasCalled); + TestCase.assertEqual(realm2.objects('StringOnlyObject').length, count + 2); + // we don't check if the file is smaller as we assume that Object Store does it + realm2.close(); + }, + + testManualCompact: function() { + const realm1 = new Realm({schema: [schemas.StringOnly]}); + realm1.write(() => { + realm1.create('StringOnlyObject', { stringCol: 'A' }); + }); + TestCase.assertTrue(realm1.compact()); + realm1.close(); + + const realm2 = new Realm({schema: [schemas.StringOnly]}); + TestCase.assertEqual(realm2.objects('StringOnlyObject').length, 1); + realm2.close(); + }, + + testManualCompactInWrite: function() { + const realm = new Realm({schema: [schemas.StringOnly]}); + realm.write(() => { + TestCase.assertThrows(() => { + realm.compact(); + }); + }); + TestCase.assertTrue(realm.empty); + }, + + testManualCompactMultipleInstances: function() { + const realm1 = new Realm({schema: [schemas.StringOnly]}); + const realm2 = new Realm({schema: [schemas.StringOnly]}); + TestCase.assertThrows(realm1.compact()); } }; diff --git a/tests/js/schemas.js b/tests/js/schemas.js index 214dfd5a..7c20f855 100644 --- a/tests/js/schemas.js +++ b/tests/js/schemas.js @@ -126,6 +126,13 @@ exports.StringPrimary = { } }; +exports.StringOnly = { + name: 'StringOnlyObject', + properties: { + stringCol: 'string', + } +}; + exports.AllTypes = { name: 'AllTypesObject', primaryKey: 'primaryCol',