Skip to content

[Discussion] large sqlite project structure #668

Open
@andykais

Description

@andykais

Hi all, I've been using better-sqlite3 for a few years on small-ish projects and libraries. In those instances, using the library doesn't require a lot of thought. It generally looks like:

  1. instantiate a database
  2. instantiate some statements
  3. start the application

However I want to take a stab at a large-ish web application using a sqlite backend. I find I'm bikeshedding a ton on the proper way to structure this project. The questions I find myself asking are:

  • How should I go about typing (in typescript) my queries?
  • Should I use model files?
  • How do I handle migrations?
    • How do I handle preparing statements that depend on migrations being ran?

The current iteration looks like the following:

  1. initialize better-sqlite3 database
  2. grab project version from a table that I know will exist for the project's lifetime
  3. if the current version > version in db, perform each migration in between
  4. initialize all the other statements

A model file essentially looks like this:

/* --============= Table Row Definitions =============-- */

interface BookTR {
  id: number
  title: string
  published: boolean
  published_date: Date
}

/* --================ Model Definition ================-- */

class BookModel extends Model {
  private insert = this.register(InsertBookStatement)
  private select_one = this.register(SelectOneBookStatement)

  public create(book_data: Omit<BookTR, 'id'>) {
    const book_id = this.insert(book_data)
    return this.select_one(book_id)
  }
}

/* --=================== Statements ===================-- */

class InsertBookStatement extends Statement {
  sql = `INSERT INTO book (title, published, published_date) VALUES (@title, @published, @published_date)`
  stmt = this.register(this.sql)

  call(book_data: Omit<BookTR, 'id'>) {
    const sql_data = {...book_data, published ? 1 : 0, published_date: book_data.published_date.toString()}
    const info = this.stmt.ref.run(sql_data)
    return info.lastInsertRowid
  }
}

class SelectOneBookStatement extends Statement {
  sql = `SELECT * FROM book WHERE id = ?`
  stmt = this.register(this.sql)

  call(book_id: BookTR['id']) {
    return this.stmt.ref.get(book_id)
  }
}

and the main database file looks like this:

class Database {
  constructor(database_path: string) {
    this.db = new BetterSqlite3(database_path)
  }
  init() {
    for (const model of this.registered_models) model.init()
  }

  // model  definitions
  book = this.register(BookModel)
}

usage:

const db = new Database('sqlite.db')
db.init()
const book = db.book.create({ title: 'Oh The Places You'll Go', published: true, published_date: new Date('5/24/1997') })

essentially all those register methods do on Statements, Models and Database is instantiate the class with a reference to the better-sqlite3 database, and store registered objects in an array that is iterated on in an init method (what this really allows for is preparing all sql statements when the init method is called).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions