Skip to content

Commit

Permalink
fewer limitattions
Browse files Browse the repository at this point in the history
  • Loading branch information
mmkal committed Sep 12, 2024
1 parent 6c02898 commit fb65c75
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 145 deletions.
42 changes: 20 additions & 22 deletions packages/typegen/src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,29 +33,27 @@ export const generate = async (inputOptions: Partial<Options>) => {
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})
Expand Down
8 changes: 2 additions & 6 deletions packages/typegen/src/pg/psql.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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})
}
}

Expand Down
47 changes: 12 additions & 35 deletions packages/typegen/src/query/column-info.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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',
Expand Down
205 changes: 123 additions & 82 deletions packages/typegen/test/limitations.test.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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:<stdin>: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:<stdin>: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,
Expand Down Expand Up @@ -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<queries.TestTable>\`
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<queries.Def>\`
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
}
}
",
}
`)
})
})

0 comments on commit fb65c75

Please sign in to comment.