SQL Driver for ObjectStack - Supports PostgreSQL, MySQL, SQLite via Knex.js.
- Multi-Database Support: PostgreSQL, MySQL, SQLite, and other Knex-supported databases
- Query Builder: Powerful Knex.js query builder integration
- Migrations: Database schema migrations with version control
- Connection Pooling: Efficient connection management
- Transactions: Full ACID transaction support
- Raw SQL: Execute raw SQL when needed
- Type-Safe: Full TypeScript support with inferred types
- Production-Ready: Battle-tested Knex.js under the hood
pnpm add @objectstack/driver-sql knexInstall the driver for your database:
# PostgreSQL
pnpm add pg
# MySQL
pnpm add mysql2
# SQLite
pnpm add better-sqlite3import { defineStack } from '@objectstack/spec';
import { DriverSQL } from '@objectstack/driver-sql';
const stack = defineStack({
driver: DriverSQL.configure({
client: 'pg',
connection: {
host: 'localhost',
port: 5432,
user: 'postgres',
password: process.env.DB_PASSWORD,
database: 'myapp',
},
pool: {
min: 2,
max: 10,
},
}),
});const stack = defineStack({
driver: DriverSQL.configure({
client: 'mysql2',
connection: {
host: 'localhost',
port: 3306,
user: 'root',
password: process.env.DB_PASSWORD,
database: 'myapp',
},
}),
});const stack = defineStack({
driver: DriverSQL.configure({
client: 'better-sqlite3',
connection: {
filename: './data/app.db',
},
useNullAsDefault: true,
}),
});interface SQLDriverConfig {
/** Knex client (pg, mysql2, better-sqlite3, etc.) */
client: string;
/** Database connection config */
connection: {
host?: string;
port?: number;
user?: string;
password?: string;
database?: string;
filename?: string; // For SQLite
};
/** Connection pool settings */
pool?: {
min?: number;
max?: number;
idleTimeoutMillis?: number;
};
/** Use NULL as default for unsupported features (SQLite) */
useNullAsDefault?: boolean;
/** Enable query debugging */
debug?: boolean;
/** Migrations configuration */
migrations?: {
directory?: string;
tableName?: string;
};
}The SQL driver implements the standard ObjectStack driver interface:
import type { IDriver } from '@objectstack/spec';
// All standard operations are supported:
// find, findOne, insert, update, delete, count// The SQL driver supports all ObjectQL query features:
const results = await kernel.getDriver().find({
object: 'opportunity',
filters: [
{ field: 'amount', operator: 'gte', value: 10000 },
{ field: 'stage', operator: 'in', value: ['proposal', 'negotiation'] },
],
sort: [{ field: 'amount', direction: 'desc' }],
limit: 100,
offset: 0,
});// migrations/001_create_users.ts
export async function up(knex) {
await knex.schema.createTable('objectstack_user', (table) => {
table.string('id').primary();
table.string('name').notNullable();
table.string('email').notNullable().unique();
table.timestamps(true, true);
});
}
export async function down(knex) {
await knex.schema.dropTable('objectstack_user');
}# Run all pending migrations
npx knex migrate:latest
# Rollback last migration
npx knex migrate:rollback
# Check migration status
npx knex migrate:statusCreate knexfile.js in your project root:
module.exports = {
development: {
client: 'pg',
connection: {
host: 'localhost',
user: 'postgres',
password: process.env.DB_PASSWORD,
database: 'myapp_dev',
},
migrations: {
directory: './migrations',
tableName: 'objectstack_migrations',
},
},
production: {
client: 'pg',
connection: process.env.DATABASE_URL,
pool: {
min: 2,
max: 10,
},
migrations: {
directory: './migrations',
tableName: 'objectstack_migrations',
},
},
};const driver = kernel.getDriver();
await driver.transaction(async (trx) => {
// All operations within this callback use the same transaction
const account = await trx.insert({
object: 'account',
data: { name: 'Acme Corp' },
});
await trx.insert({
object: 'contact',
data: {
name: 'John Doe',
account_id: account.id,
},
});
// If an error is thrown, all changes are rolled back
// If successful, changes are committed
});When ObjectQL isn't sufficient, execute raw SQL:
const driver = kernel.getDriver();
// Raw query
const results = await driver.raw(`
SELECT
c.name,
COUNT(o.id) as opportunity_count,
SUM(o.amount) as total_revenue
FROM objectstack_account c
LEFT JOIN objectstack_opportunity o ON o.account_id = c.id
WHERE o.stage = 'closed_won'
GROUP BY c.id, c.name
ORDER BY total_revenue DESC
LIMIT 10
`);
// Raw query with parameters (prevent SQL injection)
const results = await driver.raw(
'SELECT * FROM objectstack_user WHERE email = ?',
['user@example.com']
);// Use PostgreSQL-specific features
const results = await driver.raw(`
SELECT * FROM objectstack_opportunity
WHERE data @> '{"industry": "Technology"}'::jsonb
`);
// Full-text search
const results = await driver.raw(`
SELECT * FROM objectstack_article
WHERE to_tsvector('english', title || ' ' || body) @@ to_tsquery('objectstack')
`);// Use MySQL-specific features
const results = await driver.raw(`
SELECT * FROM objectstack_product
WHERE MATCH(name, description) AGAINST ('widget' IN NATURAL LANGUAGE MODE)
`);// Get underlying Knex instance
const knex = driver.getKnex();
// Check connection
await driver.checkConnection();
// Close all connections
await driver.destroy();// Create index migration
export async function up(knex) {
await knex.schema.table('objectstack_opportunity', (table) => {
table.index('account_id');
table.index('stage');
table.index(['created_at', 'stage']); // Composite index
});
}// Use explain to analyze queries
const plan = await driver.raw('EXPLAIN ANALYZE SELECT ...');
// Create covering indexes for frequently accessed columns
// Use partial indexes for filtered queries (PostgreSQL)
await knex.raw(`
CREATE INDEX idx_active_opportunities
ON objectstack_opportunity(account_id, amount)
WHERE stage NOT IN ('closed_won', 'closed_lost')
`);- Connection Pooling: Configure appropriate pool size based on load
- Migrations: Always use migrations for schema changes, never raw DDL
- Transactions: Use transactions for multi-step operations
- Prepared Statements: Use parameterized queries to prevent SQL injection
- Indexes: Create indexes on frequently queried fields
- Monitoring: Monitor slow query logs and connection pool metrics
- Backups: Implement regular database backups
// config/database.ts
export const getDatabaseConfig = () => {
const env = process.env.NODE_ENV || 'development';
const configs = {
development: {
client: 'better-sqlite3',
connection: { filename: './data/dev.db' },
useNullAsDefault: true,
debug: true,
},
test: {
client: 'better-sqlite3',
connection: { filename: ':memory:' },
useNullAsDefault: true,
},
production: {
client: 'pg',
connection: process.env.DATABASE_URL,
pool: { min: 2, max: 10 },
ssl: { rejectUnauthorized: false },
},
};
return configs[env] || configs.development;
};
const stack = defineStack({
driver: DriverSQL.configure(getDatabaseConfig()),
});// Test database connection
try {
await driver.checkConnection();
console.log('Database connected successfully');
} catch (error) {
console.error('Database connection failed:', error);
}# Check migration status
npx knex migrate:status
# Rollback and re-run
npx knex migrate:rollback
npx knex migrate:latest// Enable query logging
const stack = defineStack({
driver: DriverSQL.configure({
client: 'pg',
connection: { /* ... */ },
debug: true, // Log all queries
}),
});# Heroku automatically provides DATABASE_URL
heroku addons:create heroku-postgresql:hobby-dev
# Run migrations on deployment
echo "npx knex migrate:latest" > Procfile.release# Use Railway's DATABASE_URL
railway up// Vercel uses connection pooling
import { createClient } from '@vercel/postgres';
const stack = defineStack({
driver: DriverSQL.configure({
client: 'pg',
connection: process.env.POSTGRES_URL,
}),
});Apache-2.0
- Knex.js Documentation
- PostgreSQL Documentation
- MySQL Documentation
- @objectstack/driver-turso - Edge-first SQLite alternative
- @objectstack/driver-memory - In-memory driver for testing