From 6a812d16a78b58820e507c74fde9b316005950d7 Mon Sep 17 00:00:00 2001 From: Matt Wonlaw Date: Fri, 14 Feb 2025 15:30:56 -0500 Subject: [PATCH] feat(zero-pg): implement `makeSchemaQuery` --- packages/zero-pg/src/custom.pg-test.ts | 62 +--------------- packages/zero-pg/src/custom.ts | 9 --- packages/zero-pg/src/query.pg-test.ts | 36 ++++++++++ packages/zero-pg/src/query.ts | 98 ++++++++++++++++++++++++++ packages/zero-pg/src/test/schema.ts | 64 +++++++++++++++++ packages/zero-pg/src/web.ts | 2 +- packages/zql/src/query/static-query.ts | 6 +- 7 files changed, 205 insertions(+), 72 deletions(-) create mode 100644 packages/zero-pg/src/query.pg-test.ts create mode 100644 packages/zero-pg/src/query.ts create mode 100644 packages/zero-pg/src/test/schema.ts diff --git a/packages/zero-pg/src/custom.pg-test.ts b/packages/zero-pg/src/custom.pg-test.ts index 6b4b2ce48..76fb9ea04 100644 --- a/packages/zero-pg/src/custom.pg-test.ts +++ b/packages/zero-pg/src/custom.pg-test.ts @@ -2,47 +2,12 @@ import {testDBs} from '../../zero-cache/src/test/db.ts'; import {beforeEach, describe, expect, test} from 'vitest'; import type {PostgresDB} from '../../zero-cache/src/types/pg.ts'; -import {createSchema} from '../../zero-schema/src/builder/schema-builder.ts'; -import { - boolean, - number, - string, - table, -} from '../../zero-schema/src/builder/table-builder.ts'; + import type {DBTransaction} from './db.ts'; import {makeSchemaCRUD} from './custom.ts'; import {Transaction} from './test/util.ts'; import type {SchemaCRUD} from '../../zql/src/mutate/custom.ts'; - -const schema = createSchema(1, { - tables: [ - table('basic') - .columns({ - id: string(), - a: number(), - b: string(), - c: boolean().optional(), - }) - .primaryKey('id'), - table('names') - .from('divergent_names') - .columns({ - id: string().from('divergent_id'), - a: number().from('divergent_a'), - b: string().from('divergent_b'), - c: boolean().from('divergent_c').optional(), - }) - .primaryKey('id'), - table('compoundPk') - .columns({ - a: string(), - b: number(), - c: string().optional(), - }) - .primaryKey('a', 'b'), - ], - relationships: [], -}); +import {schema, schemaSql} from './test/schema.ts'; describe('makeSchemaCRUD', () => { let pg: PostgresDB; @@ -50,28 +15,7 @@ describe('makeSchemaCRUD', () => { beforeEach(async () => { pg = await testDBs.create('makeSchemaCRUD-test'); - await pg.unsafe(` - CREATE TABLE basic ( - id TEXT PRIMARY KEY, - a INTEGER, - b TEXT, - C BOOLEAN - ); - - CREATE TABLE divergent_names ( - divergent_id TEXT PRIMARY KEY, - divergent_a INTEGER, - divergent_b TEXT, - divergent_c BOOLEAN - ); - - CREATE TABLE "compoundPk" ( - a TEXT, - b INTEGER, - c TEXT, - PRIMARY KEY (a, b) - ); - `); + await pg.unsafe(schemaSql); crudProvider = makeSchemaCRUD(schema); }); diff --git a/packages/zero-pg/src/custom.ts b/packages/zero-pg/src/custom.ts index 92a7eb299..023c1bd1e 100644 --- a/packages/zero-pg/src/custom.ts +++ b/packages/zero-pg/src/custom.ts @@ -210,15 +210,6 @@ function makeTableCRUD(schema: TableSchema): TableCRUD { }; } -export function makeSchemaQuery( - _schema: Schema, -): (dbTransaction: DBTransaction) => SchemaQuery { - return (_dbTransaction: DBTransaction) => - // TODO: Implement this - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ({}) as any; -} - function serverName(x: {name: string; serverName?: string | undefined}) { return x.serverName ?? x.name; } diff --git a/packages/zero-pg/src/query.pg-test.ts b/packages/zero-pg/src/query.pg-test.ts new file mode 100644 index 000000000..d31f6940a --- /dev/null +++ b/packages/zero-pg/src/query.pg-test.ts @@ -0,0 +1,36 @@ +import {beforeEach, describe, expect, test} from 'vitest'; +import type {SchemaQuery} from '../../zql/src/mutate/custom.ts'; +import type {PostgresDB} from '../../zero-cache/src/types/pg.ts'; +import {schema, schemaSql, seedDataSql} from './test/schema.ts'; +import {testDBs} from '../../zero-cache/src/test/db.ts'; +import {makeSchemaQuery} from './query.ts'; +import {Transaction} from './test/util.ts'; +import type {DBTransaction} from './db.ts'; + +describe('makeSchemaQuery', () => { + let pg: PostgresDB; + let queryProvider: (tx: DBTransaction) => SchemaQuery; + + beforeEach(async () => { + pg = await testDBs.create('makeSchemaQuery-test'); + await pg.unsafe(schemaSql); + await pg.unsafe(seedDataSql); + + queryProvider = makeSchemaQuery(schema); + }); + + test('select', async () => { + await pg.begin(async tx => { + const query = queryProvider(new Transaction(tx)); + const result = await query.basic.run(); + expect(result).toEqual([{id: '1', a: 2, b: 'foo', c: true}]); + + // TODO: z2s needs to be schema-aware so it can re-map names + // const result2 = await query.names.run(); + // expect(result2).toEqual([{id: '2', a: 3, b: 'bar', c: false}]); + + const result3 = await query.compoundPk.run(); + expect(result3).toEqual([{a: 'a', b: 1, c: 'c'}]); + }); + }); +}); diff --git a/packages/zero-pg/src/query.ts b/packages/zero-pg/src/query.ts new file mode 100644 index 000000000..cdd3b4a0c --- /dev/null +++ b/packages/zero-pg/src/query.ts @@ -0,0 +1,98 @@ +import type {Schema} from '../../zero-schema/src/builder/schema-builder.ts'; +import type {SchemaQuery} from '../../zql/src/mutate/custom.ts'; +import type {DBTransaction} from './db.ts'; +import type {AST} from '../../zero-protocol/src/ast.ts'; +import type {Format} from '../../zql/src/ivm/view.ts'; +import {AbstractQuery} from '../../zql/src/query/query-impl.ts'; +import type {HumanReadable, PullRow, Query} from '../../zql/src/query/query.ts'; +import type {TypedView} from '../../zql/src/query/typed-view.ts'; +import {formatPg} from '../../z2s/src/sql.ts'; +import {compile} from '../../z2s/src/compiler.ts'; + +export function makeSchemaQuery( + schema: S, +): (dbTransaction: DBTransaction) => SchemaQuery { + class SchemaQueryHandler { + readonly #dbTransaction: DBTransaction; + constructor(dbTransaction: DBTransaction) { + this.#dbTransaction = dbTransaction; + } + + get( + target: Record< + string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Omit, 'materialize' | 'preload'> + >, + prop: string, + ) { + if (prop in target) { + return target[prop]; + } + + const q = new Z2SQuery(schema, prop, this.#dbTransaction); + target[prop] = q; + return q; + } + } + + return (dbTransaction: DBTransaction) => + new Proxy({}, new SchemaQueryHandler(dbTransaction)) as SchemaQuery; +} + +export class Z2SQuery< + TSchema extends Schema, + TTable extends keyof TSchema['tables'] & string, + TReturn = PullRow, +> extends AbstractQuery { + readonly #dbTransaction: DBTransaction; + + constructor( + schema: TSchema, + tableName: TTable, + dbTransaction: DBTransaction, + ast: AST = {table: tableName}, + format?: Format | undefined, + ) { + super(schema, tableName, ast, format); + this.#dbTransaction = dbTransaction; + } + + protected readonly _system = 'permissions'; + + protected _newQuery< + TSchema extends Schema, + TTable extends keyof TSchema['tables'] & string, + TReturn, + >( + schema: TSchema, + tableName: TTable, + ast: AST, + format: Format | undefined, + ): Query { + return new Z2SQuery(schema, tableName, this.#dbTransaction, ast, format); + } + + async run(): Promise> { + const sqlQuery = formatPg(compile(this._completeAst(), this.format)); + const result = await this.#dbTransaction.query( + sqlQuery.text, + sqlQuery.values, + ); + if (Array.isArray(result)) { + return result as HumanReadable; + } + return [...result] as HumanReadable; + } + + preload(): { + cleanup: () => void; + complete: Promise; + } { + throw new Error('Z2SQuery cannot be preloaded'); + } + + materialize(): TypedView> { + throw new Error('Z2SQuery cannot be materialized'); + } +} diff --git a/packages/zero-pg/src/test/schema.ts b/packages/zero-pg/src/test/schema.ts new file mode 100644 index 000000000..db3af75e5 --- /dev/null +++ b/packages/zero-pg/src/test/schema.ts @@ -0,0 +1,64 @@ +import {createSchema} from '../../../zero-schema/src/builder/schema-builder.ts'; +import { + boolean, + number, + string, + table, +} from '../../../zero-schema/src/builder/table-builder.ts'; + +export const schema = createSchema(1, { + tables: [ + table('basic') + .columns({ + id: string(), + a: number(), + b: string(), + c: boolean().optional(), + }) + .primaryKey('id'), + table('names') + .from('divergent_names') + .columns({ + id: string().from('divergent_id'), + a: number().from('divergent_a'), + b: string().from('divergent_b'), + c: boolean().from('divergent_c').optional(), + }) + .primaryKey('id'), + table('compoundPk') + .columns({ + a: string(), + b: number(), + c: string().optional(), + }) + .primaryKey('a', 'b'), + ], + relationships: [], +}); + +export const schemaSql = `CREATE TABLE basic ( + id TEXT PRIMARY KEY, + a INTEGER, + b TEXT, + C BOOLEAN +); + +CREATE TABLE divergent_names ( + divergent_id TEXT PRIMARY KEY, + divergent_a INTEGER, + divergent_b TEXT, + divergent_c BOOLEAN +); + +CREATE TABLE "compoundPk" ( + a TEXT, + b INTEGER, + c TEXT, + PRIMARY KEY (a, b) +);`; + +export const seedDataSql = ` +INSERT INTO basic (id, a, b, c) VALUES ('1', 2, 'foo', true); +INSERT INTO divergent_names (divergent_id, divergent_a, divergent_b, divergent_c) VALUES ('2', 3, 'bar', false); +INSERT INTO "compoundPk" (a, b, c) VALUES ('a', 1, 'c'); +`; diff --git a/packages/zero-pg/src/web.ts b/packages/zero-pg/src/web.ts index 71280fcae..a7982573e 100644 --- a/packages/zero-pg/src/web.ts +++ b/packages/zero-pg/src/web.ts @@ -11,7 +11,6 @@ import * as v from '../../shared/src/valita.ts'; import {pushBodySchema} from '../../zero-protocol/src/push.ts'; import { makeSchemaCRUD, - makeSchemaQuery, TransactionImpl, type CustomMutatorDefs, } from './custom.ts'; @@ -23,6 +22,7 @@ import { type SchemaCRUD, type SchemaQuery, } from '../../zql/src/mutate/custom.ts'; +import {makeSchemaQuery} from './query.ts'; export type PushHandler = ( headers: Headers, diff --git a/packages/zql/src/query/static-query.ts b/packages/zql/src/query/static-query.ts index 73d5745bf..3dd28c328 100644 --- a/packages/zql/src/query/static-query.ts +++ b/packages/zql/src/query/static-query.ts @@ -55,17 +55,17 @@ export class StaticQuery< } materialize(): TypedView> { - throw new Error('AuthQuery cannot be materialized'); + throw new Error('StaticQuery cannot be materialized'); } run(): Promise> { - throw new Error('AuthQuery cannot be run'); + throw new Error('StaticQuery cannot be run'); } preload(): { cleanup: () => void; complete: Promise; } { - throw new Error('AuthQuery cannot be preloaded'); + throw new Error('StaticQuery cannot be preloaded'); } }