mirror: remove legacy non-EAV `extract` (#1346)

Test Plan:
Existing tests suffice, retaining full coverage.

wchargin-branch: mirror-eav-prune-extract
This commit is contained in:
William Chargin 2019-10-19 17:58:17 -07:00 committed by GitHub
parent f577ae7c1e
commit 003efdffa7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 34 additions and 166 deletions

View File

@ -145,8 +145,8 @@ export class Mirror {
* *
* NOTE: A previous version of the schema used a separate * NOTE: A previous version of the schema used a separate
* primitives table for each GraphQL object type. These * primitives table for each GraphQL object type. These
* `primitives_*` tables are still written, but are no longer read * `primitives_*` tables are still written, but are no longer
* by default. They will be removed entirely in a future change. * read. They will be removed entirely in a future change.
* *
* We refer to node and primitive data together as "own data", because * We refer to node and primitive data together as "own data", because
* this is the data that can be queried uniformly for all elements of * this is the data that can be queried uniformly for all elements of
@ -1640,14 +1640,7 @@ export class Mirror {
* may be removed or changed at any time. For information about its * may be removed or changed at any time. For information about its
* semantics, read the current source code. * semantics, read the current source code.
*/ */
extract( extract(rootId: Schema.ObjectId): mixed {
rootId: Schema.ObjectId,
options?: {|+useEavPrimitives?: boolean|}
): mixed {
const fullOptions = {
...{useEavPrimitives: true},
...(options || {}),
};
const db = this._db; const db = this._db;
return _inTransaction(db, () => { return _inTransaction(db, () => {
// We'll compute the transitive dependencies and store them into a // We'll compute the transitive dependencies and store them into a
@ -1688,10 +1681,6 @@ export class Mirror {
ON objects.id = transitive_dependencies.id ON objects.id = transitive_dependencies.id
` `
).run({rootId}); ).run({rootId});
const typenames: $ReadOnlyArray<Schema.Typename> = db
.prepare(`SELECT DISTINCT typename FROM ${temporaryTableName}`)
.pluck()
.all();
// Check to make sure all required objects and connections have // Check to make sure all required objects and connections have
// been updated at least once. // been updated at least once.
@ -1736,12 +1725,12 @@ export class Mirror {
// Constructing the result set inherently requires mutation, // Constructing the result set inherently requires mutation,
// because the object graph can have cycles. We start by // because the object graph can have cycles. We start by
// creating a record for each object, with just that object's // creating a record for each object, with just that object's
// typename and ID (and, in legacy non-EAV mode, all primitive // typename and ID. Then, we link in primitives, node
// data). Then, we link in primitives (except in legacy non-EAV // references, and connection entries.
// mode), node references, and connection entries.
const allObjects: Map<Schema.ObjectId, Object> = new Map(); const allObjects: Map<Schema.ObjectId, Object> = new Map();
if (fullOptions.useEavPrimitives) {
// Initialize `allObjects`. // Initialize `allObjects`.
{
const getObjects = db.prepare( const getObjects = db.prepare(
`SELECT id AS id, typename AS typename FROM ${temporaryTableName}` `SELECT id AS id, typename AS typename FROM ${temporaryTableName}`
); );
@ -1751,8 +1740,10 @@ export class Mirror {
__typename: object.typename, __typename: object.typename,
}); });
} }
}
// Fill in primitive data. // Fill in primitive data.
{
const getPrimitives = db.prepare( const getPrimitives = db.prepare(
dedent`\ dedent`\
SELECT SELECT
@ -1801,99 +1792,6 @@ export class Mirror {
} }
} }
} }
} else {
// Legacy non-EAV mode.
for (const typename of typenames) {
const objectType = this._schemaInfo.objectTypes[typename];
// istanbul ignore if: should not be possible using the
// publicly accessible APIs
if (objectType == null) {
throw new Error(
`Corruption: unknown object type ${JSON.stringify(typename)}`
);
}
const primitivesTableName = _primitivesTableName(typename);
const selections: $ReadOnlyArray<string> = [].concat(
[`${primitivesTableName}.id AS id`],
objectType.primitiveFieldNames.map(
(fieldname) =>
`${primitivesTableName}."${fieldname}" AS "${fieldname}"`
),
objectType.nestedFieldNames.map(
(fieldname) =>
`${primitivesTableName}."${fieldname}" AS "${fieldname}"`
),
...objectType.nestedFieldNames.map((f1) =>
Object.keys(objectType.nestedFields[f1].primitives).map(
(f2) =>
`${primitivesTableName}."${f1}.${f2}" AS "${f1}.${f2}"`
)
)
);
const rows: $ReadOnlyArray<{|
+id: Schema.ObjectId,
+[Schema.Fieldname]: string | 0 | 1,
|}> = db
.prepare(
dedent`\
SELECT ${selections.join(", ")}
FROM ${temporaryTableName} JOIN ${primitivesTableName}
USING (id)
`
)
.all();
for (const row of rows) {
const object = {};
object.id = row.id;
object.__typename = typename;
for (const fieldname of objectType.nestedFieldNames) {
const isPresent: string | 0 | 1 = row[fieldname];
// istanbul ignore if: should not be reachable
if (isPresent !== 0 && isPresent !== 1) {
const s = JSON.stringify;
const id = object.id;
throw new Error(
`Corruption: nested field ${s(fieldname)} on ${s(id)} ` +
`set to ${String(isPresent)}`
);
}
if (isPresent) {
// We'll add primitives and links onto this object.
object[fieldname] = {};
} else {
object[fieldname] = null;
}
}
for (const key of Object.keys(row)) {
if (key === "id") continue;
const rawValue = row[key];
if (rawValue === 0 || rawValue === 1) {
// Name of a nested field; already processed.
continue;
}
const value = JSON.parse(rawValue);
const parts = key.split(".");
switch (parts.length) {
case 1:
object[key] = value;
break;
case 2: {
const [nestName, eggName] = parts;
if (object[nestName] !== null) {
object[nestName][eggName] = value;
}
break;
}
// istanbul ignore next: should not be possible
default:
throw new Error(
`Corruption: bad field name: ${JSON.stringify(key)}`
);
}
}
allObjects.set(object.id, object);
}
}
} }
// Add links. // Add links.

View File

@ -3416,10 +3416,10 @@ describe("graphql/mirror", () => {
} }
describe("extract", () => { describe("extract", () => {
// We'll run under both legacy and EAV modes. In each case, hide // Test in EAV mode, hiding the tables corresponding to the legacy
// the tables corresponding to the other mode to catch any // mode to catch any accidental reads. (We hide and unhide rather
// accidental reads. (We hide and unhide rather than deleting // than deleting because some test cases call `extract` multiple
// because some test cases call `extract` multiple times.) // times.)
function hiddenName(name) { function hiddenName(name) {
return `${name}_DO_NOT_READ`; return `${name}_DO_NOT_READ`;
} }
@ -3430,57 +3430,27 @@ describe("graphql/mirror", () => {
db.prepare(`ALTER TABLE ${hiddenName(name)} RENAME TO ${name}`).run(); db.prepare(`ALTER TABLE ${hiddenName(name)} RENAME TO ${name}`).run();
} }
describe("with legacy type-specific primitives", () => { testExtract((mirror, id) => {
testExtract((mirror, id) => { const legacyTables = mirror._db
hideTable(mirror._db, "primitives"); .prepare(
try { "SELECT name FROM sqlite_master " +
return mirror.extract(id, {useEavPrimitives: false}); "WHERE type = 'table' AND name LIKE 'primitives_%'"
} finally { )
unhideTable(mirror._db, "primitives"); .pluck()
} .all();
}); if (legacyTables.length === 0) {
}); throw new Error("Found no type-specific primitives tables?");
}
describe("with EAV primitives", () => { for (const table of legacyTables) {
testExtract((mirror, id) => { hideTable(mirror._db, table);
const legacyTables = mirror._db }
.prepare( try {
"SELECT name FROM sqlite_master " + return mirror.extract(id);
"WHERE type = 'table' AND name LIKE 'primitives_%'" } finally {
)
.pluck()
.all();
if (legacyTables.length === 0) {
throw new Error("Found no type-specific primitives tables?");
}
for (const table of legacyTables) { for (const table of legacyTables) {
hideTable(mirror._db, table); unhideTable(mirror._db, table);
} }
try { }
return mirror.extract(id, {useEavPrimitives: true});
} finally {
for (const table of legacyTables) {
unhideTable(mirror._db, table);
}
}
});
});
it("works with default options", () => {
// Simple sanity check.
const db = new Database(":memory:");
const schema = buildGithubSchema();
const mirror = new Mirror(db, schema);
mirror.registerObject({typename: "SubscribedEvent", id: "sub"});
const update = mirror._createUpdate(new Date(123));
mirror._updateOwnData(update, [
{__typename: "SubscribedEvent", id: "sub", actor: null},
]);
expect(mirror.extract("sub")).toEqual({
__typename: "SubscribedEvent",
id: "sub",
actor: null,
});
}); });
}); });