Skip to content

Commit

Permalink
feat(client): Typed SQL (prisma#24907)
Browse files Browse the repository at this point in the history
* Add types and PrismaClient generation pieces

* First semi-working version

* Add tests and fix things

* Adapt to a new engine

* Introduce preview feature

* Update snapshots

* Restore "loaded" module

* Fix serializeRawParameters

* Fix package types

* Fix invalid JS identifiers

* Enums

* Pack "sql" files

* Fix extension

* Fix type error

* Try to fix node16

* Better identiifer name check + snapshots

* Use export that acutally exists

* Add e2e test for default import

* Watch support

* Fix edge

* Fix build

* handle column nullability

* add nullability tests & sqlite tests

* fix nullable sqlite tests

* fix bytes test

* Fix node types test

* Fix lint

* Fix DA tests

* Ensure esm import works correctly

* Fix WASM runtime

* Skip planetscale test

* Fix sqlite

* Fix linter

* Throw on unsupported provider

* Improve invalid SQL handling

* Reuse raw results

* Address review

* Update snapshots

* Fix ecosystem-tests

* Remove force flag

---------

Co-authored-by: Flavian Desverne <[email protected]>
  • Loading branch information
Serhii Tatarintsev and Weakky authored Aug 23, 2024
1 parent 2afa20b commit 8957496
Show file tree
Hide file tree
Showing 267 changed files with 3,918 additions and 363 deletions.
8 changes: 8 additions & 0 deletions packages/adapter-neon/src/conversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,13 @@ function normalize_money(money: string): string {
return money.slice(1)
}

/******************/
/* XML handling */
/******************/
function normalize_xml(xml: string): string {
return xml
}

/*****************/
/* JSON handling */
/*****************/
Expand Down Expand Up @@ -401,6 +408,7 @@ export const customParsers = {
[ArrayColumnType.BYTEA_ARRAY]: normalizeByteaArray,
[ArrayColumnType.BIT_ARRAY]: normalize_array(normalizeBit),
[ArrayColumnType.VARBIT_ARRAY]: normalize_array(normalizeBit),
[ArrayColumnType.XML_ARRAY]: normalize_array(normalize_xml),
}

// https://github.com/brianc/node-postgres/pull/2930
Expand Down
10 changes: 9 additions & 1 deletion packages/adapter-pg-worker/src/conversion.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { type ColumnType, ColumnTypeEnum } from '@prisma/driver-adapter-utils'
import * as pg from '@prisma/pg-worker'
import { parse as parseArray } from 'postgres-array'

const { types } = pg
const { builtins: ScalarColumnType, getTypeParser } = types
import { type ColumnType, ColumnTypeEnum } from '@prisma/driver-adapter-utils'

/**
* PostgreSQL array column types (not defined in ScalarColumnType).
Expand Down Expand Up @@ -322,6 +322,13 @@ function normalize_money(money: string): string {
return money.slice(1)
}

/******************/
/* XML handling */
/******************/
function normalize_xml(xml: string): string {
return xml
}

/*****************/
/* JSON handling */
/*****************/
Expand Down Expand Up @@ -402,6 +409,7 @@ export const customParsers = {
[ArrayColumnType.BYTEA_ARRAY]: normalizeByteaArray,
[ArrayColumnType.BIT_ARRAY]: normalize_array(normalizeBit),
[ArrayColumnType.VARBIT_ARRAY]: normalize_array(normalizeBit),
[ArrayColumnType.XML_ARRAY]: normalize_array(normalize_xml),
}

// https://github.com/brianc/node-postgres/pull/2930
Expand Down
10 changes: 9 additions & 1 deletion packages/adapter-pg/src/conversion.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// @ts-ignore: this is used to avoid the `Module '"<path>/node_modules/@types/pg/index"' has no default export.` error.
import { type ColumnType, ColumnTypeEnum } from '@prisma/driver-adapter-utils'
import pg from 'pg'
import { parse as parseArray } from 'postgres-array'

const { types } = pg
const { builtins: ScalarColumnType, getTypeParser } = types
import { type ColumnType, ColumnTypeEnum } from '@prisma/driver-adapter-utils'

/**
* PostgreSQL array column types (not defined in ScalarColumnType).
Expand Down Expand Up @@ -323,6 +323,13 @@ function normalize_money(money: string): string {
return money.slice(1)
}

/******************/
/* XML handling */
/******************/
function normalize_xml(xml: string): string {
return xml
}

/*****************/
/* JSON handling */
/*****************/
Expand Down Expand Up @@ -403,6 +410,7 @@ export const customParsers = {
[ArrayColumnType.BYTEA_ARRAY]: normalizeByteaArray,
[ArrayColumnType.BIT_ARRAY]: normalize_array(normalizeBit),
[ArrayColumnType.VARBIT_ARRAY]: normalize_array(normalizeBit),
[ArrayColumnType.XML_ARRAY]: normalize_array(normalize_xml),
}

// https://github.com/brianc/node-postgres/pull/2930
Expand Down
19 changes: 18 additions & 1 deletion packages/cli/src/Generate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { enginesVersion } from '@prisma/engines'
import { SqlQueryOutput } from '@prisma/generator-helper'
import {
arg,
Command,
Expand Down Expand Up @@ -27,6 +28,7 @@ import path from 'path'
import resolvePkg from 'resolve-pkg'

import { getHardcodedUrlWarning } from './generate/getHardcodedUrlWarning'
import { introspectSql, sqlDirPath } from './generate/introspectSql'
import { Watcher } from './generate/Watcher'
import { breakingChangesMessage } from './utils/breakingChanges'
import { getRandomPromotion } from './utils/handlePromotions'
Expand Down Expand Up @@ -57,6 +59,7 @@ ${bold('Options')}
--no-engine Generate a client for use with Accelerate only
--no-hints Hides the hint messages but still outputs errors and warnings
--allow-no-models Allow generating a client without models
--sql Generate typed sql module
${bold('Examples')}
Expand Down Expand Up @@ -112,6 +115,7 @@ ${bold('Examples')}
'--postinstall': String,
'--telemetry-information': String,
'--allow-no-models': Boolean,
'--sql': Boolean,
})

const postinstallCwd = process.env.PRISMA_GENERATE_IN_POSTINSTALL
Expand Down Expand Up @@ -144,6 +148,10 @@ ${bold('Examples')}
let hasJsClient
let generators: Generator[] | undefined
let clientGeneratorVersion: string | null = null
let typedSql: SqlQueryOutput[] | undefined
if (args['--sql']) {
typedSql = await introspectSql(schemaPath)
}
try {
generators = await getGenerators({
schemaPath,
Expand All @@ -152,6 +160,7 @@ ${bold('Examples')}
cliVersion: pkg.version,
generatorNames: args['--generator'],
postinstall: Boolean(args['--postinstall']),
typedSql,
noEngine:
Boolean(args['--no-engine']) ||
Boolean(args['--data-proxy']) || // legacy, keep for backwards compatibility
Expand Down Expand Up @@ -281,18 +290,26 @@ Please run \`${getCommandWithExecutor('prisma generate')}\` to see the errors.`)
} else {
logUpdate(watchingText + '\n' + this.logText)

const watcher = new Watcher(schemaResult.schemaRootDir)
const watcher = new Watcher(schemaPath)
if (args['--sql']) {
watcher.add(sqlDirPath(schemaPath))
}

for await (const changedPath of watcher) {
logUpdate(`Change in ${path.relative(process.cwd(), changedPath)}`)
let generatorsWatch: Generator[] | undefined
try {
if (args['--sql']) {
typedSql = await introspectSql(schemaPath)
}

generatorsWatch = await getGenerators({
schemaPath,
printDownloadProgress: !watchMode,
version: enginesVersion,
cliVersion: pkg.version,
generatorNames: args['--generator'],
typedSql,
})

if (!generatorsWatch || generatorsWatch.length === 0) {
Expand Down
30 changes: 30 additions & 0 deletions packages/cli/src/__tests__/commands/Generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -915,3 +915,33 @@ describe('--schema from parent directory', () => {
)
})
})

describe('with --sql', () => {
it('should throw error on invalid sql', async () => {
ctx.fixture('typed-sql-invalid')
await expect(Generate.new().parse(['--sql'])).rejects.toMatchInlineSnapshot(`
"Errors while reading sql files:
In prisma/sql/invalidQuery.sql:
Error: Error describing the query.
error returned from database: (code: 1) near "Not": syntax error
"
`)
})

it('throws error on mssql', async () => {
ctx.fixture('typed-sql-invalid-mssql')
await expect(Generate.new().parse(['--sql'])).rejects.toMatchInlineSnapshot(
`"Typed SQL is supported only for postgresql, cockroachdb, mysql, sqlite providers"`,
)
})

it('throws error on mongo', async () => {
ctx.fixture('typed-sql-invalid-mongo')
await expect(Generate.new().parse(['--sql'])).rejects.toMatchInlineSnapshot(
`"Typed SQL is supported only for postgresql, cockroachdb, mysql, sqlite providers"`,
)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
generator client {
provider = "prisma-client-js"
previewFeatures = ["typedSql"]
}

datasource db {
provider = "mongodb"
url = env("TEST_MONGO_URI")
}

model User {
id String @default(auto()) @id @map("_id") @db.ObjectId
email String @unique
name String?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
WHAT WOULD YOU EVEN WRITE FOR MONGO SQL?
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
generator client {
provider = "prisma-client-js"
previewFeatures = ["typedSql"]
}

datasource db {
provider = "sqlserver"
url = env("TEST_MSSQL_JDBC_URI")
}

model User {
id Int @default(autoincrement()) @id
email String @unique
name String?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SELECT * FROM User;
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
generator client {
provider = "prisma-client-js"
previewFeatures = ["typedSql"]
}

datasource db {
provider = "sqlite"
url = "file:./dev.db"
}

model User {
id Int @default(autoincrement()) @id
email String @unique
name String?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SELECT * FROM Not!A!Valid!Table;
57 changes: 57 additions & 0 deletions packages/cli/src/generate/introspectSql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { isValidJsIdentifier } from '@prisma/internals'
import { introspectSql as migrateIntrospectSql, IntrospectSqlError, IntrospectSqlInput } from '@prisma/migrate'
import fs from 'fs/promises'
import { bold } from 'kleur/colors'
import path from 'path'

const SQL_DIR = 'sql'

export async function introspectSql(schemaPath: string) {
const sqlFiles = await readTypedSqlFiles(schemaPath)
const introspectionResult = await migrateIntrospectSql(schemaPath, sqlFiles)
if (introspectionResult.ok) {
return introspectionResult.queries
}
throw new Error(renderErrors(introspectionResult.errors))
}

export function sqlDirPath(schemaPath: string) {
return path.join(path.dirname(schemaPath), SQL_DIR)
}

async function readTypedSqlFiles(schemaPath: string): Promise<IntrospectSqlInput[]> {
const sqlPath = path.join(path.dirname(schemaPath), SQL_DIR)
const files = await fs.readdir(sqlPath)
const results: IntrospectSqlInput[] = []
for (const fileName of files) {
const { name, ext } = path.parse(fileName)
if (ext !== '.sql') {
continue
}
const absPath = path.join(sqlPath, fileName)
if (!isValidJsIdentifier(name)) {
throw new Error(`${absPath} can not be used as a typed sql query: name must be a valid JS identifier`)
}
if (name.startsWith('$')) {
throw new Error(`${absPath} can not be used as a typed sql query: name must not start with $`)
}
const source = await fs.readFile(path.join(sqlPath, fileName), 'utf8')
results.push({
name,
source,
fileName: absPath,
})
}

return results
}

function renderErrors(errors: IntrospectSqlError[]) {
const lines: string[] = [`Errors while reading sql files:\n`]
for (const { fileName, message } of errors) {
lines.push(`In ${bold(path.relative(process.cwd(), fileName))}:`)
lines.push(message)
lines.push('')
}
return lines.join('\n')
}
18 changes: 17 additions & 1 deletion packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,19 @@
"import": "./generator-build/index.js",
"default": "./generator-build/index.js"
},
"./sql": {
"require": {
"types": "./sql.d.ts",
"node": "./sql.js",
"default": "./sql.js"
},
"import": {
"types": "./sql.d.ts",
"node": "./sql.mjs",
"default": "./sql.mjs"
},
"default": "./sql.js"
},
"./*": "./*"
},
"license": "Apache-2.0",
Expand Down Expand Up @@ -154,7 +167,10 @@
"default.d.ts",
"index-browser.js",
"extension.js",
"extension.d.ts"
"extension.d.ts",
"sql.d.ts",
"sql.js",
"sql.mjs"
],
"devDependencies": {
"@cloudflare/workers-types": "4.20240614.0",
Expand Down
1 change: 1 addition & 0 deletions packages/client/sql.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from '.prisma/client/sql'
4 changes: 4 additions & 0 deletions packages/client/sql.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
'use strict'
module.exports = {
...require('.prisma/client/sql'),
}
1 change: 1 addition & 0 deletions packages/client/sql.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from '../../.prisma/client/sql/index.mjs'
Loading

0 comments on commit 8957496

Please sign in to comment.