Comparing version 0.1.0 to 0.2.0
@@ -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 |
@@ -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 |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
192492
37
4425
250
14
13
+ Addedwrap-ansi@^6.0.0
+ Addedansi-regex@5.0.1(transitive)
+ Addedansi-styles@4.3.0(transitive)
+ Addedcolor-convert@2.0.1(transitive)
+ Addedcolor-name@1.1.4(transitive)
+ Addedemoji-regex@8.0.0(transitive)
+ Addedis-fullwidth-code-point@3.0.0(transitive)
+ Addedstring-width@4.2.3(transitive)
+ Addedstrip-ansi@6.0.1(transitive)
+ Addedwrap-ansi@6.2.0(transitive)