mirror: add helper to find unused table name (#895)
Summary: Per <https://github.com/sourcecred/sourcecred/pull/894#discussion_r220406780>. Test Plan: Unit tests included; run `yarn unit`. wchargin-branch: mirror-unused-table-name
This commit is contained in:
parent
42cdfa4332
commit
90ace93f91
|
@ -1215,3 +1215,48 @@ export function _makeSingleUpdateFunction<Args: BindingDictionary>(
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a name for a new table (or index) that starts with the given
|
||||
* prefix and is not used by any current table or index.
|
||||
*
|
||||
* This function does not actually create any tables. Consider including
|
||||
* it in a transaction that subsequently creates the table.
|
||||
*
|
||||
* The provided prefix must be a SQL-safe string, or an error will be
|
||||
* thrown.
|
||||
*
|
||||
* The result will be a SQL-safe string, and will not need to be quoted
|
||||
* unless the provided prefix does.
|
||||
*
|
||||
* See: `isSqlSafe`.
|
||||
*/
|
||||
export function _nontransactionallyFindUnusedTableName(
|
||||
db: Database,
|
||||
prefix: string
|
||||
) {
|
||||
if (!isSqlSafe(prefix)) {
|
||||
throw new Error("Unsafe table name prefix: " + JSON.stringify(prefix));
|
||||
}
|
||||
const result: string = db
|
||||
.prepare(
|
||||
dedent`\
|
||||
SELECT :prefix || (IFNULL(MAX(CAST(suffix AS INTEGER)), 0) + 1)
|
||||
FROM (
|
||||
SELECT SUBSTR(name, LENGTH(:prefix) + 1) AS suffix
|
||||
FROM sqlite_master
|
||||
WHERE SUBSTR(name, 1, LENGTH(:prefix)) = :prefix
|
||||
)
|
||||
`
|
||||
)
|
||||
.pluck()
|
||||
.get({prefix});
|
||||
// istanbul ignore if: should not be possible---it only has the
|
||||
// prefix (which is safe as defined above) and a trailing integer.
|
||||
if (!isSqlSafe(result)) {
|
||||
throw new Error(
|
||||
"Invariant violation: unsafe table name: " + JSON.stringify(result)
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
_buildSchemaInfo,
|
||||
_inTransaction,
|
||||
_makeSingleUpdateFunction,
|
||||
_nontransactionallyFindUnusedTableName,
|
||||
Mirror,
|
||||
} from "./mirror";
|
||||
|
||||
|
@ -1598,4 +1599,73 @@ describe("graphql/mirror", () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("_nontransactionallyFindUnusedTableName", () => {
|
||||
it("throws if the name is not SQL-safe", () => {
|
||||
const db = new Database(":memory:");
|
||||
expect(() => {
|
||||
_nontransactionallyFindUnusedTableName(db, "w a t");
|
||||
}).toThrow('Unsafe table name prefix: "w a t"');
|
||||
});
|
||||
it("does not actually create any tables or indices", () => {
|
||||
const db = new Database(":memory:");
|
||||
db.prepare("CREATE TABLE tab (col)").run();
|
||||
db.prepare("CREATE INDEX idx ON tab (col)").run();
|
||||
const getMaster = db.prepare(
|
||||
dedent`
|
||||
SELECT
|
||||
type, name, tbl_name, rootpage, sql
|
||||
FROM sqlite_master
|
||||
ORDER BY
|
||||
type, name, tbl_name, rootpage, sql
|
||||
`
|
||||
);
|
||||
const pre = getMaster.all();
|
||||
expect(pre).toHaveLength(2); // one table, one index
|
||||
_nontransactionallyFindUnusedTableName(db, "hello");
|
||||
const post = getMaster.all();
|
||||
expect(post).toEqual(pre);
|
||||
});
|
||||
it("behaves when there are no conflicts", () => {
|
||||
const db = new Database(":memory:");
|
||||
db.prepare("CREATE TABLE three (col)").run();
|
||||
expect(_nontransactionallyFindUnusedTableName(db, "two_")).toEqual(
|
||||
"two_1"
|
||||
);
|
||||
});
|
||||
it("behaves when there are table-name conflicts", () => {
|
||||
const db = new Database(":memory:");
|
||||
db.prepare("CREATE TABLE two_1 (col)").run();
|
||||
expect(_nontransactionallyFindUnusedTableName(db, "two_")).toEqual(
|
||||
"two_2"
|
||||
);
|
||||
});
|
||||
it("behaves when there are index-name conflicts", () => {
|
||||
const db = new Database(":memory:");
|
||||
db.prepare("CREATE TABLE tab (col)").run();
|
||||
db.prepare("CREATE INDEX idx_1 ON tab (col)").run();
|
||||
expect(_nontransactionallyFindUnusedTableName(db, "idx_")).toEqual(
|
||||
"idx_2"
|
||||
);
|
||||
});
|
||||
it("behaves when there are discontinuities", () => {
|
||||
const db = new Database(":memory:");
|
||||
db.prepare("CREATE TABLE two_1 (col)").run();
|
||||
db.prepare("CREATE TABLE two_3 (col)").run();
|
||||
expect(_nontransactionallyFindUnusedTableName(db, "two_")).toEqual(
|
||||
// It would also be fine for this to return `two_2`.
|
||||
"two_4"
|
||||
);
|
||||
});
|
||||
it("behaves in the face of lexicographical discontinuities", () => {
|
||||
const db = new Database(":memory:");
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
db.prepare(`CREATE TABLE two_${i} (col)`).run();
|
||||
}
|
||||
expect(_nontransactionallyFindUnusedTableName(db, "two_")).toEqual(
|
||||
"two_11"
|
||||
);
|
||||
});
|
||||
//
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue