petcarescript
Version:
PetCareScript - A modern, expressive programming language designed for humans
375 lines (313 loc) • 10.7 kB
JavaScript
/**
* PetCareScript Database Library
* SQLite database functionality with optional sqlite3 dependency
*/
let sqlite3;
try {
sqlite3 = require('sqlite3').verbose();
} catch (error) {
// sqlite3 is optional dependency
sqlite3 = null;
}
const path = require('path');
class Database {
constructor(filename = ':memory:') {
if (!sqlite3) {
throw new Error('sqlite3 is not installed. Install it with: npm install sqlite3');
}
this.filename = filename;
this.db = null;
this.isConnected = false;
}
async connect() {
if (!sqlite3) {
throw new Error('sqlite3 is not installed. Install it with: npm install sqlite3');
}
return new Promise((resolve, reject) => {
this.db = new sqlite3.Database(this.filename, (error) => {
if (error) {
reject(error);
} else {
this.isConnected = true;
console.log(`📊 Connected to SQLite database: ${this.filename}`);
resolve();
}
});
});
}
async close() {
return new Promise((resolve, reject) => {
if (this.db) {
this.db.close((error) => {
if (error) {
reject(error);
} else {
this.isConnected = false;
console.log('📊 Database connection closed');
resolve();
}
});
} else {
resolve();
}
});
}
async query(sql, params = []) {
if (!sqlite3) {
throw new Error('sqlite3 is not installed. Install it with: npm install sqlite3');
}
return new Promise((resolve, reject) => {
if (!this.isConnected) {
reject(new Error('Database not connected'));
return;
}
this.db.all(sql, params, (error, rows) => {
if (error) {
reject(error);
} else {
resolve(rows);
}
});
});
}
async run(sql, params = []) {
if (!sqlite3) {
throw new Error('sqlite3 is not installed. Install it with: npm install sqlite3');
}
return new Promise((resolve, reject) => {
if (!this.isConnected) {
reject(new Error('Database not connected'));
return;
}
this.db.run(sql, params, function(error) {
if (error) {
reject(error);
} else {
resolve({
lastID: this.lastID,
changes: this.changes
});
}
});
});
}
async get(sql, params = []) {
if (!sqlite3) {
throw new Error('sqlite3 is not installed. Install it with: npm install sqlite3');
}
return new Promise((resolve, reject) => {
if (!this.isConnected) {
reject(new Error('Database not connected'));
return;
}
this.db.get(sql, params, (error, row) => {
if (error) {
reject(error);
} else {
resolve(row);
}
});
});
}
async createTable(tableName, columns) {
const columnDefs = Object.entries(columns).map(([name, type]) => {
return `${name} ${type}`;
}).join(', ');
const sql = `CREATE TABLE IF NOT EXISTS ${tableName} (${columnDefs})`;
return this.run(sql);
}
async insert(tableName, data) {
const columns = Object.keys(data);
const values = Object.values(data);
const placeholders = values.map(() => '?').join(', ');
const sql = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})`;
return this.run(sql, values);
}
async select(tableName, conditions = {}, options = {}) {
let sql = `SELECT * FROM ${tableName}`;
const params = [];
if (Object.keys(conditions).length > 0) {
const whereClause = Object.keys(conditions).map(key => {
params.push(conditions[key]);
return `${key} = ?`;
}).join(' AND ');
sql += ` WHERE ${whereClause}`;
}
if (options.orderBy) {
sql += ` ORDER BY ${options.orderBy}`;
if (options.order) {
sql += ` ${options.order}`;
}
}
if (options.limit) {
sql += ` LIMIT ${options.limit}`;
}
if (options.offset) {
sql += ` OFFSET ${options.offset}`;
}
return this.query(sql, params);
}
async update(tableName, data, conditions) {
const setClause = Object.keys(data).map(key => `${key} = ?`).join(', ');
const whereClause = Object.keys(conditions).map(key => `${key} = ?`).join(' AND ');
const sql = `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause}`;
const params = [...Object.values(data), ...Object.values(conditions)];
return this.run(sql, params);
}
async delete(tableName, conditions) {
const whereClause = Object.keys(conditions).map(key => `${key} = ?`).join(' AND ');
const sql = `DELETE FROM ${tableName} WHERE ${whereClause}`;
const params = Object.values(conditions);
return this.run(sql, params);
}
async transaction(callback) {
await this.run('BEGIN TRANSACTION');
try {
await callback(this);
await this.run('COMMIT');
} catch (error) {
await this.run('ROLLBACK');
throw error;
}
}
async migrate(migrations) {
// Create migrations table
await this.createTable('migrations', {
id: 'INTEGER PRIMARY KEY AUTOINCREMENT',
name: 'TEXT UNIQUE',
executed_at: 'DATETIME DEFAULT CURRENT_TIMESTAMP'
});
for (const migration of migrations) {
const existing = await this.get('SELECT * FROM migrations WHERE name = ?', [migration.name]);
if (!existing) {
console.log(`🔄 Running migration: ${migration.name}`);
await migration.up(this);
await this.insert('migrations', { name: migration.name });
console.log(`✅ Migration completed: ${migration.name}`);
}
}
}
async seed(seeds) {
for (const seed of seeds) {
console.log(`🌱 Running seed: ${seed.name}`);
await seed.run(this);
console.log(`✅ Seed completed: ${seed.name}`);
}
}
}
// Query builder
class QueryBuilder {
constructor(tableName) {
this.tableName = tableName;
this.selectFields = ['*'];
this.whereConditions = [];
this.joinClauses = [];
this.orderByClause = null;
this.limitClause = null;
this.offsetClause = null;
this.params = [];
}
select(fields) {
if (Array.isArray(fields)) {
this.selectFields = fields;
} else {
this.selectFields = [fields];
}
return this;
}
where(field, operator, value) {
if (arguments.length === 2) {
// where(field, value) - assume equality
value = operator;
operator = '=';
}
this.whereConditions.push(`${field} ${operator} ?`);
this.params.push(value);
return this;
}
join(table, condition) {
this.joinClauses.push(`JOIN ${table} ON ${condition}`);
return this;
}
leftJoin(table, condition) {
this.joinClauses.push(`LEFT JOIN ${table} ON ${condition}`);
return this;
}
orderBy(field, direction = 'ASC') {
this.orderByClause = `${field} ${direction}`;
return this;
}
limit(count) {
this.limitClause = count;
return this;
}
offset(count) {
this.offsetClause = count;
return this;
}
build() {
let sql = `SELECT ${this.selectFields.join(', ')} FROM ${this.tableName}`;
if (this.joinClauses.length > 0) {
sql += ` ${this.joinClauses.join(' ')}`;
}
if (this.whereConditions.length > 0) {
sql += ` WHERE ${this.whereConditions.join(' AND ')}`;
}
if (this.orderByClause) {
sql += ` ORDER BY ${this.orderByClause}`;
}
if (this.limitClause) {
sql += ` LIMIT ${this.limitClause}`;
}
if (this.offsetClause) {
sql += ` OFFSET ${this.offsetClause}`;
}
return { sql, params: this.params };
}
async execute(db) {
const { sql, params } = this.build();
return db.query(sql, params);
}
}
const DatabaseLib = {
connect: (filename) => {
if (!sqlite3) {
console.warn('⚠️ sqlite3 not installed. Database features will be limited.');
console.warn(' To enable database features, run: npm install sqlite3');
return {
connect: () => Promise.reject(new Error('sqlite3 not installed')),
createTable: () => Promise.reject(new Error('sqlite3 not installed')),
insert: () => Promise.reject(new Error('sqlite3 not installed')),
select: () => Promise.reject(new Error('sqlite3 not installed')),
update: () => Promise.reject(new Error('sqlite3 not installed')),
delete: () => Promise.reject(new Error('sqlite3 not installed')),
close: () => Promise.resolve()
};
}
const db = new Database(filename);
return db;
},
query: (tableName) => new QueryBuilder(tableName),
// Migration helpers
createMigration: (name, up, down = null) => ({
name,
up,
down
}),
createSeed: (name, run) => ({
name,
run
}),
// Data types for schema definition
types: {
INTEGER: 'INTEGER',
TEXT: 'TEXT',
REAL: 'REAL',
BLOB: 'BLOB',
PRIMARY_KEY: 'INTEGER PRIMARY KEY AUTOINCREMENT',
TIMESTAMP: 'DATETIME DEFAULT CURRENT_TIMESTAMP',
BOOLEAN: 'INTEGER', // SQLite doesn't have native boolean
JSON: 'TEXT' // Store JSON as text
}
};
module.exports = DatabaseLib;