Save the overhead working with SQL and structured data - from the time and effort spent figuring out relational queries to the labour managing schemas! Try a modern, simplistic take on SQL and databases in general!
Linked QL is a database query client that simplfies how you interact with your database and manage your schemas.
š„ Takes the ORM and friends out of the way and let's you just write SQL, but SQL that you will actually enjoy. (Linked QL extends standard SQL with new syntax sugars that let you write relational queries in less than half the code and without a single JOIN clause in most cases.)
ā”ļø Takes the process out of schema management and lets you just ALTER away your DB, but in a safety net. (Linked QL extends your DB behind the scenes to automatically version each edit you make and have them kept as "savepoints" that you can always rollback to.)
š„ Brings the "schema-as-code" practice to its true meaning and essentially lets you have your entire DB structure go in a single schema.json file that you edit in-place, as against the "hundreds of migration files" experience. (Linked QL essentially rewrites your "migrations" experience.)
It comes as a small library and is usable over your DB of choice - from the server-side Postgres, mariadb and MySQL, to the client-side IndexedDB, to the plain JSON object!
Obtain the Linked QL client for your target database:
For SQL databases, install the regular SQL client you use for your DB. (Typically, pg for PostgreSQL, mariadb for mariadb, mysql/mysql2 for MySQL databases.)
Using PostgreSQL as an example, install the pg client:
npm install pg
Use Linked QL as a wrapper over that:
// Import pg and LinkedQlimport pg from'pg';
importLinkedQlfrom'@linked-db/linked-ql/sql';
// Connect pgconst pgClient = new pg.Client({
host: 'localhost',
port: 5432,
});
await pgClient.connect();
// Use LinkedQl as a wrapper over thatconst client = newLinkedQl(pgClient, { dialect: 'postgres' });
Sample setup for mariadb
Note that your mariadb database must be v10.3 or higher. (MySQL v8 comparably.) In addition, Linked QL needs to be able to run multiple statements in one query. The multipleStatements connector parameter below is thus required. We also need to have the bitOneIsBoolean parameter in place.
// Import mariadb and LinkedQlimport mariadb from'mariadb';
importLinkedQlfrom'@linked-db/linked-ql/sql';
// Connect pgconst myConnection = await mariadb.createConnection({
host: '127.0.0.1',
user: 'root',
port: 3306,
// -------multipleStatements: true, // RequiredbitOneIsBoolean: true, // The default, but requiredtrace: true, // Recommended
});
// Use LinkedQl as a wrapper over thatconst client = newLinkedQl(myConnection, { dialect: 'mysql' });
For the client-side IndexedDB, import and instantiate the IDB client. (Coming soon)
// Import IDB as LinkedQlimportLinkedQlfrom'@linked-db/linked-ql/idb';
// Create an instance.const client = newLinkedQl;
To work with Linked QL's in-memory object database, import and instantiate the ODB client. (Coming soon)
// Import ODB as LinkedQlimportLinkedQlfrom'@linked-db/linked-ql/odb';
// Create an instance.constLinkedQlClient = newLinkedQl;
All client instances above implement the same interface:
client.query('SELECT fname, lname FROM users WHERE role = $1', { values: ['admin'] }).then(result => {
console.log(result);
});
const result = await client.query('SELECT fname, lname FROM users WHERE role = $1', { values: ['admin'] });
console.log(result);
Other APIs are covered right in The Linked QL API section. You'll find that, in addition to writing pure SQL, you can also programmatically compose queries if you want; an example being the client.createDatabase() API for a CREATE DATABASE statement.
Introducing Magic Paths
š„ Express relationships graphically.
JOINS can be good but can be a mess as they almost always obfuscate your entire query! But what if you didn't have to write JOINS to express certain relationships?
Meet Linked QL's magic path operators, a syntax extension to SQL, that lets you connect to columns on other tables without writing a single JOIN. Linked QL uses heuristics on your DB structure to figure out the details and the relevant JOINs behind the scenes.
Where you normally would write...
-- Regular SQLSELECT title, users.fname AS author_name FROM posts
LEFTJOIN users ON users.id = posts.author
Linked QL lets you draw a path to express the relationship:
-- Linked QLSELECT title, author ~> fname AS author_name FROM posts
And here's a scenario showing a typical schema and an example query each:
-- The users tableCREATETABLE users (
id intprimary key generated always asidentity,
title varchar,
name varchar,
role intreferences roles (id),
created_time timestamp
);
-- The books tableCREATETABLE books (
id intprimary key generated always asidentity,
title varchar,
content varchar,
author intreferences users (id),
created_time timestamp
);
-- Regular SQLSELECT book.id, book.title, content, book.created_time, user.id AS author_id, user.title AS author_title, user.name AS author_name
FROM books AS book LEFTJOIN users ASuserON user.id = book.author
-- Linked QLSELECT id, title, content, created_time, author ~> id, author ~> title, author ~> name
FROM books
āØ PRO: About 50% code, and whole namespacing exercise, now eliminated; all with zero upfront setup!
Taking that further, paths can be multi-level:
-- Linked QLSELECT*FROM books
WHERE author ~> role ~> codename ='admin'
and they can also be used to express the relationships in the reverse direction (many-to-one):
-- Linked QLSELECT*FROM users
WHERE author <~ books ~> title ='Beauty and the Beast'
(Now pivot/junction/link tables get an easier way!)
Introducing Auto-Versioning
ā”ļø Create, Alter, and Drop schemas without needing to worry about versioning.
Databases have historically lacked the concept of versioning, and that has seen all of the engineering work pushed down to the client application. If you've ever had to adopt a special process for defining and managing your schemas, wherein changes are handled through serially-named files within your application, each written as an UP/DOWN pair of actions, and in all supported by tooling...
then you've faced the problem that this defeciency in databases creates! But what if databases magically got to do the heavy lifting?
Meet Linked QL's little addition to your database that does exactly that and lets you alter your DB carefree, but in the safety net of some behind-the-scenes magic that snapshots your schema before each alteration! Meet Automatic Schema Savepoints and Rollbacks!
You:
// Alter schemaconst savepoint = await client.query('CREATE TABLE public.users (id int, name varchar)', {
description: 'Create users table',
});
Linked QL:
// A savepoint automatically created for youconsole.log(savepoint.description); // Create users tableconsole.log(savepoint.versionTag); // 1console.log(savepoint.savepointDate); // 2024-07-17T22:40:56.786Z
āØ PRO: Whole engineering work now essentially moved over to the DB where it rightly belongs; all with zero upfront setup!
Taking that further, you get a nifty rollback button should you want to:
// Rollback all associated changes (Gets the users table dropped)await savepoint.rollback();
and you can go many levels back:
// Rollback to public@3let savepoint;
while(savepoint = await client.database('public').savepoint()) {
await savepoint.rollback();
if (savepoint.versionTag === 3) break;
}
and you can "undo" a rollback, or in other words, roll forward to a point in time:
// "Undo" the last rollback (Gets the users table re-created)let savepoint = await client.database('public').savepoint({ direction: 'forward' });
await savepoint.rollback();
You essentially are able to go back in time or forward in time as randomly as iteration may demand.
Re-Introducing Schema-as-Code with schema.json
š„ Have your entire DB structure live in a single schema.json (or schema.yml) file that you edit in-place!
With schema versioning now over to the database, much of the old conventions and formalities should now be irrelevant. We found that we could essentially streamline the whole "database" footprint from spanning hundreds of migration files to fitting into a single schema.json (or schema.yml) file!
{
"name": "users", // or something like ['db1', 'tbl1'] which would translate to db1.tbl1"columns": [], // Column objects (minimum of 1)"constraints": [], // Constraint objects"indexes": [] // Index objects
}
Now, if you had that somewhere in your application, say at ./database/schema.json, Linked QL could help keep it in sync both ways with your database:
you add or remove a database object or table object or column object... and it is automatically reflected in your DB structure at the click of a command: linkedql migrate
your colleague makes new changes from their codebase... and it is automatically reflected in your local copy at your next git pull, or at the click of a command: linkedql refresh
š„ You also get to see a version number on each database object in your schema essentially incrementing on each migrate operation (whether by you or by colleague), and decrementing on each rollback operation (whether by you or by colleague).
Thanks to a DB-native schema version control system, no need to maintain past states, or risk losing them; the DB now becomes the absolute source of truth for both itself and its client applications, as against the other way around. (You may want to see how that brings us to true "Schema as Code" in practice.)
To setup:
Make a directory within your application for database concerns. Linked QL will want to look in ./database, but you will be able to point to your preferred location when running Linked QL commands.
Have a driver.js file in that directory that has a default export function that returns a Linked QL instance. This will be imported and used by Linked QL to interact with your database. This could look something like:
Have your DB structure defined in a schema.json (or schema.yml) file in that directory. (See schema.json above for a guide.)
You can always extend your schema with new objects, and you can always drop objects or edit them in-place. For an existing database, table, column, constraint, or index, names may be changed, but not in-place! A "rename" operation is done with the addition of a temporary $name attribute:
{
"name": "old_name",
"$name": "new_name"
}
The old name being in place is needed to find the target during migration. The temporary $name attribute automatically disappears after new name has been picked up by Linked QL at next linkedql migrate.
To run:
Use linkedql migrate to walk through your staged local changes and interactively perform a migration against your database.
Use linkedql rollback to walk through the latest savepoint at each database and interactively perform a rollback.
Use linkedql leaderboard to just view the latest savepoint at each database.
A query client that extends standard SQL with new syntax sugars and enables auto-versioning capabilities on any database
The npm package @linked-db/linked-ql receives a total of 6 weekly downloads. As such, @linked-db/linked-ql popularity was classified as not popular.
We found that @linked-db/linked-ql demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago.Ā It has 0 open source maintainers collaborating on the project.
Package last updated on 31 Jul 2024
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.
Newly introduced telemetry in devenv 1.4 sparked a backlash over privacy concerns, leading to the removal of its AI-powered feature after strong community pushback.
TC39 met in Seattle and advanced 9 JavaScript proposals, including three to Stage 4, introducing new features and enhancements for a future ECMAScript release.