Skip to content

Commit

Permalink
Refactor sql.js out of SQLJSPackageDB constructor (#45)
Browse files Browse the repository at this point in the history
* Refactor sql.js out of SQLJSPackageDB constructor

Instead of requiring the caller to initialize and pass in a sql.js Database, add an async initialize() function to do it within the SQLJSPackageDB implementation.

Callers can also use the new async newSQLJSPackageDB funtion to get a new initialized SQLJSPackageDB (one call instead of two).

* Fix typo: unitialized --> uninitialized

* Improve initialization logic
  • Loading branch information
cmoesel authored Dec 13, 2024
1 parent 2fb67f4 commit 014d710
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 81 deletions.
6 changes: 2 additions & 4 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ import os from 'os';
import path from 'path';
import { Command, OptionValues } from 'commander';
import fs from 'fs-extra';
import initSqlJs from 'sql.js';
import { DiskBasedPackageCache } from './cache';
import { BuildDotFhirDotOrgClient } from './current';
import { SQLJSPackageDB } from './db';
import { newSQLJSPackageDB } from './db';
import { BasePackageLoader } from './loader';
import { DefaultRegistryClient } from './registry';
import { logger } from './utils';
Expand All @@ -33,8 +32,7 @@ async function install(fhirPackages: string[], options: OptionValues) {
logger.log(level, message);
};

const SQL = await initSqlJs();
const packageDB = new SQLJSPackageDB(new SQL.Database());
const packageDB = await newSQLJSPackageDB();
const fhirCache = options.cachePath ?? path.join(os.homedir(), '.fhir', 'packages');
const packageCache = new DiskBasedPackageCache(fhirCache, { log });
const registryClient = new DefaultRegistryClient({ log });
Expand Down
134 changes: 95 additions & 39 deletions src/db/SQLJSPackageDB.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import util from 'util';
import { Database, Statement } from 'sql.js';
import initSqlJs, { Database, Statement } from 'sql.js';
import { FindResourceInfoOptions, PackageInfo, PackageStats, ResourceInfo } from '../package';
import { PackageDB } from './PackageDB';

Expand Down Expand Up @@ -105,50 +104,74 @@ const INSERT_RESOURCE = `INSERT INTO resource
const SD_FLAVORS = ['Extension', 'Logical', 'Profile', 'Resource', 'Type'];

export class SQLJSPackageDB implements PackageDB {
private db: Database;
private insertPackageStmt: Statement;
private insertResourceStmt: Statement;
private findAllPackagesStmt: Statement;
private findPackagesStmt: Statement;
private findPackageStmt: Statement;
private optimized: boolean;
constructor(
private db: Database,
initialize = true
) {
if (initialize) {
this.db.run(
[
CREATE_PACKAGE_TABLE,
CREATE_PACKAGE_TABLE_INDICES,
CREATE_RESOURCE_TABLE,
CREATE_RESOURCE_TABLE_INDICES
].join(';')
);
private initialized: boolean;

constructor() {
this.initialized = false;
}

async initialize() {
if (!this.initialized) {
const SQL = await initSqlJs();
// check initialization state once more since initSqlJs call was async (possible race condition)
if (!this.initialized) {
this.db = new SQL.Database();
this.db.run(
[
CREATE_PACKAGE_TABLE,
CREATE_PACKAGE_TABLE_INDICES,
CREATE_RESOURCE_TABLE,
CREATE_RESOURCE_TABLE_INDICES
].join(';')
);
this.insertPackageStmt = this.db.prepare(INSERT_PACKAGE);
this.insertResourceStmt = this.db.prepare(INSERT_RESOURCE);
this.findAllPackagesStmt = this.db.prepare(FIND_ALL_PACKAGES);
this.findPackagesStmt = this.db.prepare(FIND_PACKAGES);
this.findPackageStmt = this.db.prepare(FIND_PACKAGE);
this.initialized = true;
this.optimized = false;
}
}
this.insertPackageStmt = this.db.prepare(INSERT_PACKAGE);
this.insertResourceStmt = this.db.prepare(INSERT_RESOURCE);
this.findAllPackagesStmt = this.db.prepare(FIND_ALL_PACKAGES);
this.findPackagesStmt = this.db.prepare(FIND_PACKAGES);
this.findPackageStmt = this.db.prepare(FIND_PACKAGE);
this.optimized = false;
}

isInitialized() {
return this.initialized;
}

clear() {
this.db.exec('DELETE FROM package');
this.db.exec('DELETE FROM resource');
this.db.exec('VACUUM');
if (this.db) {
this.db.exec('DELETE FROM package');
this.db.exec('DELETE FROM resource');
this.db.exec('VACUUM');
this.optimized = false;
}
}

optimize() {
if (!this.optimized) {
this.db.exec('PRAGMA optimize=0x10002');
this.optimized = true;
} else {
this.db.exec('PRAGMA optimize');
if (this.db) {
if (!this.optimized) {
this.db.exec('PRAGMA optimize=0x10002');
this.optimized = true;
} else {
this.db.exec('PRAGMA optimize');
}
}
}

savePackageInfo(info: PackageInfo): void {
if (!this.db) {
throw new Error(
'SQLJSPackageDB not initialized. Please call the initialize() function before using this class.'
);
}
const binding: any = {
':name': info.name,
':version': info.version
Expand All @@ -163,6 +186,11 @@ export class SQLJSPackageDB implements PackageDB {
}

saveResourceInfo(info: ResourceInfo): void {
if (!this.db) {
throw new Error(
'SQLJSPackageDB not initialized. Please call the initialize() function before using this class.'
);
}
const binding: any = {
':resourceType': info.resourceType
};
Expand Down Expand Up @@ -221,6 +249,11 @@ export class SQLJSPackageDB implements PackageDB {
}

findPackageInfos(name: string): PackageInfo[] {
if (!this.db) {
throw new Error(
'SQLJSPackageDB not initialized. Please call the initialize() function before using this class.'
);
}
const results: PackageInfo[] = [];
const findStmt = name === '*' ? this.findAllPackagesStmt : this.findPackagesStmt;
try {
Expand All @@ -237,6 +270,11 @@ export class SQLJSPackageDB implements PackageDB {
}

findPackageInfo(name: string, version: string): PackageInfo | undefined {
if (!this.db) {
throw new Error(
'SQLJSPackageDB not initialized. Please call the initialize() function before using this class.'
);
}
try {
this.findPackageStmt.bind({ ':name': name, ':version': version });
if (this.findPackageStmt.step()) {
Expand All @@ -248,6 +286,11 @@ export class SQLJSPackageDB implements PackageDB {
}

findResourceInfos(key: string, options: FindResourceInfoOptions = {}): ResourceInfo[] {
if (!this.db) {
throw new Error(
'SQLJSPackageDB not initialized. Please call the initialize() function before using this class.'
);
}
// In case a key wasn't supplied, just use empty string. Later we might have it return ALL.
if (key == null) {
key = '';
Expand Down Expand Up @@ -355,6 +398,11 @@ export class SQLJSPackageDB implements PackageDB {
}

findResourceInfo(key: string, options: FindResourceInfoOptions = {}): ResourceInfo | undefined {
if (!this.db) {
throw new Error(
'SQLJSPackageDB not initialized. Please call the initialize() function before using this class.'
);
}
// TODO: Make this more sophisticated if/when it makes sense
const results = this.findResourceInfos(key, { ...options, limit: 1 });
if (results.length > 0) {
Expand All @@ -363,6 +411,11 @@ export class SQLJSPackageDB implements PackageDB {
}

getPackageStats(name: string, version: string): PackageStats | undefined {
if (!this.db) {
throw new Error(
'SQLJSPackageDB not initialized. Please call the initialize() function before using this class.'
);
}
const pkg = this.findPackageInfo(name, version);
if (pkg == null) {
return;
Expand All @@ -378,18 +431,21 @@ export class SQLJSPackageDB implements PackageDB {
};
}

exportDB(): Promise<{ mimeType: string; data: Buffer }> {
async exportDB(): Promise<{ mimeType: string; data: Buffer }> {
if (!this.db) {
return Promise.reject(
new Error(
'SQLJSPackageDB not initialized. Please call the initialize() function before using this class.'
)
);
}
const data = this.db.export();
return Promise.resolve({ mimeType: 'application/x-sqlite3', data: Buffer.from(data) });
}
}

logPackageTable() {
const res = this.db.exec('SELECT * FROM package');
console.log(util.inspect(res, false, 3, true));
}

logResourceTable() {
const res = this.db.exec('SELECT * FROM resource');
console.log(util.inspect(res, false, 3, true));
}
export async function newSQLJSPackageDB(): Promise<SQLJSPackageDB> {
const packageDB = new SQLJSPackageDB();
await packageDB.initialize();
return packageDB;
}
6 changes: 2 additions & 4 deletions src/loader/DefaultPackageLoader.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import os from 'os';
import path from 'path';
import initSqlJs from 'sql.js';
import { DiskBasedPackageCache } from '../cache/DiskBasedPackageCache';
import { BuildDotFhirDotOrgClient } from '../current';
import { SQLJSPackageDB } from '../db';
import { newSQLJSPackageDB } from '../db';
import { DefaultRegistryClient } from '../registry';
import { BasePackageLoader, BasePackageLoaderOptions } from './BasePackageLoader';

export async function defaultPackageLoader(options: BasePackageLoaderOptions) {
const SQL = await initSqlJs();
const packageDB = new SQLJSPackageDB(new SQL.Database());
const packageDB = await newSQLJSPackageDB();
const fhirCache = path.join(os.homedir(), '.fhir', 'packages');
const packageCache = new DiskBasedPackageCache(fhirCache, {
log: options.log
Expand Down
Loading

0 comments on commit 014d710

Please sign in to comment.