diff --git a/packages/typegen/src/generate.ts b/packages/typegen/src/generate.ts index 69c99319..69a45983 100644 --- a/packages/typegen/src/generate.ts +++ b/packages/typegen/src/generate.ts @@ -33,29 +33,27 @@ export const generate = async (inputOptions: Partial) => { url.searchParams.set('options', optionsParams.toString()) connectionString = url.toString() } + assert.ok(!/"/.test(connectionString), `Connection strings with quotes must be escaped: ${connectionString}`) const {psql} = psqlClient(`${options.psqlCommand} "${connectionString}"`) - return ( - neverthrow - .ok(inputSql) - .map(sql => sql.trim().replace(/;$/, '')) - // .andThen(sql => (sql.includes(';') ? neverthrow.ok(removeSimpleComments(sql)) : neverthrow.ok(sql))) - .asyncAndThen(simplified => { - const simplifiedCommand = `${simplified} \\gdesc` - return neverthrow.fromPromise( - psql(simplifiedCommand), // - err => { - let message = `psql failed.` - if ((err as Error).message.includes('psql: command not found')) { - message += ` If you're using docker, try using \`--psql 'docker-compose exec -T postgres psql'\`.` - } - if (searchPath) { - message += `\n\nNote: search path was set to ${searchPath}. Connection string used: ${connectionString}` - } - return new Error(message, {cause: err}) - }, - ) - }) - ) + return neverthrow + .ok(inputSql) + .map(sql => sql.trim().replace(/;$/, '')) + .asyncAndThen(simplified => { + const simplifiedCommand = `${simplified} \\gdesc` + return neverthrow.fromPromise( + psql(simplifiedCommand), // + err => { + let message = `psql failed.` + if ((err as Error).message.includes('psql: command not found')) { + message += ` If you're using docker, try using \`--psql 'docker-compose exec -T postgres psql'\`.` + } + if (searchPath) { + message += `\n\nNote: search path was set to ${searchPath}. Connection string used: ${connectionString}` + } + return new Error(message, {cause: err}) + }, + ) + }) } // const psql = memoizee(_psql, {max: 1000}) diff --git a/packages/typegen/src/pg/psql.ts b/packages/typegen/src/pg/psql.ts index bb8eb619..6f796ebd 100644 --- a/packages/typegen/src/pg/psql.ts +++ b/packages/typegen/src/pg/psql.ts @@ -1,5 +1,4 @@ import * as assert from 'assert' -import {simplifyWhitespace} from '../util' /** * Get a basic postgres client. which can execute simple queries and return row results. @@ -13,16 +12,13 @@ export const psqlClient = (psqlCommand: string) => { const psql = async (query: string) => { const {default: execa} = await import('execa') - query = simplifyWhitespace(query) - // eslint-disable-next-line no-template-curly-in-string - const echoQuery = 'echo "${TYPEGEN_QUERY}"' - const command = `${echoQuery} | ${psqlCommand} -f -` + const command = `echo "$TYPEGEN_QUERY" | ${psqlCommand} -f -` const result = await execa('sh', ['-c', command], {env: {TYPEGEN_QUERY: query}}) try { return psqlRows(result.stdout) } catch (e: unknown) { const stdout = result.stdout + result.stderr - throw new Error(`Error running psql query ${JSON.stringify(stdout)}`, {cause: e}) + throw new Error(`Error running psql query. Output:\n${JSON.stringify(stdout)}`, {cause: e}) } } diff --git a/packages/typegen/src/query/column-info.ts b/packages/typegen/src/query/column-info.ts index 87594e13..0f1873bf 100644 --- a/packages/typegen/src/query/column-info.ts +++ b/packages/typegen/src/query/column-info.ts @@ -1,4 +1,5 @@ import {Client, sql, Transactable} from '@pgkit/client' +import * as assert from 'assert' import {createHash} from 'crypto' import * as lodash from 'lodash' @@ -280,28 +281,18 @@ export const analyzeAST = async ( if (viewsWeNeedToAnalyzeFirst.size > 0) { for (const [viewName, result] of viewsWeNeedToAnalyzeFirst) { - if (!result.underlying_view_definition) { - throw new Error( - `View ${viewName} has no underlying view definition: ${JSON.stringify({viewName, result}, null, 2)}`, - ) - } + assert.ok( + result.underlying_view_definition, + `View ${viewName} has no underlying view definition: ${JSON.stringify({viewName, result}, null, 2)}`, + ) const [statement, ...rest] = parse(result.underlying_view_definition) - // if (selectStatementSql === toSql.statement(statement.ast)) { - // throw new Error( - // `Circular view dependency detected: ${selectStatementSql} depends on ${result.underlying_view_definition}`, - // ) - // } - if (statement?.type !== 'select') { - throw new Error(`Expected a select statement, got ${statement?.type}`) - } - if (rest.length > 0) { - throw new Error(`Expected a single select statement, got ${result.underlying_view_definition}`) - } + assert.ok(statement?.type === 'select', `Expected a select statement, got ${statement?.type}`) + assert.ok( + rest.length === 0, + + `Expected a single select statement, got ${result.underlying_view_definition}`, + ) const analyzed = await analyzeAST({fields: []}, tx, statement, regTypeToTypeScript) - // await insertPrerequisites(tx, schemaName, analyzed, statement.ast, { - // tableAlias: viewName, - // source: 'view', - // }) await insertTempTable(tx, { tableAlias: viewName, fields: analyzed, @@ -606,7 +597,7 @@ const generateTagOptions = (query: DescribedQuery) => { .filter(part => !['query', 'result'].includes(part)) .join('-'), ) - .map(lodash.flow(lodash.camelCase, lodash.upperFirst)) + .map(s => lodash.upperFirst(lodash.camelCase(s))) .map((_, i, arr) => arr.slice(0, i + 1).join('_')) .filter(Boolean) @@ -635,20 +626,6 @@ const generateTags = (query: DescribedQuery) => { return tags } -/** - * Create a fallback, in case we fail to analyse the query - */ -const getDefaultAnalysedQuery = (query: DescribedQuery): AnalysedQuery => ({ - ...query, - suggestedTags: generateTags(query), - fields: query.fields.map(f => ({ - ...f, - nullability: 'unknown', - comment: undefined, - column: undefined, - })), -}) - const nonNullableExpressionTypes = new Set([ 'integer', 'numeric', diff --git a/packages/typegen/test/limitations.test.ts b/packages/typegen/test/limitations.test.ts index 69a4666d..7c386647 100644 --- a/packages/typegen/test/limitations.test.ts +++ b/packages/typegen/test/limitations.test.ts @@ -1,5 +1,5 @@ import * as fsSyncer from 'fs-syncer' -import {test, beforeEach, expect, vi as jest} from 'vitest' +import {test, beforeEach, expect, vi as jest, describe} from 'vitest' import * as typegen from '../src' import {getPureHelper as getHelper} from './helper' @@ -228,87 +228,6 @@ test('simple', async () => { `) }) -test('queries with comments are modified', async () => { - const syncer = fsSyncer.testFixture({ - expect, - targetState: { - 'index.ts': ` - import {sql} from '@pgkit/client' - - export default sql\` - select - 1 as a, -- comment - -- comment - 2 as b, - '--' as c, -- comment - id - from - -- comment - test_table -- comment - \` - `, - }, - }) - - syncer.sync() - const before = syncer.read() - - await typegen.generate(typegenOptions(syncer.baseDir)) - - expect(logger.warn).toHaveBeenCalled() - expect(logger.warn).toMatchInlineSnapshot(` - - - > - Error: - ./test/fixtures/limitations.test.ts/queries-with-comments-are-modified/index.ts:3 - [!] Extracting types from query failed. Try moving comments to dedicated - lines. - Caused by: Error: psql failed. - Caused by: Error: Error running psql query "psql::1: ERROR: syntax error at end of input\\nLINE 1: ..., -- comment id from -- comment test_table -- comment \\\\gdesc\\n ^" - Caused by: AssertionError [ERR_ASSERTION]: Empty output received - `) - - expect(syncer.read()).toEqual(before) // no update expected -}) - -test('queries with complex CTEs and comments fail with helpful warning', async () => { - const syncer = fsSyncer.testFixture({ - expect, - targetState: { - 'index.ts': ` - import {sql} from '@pgkit/client' - - export default sql\` - with abc as ( - select table_name -- comment - from information_schema.tables - ), - def as ( - select table_schema - from information_schema.tables, abc - ) - select * from def - \` - `, - }, - }) - - syncer.sync() - - await typegen.generate(typegenOptions(syncer.baseDir)) - - expect(logger.warn).toHaveBeenCalled() - expect(logger.warn).toMatchInlineSnapshot(` - - - > - Error: - ./test/fixtures/limitations.test.ts/queries-with-complex-ctes-and-comments-fail-with-helpful-warning/index.ts:3 - [!] Extracting types from query failed. Try moving comments to dedicated - lines. - Caused by: Error: psql failed. - Caused by: Error: Error running psql query "psql::1: ERROR: syntax error at end of input\\nLINE 1: ...om information_schema.tables, abc ) select * from def \\\\gdesc\\n ^" - Caused by: AssertionError [ERR_ASSERTION]: Empty output received - `) -}) - test('queries with semicolons are rejected', async () => { const syncer = fsSyncer.testFixture({ expect, @@ -342,3 +261,125 @@ test('queries with semicolons are rejected', async () => { Caused by: update semicolon_query_table2 set col=2 returning 1; -- I love semicolons `) }) + +describe('no longer limitations', () => { + test('queries with inline comments work', async () => { + const syncer = fsSyncer.testFixture({ + expect, + targetState: { + 'index.ts': ` + import {sql} from '@pgkit/client' + + export default sql\` + select + 1 as a, -- comment + -- comment + 2 as b, + '--' as c, -- comment + id + from + -- comment + test_table -- comment + \` + `, + }, + }) + + syncer.sync() + + await typegen.generate(typegenOptions(syncer.baseDir)) + + expect(logger.warn).not.toHaveBeenCalled() + + expect(syncer.read()).toMatchInlineSnapshot(` + { + "index.ts": "import {sql} from '@pgkit/client' + + export default sql\` + select + 1 as a, -- comment + -- comment + 2 as b, + '--' as c, -- comment + id + from + -- comment + test_table -- comment + \` + + export declare namespace queries { + // Generated by @pgkit/typegen + + /** - query: \`select 1 as a, -- comment -- comment 2 as b, '--' as c, -- comment id from -- comment test_table -- comment\` */ + export interface TestTable { + /** column: \`public.test_table.id\`, not null: \`true\`, regtype: \`integer\` */ + id: number + } + } + ", + } + `) + }) + + test('queries with complex CTEs and comments fail with helpful warning', async () => { + const syncer = fsSyncer.testFixture({ + expect, + targetState: { + 'index.ts': ` + import {sql} from '@pgkit/client' + + export default sql\` + with abc as ( + select table_name -- comment + from information_schema.tables + ), + def as ( + select table_schema + from information_schema.tables, abc + ) + select * from def + \` + `, + }, + }) + + syncer.sync() + + await typegen.generate(typegenOptions(syncer.baseDir)) + + expect(logger.warn).not.toHaveBeenCalled() + + expect(syncer.read()).toMatchInlineSnapshot(` + { + "index.ts": "import {sql} from '@pgkit/client' + + export default sql\` + with abc as ( + select table_name -- comment + from information_schema.tables + ), + def as ( + select table_schema + from information_schema.tables, abc + ) + select * from def + \` + + export declare namespace queries { + // Generated by @pgkit/typegen + + /** - query: \`with abc as ( select table_name -- comme... [truncated] ...n_schema.tables, abc ) select * from def\` */ + export interface Def { + /** + * From CTE subquery "def", column source: information_schema.tables.table_schema + * + * column: \`✨.def.table_schema\`, regtype: \`information_schema.sql_identifier\` + */ + table_schema: string | null + } + } + ", + } + `) + }) +})