Skip to content

Commit

Permalink
Better DDL + provider constraints
Browse files Browse the repository at this point in the history
- Respect the case of table name
- Add provider to constraint
- Removed unused line
  • Loading branch information
acrodrig committed Apr 26, 2024
1 parent 853b526 commit f3ffa00
Show file tree
Hide file tree
Showing 5 changed files with 41 additions and 41 deletions.
2 changes: 1 addition & 1 deletion resources/account.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "Account",
"name": "accounts",
"properties": {
"id": { "type": "integer", "required": true, "primaryKey": true, "comment": "Unique identifier, auto-generated. It's the primary key." },
"inserted": { "type": "date", "required": false, "dateOn": "insert", "comment": "Timestamp when current record is inserted" },
Expand Down
18 changes: 9 additions & 9 deletions src/ddl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ const _BaseSchema: DB.Schema = {

export class DDL {
static padWidth = 4;
static milliPrecision = 3;
static defaultWidth = 256;

// Enhance schema with standard properties
Expand Down Expand Up @@ -87,16 +86,15 @@ export class DDL {

const name = indice.name ?? "";
const unique = indice.unique ? "UNIQUE " : "";
return `${pad}CREATE ${unique}INDEX ${table.toLowerCase()}_${name} ON ${table} (${columns.join(",")});\n`;
return `${pad}CREATE ${unique}INDEX ${table}_${name} ON ${table} (${columns.join(",")});\n`;
}

static createFullTextIndex(dbType: string, columns: string[], padWidth = 4, table: string, name = "fulltext"): string {
const pad = "".padEnd(padWidth);

const wrapper = (columns: string[], s = ",", w = false) => columns.map((c) => w ? "COALESCE(" + c + ",'')" : c).join(s);
const tlc = table.toLowerCase();
if (dbType === DB.Provider.MYSQL) return `${pad}CREATE FULLTEXT INDEX ${tlc}_${name} ON ${table} (${wrapper(columns, ",")});\n`;
if (dbType === DB.Provider.POSTGRES) return `${pad}CREATE INDEX ${tlc}_${name} ON ${table} USING GIN (TO_TSVECTOR('english', ${wrapper(columns, "||' '||", true)}));`;
if (dbType === DB.Provider.MYSQL) return `${pad}CREATE FULLTEXT INDEX ${table}_${name} ON ${table} (${wrapper(columns, ",")});\n`;
if (dbType === DB.Provider.POSTGRES) return `${pad}CREATE INDEX ${table}_${name} ON ${table} USING GIN (TO_TSVECTOR('english', ${wrapper(columns, "||' '||", true)}));`;

return "";
}
Expand All @@ -106,7 +104,7 @@ export class DDL {
const pad = "".padEnd(padWidth);
const da = relation.delete ? " ON DELETE " + relation.delete?.toUpperCase().replace(/-/g, " ") : "";
const ua = relation.update ? " ON DELETE " + relation.update?.toUpperCase().replace(/-/g, " ") : "";
name = (parent + "_" + name).toLowerCase();
name = parent + "_" + name;
return `${pad}CONSTRAINT ${name} FOREIGN KEY (${relation.join}) REFERENCES ${relation.target} (id)${da}${ua},\n`;
}

Expand All @@ -117,7 +115,8 @@ export class DDL {
let expr = "";
if (column.maximum) expr += `${name} >= ${value(column.maximum)}`;
if (column.minimum) expr += `${name} >= ${value(column.minimum)}`;
return expr ? `${pad}${name ? "CONSTRAINT " + (parent + "_" + name).toLowerCase() + " " : ""}CHECK (${expr}),\n` : "";
name = parent + "_" + name;
return expr ? `${pad}${name ? "CONSTRAINT " + name + " " : ""}CHECK (${expr}),\n` : "";
}

// Constraint independent generator
Expand All @@ -128,7 +127,7 @@ export class DDL {
return `${pad}${name ? "CONSTRAINT " + name + " " : ""}CHECK (${expr}),\n`;
}

// Uses the most standard MySQL syntax and then it is fixed afterwards
// Uses the most standard MySQL syntax, and then it is fixed afterward
static createTable(schema: Schema, dbType: DB.Provider = DB.Provider.MYSQL, nameOverride?: string): string {
// Get name padding
const namePad = Math.max(...Object.keys(schema.properties).map((n) => n.length || 0)) + 1;
Expand All @@ -142,8 +141,9 @@ export class DDL {
const relations = !sqlite && Object.entries(schema.relations || []).map(([n, r]) => this.createRelation(dbType, schema.name, n, r!)).join("") || "";

// Create constraints
const filter = (c: Constraint) => !c.provider || c.provider === dbType;
const columnConstraints = Object.entries(schema.properties || {}).map(([n, c]) => this.createColumnConstraint(dbType, schema.name, n, c));
const independentConstraints = (schema.constraints || []).map((c) => this.createIndependentConstraint(dbType, schema.name, c));
const independentConstraints = (schema.constraints || []).filter(filter).map((c) => this.createIndependentConstraint(dbType, schema.name, c));
const constraints = !sqlite && [...columnConstraints, ...independentConstraints].join("") || "";

// Create sql
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export interface Relation {
type: "many-to-one" | "many-to-many";
}

export type Constraint = string | { name?: string; check: string; enforced?: boolean; comment?: string };
export type Constraint = { name?: string; check: string; enforced?: boolean; comment?: string; provider?: string };

export interface Schema {
name: string;
Expand Down
12 changes: 6 additions & 6 deletions test/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const DB = await dbInit(getProvider(), [AccountSchema as Schema]);

let id = -1;

const repo = await DB.getRepository(AccountModel);
const repo = DB.getRepository("accounts");

test("Default repository has 1,000 capacity", options, function () {
assertEquals(repo.capacity, DB.DEFAULT_CAPACITY);
Expand Down Expand Up @@ -52,7 +52,7 @@ test("Retrieve via another column", options, async function () {
});

test("Retrieve via SQL query", options, async function () {
const records = await DB.query(`SELECT * FROM Account WHERE name = ?`, [NAME]);
const records = await DB.query(`SELECT * FROM accounts WHERE name = ?`, [NAME]);
const accounts = records.map((r) => new AccountModel(r as unknown as AccountModel));
assertEquals(accounts.length, 1);
assertEquals(accounts[0].name, NAME);
Expand Down Expand Up @@ -106,7 +106,7 @@ test("Find by ID and update", options, async function () {
assertEquals(account.comments, comments);
});

// test("Clean", options, async function () {
// const ok = await repo.deleteById(id);
// assert(ok);
// });
test("Clean", options, async function () {
const ok = await repo.deleteById(id);
assert(ok);
});
48 changes: 24 additions & 24 deletions test/ddl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const HR = "-".repeat(80);
const DB = await dbInit(getProvider());

const SQLITE = `
CREATE TABLE IF NOT EXISTS Account (
CREATE TABLE IF NOT EXISTS accounts (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
inserted DATETIME DEFAULT CURRENT_TIMESTAMP,
updated DATETIME DEFAULT CURRENT_TIMESTAMP,
Expand All @@ -32,19 +32,19 @@ CREATE TABLE IF NOT EXISTS Account (
preferences JSON NOT NULL DEFAULT ('{"wrap":true,"minAge":18}'),
valueList JSON GENERATED ALWAYS AS (JSON_EXTRACT(preferences, '$.*')) STORED
);
CREATE INDEX account_inserted ON Account (inserted);
CREATE INDEX account_updated ON Account (updated);
CREATE INDEX account_valueList ON Account (id,(CAST(valueList AS CHAR(32))),enabled);
CREATE INDEX accounts_inserted ON accounts (inserted);
CREATE INDEX accounts_updated ON accounts (updated);
CREATE INDEX accounts_valueList ON accounts (id,(CAST(valueList AS CHAR(32))),enabled);
`.trim();

test("Table Creation SQLite", function () {
const ddl = DDL.createTable(AccountSchema as Schema, "sqlite", "Account");
const ddl = DDL.createTable(AccountSchema as Schema, "sqlite", "accounts");
if (DEBUG) console.log(`\nSQLite\n${HR}\n${ddl}\n\n`);
assertEquals(ddl.trim(), SQLITE);
});

const MYSQL = `
CREATE TABLE IF NOT EXISTS Account (
CREATE TABLE IF NOT EXISTS accounts (
id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT 'Unique identifier, auto-generated. It''s the primary key.',
inserted DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'Timestamp when current record is inserted',
updated DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Timestamp when current record is updated',
Expand All @@ -59,24 +59,24 @@ CREATE TABLE IF NOT EXISTS Account (
name VARCHAR(256) NOT NULL UNIQUE COMMENT 'Descriptive name to identify the instance',
preferences JSON NOT NULL DEFAULT ('{"wrap":true,"minAge":18}') COMMENT 'All the general options associated with the account.',
valueList JSON GENERATED ALWAYS AS (JSON_EXTRACT(preferences, '$.*')) STORED,
CONSTRAINT account_established CHECK (established >= '2020-01-01'),
CONSTRAINT account_email CHECK (email IS NULL OR email RLIKE '^[^@]+@[^@]+[.][^@]{2,}$'),
CONSTRAINT account_phone CHECK (phone IS NULL OR phone RLIKE '^[0-9]{8,16}$')
CONSTRAINT accounts_established CHECK (established >= '2020-01-01'),
CONSTRAINT accounts_email CHECK (email IS NULL OR email RLIKE '^[^@]+@[^@]+[.][^@]{2,}$'),
CONSTRAINT accounts_phone CHECK (phone IS NULL OR phone RLIKE '^[0-9]{8,16}$')
);
CREATE INDEX account_inserted ON Account (inserted);
CREATE INDEX account_updated ON Account (updated);
CREATE INDEX account_valueList ON Account (id,(CAST(valueList AS CHAR(32) ARRAY)),enabled);
CREATE FULLTEXT INDEX account_fulltext ON Account (comments,country,phone,name);
CREATE INDEX accounts_inserted ON accounts (inserted);
CREATE INDEX accounts_updated ON accounts (updated);
CREATE INDEX accounts_valueList ON accounts (id,(CAST(valueList AS CHAR(32) ARRAY)),enabled);
CREATE FULLTEXT INDEX accounts_fulltext ON accounts (comments,country,phone,name);
`.trim();

test("Table Creation MySQL", function () {
const ddl = DDL.createTable(AccountSchema as Schema, "mysql", "Account");
const ddl = DDL.createTable(AccountSchema as Schema, "mysql", "accounts");
if (DEBUG) console.log(`\nMYSQL\n${HR}\n${ddl}\n\n`);
assertEquals(ddl.trim(), MYSQL);
});

const POSTGRES = `
CREATE TABLE IF NOT EXISTS Account (
CREATE TABLE IF NOT EXISTS accounts (
id SERIAL NOT NULL PRIMARY KEY,
inserted TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
Expand All @@ -91,14 +91,14 @@ CREATE TABLE IF NOT EXISTS Account (
name VARCHAR(256) NOT NULL UNIQUE,
preferences JSONB NOT NULL DEFAULT ('{"wrap":true,"minAge":18}'),
valueList JSONB GENERATED ALWAYS AS (JSONB_EXTRACT_PATH(preferences, '$.*')) STORED,
CONSTRAINT account_established CHECK (established >= '2020-01-01'),
CONSTRAINT account_email CHECK (email IS NULL OR email ~* '^[^@]+@[^@]+[.][^@]{2,}$'),
CONSTRAINT account_phone CHECK (phone IS NULL OR phone ~* '^[0-9]{8,16}$')
CONSTRAINT accounts_established CHECK (established >= '2020-01-01'),
CONSTRAINT accounts_email CHECK (email IS NULL OR email ~* '^[^@]+@[^@]+[.][^@]{2,}$'),
CONSTRAINT accounts_phone CHECK (phone IS NULL OR phone ~* '^[0-9]{8,16}$')
);
CREATE INDEX account_inserted ON Account (inserted);
CREATE INDEX account_updated ON Account (updated);
CREATE INDEX account_valueList ON Account (id,(CAST(valueList AS CHAR(32))),enabled);
CREATE INDEX account_fulltext ON Account USING GIN (TO_TSVECTOR('english', COALESCE(comments,'')||' '||COALESCE(country,'')||' '||COALESCE(phone,'')||' '||COALESCE(name,'')));
CREATE INDEX accounts_inserted ON accounts (inserted);
CREATE INDEX accounts_updated ON accounts (updated);
CREATE INDEX accounts_valueList ON accounts (id,(CAST(valueList AS CHAR(32))),enabled);
CREATE INDEX accounts_fulltext ON accounts USING GIN (TO_TSVECTOR('english', COALESCE(comments,'')||' '||COALESCE(country,'')||' '||COALESCE(phone,'')||' '||COALESCE(name,'')));
`.trim();

test("Table Creation Postgres", function () {
Expand All @@ -112,15 +112,15 @@ test("Actual Table", async function () {
const provider = getProvider();
await createTables([AccountSchema as Schema]);

let sql = "SELECT * FROM information_schema.tables WHERE table_name = 'Account' OR table_name = 'account'";
let sql = "SELECT * FROM information_schema.tables WHERE table_name = 'accounts' OR table_name = 'account'";
if (provider === "sqlite") sql = sql.replace("information_schema.tables", "information_schema_tables");

// Select table from Information Schema
const oneTable = await DB.query(sql);
assertEquals(oneTable.length, 1);

// Delete table
await DB.execute("DROP TABLE Account;");
await DB.execute("DROP TABLE accounts;");

// Select table from Information Schema
const noTable = await DB.query(sql);
Expand Down

0 comments on commit f3ffa00

Please sign in to comment.