Product
Introducing Socket Optimize
We're excited to introduce Socket Optimize, a powerful CLI command to secure open source dependencies with tested, optimized package overrides.
mfsqlchecker
Advanced tools
Statically validate correctness of all your SQL queries. TypeScript, PostgreSQL
Statically validate correctness of all your SQL queries. TypeScript, PostgreSQL
mfsqlchecker is a thin layer on top of node-postgres. You continue to write queries using regular SQL to interact with your PostgreSQL database, but mfsqlchecker automatically verifies that all your queries are correct, and that the result columns are of the expected type. This verification step is performed at compile time, so that you can have a high level of confidence that there will be no errors during production.
For small projects that only have a few dozen queries, using node-postgres directly works fine.
But for large projects containing hundreds of SQL queries, things start to break down to due the lack of compile-time checking, and un-ergonomic composability
mfsqlchecker helps address these issues:
The first benefit that you get from the static checking provided by mfsqlchecker is rapid feedback during development. Your queries are checked while you are writing them in your editor. Syntax errors will be immediately detected, as well as typos in table names or column names, invalid type comparisons, and any other SQL error.
Additionally, mfsqlchecker will verify that the column types returned by your query match the TypeScript types in your code. mfsqlchecker will actually automatically infer the returned column types for each query, and can generate a corresponding TypeScript shim directly inline with the query.
For example: if your SQL query returns a result from a TEXT
column in your
database, but you try to assign the result to a TypeScript variable of type
number
, then you will get a compile-time error.
The TypeScript/PostgreSQL type-checking also works in the other direction: mfsqlchecker validates that the types of the TypeScript parameters are valid based on their usage in the SQL query.
This rapid-feedback speeds up development, but more importantly, the static-checking system allows much more fluid refactoring of your code and of your database schema.
Example: You have a person table with a first_name column and a last_name column. You want to combine them into a single name column. With mfsqlchecker, simply write a migration to perform the change. Then, the tool will notify you of all of the SQL queries in your project that now fail because they reference first_name or last_name. After you correct them and have no more errors, you can be confident that your refactoring is succesfully complete. This technique scales nicely to more complex refactorings (such as extracting a one-to-one foreign key to a new many-to-many junction table, etc...)
This ease-of-refactoring is familiar to proponents of statically-typed languages, and mfsqlchecker's static checker offers the same benefits.
interface Employee {
name: string;
departmentName: string;
phoneNumber: string | null;
salary: number;
}
export async function getEmployeesWithMinSalary(conn: Connection, minSalary: number): Promise<Employee[]> {
const rows = await conn.query<{
name: Req<string>, // /---------------------------\
department_name: Req<string>, // | This code block is |
phonenumber: Opt<string>, // | generated automatically |
salary: Req<number> // \---------------------------/
}>(conn.sql
`
SELECT
employee.name,
department.name AS department_name,
phonenumber,
salary
FROM employee
JOIN department ON employee.department_id = department.id
WHERE salary >= ${minSalary}
`);
return rows.map<Employee>(row => ({
name: row.name.val(),
departmentName: row.department_name.val(),
phoneNumber: row.phonenumber.valOpt(),
salary: row.salary.val()
}));
}
When you run the mfsqlchecker tool, all of the following will be validated:
minSalary
parameter is allowed to be compared with the "salary"
column (In this case everything is good: minSalary
is a TypeScript
"number", the "salary"
column is a PostgreSQL INT
, and so the comparison
is valid).Req
(Required) or Opt
(Optional)Req
means that the column always returns a non-null value. This is
determined automatically by examining the originating table where the column
comes from, and checking if it is declared NOT NULL
.Opt
means that the column may contain a null
value. This can happen when
the originating table column may be null, or if the column is a result of
some SQL expression. If you are sure that the column does not contain null
(for example, if it is the concatenation of two non-null text columns), then
you can access it using the forceNotNull()
method (instead of valOpt()
)Composability means that when we have 2 queries that are similar, we don't want ...
Even with an experienced team and strict dicipline, it is still
Since all queries are guaranteed to ..., therefore SQL injection vulnerabilities are impossible
Because each query is known to be statically formed, we are guaranteed to get maximum effectiveness of the database's internal query cache, minimizing query parse time and query planning time.
If you don't want to use the built-in mfsqlchecker migration engine, then you
can dump your PostgreSQL database schema to an .sql file and point
mfsqlchecker
to it
To make a query call one of the following 3 methods on your connection object:
query
: For queries that can contain any number of rows(zero, one, or
more). This is usually what you want. This will return an array of "Row"
objects.
queryOne
: For queries that will always return exactly one row. This should
only be used when the structure of your query guarantees that it will always
return a single row. For example SELECT COUNT(*) FROM [..]
, or a top-level
SELECT EXISTS(..)
. This will return a single "Row" object.
queryOneOrNone
: For queries that always return either a single row, or no
rows. This should be used for queries that select a single row using based
on a unique id. If a matching row is found then returns a "Row" object,
otherwise returns null
.
... defineSqlView ...
... sqlFrag ...
When using mfsqlchecker, you use regular SQL syntax for all queries. But there is an optional dedicated syntax for "INSERT". You can still write INSERT queries as regular queries, but it is recommended to use the dedicated insert functionality because:
Here is an example "raw" INSERT query:
export async function insertEmployee(conn: Connection, employee: Employee): Promise<void> {
await conn.query(conn.sql
`
INSERT INTO employee
(salary, phonenumber, name, manager_id)
VALUES
(${employee.salary}, ${employee.phoneNumber}, ${employee.name}, NULL)
`);
}
It is difficult and error-prone to verify that the column names match up with the values. It is very easy to make a mistake by, for example, mixing the order of the phonenumber and name columns. This becomes more likely when there are lots of columns.
Here it is rewritten using mfsqlchecker insert
method:
export async function insertEmployee(conn: Connection, employee: Employee): Promise<void> {
await conn.insert("employee", {
salary: employee.salary,
phonenumber: employee.phoneNumber,
name: employee.name,
manager_id: null
});
}
Now we can more easily see each value together with its column. mfsqlchecker will still validate that the type of each value matches that of its table column, and additionally it will check that all required columns are listed.
If we need to add additional SQL to our INSERT statement, such as a
RETURNING
clause, or an ON CONFLICT
clause, we can add it as the 3rd
argument:
export async function insertEmployee(conn: Connection, employee: Employee): Promise<number> {
const row = await conn.insert<{
id: Req<EmployeeId>
}>("employee", {
salary: employee.salary,
phonenumber: employee.phoneNumber,
name: employee.name,
manager_id: null
}, conn.sql
`
RETURNING id
`);
return row.id.val();
}
NOTE: If your query contains an ON CONFLICT DO NOTHING
clause, or an ON CONFLICT DO UPDATE ... WHERE
clause, then you should use the insertMaybe()
method instead of insert()
, because it is possible for such a query to
return 0 rows.
To insert multiple rows, use the insertMany()
method (which also will
perform all of the static checks discussed earlier):
export async function insertEmployees(conn: Connection, employees: Employee[]): Promise<void> {
const vals = [];
for (const employee of employees) {
vals.push({
salary: employee.salary,
phonenumber: employee.phoneNumber,
name: employee.name,
manager_id: null
});
}
await conn.insertMany("employee", vals);
}
insertMany
also supports an optional 3rd argument, in case you need to add a
RETURNING
clause or ON CONFLICT
clause.
NOTE: insert
, insertMaybe
, and insertMany
do not support more
complicated inserts involving subqueries, or computed SQL expressions. For
those, just write a regular INSERT query (using the query
method).
... TODO ...
For the most part, if a query passes validation by mfsqlchecker's static checker, then the query is guaranteed to succeed also during runtime. But there are a few known cases where things break down.
Req
Columnsmfsqlchecker is currently unable to detect the usage of outer joins. A query containing an outer
... add a cast ...
... here is a trick that works ...
mfsqlchecker analyzes all of TypeScript source code of your project, searching for all the SQL queries. It submits each one to a temporary PostgreSQL database, as a prepared statement. This allows it to check that the query is valid (correct syntax, valid table & column names, etc...), as well as query the type of each of the returned columns.
FAQs
Statically validate correctness of all your SQL queries. TypeScript, PostgreSQL
The npm package mfsqlchecker receives a total of 17 weekly downloads. As such, mfsqlchecker popularity was classified as not popular.
We found that mfsqlchecker demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 4 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Product
We're excited to introduce Socket Optimize, a powerful CLI command to secure open source dependencies with tested, optimized package overrides.
Product
We're excited to announce that Socket now supports the Java programming language.
Security News
Socket detected a malicious Python package impersonating a popular browser cookie library to steal passwords, screenshots, webcam images, and Discord tokens.