Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
phpnode committed Sep 19, 2020
0 parents commit 4971a29
Show file tree
Hide file tree
Showing 20 changed files with 1,559 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.vscode
node_modules
lib
playground.ts
Empty file added .npmignore
Empty file.
25 changes: 25 additions & 0 deletions README.md

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "@codemix/ts-sql",
"version": "1.0.0",
"description": "SQL database engine implemented purely in TypeScript type definitions.",
"main": "lib/index.js",
"author": "Charles Pick <[email protected]>",
"license": "MIT",
"dependencies": {},
"devDependencies": {
"prettier": "^2.1.2",
"typescript": "beta"
},
"scripts": {
"build": "tsc -b",
"playground": "node scripts/bundle.js > playground.ts"
}
}
27 changes: 27 additions & 0 deletions scripts/Header.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
```
▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄
▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░▌
▀▀▀▀█░█▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀█░▌▐░▌
▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌▐░▌
▐░▌ ▐░█▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄▄▄ ▐░▌ ▐░▌▐░▌
▐░▌ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░▌ ▐░▌▐░▌
▐░▌ ▀▀▀▀▀▀▀▀▀█░▌ ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀█░▌▐░█▄▄▄▄▄▄▄█░▌▐░▌
▐░▌ ▐░▌ ▐░▌▐░░░░░░░░░░░▌▐░▌
▐░▌ ▄▄▄▄▄▄▄▄▄█░▌ ▄▄▄▄▄▄▄▄▄█░▌ ▀▀▀▀▀▀█░█▀▀ ▐░█▄▄▄▄▄▄▄▄▄
▐░▌ ▐░░░░░░░░░░░▌ ▐░░░░░░░░░░░▌ ▐░▌ ▐░░░░░░░░░░░▌
▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀▀▀▀▀▀▀▀▀▀▀
```

This is a SQL database implemented purely in TypeScript type annotations.
This means that it operates solely on types - you define a "database",
(just a type annotation) and then query it using some more type annotations.
It supports a subset of SQL, including SELECT, INSERT, UPDATE and DELETE statements.

This project lives at https://github.com/codemix/ts-sql

This was written by Charles Pick ( [email protected], https://twitter.com/c_pick ).

You can install ts-sql in your own project with `npm install @codemix/ts-sql` or
`yarn install @codemix/ts-sql` (TypeScript 4.1 is required).

(Tip: hover over the type aliases below to see the results of your queries)
65 changes: 65 additions & 0 deletions scripts/bundle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// @ts-check

const fs = require("fs");
const path = require("path");

const header = fs
.readFileSync(path.resolve(__dirname, "Header.md"), "utf-8")
.split(/\n/)
.map((line) => ` * ${line}`)
.join("\n");

const files = Array.from(findFiles(path.resolve(__dirname, "..", "src")));
const sources = files
.filter((file) => !file.endsWith(".test.ts"))
.map(loadFile);
const tests = files.filter((file) => file.endsWith(".test.ts")).map(loadFile);

const outputs = [
`/**\n${header}\n */\n`,
...tests,
`
/**
* ========================================================================================
*
*
* END OF EXAMPLES, START OF IMPLEMENTATION
*
*
* ========================================================================================
*/
`
.split(/\n/)
.map((line) => line.trim())
.join("\n"),
...sources,
];

console.log(outputs.join("\n\n"));

/**
* @param {string} dir The path to search in.
*/
function* findFiles(dir) {
for (const name of fs.readdirSync(dir)) {
const filename = path.join(dir, name);
if (name.endsWith(".ts")) {
yield filename;
} else if (fs.statSync(filename).isDirectory()) {
yield* findFiles(filename);
}
}
}

/**
* @param {string} filename
*/
function loadFile(filename) {
const raw = fs.readFileSync(filename, "utf-8");
const content = raw
.replace(/export\s+type/g, "type")
.replace(/export\s+\*\s+from\s+"(.*)";?/g, "")
.replace(/import\s+\{([\s\S]*)\}\s+from\s+"(.*)";?/g, "")
.trim();
return content;
}
152 changes: 152 additions & 0 deletions src/AST.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
export type Identifier<Name extends string = string> = {
type: "Identifier";
name: Name;
};

export type MemberExpression<
Object extends string = string,
Property extends string = string
> = {
type: "MemberExpression";
object: Object;
property: Property;
};

export type NumericLiteral<Value extends number = number> = {
type: "NumericLiteral";
value: Value;
};
export type StringLiteral<Value extends string = string> = {
type: "StringLiteral";
value: Value;
};
export type BooleanLiteral<Value extends boolean = boolean> = {
type: "BooleanLiteral";
value: Value;
};

export type NullLiteral = { type: "NullLiteral"; value: null };

export type BinaryOperator = "=" | "!=" | "LIKE";
export type BinaryExpression<
Left extends Expression = Expression,
Operator extends BinaryOperator = BinaryOperator,
Right extends Expression = Expression
> = {
type: "BinaryExpression";
left: Left;
operator: Operator;
right: Right;
};

export type LogicalOperator = "AND" | "OR" | "&&" | "||";
export type LogicalExpression<
Left extends Expression = Expression,
Operator extends LogicalOperator = LogicalOperator,
Right extends Expression = Expression
> = {
type: "LogicalExpression";
left: Left;
operator: Operator;
right: Right;
};

export type FieldSpecifier<
Source extends Identifier | MemberExpression = Identifier,
Alias extends Identifier = Identifier
> = {
type: "FieldSpecifier";
source: Source;
alias: Alias;
};

export type TableSpecifier<
Source extends Identifier = Identifier,
Alias extends Identifier = Source
> = {
type: "TableSpecifier";
source: Source;
alias: Alias;
};

export type AssignmentExpression<
Key extends Identifier = Identifier,
Value extends Expression = Expression
> = {
type: "AssignmentExpression";
key: Key;
value: Value;
};

export type Expression =
| LogicalExpression<any, LogicalOperator, any>
| BinaryExpression<any, BinaryOperator, any>
| MemberExpression
| Identifier
| StringLiteral
| NumericLiteral
| BooleanLiteral
| NullLiteral;

export type InnerJoinSpecifier<
From extends TableSpecifier<any> = TableSpecifier<any>,
Where extends Expression = Expression
> = {
type: "InnerJoinSpecifier";
from: From;
where: Where;
};

export type JoinSpecifier = InnerJoinSpecifier;

export type SelectStatement<
Fields extends FieldSpecifier<any>[] = FieldSpecifier<any>[],
From extends TableSpecifier<any> = TableSpecifier<any>,
Joins extends JoinSpecifier[] = JoinSpecifier[],
Where extends Expression = Expression,
Offset extends number = number,
Limit extends number = number
> = {
type: "SelectStatement";
fields: Fields;
from: From;
joins: Joins | [];
where: Where;
offset: Offset;
limit: Limit;
};

export type InsertStatement<
TableName extends string = any,
Values extends readonly AssignmentExpression<any, any>[] = any
> = {
type: "InsertStatement";
tableName: TableName;
values: Values;
};

export type UpdateStatement<
TableName extends string = string,
Values extends AssignmentExpression[] = AssignmentExpression[],
Where extends Expression = Expression
> = {
type: "UpdateStatement";
tableName: TableName;
values: Values;
where: Where;
};

export type DeleteStatement<
TableName extends string = string,
Where extends Expression = Expression
> = {
type: "DeleteStatement";
tableName: TableName;
where: Where;
};

export type Statement =
| SelectStatement
| InsertStatement
| UpdateStatement
| DeleteStatement;
106 changes: 106 additions & 0 deletions src/All.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Query } from "./Query";

// # Usage

// First we define the data that we're going to be querying against.
const simpsons = {
people: [
{ id: 1, firstName: "Bart", lastName: "Simpson", isChild: true },
{ id: 2, firstName: "Lisa", lastName: "Simpson", isChild: true },
{ id: 3, firstName: "Maggie", lastName: "Simpson", isChild: true },
{ id: 4, firstName: "Marge", lastName: "Simpson", isChild: false },
{ id: 5, firstName: "Homer", lastName: "Simpson", isChild: false },
{ id: 6, firstName: "Montgomery", lastName: "Burns", isChild: false },
{ id: 7, firstName: "Whelan", lastName: "Smithers", isChild: false },
{ id: 8, firstName: "Nillhouse", lastName: "Van Houten", isChild: true },
{ id: 9, firstName: "Nelson", lastName: "Muntz", isChild: true },
{ id: 10, firstName: "Principal", lastName: "Skinner", isChild: false },
],
places: [
{ name: "Springfield Elementary", ownerId: 10, isActive: true },
{ name: "Nuclear Power Plant", ownerId: 6, isActive: true },
{ name: "Evergreen Terrace", ownerId: 4, isActive: true },
{ name: "Monorail", ownerId: 8, isActive: false },
{ name: "Treehouse of Horror", ownerId: 1, isActive: true },
{ name: "Playground", ownerId: 9, isActive: true },
],
} as const;

// This is the initial version of our database.
export type DBv1 = typeof simpsons;

// Let's find the names of people with the last name "Simpson".
// We use the `Query` type to execute queries, it takes a SQL
// query as first type parameter, and a database as the second.
// Note that all SQL keywords are UPPERCASE.
export type EX1 = Query<
"SELECT firstName FROM people WHERE lastName = 'Simpson'",
DBv1
>;

// Let's find the first and last names of all the children in the simpsons
export type EX2 = Query<
"SELECT firstName, lastName FROM people WHERE isChild = true",
DBv1
>;

// Let's find the adults whose names begin with M
export type EX3 = Query<
"SELECT * FROM people WHERE isChild = false AND firstName LIKE 'M%'",
DBv1
>;

// Let's correct the typo in Milhouse's name. Since all types are immutable in TypeScript our
// database is immutable too, so this query returns a new database.
export type DBv2 = Query<
"UPDATE people SET firstName = 'Milhouse' WHERE id = 8",
DBv1
>;

// Let's check that our update worked

export type EX4 = Query<"SELECT * FROM people WHERE id = 8", DBv2>;

// Let's add a new character, again this returns a new database.
export type DBv3 = Query<
"INSERT INTO people SET id = 11, firstName = 'Troy', lastName = 'McClure', isChild = false",
DBv2
>;

export type EX5 = Query<"SELECT * FROM people WHERE firstName = 'Troy'", DBv3>;

// Unfortunately, we need to delete Troy McClure
export type DBv4 = Query<
"DELETE FROM people WHERE firstName = 'Troy' AND lastName = 'McClure'",
DBv3
>;

export type EX6 = Query<
"SELECT firstName FROM people WHERE isChild = false",
DBv4
>;

// We can add Moe tho

export type DBv5 = Query<
"INSERT INTO people SET id = 11, firstName = 'Moe', lastName = 'Szyslak', isChild = false",
DBv4
>;

export type EX7 = Query<
"SELECT id, firstName FROM people WHERE isChild = false AND firstName LIKE 'M%'",
DBv5
>;

// Let's add Moe's Tavern
export type DBv6 = Query<
'INSERT INTO places SET name = "Moe\'s Tavern", ownerId = 11, isActive = true',
DBv5
>;

// Joins! Let's get all the places along with the names of their owners.

export type EX9 = Query<
"SELECT name, person.firstName, person.lastName FROM places INNER JOIN people AS person ON places.ownerId = person.id",
DBv1
>;
Loading

0 comments on commit 4971a29

Please sign in to comment.