Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions packages/zero-cache/src/auth/write-authorizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ import {
import {simplifyCondition} from '../../../zql/src/query/expression.ts';
import type {Query} from '../../../zql/src/query/query.ts';
import {StaticQuery, staticQuery} from '../../../zql/src/query/static-query.ts';
import {
DatabaseStorage,
type ClientGroupStorage,
} from '../../../zqlite/src/database-storage.ts';
import {Database} from '../../../zqlite/src/db.ts';
import {compile, sql} from '../../../zqlite/src/internal/sql.ts';
import {
Expand All @@ -42,10 +46,6 @@ import type {LogConfig, ZeroConfig} from '../config/zero-config.ts';
import {computeZqlSpecs} from '../db/lite-tables.ts';
import type {LiteAndZqlSpec} from '../db/specs.ts';
import {StatementRunner} from '../db/statements.ts';
import {
DatabaseStorage,
type ClientGroupStorage,
} from '../../../zqlite/src/database-storage.ts';
import {mapLiteDataTypeToZqlSchemaValue} from '../types/lite.ts';
import {
getSchema,
Expand Down Expand Up @@ -433,7 +433,7 @@ export class WriteAuthorizerImpl implements WriteAuthorizer {
if (ret === undefined) {
return ret;
}
return fromSQLiteTypes(spec.zqlSpec, ret);
return fromSQLiteTypes(spec.zqlSpec, ret, op.tableName);
}

#requirePreMutationRow(op: UpdateOp | DeleteOp) {
Expand Down
6 changes: 3 additions & 3 deletions packages/zero-cache/src/services/view-syncer/snapshotter.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {LogContext} from '@rocicorp/logger';
import {assert} from '../../../../shared/src/asserts.ts';
import {stringify, type JSONValue} from '../../../../shared/src/bigint-json.ts';
import {must} from '../../../../shared/src/must.ts';
import * as v from '../../../../shared/src/valita.ts';
import {Database} from '../../../../zqlite/src/db.ts';
import {fromSQLiteTypes} from '../../../../zqlite/src/table-source.ts';
import type {LiteAndZqlSpec, LiteTableSpecWithKeys} from '../../db/specs.ts';
import {StatementRunner} from '../../db/statements.ts';
import {stringify, type JSONValue} from '../../../../shared/src/bigint-json.ts';
import {
normalizedKeyOrder,
type RowKey,
Expand Down Expand Up @@ -419,10 +419,10 @@ class Diff implements SnapshotDiff {
// Modify the values in place when converting to ZQL rows
// This is safe since we're the first node in the iterator chain.
if (prevValue) {
prevValue = fromSQLiteTypes(zqlSpec, prevValue);
prevValue = fromSQLiteTypes(zqlSpec, prevValue, table);
}
if (nextValue) {
nextValue = fromSQLiteTypes(zqlSpec, nextValue);
nextValue = fromSQLiteTypes(zqlSpec, nextValue, table);
}
return {
value: {table, prevValue, nextValue, rowKey} satisfies Change,
Expand Down
118 changes: 117 additions & 1 deletion packages/zqlite/src/table-source.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {describe, expect, test} from 'vitest';
import {testLogConfig} from '../../otel/src/test-log-config.ts';
import type {JSONValue} from '../../shared/src/json.ts';
import {createSilentLogContext} from '../../shared/src/logging-test-utils.ts';
import type {Ordering} from '../../zero-protocol/src/ast.ts';
Expand All @@ -11,10 +12,10 @@ import {Database} from './db.ts';
import {format} from './internal/sql.ts';
import {
filtersToSQL,
fromSQLiteTypes,
TableSource,
UnsupportedValueError,
} from './table-source.ts';
import {testLogConfig} from '../../otel/src/test-log-config.ts';

const columns = {
id: {type: 'string'},
Expand Down Expand Up @@ -939,3 +940,118 @@ describe('optional filters to sql', () => {
});

// TODO: Add constraint test with compound keys

describe('fromSQLiteTypes error messages', () => {
test('invalid column error includes table name', () => {
const valueTypes = {
id: {type: 'string'},
name: {type: 'string'},
} as const;

const row = {
id: '123',
name: 'test',
invalidColumn: 'oops',
};

expect(() => fromSQLiteTypes(valueTypes, row, 'users')).toThrow(
'Invalid column "invalidColumn" for table "users". Synced columns include id, name',
);
});

test('bigint out of range error includes table name and column', () => {
const db = new Database(createSilentLogContext(), ':memory:');
db.exec(
/* sql */ `CREATE TABLE test_table (id TEXT PRIMARY KEY, big_value INTEGER);`,
);
db.exec(
/* sql */ `INSERT INTO test_table (id, big_value) VALUES ('1', ${
BigInt(Number.MAX_SAFE_INTEGER) + 1n
});`,
);

const source = new TableSource(
lc,
testLogConfig,
db,
'test_table',
{
id: {type: 'string'},
big_value: {type: 'number'},
},
['id'],
);
const input = source.connect([['id', 'asc']]);

expect(() => [...input.fetch({})]).toThrow(
/value .* \(in test_table\.big_value\) is outside of supported bounds/,
);
});

test('invalid JSON error includes table name and column', () => {
const db = new Database(createSilentLogContext(), ':memory:');
db.exec(
/* sql */ `CREATE TABLE test_table (id TEXT PRIMARY KEY, json_data TEXT);`,
);
db.exec(
/* sql */ `INSERT INTO test_table (id, json_data) VALUES ('1', 'invalid json {');`,
);

const source = new TableSource(
lc,
testLogConfig,
db,
'test_table',
{
id: {type: 'string'},
json_data: {type: 'json'},
},
['id'],
);
const input = source.connect([['id', 'asc']]);

expect(() => [...input.fetch({})]).toThrow(
/Failed to parse JSON for test_table\.json_data/,
);
});

test('error cause is preserved for JSON parse errors', () => {
const db = new Database(createSilentLogContext(), ':memory:');
db.exec(
/* sql */ `CREATE TABLE test_table (id TEXT PRIMARY KEY, json_data TEXT);`,
);
db.exec(
/* sql */ `INSERT INTO test_table (id, json_data) VALUES ('1', 'not valid json');`,
);

const source = new TableSource(
lc,
testLogConfig,
db,
'test_table',
{
id: {type: 'string'},
json_data: {type: 'json'},
},
['id'],
);
const input = source.connect([['id', 'asc']]);

let caughtError: unknown;
try {
for (const _ of input.fetch({})) {
// Consume the iterator to trigger the error
}
} catch (error) {
caughtError = error;
}

expect(caughtError).toBeInstanceOf(UnsupportedValueError);
expect((caughtError as Error).message).toMatchInlineSnapshot(
`"Failed to parse JSON for test_table.json_data: Unexpected token 'o', "not valid json" is not valid JSON"`,
);
expect((caughtError as Error).cause).toMatchInlineSnapshot(
`[SyntaxError: Unexpected token 'o', "not valid json" is not valid JSON]`,
);
});
});
19 changes: 7 additions & 12 deletions packages/zqlite/src/table-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {
SchemaValue,
ValueType,
} from '../../zero-schema/src/table-schema.ts';
import type {DebugDelegate} from '../../zql/src/builder/debug-delegate.ts';
import {
createPredicate,
transformFilters,
Expand All @@ -43,7 +44,6 @@ import type {Stream} from '../../zql/src/ivm/stream.ts';
import {Database, Statement} from './db.ts';
import {compile, format, sql} from './internal/sql.ts';
import {StatementCache} from './internal/statement-cache.ts';
import type {DebugDelegate} from '../../zql/src/builder/debug-delegate.ts';

type Statements = {
readonly cache: StatementCache;
Expand Down Expand Up @@ -785,17 +785,15 @@ export function toSQLiteTypeName(type: ValueType) {
export function fromSQLiteTypes(
valueTypes: Record<string, SchemaValue>,
row: Row,
tableName?: string,
tableName: string,
): Row {
const newRow: Writable<Row> = {};
for (const key of Object.keys(row)) {
const valueType = valueTypes[key];
if (valueType === undefined) {
const columnList = Object.keys(valueTypes).sort().join(', ');
throw new Error(
tableName
? `Invalid column "${key}" for table "${tableName}". Synced columns include ${columnList}`
: `Invalid column "${key}". Synced columns include ${columnList}`,
`Invalid column "${key}" for table "${tableName}". Synced columns include ${columnList}`,
);
}
newRow[key] = fromSQLiteType(valueType.type, row[key], key, tableName);
Expand All @@ -807,7 +805,7 @@ function fromSQLiteType(
valueType: ValueType,
v: Value,
column: string,
tableName: string | undefined,
tableName: string,
): Value {
if (v === null) {
return null;
Expand All @@ -821,9 +819,7 @@ function fromSQLiteType(
if (typeof v === 'bigint') {
if (v > Number.MAX_SAFE_INTEGER || v < Number.MIN_SAFE_INTEGER) {
throw new UnsupportedValueError(
tableName
? `value ${v} (in ${tableName}.${column}) is outside of supported bounds`
: `value ${v} (in column ${column}) is outside of supported bounds`,
`value ${v} (in ${tableName}.${column}) is outside of supported bounds`,
);
}
return Number(v);
Expand All @@ -836,9 +832,8 @@ function fromSQLiteType(
const errorMessage =
error instanceof Error ? error.message : String(error);
throw new UnsupportedValueError(
tableName
? `Failed to parse JSON for ${tableName}.${column}: ${errorMessage}`
: `Failed to parse JSON for column "${column}": ${errorMessage}`,
`Failed to parse JSON for ${tableName}.${column}: ${errorMessage}`,
{cause: error},
);
}
}
Expand Down
Loading