Skip to content

Commit

Permalink
neverthrowify isUntypeable
Browse files Browse the repository at this point in the history
  • Loading branch information
mmkal committed Sep 4, 2024
1 parent fa9b21b commit 1ca75b9
Show file tree
Hide file tree
Showing 4 changed files with 33 additions and 77 deletions.
11 changes: 5 additions & 6 deletions packages/typegen/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@ import {glob} from 'glob'
import * as lodash from 'lodash'
import memoizee = require('memoizee')
import * as path from 'path'

import {parseFirst, toSql} from 'pgsql-ast-parser'
import * as defaults from './defaults'
import {migrateLegacyCode} from './migrate'
import {getEnumTypes, getRegtypeToPGType, psqlClient} from './pg'
import {AnalyseQueryError, getColumnInfo, isUntypeable, removeSimpleComments} from './query'
import {AnalyseQueryError, getColumnInfo, getTypeability, removeSimpleComments} from './query'
import {getParameterTypes} from './query/parameters'
import {AnalysedQuery, DescribedQuery, ExtractedQuery, Options, QueryField, QueryParameter} from './types'
import {changedFiles, checkClean, containsIgnoreComment, globList, maybeDo, tryOrDefault} from './util'
Expand Down Expand Up @@ -233,9 +231,10 @@ export const generate = async (params: Partial<Options>) => {
}

// uses _gdesc or fallback to attain basic type information
const describeQuery = async (query: ExtractedQuery) => {//}: Promise<DescribedQuery | null> => {
if (isUntypeable(query.template)) {
return neverthrow.err(new Error(`${getLogQueryReference(query)} [!] Query is not typeable.`))
const describeQuery = async (query: ExtractedQuery) => {
const typeability = getTypeability(query.template)
if (typeability.isErr()) {
return typeability.mapErr(err => new Error(`${getLogQueryReference(query)} [!] Query is not typeable.`, {cause: err}))
}

const fieldsResult = await getFields(query)
Expand Down
52 changes: 25 additions & 27 deletions packages/typegen/src/query/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as assert from 'assert'
import {match} from 'io-ts-extra'
import * as lodash from 'lodash'
import * as pgsqlAST from 'pgsql-ast-parser'
import {QName} from 'pgsql-ast-parser'
import * as neverthrow from 'neverthrow'
import * as pluralize from 'pluralize'

import {pascalCase} from '../util'
Expand All @@ -25,34 +25,32 @@ export const templateToValidSql = (template: string[]) => template.join('null')
* - multi statements (that pgsql-ast-parser is able to process) e.g. `insert into foo(id) values (1); insert into foo(id) values (2);`
* - statements that use identifiers (as opposed to param values) e.g. `select * from ${sql.identifier([tableFromVariableName])}`
*/
// todo: change to `getTypeability` and return `neverthrow.Result<true, Error>` or something
export const isUntypeable = (template: string[]) => {
let untypeable = false
try {
const delimiter = `t${Math.random()}`.replace('0.', '')
const visitor = pgsqlAST.astVisitor(map => ({
tableRef(t) {
if (t.name === delimiter) {
untypeable = true // can only get type when delimiter is used as a parameter, not an identifier
}
export const getTypeability = (template: string[]): neverthrow.Result<true, Error> => {
const delimiter = `t${Math.random()}`.replace('0.', '')
const problems: string[] = []
const visitor = pgsqlAST.astVisitor(map => ({
tableRef(t) {
if (t.name === delimiter) {
problems.push('delimiter is used as identifier')
}

map.super().tableRef(t)
},
}))
map.super().tableRef(t)
},
}))
const safeWalk = neverthrow.fromThrowable(() => {
visitor.statement(getASTModifiedToSingleSelect(template.join(delimiter)).ast)
} catch {

// never mind?
}

// too many statements
try {
untypeable ||= pgsqlAST.parse(templateToValidSql(template)).length !== 1
} catch {
untypeable ||= templateToValidSql(template).trim().replaceAll('\n', ' ').replace(/;$/, '').includes(';')
}

return untypeable
return problems
}, err => new Error(`Walking AST failed`, {cause: err}))
return safeWalk()
.andThen(problems => problems.length === 0 ? neverthrow.ok(true as const) : neverthrow.err(new Error('Problems found:\n' + problems.join('\n'), {cause: template})))
.andThen(ok => {
const statements = pgsqlAST.parse(templateToValidSql(template))
return statements.length === 1 ? neverthrow.ok(ok) : neverthrow.err(new Error('Too many statements', {cause: template}))
})
.andThen(ok => {
const containsSemicolon = templateToValidSql(template).trim().replaceAll('\n', ' ').replace(/;$/, '').includes(';')
return containsSemicolon ? neverthrow.err(new Error('Contains semicolon', {cause: template})) : neverthrow.ok(ok)
})
}

// todo: return null if statement is not a select
Expand Down
24 changes: 1 addition & 23 deletions packages/typegen/test/limitations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,28 +380,6 @@ test('queries with semicolons are rejected', async () => {
expect(logger.warn).toMatchInlineSnapshot(`
- - >-
./test/fixtures/limitations.test.ts/queries-with-semicolons-are-rejected/index.ts:4
[!] Extracting types from query failed:
Error: Query failed with Error: Error running psql query.
Query: "update semicolon_query_table2 set col=2 returning 1; -- I love
semicolons \\\\gdesc"
Result: "psql:<stdin>:1: ERROR: relation \\"semicolon_query_table2\\" does
not exist\\nLINE 1: update semicolon_query_table2 set col=2 returning
1;\\n ^"
Error: Empty output received
Connection string:
postgresql://postgres:postgres@localhost:5432/limitations_test:
---
update semicolon_query_table2 set col=2 returning 1; -- I love semicolons
---
Try moving comments to dedicated lines. Try removing trailing semicolons, separating multi-statement queries into separate queries, using a template variable for semicolons inside strings, or ignoring this query.
[!] Query is not typeable.
`)
})
23 changes: 2 additions & 21 deletions packages/typegen/test/options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,28 +488,9 @@ test(`queries with syntax errors don't affect others`, async () => {

expect(logger.warn).toHaveBeenCalledTimes(1)
expect(logger.warn).toMatchInlineSnapshot(`
- - >+
- - >-
./test/fixtures/options.test.ts/queries-with-syntax-errors-don-t-affect-others/index.ts:4
[!] Extracting types from query failed:
Error: Query failed with Error: Error running psql query.
Query: "select this is a nonsense query which will cause an error \\\\gdesc"
Result: "psql:<stdin>:1: ERROR: syntax error at or near \\"a\\"\\nLINE 1:
select this is a nonsense query which will cause an error
\\n ^"
Error: Empty output received
Connection string:
postgresql://postgres:postgres@localhost:5432/options_test:
---
select this is a nonsense query which will cause an error
---
[!] Query is not typeable.
`)

expect(syncer.yaml()).toMatchInlineSnapshot(`
Expand Down

0 comments on commit 1ca75b9

Please sign in to comment.