Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

sqltyper

Package Overview
Dependencies
Maintainers
1
Versions
16
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

sqltyper - npm Package Compare versions

Comparing version 0.1.0 to 0.2.0

CHANGELOG.md

18

dist/ast.js

@@ -31,2 +31,6 @@ "use strict";

Expression.createBinaryOp = createBinaryOp;
function createTernaryOp(lhs, op, rhs1, rhs2) {
return { kind: 'TernaryOp', lhs, op, rhs1, rhs2 };
}
Expression.createTernaryOp = createTernaryOp;
function createExistsOp(subquery) {

@@ -68,2 +72,4 @@ return { kind: 'ExistsOp', subquery };

return handlers.binaryOp == null ? elseVal : handlers.binaryOp(expr);
case 'TernaryOp':
return handlers.ternaryOp == null ? elseVal : handlers.ternaryOp(expr);
case 'ExistsOp':

@@ -100,2 +106,4 @@ return handlers.existsOp == null ? elseVal : handlers.existsOp(expr);

return handlers.binaryOp(expr);
case 'TernaryOp':
return handlers.ternaryOp(expr);
case 'ExistsOp':

@@ -143,2 +151,8 @@ return handlers.existsOp(expr);

equals(a.rhs, b.lhs)));
case 'TernaryOp':
if (a.kind !== b.kind || a.op != b.op)
return false;
return (equals(a.lhs, b.lhs) &&
equals(a.rhs1, b.rhs1) &&
equals(a.rhs2, b.rhs2));
case 'ExistsOp':

@@ -250,4 +264,4 @@ if (a.kind !== b.kind)

(function (SelectBody) {
function create(selectList, from, where, groupBy) {
return { selectList, from, where, groupBy };
function create(selectList, from, where, groupBy, having) {
return { selectList, from, where, groupBy, having };
}

@@ -254,0 +268,0 @@ SelectBody.create = create;

@@ -25,2 +25,3 @@ #!/usr/bin/env node

const TaskEither = require("fp-ts/lib/TaskEither");
const function_1 = require("fp-ts/lib/function");
const pipeable_1 = require("fp-ts/lib/pipeable");

@@ -31,3 +32,3 @@ const yargs = require("yargs");

const fp_utils_1 = require("./fp-utils");
const function_1 = require("fp-ts/lib/function");
const warnings_1 = require("./warnings");
function main() {

@@ -40,2 +41,6 @@ return __awaiter(this, void 0, void 0, function* () {

}
if (args.watch && args.check) {
console.error('Cannot use --watch and --check together');
return 1;
}
const options = {

@@ -46,2 +51,3 @@ verbose: args.verbose,

pgModule: args['pg-module'],
terminalColumns: process.stdout.columns,
};

@@ -62,5 +68,14 @@ const dirPaths = [];

}
let status = 0;
if (args.watch) {
yield watchDirectories(clients.right, fileExtensions, dirPaths, options);
}
else if (args.check) {
const result = yield checkDirectories(clients.right, fileExtensions, dirPaths, options)();
if (!result.every(function_1.identity)) {
console.error(`
Some files are out of date!`);
status = 1;
}
}
else {

@@ -70,3 +85,3 @@ yield processDirectories(clients.right, fileExtensions, dirPaths, options)();

yield clients_1.disconnect(clients.right);
return 0;
return status;
});

@@ -107,2 +122,10 @@ }

})
.option('check', {
alias: 'c',
description: 'Check whether all output files are up-to-date without actually updating ' +
'them. If they are, exit with status 0, otherwise exit with status 1. ' +
'Useful for CI or pre-commit hooks.',
type: 'boolean',
default: false,
})
.option('prettify', {

@@ -173,3 +196,3 @@ alias: 'p',

case 'update':
result = pipeable_1.pipe(processSQLFile(clients, sqlFilePath, options), Task.map(tsModuleOption => pipeable_1.pipe(tsModuleOption, Option.map(tsModule => replaceOrAddTsModule(tsModule, tsModules)), Option.getOrElse(() => tsModules))));
result = pipeable_1.pipe(processSQLFile(clients, sqlFilePath, false, options), Task.map(tsModuleOption => pipeable_1.pipe(tsModuleOption, Option.map(tsModule => replaceOrAddTsModule(tsModule, tsModules)), Option.getOrElse(() => tsModules))));
break;

@@ -197,17 +220,53 @@ case 'remove':

function processDirectories(clients, fileExtensions, dirPaths, options) {
return fp_utils_1.traverseATs(dirPaths, dirPath => processDirectory(clients, dirPath, fileExtensions, options));
return mapDirectories(dirPaths, fileExtensions, filePath => processSQLFile(clients, filePath, false, options), (dirPath, tsModules) => processSQLDirectory(dirPath, tsModules, options));
}
function processDirectory(clients, dirPath, fileExtensions, options) {
return pipeable_1.pipe(findSQLFilePaths(dirPath, fileExtensions), Task.chain(filePaths => pipeable_1.pipe(fp_utils_1.traverseATs(filePaths, filePath => processSQLFile(clients, filePath, options)), Task.map(Array.filterMap(function_1.identity)))), Task.chain(modules => maybeWriteIndexModule(options.index, dirPath, modules, options.prettify)), Task.map(modules => ({ dirPath, modules })));
function checkDirectories(clients, fileExtensions, dirPaths, options) {
return mapDirectories(dirPaths, fileExtensions, filePath => processSQLFile(clients, filePath, true, options), (_dirPath, tsModules) => checkDirectoryResult(tsModules));
}
function processSQLFile(clients, filePath, options) {
function checkDirectoryResult(tsModules) {
return Task.of(tsModules.every(Option.isSome));
}
function mapDirectories(dirPaths, fileExtensions, fileProcessor, dirProcessor) {
return fp_utils_1.traverseATs(dirPaths, dirPath => mapDirectory(dirPath, fileExtensions, fileProcessor, dirProcessor));
}
function mapDirectory(dirPath, fileExtensions, fileProcessor, dirProcessor) {
return pipeable_1.pipe(findSQLFilePaths(dirPath, fileExtensions), Task.chain(filePaths => fp_utils_1.traverseATs(filePaths, fileProcessor)), Task.chain(results => dirProcessor(dirPath, results)));
}
function processSQLFile(clients, filePath, checkOnly, options) {
const tsPath = getOutputPath(filePath);
const fnName = funcName(filePath);
console.log('---------------------------------------------------------');
console.log(`${filePath} => ${tsPath}`);
return pipeable_1.pipe(Task.of(fs_1.promises.readFile(filePath)), Task.map(s => s.toString()), Task.chain(source => index_1.sqlToTS(clients, source, fnName, {
return pipeable_1.pipe(() => __awaiter(this, void 0, void 0, function* () {
console.log('---------------------------------------------------------');
if (checkOnly) {
console.log(`Checking ${filePath}`);
}
else {
console.log(`${filePath} => ${tsPath}`);
}
}), Task.chain(() => () => fs_1.promises.readFile(filePath)), Task.map(sql => sql.toString()), Task.chain(sql => index_1.sqlToStatementDescription(clients, sql)), TaskEither.map(stmt => {
if (warnings_1.hasWarnings(stmt)) {
console.warn(warnings_1.formatWarnings(stmt, options.verbose, options.terminalColumns || 78));
}
return stmt;
}), TaskEither.chain(source => index_1.generateTSCode(clients, source, fnName, {
prettierFileName: options.prettify ? tsPath : undefined,
pgModule: options.pgModule,
verbose: options.verbose,
})), TaskEither.chain(tsCode => () => fs_1.promises.writeFile(tsPath, tsCode).then(Either.right)), TaskEither.mapLeft(errorMessage => {
})), TaskEither.chain(tsCode => () => __awaiter(this, void 0, void 0, function* () {
if (checkOnly) {
let oldTsCode;
try {
oldTsCode = yield fs_1.promises.readFile(tsPath, 'utf-8');
}
catch (_err) {
oldTsCode = null;
}
if (oldTsCode != tsCode) {
return Either.left(`=> out of date`);
}
}
else {
yield fs_1.promises.writeFile(tsPath, tsCode).then(Either.right);
}
return Either.right(undefined);
})), TaskEither.mapLeft(errorMessage => {
console.error(errorMessage);

@@ -220,2 +279,6 @@ }), TaskEither.map(() => ({

}
function processSQLDirectory(dirPath, modules, options) {
const successfulModules = pipeable_1.pipe(modules, Array.filterMap(function_1.identity));
return pipeable_1.pipe(maybeWriteIndexModule(options.index, dirPath, successfulModules, options.prettify), Task.map(modules => ({ dirPath, modules })));
}
function maybeWriteIndexModule(write, dirPath, tsModules, prettify) {

@@ -222,0 +285,0 @@ const tsPath = path.join(dirPath, 'index.ts');

@@ -24,2 +24,3 @@ "use strict";

'GROUP',
'HAVING',
'ILIKE',

@@ -53,2 +54,3 @@ 'IN',

'SIMILAR',
'SYMMETRIC',
'TRUE',

@@ -65,5 +67,24 @@ 'UNION',

// name (upper case!), commutative, nullSafety
op('OR', true, 'safe'),
op('AND', true, 'safe'),
// 9.1. Logical operators
// FALSE AND NULL evaluates to NULL => unsafe. Not commutitave due
// to short-circuiting.
op('AND', false, 'unsafe'),
// TRUE OR NULL evaluates to TRUE => unsafe. Not commutitave due to
// short-circuiting.
op('OR', false, 'unsafe'),
op('NOT', null, 'safe'),
// 9.2. Comparison Functions and Operators
op('<', false, 'safe'),
op('>', false, 'safe'),
op('<=', false, 'safe'),
op('>=', false, 'safe'),
op('=', true, 'safe'),
op('<>', true, 'safe'),
op('!=', true, 'safe'),
op('BETWEEN', null, 'safe'),
op('NOT BETWEEN', null, 'safe'),
op('BETWEEN SYMMETRIC', null, 'safe'),
op('NOT BETWEEN SYMMETRIC', null, 'safe'),
op('IS DISTINCT FROM', false, 'neverNull'),
op('IS NOT DISTINCT FROM', false, 'neverNull'),
op('IS NULL', null, 'neverNull'),

@@ -73,9 +94,9 @@ op('IS NOT NULL', null, 'neverNull'),

op('NOTNULL', null, 'neverNull'),
op('LIKE', false, 'safe'),
op('<', false, 'safe'),
op('>', false, 'safe'),
op('=', true, 'safe'),
op('<>', true, 'safe'),
op('<=', false, 'safe'),
op('>=', false, 'safe'),
op('IS TRUE', null, 'neverNull'),
op('IS NOT TRUE', null, 'neverNull'),
op('IS FALSE', null, 'neverNull'),
op('IS NOT FALSE', null, 'neverNull'),
op('IS UNKNOWN', null, 'neverNull'),
op('IS NOT UNKNOWN', null, 'neverNull'),
// 9.3. Mathematical Functions and Operators
op('+', true, 'safe'),

@@ -85,13 +106,58 @@ op('-', false, 'safe'),

op('/', false, 'safe'),
op('%', false, 'safe'),
op('^', false, 'safe'),
op('|/', null, 'safe'),
op('||/', null, 'safe'),
op('!', null, 'safe'),
op('!!', null, 'safe'),
op('@', null, 'safe'),
op('&', true, 'safe'),
op('|', true, 'safe'),
op('#', true, 'safe'),
op('~', null, 'safe'),
op('<<', false, 'safe'),
op('>>', false, 'safe'),
// 9.4. String Functions and Operators
op('||', false, 'safe'),
// Not yet categorized
op('LIKE', false, 'safe'),
op('::', false, 'safe'),
op('||', false, 'safe'),
];
exports.functions = [
// name (lower case!), nullSafety
func('bool', 'safe'),
func('now', 'neverNull'),
func('count', 'neverNull'),
func('sum', 'safe'),
func('to_char', 'safe'),
// 9.4 String Functions and Operators
// 9.2. Comparison Functions and Operators
func('num_nonnulls', 'neverNull'),
func('num_nulls', 'neverNull'),
// 9.3. Mathematical Functions and Operators
func('abs', 'safe'),
func('cbrt', 'safe'),
func('ceil', 'safe'),
func('ceiling', 'safe'),
func('degrees', 'safe'),
func('div', 'safe'),
func('exp', 'safe'),
func('floor', 'safe'),
func('ln', 'safe'),
func('log', 'safe'),
func('mod', 'safe'),
func('pi', 'neverNull'),
func('power', 'safe'),
func('radians', 'safe'),
func('round', 'safe'),
func('scale', 'safe'),
func('sign', 'safe'),
func('sqrt', 'safe'),
func('trunc', 'safe'),
func('width_bucket', 'safe'),
func('random', 'safe'),
func('setseed', 'neverNull'),
func('acos', 'safe'),
func('asin', 'safe'),
func('atan', 'safe'),
func('atan2', 'safe'),
func('cos', 'safe'),
func('cot', 'safe'),
func('sin', 'safe'),
func('tan', 'safe'),
// 9.4. String Functions and Operators
func('bit_length', 'safe'),

@@ -107,3 +173,2 @@ func('char_length', 'safe'),

func('upper', 'safe'),
// ---
func('ascii', 'safe'),

@@ -113,2 +178,3 @@ func('btrim', 'safe'),

func('concat', 'neverNull'),
func('concat_ws', 'neverNull'),
func('convert', 'safe'),

@@ -149,2 +215,8 @@ func('convert_from', 'safe'),

func('translate', 'safe'),
// Not yet categorized
func('bool', 'safe'),
func('now', 'neverNull'),
func('count', 'neverNull'),
func('sum', 'safe'),
func('to_char', 'safe'),
];

@@ -151,0 +223,0 @@ /// Helpers

6

dist/describe.js

@@ -8,3 +8,3 @@ "use strict";

function describeStatement(pgClient, sql, paramNames) {
return pipeable_1.pipe(TaskEither.tryCatch(() => pgClient.query({ text: sql, describe: true }), error => describeError(error, sql)), TaskEither.map(queryResult => describeResult(sql, paramNames, queryResult)));
return pipeable_1.pipe(TaskEither.tryCatch(() => pgClient.query({ text: sql, describe: true }), error => pgErrorToString(error, sql)), TaskEither.map(queryResult => describeResult(sql, paramNames, queryResult)));
}

@@ -26,5 +26,6 @@ exports.describeStatement = describeStatement;

sql,
warnings: [],
};
}
function describeError(error, sql) {
function pgErrorToString(error, sql) {
const sourceLines = sql.split('\n');

@@ -37,2 +38,3 @@ const errorPos = error.position && findPositionInFile(Number(error.position), sourceLines);

}
exports.pgErrorToString = pgErrorToString;
function findPositionInFile(characterOffset, sourceLines) {

@@ -39,0 +41,0 @@ const init = { offset: 0, lineNo: 1, result: null };

@@ -11,13 +11,13 @@ "use strict";

const prettify_1 = require("./prettify");
function sqlToStatementDescription(clients, sql, verbose = false) {
return pipeable_1.pipe(Task.of(sql), Task.map(preprocess_1.preprocessSQL), TaskEither.chain(processed => describe_1.describeStatement(clients.pg, processed.sql, processed.paramNames)), TaskEither.chain(stmt => Task.of(codegen_1.validateStatement(stmt))), TaskEither.chain(stmt => infer_1.inferStatementNullability(clients.schema, verbose, stmt)));
function sqlToStatementDescription(clients, sql) {
return pipeable_1.pipe(Task.of(sql), Task.map(preprocess_1.preprocessSQL), TaskEither.chain(processed => describe_1.describeStatement(clients.pg, processed.sql, processed.paramNames)), TaskEither.chain(stmt => Task.of(codegen_1.validateStatement(stmt))), TaskEither.chain(stmt => TaskEither.rightTask(infer_1.inferStatementNullability(clients.schema, stmt))));
}
exports.sqlToStatementDescription = sqlToStatementDescription;
function sqlToTS(clients, sql, funcName, options) {
const { prettierFileName = null, pgModule = 'pg', verbose = false } = options || {};
return pipeable_1.pipe(sqlToStatementDescription(clients, sql, verbose), TaskEither.chain(stmt => TaskEither.rightTask(codegen_1.generateTypeScript(clients.types, pgModule, funcName, stmt))), TaskEither.chain(tsCode => prettierFileName != null
function generateTSCode(clients, stmt, funcName, options) {
const { prettierFileName = null, pgModule = 'pg' } = options || {};
return pipeable_1.pipe(TaskEither.right(stmt), TaskEither.chain(stmt => TaskEither.rightTask(codegen_1.generateTypeScript(clients.types, pgModule, funcName, stmt))), TaskEither.chain(tsCode => prettierFileName != null
? TaskEither.rightTask(() => prettify_1.runPrettier(prettierFileName, tsCode))
: TaskEither.right(tsCode)));
}
exports.sqlToTS = sqlToTS;
exports.generateTSCode = generateTSCode;
function indexModuleTS(tsModules, options) {

@@ -24,0 +24,0 @@ const { prettierFileName = null } = options || {};

@@ -16,2 +16,3 @@ "use strict";

const Option = require("fp-ts/lib/Option");
const Task = require("fp-ts/lib/Task");
const TaskEither = require("fp-ts/lib/TaskEither");

@@ -25,2 +26,3 @@ const function_1 = require("fp-ts/lib/function");

const types_1 = require("./types");
const warnings_1 = require("./warnings");
var FieldNullability;

@@ -64,30 +66,4 @@ (function (FieldNullability) {

}
function inferStatementNullability(client, verbose, statement) {
return pipeable_1.pipe(TaskEither.fromEither(parser_1.parse(statement.sql)), TaskEither.chain(astNode => pipeable_1.pipe(inferColumnNullability(client, statement, astNode), TaskEither.chain(stmt => inferParamNullability(client, stmt, astNode)), TaskEither.map(stmt => inferRowCount(stmt, astNode)))), TaskEither.orElse(parseErrorStr => {
// tslint-disable:no-console
console.warn(`
WARNING: The internal SQL parser failed to parse the SQL statement. The
inferred types may be inaccurate with respect to nullability.
`);
if (verbose) {
console.warn(`\
Parse error: ${parseErrorStr}
Please open an issue on https://github.com/akheron/sqltyper.
Include the above error message, relevant parts of your database
schema (CREATE TABLE statements, CREATE TYPE statements, etc.) and the
SQL statement that failed to parse.
Thank you in advance!
`);
}
else {
console.warn(`\
Re-run with --verbose for instructions on how to report or fix this.
`);
}
// tslint-enable:no-console
return TaskEither.right(statement);
}));
function inferStatementNullability(client, statement) {
return pipeable_1.pipe(TaskEither.fromEither(parser_1.parse(statement.sql)), TaskEither.chain(astNode => pipeable_1.pipe(inferColumnNullability(client, statement, astNode), TaskEither.chain(stmt => inferParamNullability(client, stmt, astNode)), TaskEither.map(stmt => inferRowCount(stmt, astNode)))), TaskEither.getOrElse(parseErrorStr => Task.of(warnings_1.warn('The internal SQL parser failed to parse the SQL statement.', `Parse error: ${parseErrorStr}`, statement))));
}

@@ -102,5 +78,5 @@ exports.inferStatementNullability = inferStatementNullability;

select: ({ ctes, body, setOps }) => pipeable_1.pipe(combineVirtualTables(outsideCTEs, getVirtualTablesForWithQueries(client, ctes)), TaskEither.chain(combinedCTEs => inferSetOpsOutput(client, combinedCTEs, body, setOps))),
insert: ({ table, as, returning }) => pipeable_1.pipe(getSourceColumnsForTable(client, outsideCTEs, table, as), TaskEither.chain(sourceColumns => inferSelectListOutput(client, outsideCTEs, sourceColumns, null, returning))),
update: ({ ctes, table, as, from, where, returning }) => pipeable_1.pipe(combineVirtualTables(outsideCTEs, getVirtualTablesForWithQueries(client, ctes)), TaskEither.chain(combinedCTEs => combineSourceColumns(getSourceColumnsForTableExpr(client, combinedCTEs, from), getSourceColumnsForTable(client, combinedCTEs, table, as))), TaskEither.chain(sourceColumns => inferSelectListOutput(client, outsideCTEs, sourceColumns, where, returning))),
delete: ({ table, as, where, returning }) => pipeable_1.pipe(getSourceColumnsForTable(client, outsideCTEs, table, as), TaskEither.chain(sourceColumns => inferSelectListOutput(client, outsideCTEs, sourceColumns, where, returning))),
insert: ({ table, as, returning }) => pipeable_1.pipe(getSourceColumnsForTable(client, outsideCTEs, table, as), TaskEither.chain(sourceColumns => inferSelectListOutput(client, outsideCTEs, sourceColumns, [], returning))),
update: ({ ctes, table, as, from, where, returning }) => pipeable_1.pipe(combineVirtualTables(outsideCTEs, getVirtualTablesForWithQueries(client, ctes)), TaskEither.chain(combinedCTEs => combineSourceColumns(getSourceColumnsForTableExpr(client, combinedCTEs, from), getSourceColumnsForTable(client, combinedCTEs, table, as))), TaskEither.chain(sourceColumns => inferSelectListOutput(client, outsideCTEs, sourceColumns, [where], returning))),
delete: ({ table, as, where, returning }) => pipeable_1.pipe(getSourceColumnsForTable(client, outsideCTEs, table, as), TaskEither.chain(sourceColumns => inferSelectListOutput(client, outsideCTEs, sourceColumns, [where], returning))),
});

@@ -160,6 +136,6 @@ }

function inferSelectBodyOutput(client, outsideCTEs, body) {
return pipeable_1.pipe(getSourceColumnsForTableExpr(client, outsideCTEs, body.from), TaskEither.chain(sourceColumns => inferSelectListOutput(client, outsideCTEs, sourceColumns, body.where, body.selectList)));
return pipeable_1.pipe(getSourceColumnsForTableExpr(client, outsideCTEs, body.from), TaskEither.chain(sourceColumns => inferSelectListOutput(client, outsideCTEs, sourceColumns, [body.where, body.having], body.selectList)));
}
function inferSelectListOutput(client, outsideCTEs, sourceColumns, where, selectList) {
return pipeable_1.pipe(TaskEither.right(getNonNullSubExpressions(where)), TaskEither.chain(nonNullExpressions => pipeable_1.pipe(fp_utils_1.traverseATE(selectList, item => inferSelectListItemOutput(client, outsideCTEs, sourceColumns, nonNullExpressions, item)), TaskEither.map(R.flatten))));
function inferSelectListOutput(client, outsideCTEs, sourceColumns, conditions, selectList) {
return pipeable_1.pipe(TaskEither.right(pipeable_1.pipe(conditions.map(cond => getNonNullSubExpressionsFromRowCond(cond)), Array.flatten)), TaskEither.chain(nonNullExpressions => pipeable_1.pipe(fp_utils_1.traverseATE(selectList, item => inferSelectListItemOutput(client, outsideCTEs, sourceColumns, nonNullExpressions, item)), TaskEither.map(R.flatten))));
}

@@ -230,3 +206,2 @@ function inferSelectListItemOutput(client, outsideCTEs, sourceColumns, nonNullExpressions, selectListItem) {

case 'unsafe':
case 'alwaysNull':
return anyTE(true);

@@ -244,8 +219,11 @@ case 'neverNull':

// of its operands are non-NULL
binaryOp: ({ op, lhs, rhs }) => {
switch (const_utils_1.operatorNullSafety(op)) {
binaryOp: ({ lhs, op, rhs }) => {
// AND and OR are unsafe from the result side (e.g. FALSE AND
// NULL => NULL), but if both args are non-null, then the
// result is also guaranteed to be non-null.
const nullSafety = op == 'AND' || op == 'OR' ? 'safe' : const_utils_1.operatorNullSafety(op);
switch (nullSafety) {
case 'safe':
return pipeable_1.pipe(TaskEither.right(FieldNullability.disjunction), TaskEither.ap(inferExpressionNullability(client, outsideCTEs, sourceColumns, nonNullExprs, lhs)), TaskEither.ap(inferExpressionNullability(client, outsideCTEs, sourceColumns, nonNullExprs, rhs)));
case 'unsafe':
case 'alwaysNull':
return anyTE(true);

@@ -256,2 +234,12 @@ case 'neverNull':

},
ternaryOp: ({ lhs, op, rhs1, rhs2 }) => {
switch (const_utils_1.operatorNullSafety(op)) {
case 'safe':
return pipeable_1.pipe(TaskEither.right((a) => (b) => (c) => FieldNullability.disjunction(FieldNullability.disjunction(a)(b))(c)), TaskEither.ap(inferExpressionNullability(client, outsideCTEs, sourceColumns, nonNullExprs, lhs)), TaskEither.ap(inferExpressionNullability(client, outsideCTEs, sourceColumns, nonNullExprs, rhs1)), TaskEither.ap(inferExpressionNullability(client, outsideCTEs, sourceColumns, nonNullExprs, rhs2)));
case 'unsafe':
return anyTE(true);
case 'neverNull':
return anyTE(false);
}
},
// EXISTS (subquery) never returns NULL

@@ -272,3 +260,2 @@ existsOp: () => anyTE(false),

case 'unsafe':
case 'alwaysNull':
return anyTE(true);

@@ -301,3 +288,3 @@ case 'neverNull':

}
function getNonNullSubExpressions(expression) {
function getNonNullSubExpressionsFromRowCond(expression, logicalNegation = false) {
if (expression == null) {

@@ -313,9 +300,42 @@ return [];

},
unaryOp: ({ op, operand }) => {
if (op === 'IS NOT NULL' || op === 'NOTNULL') {
// IS NOT NULL / NOTNULL promise that the operand is not null
return getNonNullSubExpressionsFromRowCond(operand, logicalNegation);
}
if (op === 'NOT') {
// Track logical negation across NOTs
return getNonNullSubExpressionsFromRowCond(operand, !logicalNegation);
}
if (const_utils_1.operatorNullSafety(op) === 'safe') {
// For safe operators, the operator must non-nullable for the
// result to evaluate to non-null
return getNonNullSubExpressionsFromRowCond(operand, logicalNegation);
}
// Otherwise, the whole expression is non-null because it must
// evaluate to true, but cannot say anything about the operands
return [expression];
},
binaryOp: ({ lhs, op, rhs }) => {
if (const_utils_1.operatorNullSafety(op) === 'safe' && op !== 'OR') {
if (op === 'AND') {
if (logicalNegation) {
// `FALSE AND NULL` evaluates to NULL => NOT (FALSE AND
// NULL) evaluates to true, so we cannot say anything about
// the right hand side!
return [...getNonNullSubExpressionsFromRowCond(lhs, logicalNegation)];
}
else {
// `a AND b` evaluates to TRUE
return [
...getNonNullSubExpressionsFromRowCond(lhs, logicalNegation),
...getNonNullSubExpressionsFromRowCond(rhs, logicalNegation),
];
}
}
if (op === 'AND' || const_utils_1.operatorNullSafety(op) === 'safe') {
// For safe operators, both sides must be non-nullable for the
// result to be non-nullable
// result to be non-nullable.
return [
...getNonNullSubExpressions(lhs),
...getNonNullSubExpressions(rhs),
...getNonNullSubExpressionsFromRowCond(lhs, logicalNegation),
...getNonNullSubExpressionsFromRowCond(rhs, logicalNegation),
];

@@ -327,11 +347,11 @@ }

},
unaryOp: ({ op, operand }) => {
if (op === 'IS NOT NULL' || op === 'NOTNULL') {
// IS NOT NULL / NOTNULL promise that the operand is not null
return getNonNullSubExpressions(operand);
}
ternaryOp: ({ lhs, op, rhs1, rhs2 }) => {
if (const_utils_1.operatorNullSafety(op) === 'safe') {
// For safe operators, the operator must non-nullable for the
// result to evaluate to non-null
return getNonNullSubExpressions(operand);
// For safe operators, all operands must be non-nullable for the
// result to be non-nullable.
return [
...getNonNullSubExpressionsFromRowCond(lhs, logicalNegation),
...getNonNullSubExpressionsFromRowCond(rhs1, logicalNegation),
...getNonNullSubExpressionsFromRowCond(rhs2, logicalNegation),
];
}

@@ -344,3 +364,3 @@ // Otherwise, the whole expression is non-null because it must

if (const_utils_1.functionNullSafety(funcName) === 'safe') {
return pipeable_1.pipe(argList, Array.map(getNonNullSubExpressions), Array.flatten);
return pipeable_1.pipe(argList, Array.map(arg => getNonNullSubExpressionsFromRowCond(arg, logicalNegation)), Array.flatten);
}

@@ -347,0 +367,0 @@ return [expression];

{
"name": "sqltyper",
"version": "0.1.0",
"version": "0.2.0",
"description": "Typed SQL queries in PostgreSQL",

@@ -17,2 +17,3 @@ "main": "dist/index.js",

"sql": "ts-node src/cli.ts --prettify --pg-module ../pg src/sql/",
"sql:check": "yarn run sql --check",
"sql:watch": "yarn run sql --watch",

@@ -27,2 +28,3 @@ "build": "tsc",

"@types/ramda": "^0.26.21",
"@types/wrap-ansi": "^3.0.0",
"@types/yargs": "^13.0.2",

@@ -50,4 +52,5 @@ "jest": "^24.9.0",

"typescript": "^3.6.2",
"wrap-ansi": "^6.0.0",
"yargs": "^14.0.0"
}
}
# sqltyper - Type your SQL queries!
[![CircleCI](https://circleci.com/gh/akheron/sqltyper.svg?style=shield)](https://circleci.com/gh/akheron/sqltyper)
SQL is a typed language. sqltyper takes raw PostgreSQL queries and

@@ -81,6 +83,5 @@ generates TypeScript functions that run those queries AND are typed

|-- ...
`-- sqls
`-- sqls/
|-- my-query.sql
|-- other-query.sql
`-- ...
`-- other-query.sql
```

@@ -91,3 +92,3 @@

```
yarn sqltyper src/sqls
yarn sqltyper --database postgres://user:pass@host/dbname src/sqls

@@ -97,2 +98,7 @@ # or npx sqltyper, or ./node_modules/.bin/sqltyper, ...

sqltyper connects to the PostgreSQL database you give in the
`--database` option, finds out the input and output types of each of
the SQL queries, and outputs the corresponding TypeScript functions in
the same directory.
You should now have the following files:

@@ -103,3 +109,3 @@ ```

|-- ...
`-- sqls
`-- sqls/
|-- index.ts

@@ -109,6 +115,10 @@ |-- my-query.sql

|-- other-query.sql
|-- other-query.ts
`-- ...
`-- other-query.ts
```
Each `.sql` file got a `.ts` file next to it. Each `.ts` file exports
a single function, whose name is the `.sql` file name with the
extension removed and camelCased. Furthermore, it generates an
`index.ts` file that re-exports all these functions.
In `app.ts`, import the SQL query functions:

@@ -179,2 +189,8 @@

`--check`, `-c`
Check whether all output files are up-to-date without actually
updating them. If they are, exit with status 0, otherwise exit with
status 1. Useful for CI or pre-commit hooks.
`--prettify`, `-p`

@@ -234,5 +250,7 @@

- [sqlτyped](https://github.com/jonifreeman/sqltyped) - a macro which infers Scala types by analysing SQL statements
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.
[sqlτyped]: https://github.com/jonifreeman/sqltyped
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc