-
Notifications
You must be signed in to change notification settings - Fork 54
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 4971a29
Showing
20 changed files
with
1,559 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
.vscode | ||
node_modules | ||
lib | ||
playground.ts |
Empty file.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
>; |
Oops, something went wrong.