Files
smartclickhouse/test/test.nonci.ts

322 lines
9.3 KiB
TypeScript

import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as smartclickhouse from '../ts/index.js';
let testClickhouseDb: smartclickhouse.SmartClickHouseDb;
// ============================================================
// Connection
// ============================================================
tap.test('should create a SmartClickHouseDb instance', async () => {
testClickhouseDb = new smartclickhouse.SmartClickHouseDb({
url: 'http://localhost:8123',
database: 'test_smartclickhouse',
unref: true,
});
});
tap.test('should start the clickhouse db', async () => {
await testClickhouseDb.start(true);
});
// ============================================================
// Backward-compatible TimeDataTable tests
// ============================================================
let timeTable: smartclickhouse.TimeDataTable;
tap.test('should create a TimeDataTable via getTable()', async () => {
timeTable = await testClickhouseDb.getTable('analytics');
expect(timeTable).toBeInstanceOf(smartclickhouse.TimeDataTable);
});
tap.test('should insert data via addData()', async () => {
for (let i = 0; i < 50; i++) {
await timeTable.addData({
timestamp: Date.now(),
message: `hello this is a message ${i}`,
wow: 'hey',
deep: {
so: 'hello',
myArray: ['array1', 'array2'],
},
});
}
});
tap.test('should retrieve the last 10 entries', async () => {
const entries = await timeTable.getLastEntries(10);
expect(entries.length).toEqual(10);
});
tap.test('should retrieve entries newer than a specific timestamp', async () => {
const timestamp = Date.now() - 60000;
const entries = await timeTable.getEntriesNewerThan(timestamp);
expect(entries.length).toBeGreaterThan(0);
});
tap.test('should retrieve entries between two timestamps', async () => {
const startTimestamp = Date.now() - 120000;
const endTimestamp = Date.now() + 5000;
const entries = await timeTable.getEntriesBetween(startTimestamp, endTimestamp);
expect(entries.length).toBeGreaterThan(0);
});
tap.test('should delete the time data table', async () => {
await timeTable.delete();
const result = await testClickhouseDb.clickhouseHttpClient.queryPromise(`
SHOW TABLES FROM ${testClickhouseDb.options.database} LIKE '${timeTable.options.tableName}'
`);
expect(result.length).toEqual(0);
});
// ============================================================
// New typed ClickhouseTable API
// ============================================================
interface ILogEntry {
timestamp: number;
level: string;
message: string;
service: string;
duration: number;
}
let logTable: smartclickhouse.ClickhouseTable<ILogEntry>;
tap.test('should create a typed table via createTable()', async () => {
logTable = await testClickhouseDb.createTable<ILogEntry>({
tableName: 'logs',
orderBy: 'timestamp',
columns: [
{ name: 'timestamp', type: "DateTime64(3, 'Europe/Berlin')" },
{ name: 'level', type: 'String' },
{ name: 'message', type: 'String' },
{ name: 'service', type: 'String' },
{ name: 'duration', type: 'Float64' },
],
ttl: { column: 'timestamp', interval: '30 DAY' },
});
expect(logTable).toBeInstanceOf(smartclickhouse.ClickhouseTable);
});
tap.test('should insert a single row', async () => {
await logTable.insert({
timestamp: Date.now(),
level: 'info',
message: 'Server started',
service: 'api',
duration: 0,
});
});
tap.test('should insert many rows', async () => {
const rows: ILogEntry[] = [];
for (let i = 0; i < 100; i++) {
rows.push({
timestamp: Date.now(),
level: i % 10 === 0 ? 'error' : 'info',
message: `Log message ${i}`,
service: i % 2 === 0 ? 'api' : 'worker',
duration: Math.random() * 2000,
});
}
await logTable.insertMany(rows);
});
tap.test('should query with builder - basic where', async () => {
const errors = await logTable.query()
.where('level', '=', 'error')
.orderBy('timestamp', 'DESC')
.toArray();
expect(errors.length).toBeGreaterThan(0);
for (const entry of errors) {
expect(entry.level).toEqual('error');
}
});
tap.test('should query with builder - limit and offset', async () => {
const result = await logTable.query()
.orderBy('timestamp', 'DESC')
.limit(5)
.toArray();
expect(result.length).toEqual(5);
});
tap.test('should query with builder - multiple conditions', async () => {
const result = await logTable.query()
.where('service', '=', 'api')
.and('level', '=', 'info')
.orderBy('timestamp', 'DESC')
.limit(10)
.toArray();
for (const entry of result) {
expect(entry.service).toEqual('api');
expect(entry.level).toEqual('info');
}
});
tap.test('should query with builder - IN operator', async () => {
const result = await logTable.query()
.where('level', 'IN', ['error', 'info'])
.limit(10)
.toArray();
expect(result.length).toBeGreaterThan(0);
});
tap.test('should query first()', async () => {
const entry = await logTable.query()
.orderBy('timestamp', 'DESC')
.first();
expect(entry).toBeTruthy();
expect(entry.level).toBeTruthy();
});
tap.test('should query count()', async () => {
const count = await logTable.query().count();
expect(count).toBeGreaterThan(100);
});
tap.test('should get row count', async () => {
const count = await logTable.getRowCount();
expect(count).toBeGreaterThan(100);
});
tap.test('should generate SQL with toSQL()', async () => {
const sql = logTable.query()
.where('level', '=', 'error')
.orderBy('timestamp', 'DESC')
.limit(10)
.toSQL();
expect(sql).toInclude('WHERE');
expect(sql).toInclude('level');
expect(sql).toInclude('ORDER BY');
expect(sql).toInclude('LIMIT');
console.log('Generated SQL:', sql);
});
tap.test('should execute result set operations', async () => {
const resultSet = await logTable.query()
.orderBy('timestamp', 'DESC')
.limit(20)
.execute();
expect(resultSet.isEmpty()).toBeFalse();
expect(resultSet.rowCount).toEqual(20);
expect(resultSet.first()).toBeTruthy();
expect(resultSet.last()).toBeTruthy();
const filtered = resultSet.filter((row) => row.level === 'error');
expect(filtered.rows.length).toBeLessThanOrEqual(20);
const services = resultSet.map((row) => row.service);
expect(services.length).toEqual(20);
});
// ============================================================
// UPDATE (mutation)
// ============================================================
tap.test('should update rows via mutation', async () => {
// First, insert a specific row to update
await logTable.insert({
timestamp: Date.now(),
level: 'warning',
message: 'Deprecated API call',
service: 'api',
duration: 50,
});
// Update it
await logTable.update(
{ level: 'warn' },
(q) => q.where('level', '=', 'warning'),
);
// Verify: no more 'warning' level entries
const warnings = await logTable.query()
.where('level', '=', 'warning')
.toArray();
expect(warnings.length).toEqual(0);
// Verify: 'warn' entries exist
const warns = await logTable.query()
.where('level', '=', 'warn')
.toArray();
expect(warns.length).toBeGreaterThan(0);
});
// ============================================================
// DELETE (targeted)
// ============================================================
tap.test('should delete rows with targeted where clause', async () => {
const countBefore = await logTable.query()
.where('level', '=', 'warn')
.count();
expect(countBefore).toBeGreaterThan(0);
await logTable.deleteWhere(
(q) => q.where('level', '=', 'warn'),
);
const countAfter = await logTable.query()
.where('level', '=', 'warn')
.count();
expect(countAfter).toEqual(0);
});
// ============================================================
// Auto-schema evolution on typed table
// ============================================================
tap.test('should auto-evolve schema when inserting new fields', async () => {
const flexTable = await testClickhouseDb.createTable<any>({
tableName: 'flex_data',
orderBy: 'timestamp' as any,
autoSchemaEvolution: true,
});
await flexTable.insert({
timestamp: Date.now(),
message: 'first insert',
});
// Insert with a new field — should trigger schema evolution
await flexTable.insert({
timestamp: Date.now(),
message: 'second insert',
newField: 'surprise!',
count: 42,
});
const columns = await flexTable.updateColumns();
const columnNames = columns.map((c) => c.name);
expect(columnNames).toContain('newField');
expect(columnNames).toContain('count');
await flexTable.drop();
});
// ============================================================
// Raw query on db
// ============================================================
tap.test('should execute raw query via db.query()', async () => {
const result = await testClickhouseDb.query<{ cnt: string }>(
`SELECT count() as cnt FROM ${testClickhouseDb.options.database}.logs FORMAT JSONEachRow`
);
expect(result.length).toEqual(1);
expect(parseInt(result[0].cnt, 10)).toBeGreaterThan(0);
});
// ============================================================
// Cleanup
// ============================================================
tap.test('should drop the logs table', async () => {
await logTable.drop();
});
export default tap.start();