Comparing version 0.2.0 to 0.2.1
# Changelog | ||
=========== | ||
## 0.2.1 | ||
- **Enhancements** | ||
- Infer the row count of a "calculator" SELECT statement as one | ||
- Support boolean, null and float constants | ||
- Support the CASE expression | ||
- **Bug fixes** | ||
- Treat qualified and unqualified column as equal if the column name | ||
is the same | ||
- Param is not replaced correctly if it's used more than once | ||
## 0.2.0 | ||
@@ -5,0 +17,0 @@ |
@@ -51,2 +51,6 @@ "use strict"; | ||
Expression.createArraySubQuery = createArraySubQuery; | ||
function createCase(branches, else_) { | ||
return { kind: 'Case', branches, else_ }; | ||
} | ||
Expression.createCase = createCase; | ||
function createTypeCast(lhs, targetType) { | ||
@@ -86,2 +90,4 @@ return { kind: 'TypeCast', lhs, targetType }; | ||
: handlers.arraySubQuery(expr); | ||
case 'Case': | ||
return handlers.case == null ? elseVal : handlers.case(expr); | ||
case 'TypeCast': | ||
@@ -116,2 +122,4 @@ return handlers.typeCast == null ? elseVal : handlers.typeCast(expr); | ||
return handlers.arraySubQuery(expr); | ||
case 'Case': | ||
return handlers.case(expr); | ||
case 'TypeCast': | ||
@@ -125,2 +133,7 @@ return handlers.typeCast(expr); | ||
case 'ColumnRef': | ||
// `tbl.col` and `col` in an expression context must point to | ||
// the same column. Otherwise the expression would be invalid | ||
// because of an unambiguous column reference. | ||
if (b.kind === 'TableColumnRef') | ||
return a.column === b.column; | ||
if (a.kind !== b.kind) | ||
@@ -130,2 +143,7 @@ return false; | ||
case 'TableColumnRef': | ||
// `tbl.col` and `col` in an expression context must point to | ||
// the same column. Otherwise the expression would be invalid | ||
// because of an unambiguous column reference. | ||
if (b.kind === 'ColumnRef') | ||
return a.column === b.column; | ||
if (a.kind !== b.kind) | ||
@@ -176,2 +194,8 @@ return false; | ||
return false; // TODO | ||
case 'Case': | ||
if (a.kind !== b.kind) | ||
return false; | ||
return (R.zip(a.branches, b.branches).every(([ab, bb]) => equals(ab.condition, bb.condition) && equals(ab.result, bb.result)) && | ||
((a.else_ !== null && b.else_ !== null && equals(a.else_, b.else_)) || | ||
(a.else_ === null && b.else_ === null))); | ||
case 'TypeCast': | ||
@@ -178,0 +202,0 @@ if (a.kind !== b.kind) |
@@ -12,2 +12,3 @@ "use strict"; | ||
'BY', | ||
'CASE', | ||
'CROSS', | ||
@@ -18,2 +19,4 @@ 'DEFAULT', | ||
'DISTINCT', | ||
'ELSE', | ||
'END', | ||
'EXCEPT', | ||
@@ -56,2 +59,3 @@ 'EXISTS', | ||
'SYMMETRIC', | ||
'THEN', | ||
'TRUE', | ||
@@ -63,2 +67,3 @@ 'UNION', | ||
'VALUES', | ||
'WHEN', | ||
'WHERE', | ||
@@ -65,0 +70,0 @@ 'WITH', |
@@ -270,7 +270,18 @@ "use strict"; | ||
})), | ||
case: ({ branches, else_ }) => { | ||
if (else_ === null) { | ||
// No ELSE branch => Rows that match none of the branches | ||
// will be NULL | ||
return anyTE(true); | ||
} | ||
return pipeable_1.pipe(TaskEither.right((branchNullabilities) => (elseNullability) => branchNullabilities.reduce((a, b) => FieldNullability.disjunction(a)(b), elseNullability)), TaskEither.ap(pipeable_1.pipe(branches.map(({ condition, result }) => { | ||
const nonNullExprsByCond = getNonNullSubExpressionsFromRowCond(condition); | ||
return inferExpressionNullability(client, outsideCTEs, sourceColumns, [...nonNullExprsByCond, ...nonNullExprs], result); | ||
}), fp_utils_1.sequenceATE)), TaskEither.ap(inferExpressionNullability(client, outsideCTEs, sourceColumns, nonNullExprs, else_))); | ||
}, | ||
// A type cast evaluates to NULL if the expression to be casted is | ||
// NULL. | ||
typeCast: ({ lhs }) => inferExpressionNullability(client, outsideCTEs, sourceColumns, nonNullExprs, lhs), | ||
// A constant is never NULL | ||
constant: () => anyTE(false), | ||
// NULL is the only nullable constant | ||
constant: ({ valueText }) => anyTE(valueText === 'NULL'), | ||
// A parameter can be NULL | ||
@@ -407,5 +418,13 @@ parameter: () => anyTE(true), | ||
const rowCount = ast.walk(astNode, { | ||
select: ({ limit }) => limit && limit.count && isConstantExprOf('1', limit.count) | ||
? 'zeroOrOne' // LIMIT 1 => zero or one rows | ||
: 'many', | ||
select: ({ body, setOps, limit }) => { | ||
if (setOps.length === 0 && body.from === null) { | ||
// No UNION/INTERSECT/EXCEPT, no FROM clause => one row | ||
return 'one'; | ||
} | ||
if (limit && limit.count && isConstantExprOf('1', limit.count)) { | ||
// LIMIT 1 => zero or one row | ||
return 'zeroOrOne'; | ||
} | ||
return 'many'; | ||
}, | ||
insert: ({ values, returning }) => ast.Values.walk(values, { | ||
@@ -412,0 +431,0 @@ // INSERT INTO xxx DEFAULT VALUES always creates a single row |
@@ -22,2 +22,5 @@ "use strict"; | ||
const arraySubQueryExpr = typed_parser_1.seq((_arr, subquery) => ast_1.Expression.createArraySubQuery(subquery), token_1.reservedWord('ARRAY'), parenthesized(typed_parser_1.lazy(() => select))); | ||
const caseBranch = typed_parser_1.seq((_when, condition, _then, result) => ({ condition, result }), token_1.reservedWord('WHEN'), typed_parser_1.lazy(() => expression), token_1.reservedWord('THEN'), typed_parser_1.lazy(() => expression)); | ||
const caseElse = typed_parser_1.seq(typed_parser_1.$2, token_1.reservedWord('ELSE'), typed_parser_1.lazy(() => expression)); | ||
const caseExpr = typed_parser_1.seq((_case, branch1, branches, else_, _end) => ast_1.Expression.createCase([branch1, ...branches], else_), token_1.reservedWord('CASE'), caseBranch, typed_parser_1.many(caseBranch), utils_1.optional(caseElse), token_1.reservedWord('END')); | ||
const functionArguments = parenthesized(typed_parser_1.oneOf( | ||
@@ -34,7 +37,7 @@ // func(*} means no arguments | ||
const stringConstant = typed_parser_1.seq(typed_parser_1.$2, token_1.symbolKeepWS("'"), strInner, token_1.symbol("'")); | ||
const constantExpr = typed_parser_1.seq((val, _ws) => ast_1.Expression.createConstant(val), typed_parser_1.oneOf(typed_parser_1.match('[0-9]+'), stringConstant), token_1._); | ||
const constantExpr = typed_parser_1.seq((val, _ws) => ast_1.Expression.createConstant(val), typed_parser_1.oneOf(token_1.expectIdentifier('TRUE'), token_1.expectIdentifier('FALSE'), token_1.expectIdentifier('NULL'), typed_parser_1.match('[0-9]+(\\.[0-9]+)?([eE]-?[0-9]+)?'), typed_parser_1.match('\\.[0-9]+'), stringConstant), token_1._); | ||
const parameterExpr = typed_parser_1.seq((_$, index, _ws) => ast_1.Expression.createParameter(index), token_1.symbolKeepWS('$'), typed_parser_1.int('[0-9]+'), token_1._); | ||
const parenthesizedExpr = parenthesized(typed_parser_1.lazy(() => expression)); | ||
const typeName = typed_parser_1.seq((id, arraySuffix) => id + arraySuffix, token_1.identifier, typed_parser_1.oneOf(typed_parser_1.seq(_ => '[]', token_1.symbol('['), token_1.symbol(']')), typed_parser_1.seq(_ => '', token_1._))); | ||
const primaryExpr = typed_parser_1.seq((expr, typeCast) => typeCast != null ? ast_1.Expression.createTypeCast(expr, typeCast) : expr, typed_parser_1.oneOf(arraySubQueryExpr, typed_parser_1.attempt(special_functions_1.specialFunctionCall(typed_parser_1.lazy(() => primaryExpr))), columnRefOrFunctionCallExpr, constantExpr, parameterExpr, parenthesizedExpr), utils_1.optional(typed_parser_1.seq(typed_parser_1.$2, token_1.symbol('::'), typeName))); | ||
const primaryExpr = typed_parser_1.seq((expr, typeCast) => typeCast != null ? ast_1.Expression.createTypeCast(expr, typeCast) : expr, typed_parser_1.oneOf(arraySubQueryExpr, caseExpr, typed_parser_1.attempt(special_functions_1.specialFunctionCall(typed_parser_1.lazy(() => primaryExpr))), columnRefOrFunctionCallExpr, constantExpr, parameterExpr, parenthesizedExpr), utils_1.optional(typed_parser_1.seq(typed_parser_1.$2, token_1.symbol('::'), typeName))); | ||
function makeUnaryOp(oper, nextExpr) { | ||
@@ -41,0 +44,0 @@ return typed_parser_1.seq((ops, next) => ops.length > 0 |
@@ -25,3 +25,3 @@ "use strict"; | ||
function expectIdentifier(ident) { | ||
return typed_parser_1.seq(typed_parser_1.$1, typed_parser_1.attempt(typed_parser_1.map((match, toError) => match.toLowerCase() !== ident.toLowerCase() | ||
return typed_parser_1.seq(_ => ident, typed_parser_1.attempt(typed_parser_1.map((match, toError) => match.toLowerCase() !== ident.toLowerCase() | ||
? toError(`Expected ${ident}, got ${match}`) | ||
@@ -28,0 +28,0 @@ : ident, exports.matchIdentifier)), exports._); |
@@ -42,3 +42,3 @@ "use strict"; | ||
for (const [name, index] of paramIndices) { | ||
mangledSQL = mangledSQL.replace('${' + name + '}', '$' + index); | ||
mangledSQL = mangledSQL.replace(new RegExp('\\$\\{' + name + '\\}', 'g'), '$' + index); | ||
} | ||
@@ -45,0 +45,0 @@ // Iterating a Map is guaranteed to yield in the insertion order |
{ | ||
"name": "sqltyper", | ||
"version": "0.2.0", | ||
"version": "0.2.1", | ||
"description": "Typed SQL queries in PostgreSQL", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
@@ -5,6 +5,11 @@ # sqltyper - Type your SQL queries! | ||
SQL is a typed language. sqltyper takes raw PostgreSQL queries and | ||
generates TypeScript functions that run those queries AND are typed | ||
correctly, based on the database schema. | ||
SQL is a typed language, but most solutions for using an SQL database | ||
from typed languages don't make use of that typing information in a | ||
way that would actually help you catch bugs during development. | ||
**sqltyper** takes raw PostgreSQL queries and generates TypeScript | ||
functions that run those queries AND are typed correctly, based on the | ||
database schema. This makes it possible for the TypeScript compiler to | ||
find bugs in your code that interacts with an SQL database. | ||
For example, given the following schema: | ||
@@ -15,3 +20,3 @@ | ||
name text NOT NULL, | ||
age integer NOT NULL, | ||
age integer, | ||
shoe_size integer | ||
@@ -24,3 +29,6 @@ ) | ||
```sql | ||
SELECT initcap(name) as name_capitalized, age, shoe_size | ||
SELECT | ||
initcap(name) as name_capitalized, | ||
age, | ||
shoe_size | ||
FROM person | ||
@@ -131,6 +139,6 @@ WHERE | ||
These functions a `Client` or `Pool` from [node-postgres] as the first | ||
argument and possible query parameters as the second parameter. | ||
These functions take a `Client` or `Pool` from [node-postgres] as the | ||
first argument, and possible query parameters as the second parameter. | ||
It will return one of the following, wrapped in a `Promise`: | ||
They will return one of the following, wrapped in a `Promise`: | ||
@@ -142,6 +150,6 @@ - An array of result objects, with object keys corresponding to output | ||
- A single result row or `null` if the query only ever returns zero or | ||
one row (e.g. `SELECT` query with `LIMIT 1`). | ||
- A single result object or `null` if the query only ever returns zero | ||
or one row (e.g. `SELECT` query with `LIMIT 1`). | ||
- A number which denotes the number of affected rows (`INSERT`, | ||
- A number which denotes the number of affected rows (e.g. `INSERT`, | ||
`UPDATE` or `DELETE` without a `RETURNING` clause). | ||
@@ -172,4 +180,4 @@ | ||
Database URI to connect to, e.g. `-d postgres://user:pass@localhost:5432/mydb`. | ||
By default, uses the [connecting logic] of node-postgres that | ||
relies on environment variables. | ||
If not given, uses the [connecting logic] of node-postgres that relies | ||
on [libpq environment variables]. | ||
@@ -194,7 +202,7 @@ `--ext`, `-e` | ||
updating them. If they are, exit with status 0, otherwise exit with | ||
status 1. Useful for CI or pre-commit hooks. | ||
status 1. Useful for CI or pre-commit hooks. Default: `false`. | ||
`--prettify`, `-p` | ||
Apply `prettier` to output TypeScript files. `prettier` must be | ||
Apply [prettier] to generated TypeScript files. [prettier] must be | ||
installed and configured for your project. Default: `false`. | ||
@@ -212,2 +220,4 @@ | ||
[connecting logic]: https://node-postgres.com/features/connecting | ||
[libpq environment variables]: https://www.postgresql.org/docs/current/libpq-envars.html | ||
[prettier]: https://prettier.io/ | ||
@@ -252,7 +262,7 @@ | ||
The main motivator for sqltyper was [sqlτyped] by @jonifreeman. It | ||
does more or less the same as sqltyper, but for Scala, and meant to be | ||
used with MySQL. It uses JDBC, and is implemented as a Scala macro | ||
rather than an offline code generation tool. | ||
The main motivator for sqltyper was [sqlτyped] by Joni Freeman. It | ||
does more or less the same as sqltyper, but for Scala, and is designed | ||
to be used with MySQL. It uses JDBC, and is implemented as a Scala | ||
macro rather than an offline code generation tool. | ||
[sqlτyped]: https://github.com/jonifreeman/sqltyped |
196710
4477
260