Skip to content

Commit

Permalink
chore(common): add max column check
Browse files Browse the repository at this point in the history
  • Loading branch information
DominicGBauer committed Aug 22, 2024
1 parent be40971 commit faf1f6c
Show file tree
Hide file tree
Showing 5 changed files with 315 additions and 4 deletions.
2 changes: 1 addition & 1 deletion packages/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"rsocket-websocket-client": "1.0.0-alpha.3",
"text-encoding": "^0.7.0",
"typescript": "^5.5.3",
"vitest": "^1.5.2",
"vitest": "^2.0.5",
"web-streams-polyfill": "3.2.1"
}
}
8 changes: 7 additions & 1 deletion packages/common/src/db/schema/Table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export const DEFAULT_TABLE_OPTIONS: Partial<TableOptions> = {
localOnly: false
};

const MAX_AMOUNT_OF_COLUMNS = 63

export const InvalidSQLCharacters = /["'%,.#\s[\]]/;

export class Table {
Expand Down Expand Up @@ -103,11 +105,15 @@ export class Table {
throw new Error(`Invalid characters in view name: ${this.viewNameOverride}`);
}

if(this.columns.length >= MAX_AMOUNT_OF_COLUMNS) {
throw new Error(`Table ${this.name} has too many columns. The maximum number of columns is ${MAX_AMOUNT_OF_COLUMNS}.`);
}

const columnNames = new Set<string>();
columnNames.add('id');
for (const column of this.columns) {
const { name: columnName } = column;
if (column.name == 'id') {
if (column.name === 'id') {
throw new Error(`${this.name}: id column is automatically added, custom id columns are not supported`);
}
if (columnNames.has(columnName)) {
Expand Down
27 changes: 27 additions & 0 deletions packages/common/src/db/schema/TableV2.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ColumnType } from '../Column';
import { Index } from './Index';
import { IndexedColumn } from './IndexedColumn';
import { InvalidSQLCharacters } from './Table';

export type BaseColumnType<T extends number | string | null> = {
type: ColumnType;
Expand All @@ -18,6 +19,10 @@ const real: BaseColumnType<number | null> = {
type: ColumnType.REAL
};

// There is maximum of 127 arguments for any function in SQLite. Currently we use json_object which uses 1 arg per key (column name)
// and one per value, which limits it to 63 arguments.
const MAX_AMOUNT_OF_COLUMNS = 63;

export const column = {
text,
integer,
Expand Down Expand Up @@ -53,6 +58,8 @@ export class TableV2<Columns extends ColumnsType = ColumnsType> {
public columns: Columns,
public options: TableV2Options = {}
) {
this.validateTable(columns);

if (options?.indexes) {
this.indexes = Object.entries(options.indexes).map(([name, columns]) => {
if (name.startsWith('-')) {
Expand All @@ -69,4 +76,24 @@ export class TableV2<Columns extends ColumnsType = ColumnsType> {
});
}
}

private validateTable(columns: Columns) {
const columnNames = Object.keys(columns);
const columnLength = columnNames.length;

if (columnNames.includes('id')) {
throw new Error(`An id column is automatically added, custom id columns are not supported`);
}

if (columnLength > MAX_AMOUNT_OF_COLUMNS) {
throw new Error(`TableV2 cannot have more than ${MAX_AMOUNT_OF_COLUMNS} columns`);
}

columnNames
.map((column) => {
if (InvalidSQLCharacters.test(column)) {
throw new Error(`Invalid characters in column name: ${column}`);
}
})
}
}
86 changes: 86 additions & 0 deletions packages/common/tests/db/schema/TableV2.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { describe, it, expect } from 'vitest';
import { TableV2, column } from '../../../src/db/schema/TableV2'; // Adjust the import path as needed
import { ColumnType } from '../../../src/db/Column';

describe('TableV2', () => {
it('should create a table with valid columns', () => {
const table = new TableV2({
name: column.text,
age: column.integer,
height: column.real
});

expect(table.columns).toEqual({
name: { type: ColumnType.TEXT },
age: { type: ColumnType.INTEGER },
height: { type: ColumnType.REAL }
});
});

it('should throw an error if more than 63 columns are provided', () => {
const columns = {};
for (let i = 0; i < 64; i++) {
columns[`column${i}`] = column.text;
}

expect(() => new TableV2(columns)).toThrowError('TableV2 cannot have more than 63 columns');
});

it('should throw an error if an id column is provided', () => {
expect(() => new TableV2({
id: column.text,
name: column.text
})).toThrowError('An id column is automatically added, custom id columns are not supported');
});

it('should throw an error if a column name contains invalid SQL characters', () => {
expect(() => new TableV2({
'#invalid-name': column.text
})).toThrowError('Invalid characters in column name: #invalid-name');
});

it('should create indexes correctly', () => {
const table = new TableV2(
{
name: column.text,
age: column.integer
},
{
indexes: {
nameIndex: ['name'],
'-ageIndex': ['age']
}
}
);

expect(table.indexes).toHaveLength(2);
expect(table.indexes[0].name).toBe('nameIndex');
expect(table.indexes[0].columns[0].ascending).toBe(true);
expect(table.indexes[1].name).toBe('ageIndex');
expect(table.indexes[1].columns[0].ascending).toBe(false);
});

it('should allow creating a table with exactly 63 columns', () => {
const columns = {};
for (let i = 0; i < 63; i++) {
columns[`column${i}`] = column.text;
}

expect(() => new TableV2(columns)).not.toThrow();
});

it('should allow creating a table with no columns', () => {
expect(() => new TableV2({})).not.toThrow();
});

it('should allow creating a table with no options', () => {
const table = new TableV2({ name: column.text });
expect(table.options).toEqual({});
});

it('should correctly set options', () => {
const options = { localOnly: true, insertOnly: false, viewName: 'TestView' };
const table = new TableV2({ name: column.text }, options);
expect(table.options).toEqual(options);
});
});
Loading

0 comments on commit faf1f6c

Please sign in to comment.