syncbase: new naming scheme
see v.io/c/15923 for full description
MultiPart: 3/8
Change-Id: I982933cdd8ad89c9cf9b91971c3b3cdf90ae03e6
diff --git a/src/app.js b/src/app.js
index ed9a1db..3fdc0f2 100644
--- a/src/app.js
+++ b/src/app.js
@@ -27,7 +27,9 @@
return new App(parentFullName, relativeName);
}
- util.addNameProperties(this, parentFullName, relativeName, false);
+ var fullName = vanadium.naming.join(
+ parentFullName, util.escape(relativeName));
+ util.addNameProperties(this, parentFullName, relativeName, fullName);
// TODO(nlacasse): Use the prr module to simplify all the
// 'Object.defineProperty' calls scattered throughout the project.
@@ -48,12 +50,12 @@
// must not contain slashes. schema can be null or undefined only if a schema
// was never set for the database in the first place.
App.prototype.noSqlDatabase = function(relativeName, schema) {
- return new Database(this.fullName, relativeName, schema);
+ return new Database(this.fullName, relativeName, '', schema);
};
// listDatabases returns of all database names.
App.prototype.listDatabases = function(ctx, cb) {
- this._wire(ctx).listDatabases(ctx, cb);
+ util.listChildren(ctx, this.fullName, cb);
};
// create creates this app. If perms is empty, we inherit (copy) the Service
diff --git a/src/nosql/database.js b/src/nosql/database.js
index 34ca05b..aef4454 100644
--- a/src/nosql/database.js
+++ b/src/nosql/database.js
@@ -36,12 +36,18 @@
* @inner
* @memberof {module:syncbase.nosql}
*/
-function Database(parentFullName, relativeName, schema) {
+function Database(parentFullName, relativeName, batchSuffix, schema) {
if (!(this instanceof Database)) {
return new Database(parentFullName, relativeName);
}
- util.addNameProperties(this, parentFullName, relativeName, true);
+ // Escape relativeName so that any forward slashes get dropped, thus ensuring
+ // that the server will interpret fullName as referring to a database object.
+ // Note that the server will still reject this name if util.ValidDatabaseName
+ // returns false.
+ var fullName = vanadium.naming.join(
+ parentFullName, util.escape(relativeName) + batchSuffix);
+ util.addNameProperties(this, parentFullName, relativeName, fullName);
Object.defineProperty(this, 'schema', {
enumerable: false,
@@ -179,7 +185,7 @@
* @param {function} cb Callback.
*/
Database.prototype.listTables = function(ctx, cb) {
- this._wire(ctx).listTables(ctx, cb);
+ util.listChildren(ctx, this.fullName, cb);
};
/**
@@ -217,7 +223,7 @@
*/
Database.prototype.watch = function(ctx, tableName, prefix, resumeMarker, cb) {
var globReq = new watchVdl.GlobRequest({
- pattern: vanadium.naming.join(tableName, util.NAME_SEP, prefix + '*'),
+ pattern: vanadium.naming.join(tableName, prefix + '*'),
resumeMarker: resumeMarker
});
@@ -236,11 +242,8 @@
return cb(new Error('invalid change state ' + change.state));
}
- // NOTE(sadovsky): We call stripBasename twice to convert "<table>/$/<row>"
- // to "<table>".
var wc = new watch.WatchChange({
- tableName: vanadium.naming.stripBasename(
- vanadium.naming.stripBasename(change.name)),
+ tableName: vanadium.naming.stripBasename(change.name),
rowName: vanadium.naming.basename(change.name),
changeType: changeType,
valueBytes: changeType === 'put' ? change.value.value : null,
@@ -253,11 +256,10 @@
var stream = this._wire(ctx).watchGlob(ctx, globReq, cb).stream;
- // TODO(sadovsky): Our JS watch test times out after 20s when pattern is
- // "<table>/<prefix>*", i.e. when it's missing the "/$/" separator. That's
- // strange, because the server should immediately return an RPC error (since
- // util.ParseTableRowPair returns an error). Does the JS watch test not check
- // for this error?
+ // TODO(sadovsky): Our JS watch test times out after 20s when globReq is
+ // invalid. That's strange, because the server should immediately return an
+ // RPC error (since util.ParseTableRowPair returns an error). Does the JS
+ // watch test not check for this error?
var watchChangeStream = stream.pipe(watchChangeEncoder);
stream.on('error', function(err) {
watchChangeStream.emit('error', err);
@@ -324,16 +326,12 @@
Database.prototype.beginBatch = function(ctx, opts, cb) {
var self = this;
this._wire(ctx).beginBatch(ctx, this.schemaVersion, opts,
- function(err, relativeName) {
+ function(err, batchSuffix) {
if (err) {
return cb(err);
}
-
- // The relativeName returned from the beginBatch() call above is different
- // than the relativeName of the current database. We must create a new
- // Database with this new relativeName, and then create a BatchDatabase
- // from that new Database.
- var db = new Database(self._parentFullName, relativeName);
+ var db = new Database(self._parentFullName, self.name, batchSuffix,
+ self.schema);
return cb(null, new BatchDatabase(db));
});
};
diff --git a/src/nosql/row.js b/src/nosql/row.js
index ae6f31a..c2702ec 100644
--- a/src/nosql/row.js
+++ b/src/nosql/row.js
@@ -25,7 +25,10 @@
return new Row(parentFullName, key, schemaVersion);
}
- util.addNameProperties(this, parentFullName, key, true);
+ // Note, we immediately unescape row keys on the server side. See comment in
+ // server/nosql/dispatcher.go for explanation.
+ var fullName = vanadium.naming.join(parentFullName, util.escape(key));
+ util.addNameProperties(this, parentFullName, key, fullName);
this.schemaVersion = schemaVersion;
diff --git a/src/nosql/table.js b/src/nosql/table.js
index ed5a8de..ea6190c 100644
--- a/src/nosql/table.js
+++ b/src/nosql/table.js
@@ -30,7 +30,13 @@
return new Table(parentFullName, relativeName, schemaVersion);
}
- util.addNameProperties(this, parentFullName, relativeName, true);
+ // Escape relativeName so that any forward slashes get dropped, thus ensuring
+ // that the server will interpret fullName as referring to a table object.
+ // Note that the server will still reject this name if util.ValidTableName
+ // returns false.
+ var fullName = vanadium.naming.join(
+ parentFullName, util.escape(relativeName));
+ util.addNameProperties(this, parentFullName, relativeName, fullName);
this.schemaVersion = schemaVersion;
diff --git a/src/service.js b/src/service.js
index 72f7088..48c9469 100644
--- a/src/service.js
+++ b/src/service.js
@@ -5,6 +5,7 @@
var vanadium = require('vanadium');
var App = require('./app');
+var util = require('./util');
var vdl = require('./gen-vdl/v.io/v23/services/syncbase');
// TODO(aghassemi): This looks clunky,
@@ -55,7 +56,7 @@
// listApps returns a list of all app names.
Service.prototype.listApps = function(ctx, cb) {
- this._wire(ctx).listApps(ctx, cb);
+ util.listChildren(ctx, this.fullName, cb);
};
Service.prototype.getPermissions = function(ctx, cb) {
diff --git a/src/util.js b/src/util.js
index 06e26c6..0a1e52b 100644
--- a/src/util.js
+++ b/src/util.js
@@ -4,27 +4,21 @@
var vanadium = require('vanadium');
-var NAME_SEP = '$';
-
module.exports = {
addNameProperties: addNameProperties,
+ listChildren: listChildren,
prefixRangeLimit: prefixRangeLimit,
stringToUTF8Bytes: stringToUTF8Bytes,
- NAME_SEP: NAME_SEP
+ escape: escape,
+ unescape: unescape
};
/**
- * Creates the 'name' and 'fullName' properties on an object.
+ * Creates public 'name' and 'fullName' properties on an object, as well as a
+ * private '_parentFullName' property.
* @private
*/
-function addNameProperties(self, parentFullName, relativeName, addNameSep) {
- var fullName;
- if (addNameSep) {
- fullName = vanadium.naming.join(parentFullName, NAME_SEP, relativeName);
- } else {
- fullName = vanadium.naming.join(parentFullName, relativeName);
- }
-
+function addNameProperties(self, parentFullName, name, fullName) {
/**
* @property _parentFullName
* @private
@@ -41,7 +35,7 @@
* @type {string}
*/
Object.defineProperty(self, 'name', {
- value: relativeName,
+ value: name,
writable: false,
enumerable: true
});
@@ -58,6 +52,42 @@
}
/**
+ * listChildren returns the relative names of all children of parentFullName.
+ * @private
+ */
+function listChildren(ctx, parentFullName, cb) {
+ var rt = vanadium.runtimeForContext(ctx);
+ var globPattern = vanadium.naming.join(parentFullName, '*');
+
+ var childNames = [];
+ var streamErr = null;
+ var stream = rt.getNamespace().glob(ctx, globPattern, function(err) {
+ if (err) {
+ return cb(err);
+ }
+ if (streamErr) {
+ return cb(streamErr);
+ }
+ cb(null, childNames);
+ }).stream;
+
+ stream.on('data', function(globResult) {
+ var fullName = globResult.name;
+ var escName = vanadium.naming.basename(fullName);
+ // Component names within object names are always escaped. See comment in
+ // server/nosql/dispatcher.go for explanation.
+ // If unescape throws an exception, there's a bug in the Syncbase server.
+ // Glob should return names with escaped components.
+ childNames.push(unescape(escName));
+ });
+
+ stream.on('error', function(err) {
+ console.error('Stream error: ' + JSON.stringify(err));
+ streamErr = streamErr || err.error;
+ });
+}
+
+/**
* prefixRangeLimit modifies the input bytes to be the limit of the row range
* for the given prefix.
* TODO(sadovsky): Why do we modify the input bytes, rather than returning a new
@@ -97,3 +127,27 @@
return bytes;
}
+
+/**
+ * escape escapes a component name for use in a Syncbase object name. In
+ * particular, it replaces bytes "%" and "/" with the "%" character followed by
+ * the byte's two-digit hex code. Clients using the client library need not
+ * escape names themselves; the client library does so on their behalf.
+ * @param {string} s String to escape.
+ * @return {string} Escaped string.
+ */
+function escape(s) {
+ return s
+ .replace(/%/g, '%25')
+ .replace(/\//g, '%2F');
+}
+
+/**
+ * unescape applies the inverse of escape. Throws exception if the given string
+ * is not a valid escaped string.
+ * @param {string} s String to unescape.
+ * @return {string} Unescaped string.
+ */
+function unescape(s) {
+ return decodeURIComponent(s);
+}
diff --git a/test/integration/test-database.js b/test/integration/test-database.js
index d9258e4..262d453 100644
--- a/test/integration/test-database.js
+++ b/test/integration/test-database.js
@@ -42,7 +42,7 @@
db.name = 'foo';
t.equal(db.name, dbName, 'Setting the name has no effect.');
- var expectedFullName = naming.join(o.app.fullName, '$', dbName);
+ var expectedFullName = naming.join(o.app.fullName, dbName);
t.equal(db.fullName, expectedFullName, 'Database has correct fullName.');
db.fullName = 'bar';
@@ -76,21 +76,6 @@
});
});
-test('app.noSqlDatabase with slashes in the name', function(t) {
- setupApp(t, function(err, o) {
- if (err) {
- return t.end(err);
- }
-
- var dbName = 'bad/name';
- t.doesNotThrow(function() {
- o.app.noSqlDatabase(dbName);
- }, 'should throw');
-
- o.teardown(t.end);
- });
-});
-
test('db.create() creates a database', function(t) {
setupApp(t, function(err, o) {
if (err) {
@@ -238,7 +223,7 @@
t.ok(table, 'table is created.');
t.ok(table instanceof Table, 'table is a Table object.');
t.equal(table.name, tableName, 'table has the correct name.');
- t.equal(table.fullName, vanadium.naming.join(db.fullName, '$', tableName),
+ t.equal(table.fullName, vanadium.naming.join(db.fullName, tableName),
'table has the correct fullName.');
o.teardown(t.end);