Skip to content

Commit ca5c473

Browse files
committed
implement attach database
1 parent fba3951 commit ca5c473

4 files changed

Lines changed: 100 additions & 30 deletions

File tree

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ You can access the database instance directly by `orm.db`. If you are using an e
1616
}
1717
```
1818

19-
**Create a model:**
19+
**Create a model:**<br>
20+
Use the `@orm.model()` decorator for creating a new model. After all models are loaded, call `orm.modelsLoaded()`
2021
```typescript
2122
@orm.model()
2223
class Foo extends SqlTable {
2324

2425
}
26+
orm.modelsLoaded()
2527
```
2628
Incase `Foo` exists in the database but has a different name, use `@orm.model('bar')`. All Tables have `id` as a primary key.
2729
It can be removed by overriding it and using `@orm.ignoreColumn()`. Tables are created if they don't exist. If new columns
@@ -171,6 +173,7 @@ class Foo extends SqlTable {
171173
public ignored = ''
172174
}
173175

176+
orm.modelsLoaded()
174177
const obj = new Foo()
175178

176179
// save the obj

src/builder.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ function getDefaultValue(type: ColumnType, value: any) {
4242
}
4343

4444
export function buildTableQuery(model: Model) {
45-
const str = [`CREATE TABLE '${model.tableName}' (`];
45+
const str = [`CREATE TABLE ${model.database}.'${model.tableName}' (`];
4646
for (const column of model.columns) {
4747
str.push(buildColumnQuery(column) + ',');
4848
}
@@ -64,7 +64,7 @@ export function buildAlterQuery(existingModel: Model, actualModel: Model) {
6464

6565
return actualModel.columns
6666
.filter((col) => existingModel.columns.find((c) => c.name === col.name || c.name === col.mappedTo) == null)
67-
.map((col) => `ALTER TABLE '${actualModel.tableName}' ADD COLUMN ${buildColumnQuery(col)}`);
67+
.map((col) => `ALTER TABLE '${actualModel.database}.${actualModel.tableName}' ADD COLUMN ${buildColumnQuery(col)}`);
6868
}
6969

7070
export function buildModelFromData(ogModel: Model, data: any[]): Model {
@@ -85,7 +85,7 @@ export function buildModelFromData(ogModel: Model, data: any[]): Model {
8585
});
8686
}
8787

88-
return new Model(ogModel.tableName, cols);
88+
return new Model(ogModel.tableName, cols, ogModel.database);
8989
}
9090

9191
function buildBaseFilterQuery(query: Partial<SelectQuery>): BuiltQuery {
@@ -113,15 +113,15 @@ function buildBaseFilterQuery(query: Partial<SelectQuery>): BuiltQuery {
113113
};
114114
}
115115

116-
export function buildSelectQuery(query: SelectQuery, table: string): BuiltQuery {
116+
export function buildSelectQuery(query: SelectQuery, model: Model): BuiltQuery {
117117
const base = buildBaseFilterQuery(query);
118-
base.query = `SELECT * FROM '${table}' ${base.query}`;
118+
base.query = `SELECT * FROM '${model.database}.${model.tableName}' ${base.query}`;
119119
return base;
120120
}
121121

122-
export function buildDeleteQuery(query: DeleteQuery, table: string): BuiltQuery {
122+
export function buildDeleteQuery(query: DeleteQuery, model: Model): BuiltQuery {
123123
const base = buildBaseFilterQuery(query);
124-
base.query = `DELETE FROM '${table}' ${base.query}`;
124+
base.query = `DELETE FROM '${model.database}.${model.tableName}' ${base.query}`;
125125
return base;
126126
}
127127

@@ -167,16 +167,16 @@ export function buildUpdateQuery(model: Model, data: Record<string, unknown>): B
167167
};
168168
}
169169

170-
export function buildCountWhereQuery(query: WhereClause, table: string): BuiltQuery {
170+
export function buildCountWhereQuery(query: WhereClause, model: Model): BuiltQuery {
171171
return {
172-
query: `SELECT COUNT(*) FROM '${table}' WHERE ${query.where.clause}`,
172+
query: `SELECT COUNT(*) FROM '${model.database}.${model.tableName}' WHERE ${query.where.clause}`,
173173
params: query.where.values ?? [],
174174
};
175175
}
176176

177-
export function buildAggregateQuery(query: AggregateSelectQuery, table: string): BuiltQuery {
177+
export function buildAggregateQuery(query: AggregateSelectQuery, model: Model): BuiltQuery {
178178
const params: any[] = [];
179-
const str = [`SELECT ${query.select.clause} FROM '${table}'`];
179+
const str = [`SELECT ${query.select.clause} FROM '${model.database}.${model.tableName}'`];
180180

181181
if (query.where) {
182182
str.push(`WHERE ${query.where.clause}`);

src/model-reader.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type { Model } from './orm.ts';
2+
3+
interface ModelData {
4+
version: number;
5+
models: Record<string, Model>;
6+
}
7+
8+
type Versions = Record<string, Model> | ModelData;
9+
10+
function readVersion0(data: Record<string, Model>): ModelData {
11+
return {
12+
version: 1,
13+
models: data,
14+
};
15+
}
16+
17+
export function read(dbPath: string): Record<string, Model> {
18+
let data: Versions;
19+
20+
try {
21+
data = JSON.parse(Deno.readTextFileSync(`${dbPath}.model.json`));
22+
} catch (_e) {
23+
return {};
24+
}
25+
26+
if (!('version' in data)) {
27+
data = readVersion0(data);
28+
}
29+
30+
if ('version' in data && data.version === 1) {
31+
return (data as ModelData).models;
32+
}
33+
34+
throw new Error(`unknown version for models ${dbPath}.model.json`);
35+
}
36+
37+
export function write(models: Record<string, Model>, dbPath: string) {
38+
Deno.writeTextFileSync(
39+
`${dbPath}.model.json`,
40+
JSON.stringify(
41+
{
42+
version: 1,
43+
models,
44+
},
45+
null,
46+
2,
47+
),
48+
);
49+
}

src/orm.ts

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { Database as SqliteDatabase, DatabaseOpenOptions } from 'https://deno.land/x/sqlite3@0.8.1/mod.ts';
22
import { buildAggregateQuery, buildAlterQuery, buildCountWhereQuery, buildDeleteQuery, buildInsertQuery, buildModelFromData, buildSelectQuery, buildTableQuery, buildUpdateQuery, isProvidedTypeValid } from './builder.ts';
3-
import { DBInvalidData, DBInvalidTable, DBModelNotFound, DBNotFound } from './errors.ts';
3+
import { DBError, DBInvalidData, DBInvalidTable, DBModelNotFound, DBNotFound } from './errors.ts';
44
import { dejsonify, jsonify } from './json.ts';
55
import { prettyPrintDiff } from './util.ts';
66
import { basename, join } from 'https://deno.land/std@0.192.0/path/mod.ts';
7+
import * as ModelReader from './model-reader.ts';
78

89
interface OrmOptions {
910
/**
@@ -127,7 +128,7 @@ export type PrimitiveTypes = number | string | boolean;
127128
export type DeleteQuery = Partial<SelectQuery>;
128129

129130
export class Model {
130-
constructor(public tableName: string, public columns: TableColumn[]) {}
131+
constructor(public tableName: string, public columns: TableColumn[], public readonly database: string) {}
131132
}
132133

133134
const gitBranch = new TextDecoder().decode(
@@ -149,8 +150,10 @@ export class SqliteOrm {
149150
private hasChangesSinceBackup = false;
150151
private backupsEnabled = false;
151152
private hasModelChanges = false;
153+
private attachedDatabases: string[] = [];
152154

153155
public models: Record<string, Model> = {};
156+
154157
private tempModelData: TableColumn[] = [];
155158
private ignoredColumns: string[] = [];
156159

@@ -185,11 +188,7 @@ export class SqliteOrm {
185188
SqliteOrm.logInfo(this.opts, 'opening database');
186189

187190
this.db = new SqliteDatabase(options.dbPath, options.openOptions);
188-
try {
189-
this.lastModels = JSON.parse(Deno.readTextFileSync(`${options.dbPath}.model.json`));
190-
} catch (_e) {
191-
this.lastModels = {};
192-
}
191+
this.lastModels = ModelReader.read(options.dbPath);
193192
}
194193

195194
//#region table logic
@@ -209,7 +208,7 @@ export class SqliteOrm {
209208
},
210209
limit: 1,
211210
},
212-
this.models[table.name].tableName,
211+
this.models[table.name],
213212
);
214213

215214
const found = this.db.prepare(query.query).get(...query.params);
@@ -244,7 +243,7 @@ export class SqliteOrm {
244243
public findMany<T extends SqlTable>(table: new () => T, query: SelectQuery): T[] {
245244
if (this.models[table.name] == null) throw new DBModelNotFound(table);
246245

247-
const builtQuery = buildSelectQuery(query, this.models[table.name].tableName);
246+
const builtQuery = buildSelectQuery(query, this.models[table.name]);
248247

249248
const data = this.db.prepare(builtQuery.query).all(...builtQuery.params);
250249
const parsedAll: T[] = [];
@@ -264,14 +263,14 @@ export class SqliteOrm {
264263
public countWhere<T extends SqlTable>(table: new () => T, query: WhereClause): number {
265264
if (this.models[table.name] == null) throw new DBModelNotFound(table);
266265

267-
const builtQuery = buildCountWhereQuery(query, this.models[table.name].tableName);
266+
const builtQuery = buildCountWhereQuery(query, this.models[table.name]);
268267
return this.db.prepare(builtQuery.query).get<{ 'COUNT(*)': number }>(...builtQuery.params)!['COUNT(*)'];
269268
}
270269

271270
public aggregateSelect<Row extends Array<any>, T extends SqlTable = SqlTable>(table: new () => T, query: AggregateSelectQuery): Row[] {
272271
if (this.models[table.name] == null) throw new DBModelNotFound(table);
273272

274-
const builtQuery = buildAggregateQuery(query, this.models[table.name].tableName);
273+
const builtQuery = buildAggregateQuery(query, this.models[table.name]);
275274
return this.db.prepare(builtQuery.query).values(...builtQuery.params);
276275
}
277276

@@ -304,7 +303,7 @@ export class SqliteOrm {
304303
}
305304

306305
public delete<T extends SqlTable>(table: new () => T, query: DeleteQuery) {
307-
const built = buildDeleteQuery(query, this.models[table.name].tableName);
306+
const built = buildDeleteQuery(query, this.models[table.name]);
308307
this.db.exec(built.query, ...built.params);
309308
this.hasChangesSinceBackup = true;
310309
}
@@ -382,11 +381,12 @@ export class SqliteOrm {
382381
};
383382
}
384383

384+
// todo use an object
385385
/**
386386
* Adds a class to orm models.
387387
* @param tableName name of table in database
388388
*/
389-
public model(tableName?: string) {
389+
public model(tableName?: string, database = 'main') {
390390
return (model: new () => SqlTable) => {
391391
const tempModel = new model();
392392
const hasPrimaryKey = this.tempModelData.find((i) => i.isPrimaryKey) != null;
@@ -428,18 +428,18 @@ export class SqliteOrm {
428428
);
429429
}
430430

431-
const builtModel = new Model(tableName ?? model.name, this.tempModelData);
431+
const builtModel = new Model(tableName ?? model.name, this.tempModelData, database);
432432
this.models[model.name] = builtModel;
433433
this.tempModelData = [];
434434

435435
// create table if it doesn't exist
436-
const info = this.db.prepare(`PRAGMA table_info('${model.name}')`).all();
436+
const info = this.db.prepare(`PRAGMA ${database}.table_info('${model.name}')`).all();
437437
if (info.length === 0) {
438438
this.hasModelChanges = true;
439439
this.db.exec(buildTableQuery(builtModel));
440440
} else {
441441
// add missing columns
442-
const info = this.db.prepare(`PRAGMA table_info('${model.name}')`).all();
442+
const info = this.db.prepare(`PRAGMA ${database}.table_info('${model.name}')`).all();
443443
buildAlterQuery(buildModelFromData(builtModel, info), builtModel).forEach((c) => {
444444
this.hasModelChanges = true;
445445
this.db.exec(c);
@@ -452,6 +452,12 @@ export class SqliteOrm {
452452
} else {
453453
const oldCols = this.lastModels[model.name].columns;
454454
const newCols = builtModel.columns;
455+
const oldDatabase = this.lastModels[model.name].database;
456+
457+
if (oldDatabase != null && oldDatabase !== database) {
458+
SqliteOrm.logInfo(this.opts, `database change from ${oldDatabase} to ${database}`);
459+
this.hasModelChanges = true;
460+
}
455461

456462
for (const oldCol of oldCols) {
457463
const newCol = newCols.find((c) => (c.mappedTo ?? c.name) === (oldCol.mappedTo ?? oldCol.name));
@@ -564,11 +570,23 @@ export class SqliteOrm {
564570
}
565571

566572
if (this.hasModelChanges) {
567-
this.saveModel();
568573
this.doBackup('model-changes');
569574
}
570575

571576
this.hasModelChanges = false;
577+
this.saveModel();
578+
}
579+
580+
public attach(databasePath: string, name?: string) {
581+
if (name == null) {
582+
name = basename(databasePath);
583+
}
584+
585+
if (this.attachedDatabases.includes(name)) throw new DBError(`${databasePath} is already attached`);
586+
this.db.exec(`ATTACH DATABASE ? AS ?`, databasePath, name);
587+
this.attachedDatabases.push(name);
588+
589+
SqliteOrm.logInfo(this.opts, `attached ${databasePath} as ${name}`);
572590
}
573591

574592
//#endregion misc
@@ -676,6 +694,6 @@ export class SqliteOrm {
676694
}
677695

678696
private saveModel() {
679-
Deno.writeTextFileSync(`${this.opts.dbPath}.model.json`, JSON.stringify(this.models, null, 2));
697+
ModelReader.write(this.models, this.opts.dbPath);
680698
}
681699
}

0 commit comments

Comments
 (0)