Skip to content

Commit 22659cd

Browse files
committed
Add support for auto generating sqlite schema
1 parent b89827b commit 22659cd

File tree

9 files changed

+285
-31
lines changed

9 files changed

+285
-31
lines changed

Diff for: .gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ node_modules
66
.dist
77
/junit
88
.eslintcache
9+
schema_tmp.sql

Diff for: packages/host/config/environment.js

+23-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
const fs = require('fs');
44
const path = require('path');
55

6-
let sqlSchema = fs.readFileSync(path.join(__dirname, 'schema.sql'), 'utf8');
6+
let sqlSchema = fs.readFileSync(getLatestSchemaFile(), 'utf8');
77

88
module.exports = function (environment) {
99
const ENV = {
@@ -94,3 +94,25 @@ module.exports = function (environment) {
9494

9595
return ENV;
9696
};
97+
98+
function getLatestSchemaFile() {
99+
const migrationsDir = path.resolve(
100+
path.join(__dirname, '..', '..', 'realm-server', 'migrations'),
101+
);
102+
let migrations = fs.readdirSync(migrationsDir);
103+
let lastMigration = migrations
104+
.filter((f) => f !== '.eslintrc.js')
105+
.sort()
106+
.pop();
107+
const schemaDir = path.join(__dirname, 'schema');
108+
let files = fs.readdirSync(schemaDir);
109+
let latestSchemaFile = files.sort().pop();
110+
if (
111+
lastMigration.replace(/_.*/, '') !== latestSchemaFile.replace(/_.*/, '')
112+
) {
113+
throw new Error(
114+
`The sqlite schema file is out of date--please regenerate the sqlite schema file`,
115+
);
116+
}
117+
return path.join(schemaDir, latestSchemaFile);
118+
}

Diff for: packages/host/config/schema.sql

-29
This file was deleted.

Diff for: packages/host/config/schema/1712771547705_schema.sql

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
-- This is auto-generated from packages/realm-server/scripts/convert-to-sqlite.ts
2+
-- Please don't directly modify this file
3+
4+
CREATE TABLE IF NOT EXISTS indexed_cards (
5+
card_url TEXT NOT NULL,
6+
realm_version TEXT NOT NULL,
7+
realm_url TEXT NOT NULL,
8+
pristine_doc JSON,
9+
search_doc JSON,
10+
error_doc JSON,
11+
deps JSON,
12+
types JSON,
13+
embedded_html TEXT,
14+
isolated_html TEXT,
15+
indexed_at INTEGER,
16+
is_deleted BOOLEAN,
17+
PRIMARY KEY ( card_url, realm_version )
18+
);
19+
20+
CREATE TABLE IF NOT EXISTS realm_versions (
21+
realm_url TEXT NOT NULL,
22+
current_version INTEGER NOT NULL,
23+
PRIMARY KEY ( realm_url )
24+
);

Diff for: packages/realm-server/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"qs": "^6.10.5",
5757
"qunit": "^2.20.0",
5858
"sane": "^5.0.1",
59+
"sql-parser-cst": "^0.28.0",
5960
"start-server-and-test": "^1.14.0",
6061
"supertest": "^6.2.4",
6162
"testem": "^3.10.1",
@@ -95,7 +96,8 @@
9596
"lint:js": "eslint . --cache",
9697
"lint:js:fix": "eslint . --fix",
9798
"lint:glint": "glint",
98-
"migrate": "node-pg-migrate"
99+
"migrate": "PGDATABASE=boxel ./scripts/ensure-db-exists.sh && PGPORT=5435 PGDATABASE=boxel PGUSER=postgres node-pg-migrate",
100+
"schema": "./scripts/schema-dump.sh"
99101
},
100102
"volta": {
101103
"extends": "../../package.json"

Diff for: packages/realm-server/scripts/convert-to-sqlite.ts

+202
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/* eslint-env node */
2+
import { readFileSync, readdirSync, writeFileSync } from 'fs-extra';
3+
import { resolve, join } from 'path';
4+
import {
5+
parse,
6+
type CreateTableStmt,
7+
type AlterTableStmt,
8+
type Program,
9+
} from 'sql-parser-cst';
10+
11+
// Currently this script only cares about CREATE TABLE statements and ALTER
12+
// TABLE statements that add primary key constraints. All the other schema aspects of the
13+
// pg_dump are generally beyond the capability of SQLite. Perhaps index creation
14+
// can be added but it will get really tricky fast since SQLite's indices are
15+
// more more simplistic than postgres.
16+
17+
const args = process.argv;
18+
const migrationsDir = resolve(join(__dirname, '..', 'migrations'));
19+
const sqliteSchemaDir = resolve(
20+
join(__dirname, '..', '..', 'host', 'config', 'schema'),
21+
);
22+
const INDENT = ' ';
23+
24+
let pgDumpFile = args[2];
25+
if (!pgDumpFile) {
26+
console.error(`please specify the path of the pg_dump file`);
27+
process.exit(-1);
28+
}
29+
let pgDump = readFileSync(pgDumpFile, 'utf8');
30+
31+
let cst = parse(prepareDump(pgDump), {
32+
dialect: 'postgresql',
33+
includeSpaces: true,
34+
includeNewlines: true,
35+
includeComments: true,
36+
includeRange: true,
37+
});
38+
39+
let sql: string[] = [
40+
`
41+
-- This is auto-generated from packages/realm-server/scripts/convert-to-sqlite.ts
42+
-- Please don't directly modify this file
43+
44+
`,
45+
];
46+
for (let statement of cst.statements) {
47+
if (statement.type !== 'create_table_stmt') {
48+
continue;
49+
}
50+
sql.push('CREATE TABLE IF NOT EXISTS');
51+
if (
52+
statement.name.type === 'member_expr' &&
53+
statement.name.property.type === 'identifier'
54+
) {
55+
let tableName = statement.name.property.name;
56+
sql.push(statement.name.property.name, '(\n');
57+
createColumns(cst, tableName, statement, sql);
58+
} else {
59+
throw new Error(`could not determine table name to be created`);
60+
}
61+
62+
sql.push('\n);\n\n');
63+
}
64+
65+
let result = sql.join(' ').trim();
66+
let filename = getSchemaFilename();
67+
let schemaFile = join(sqliteSchemaDir, filename);
68+
writeFileSync(schemaFile, result);
69+
console.log(`created SQLite schema file ${schemaFile}`);
70+
71+
function createColumns(
72+
cst: Program,
73+
tableName: string,
74+
statement: CreateTableStmt,
75+
sql: string[],
76+
) {
77+
if (!statement.columns) {
78+
return;
79+
}
80+
let columns: string[] = [];
81+
for (let item of statement.columns.expr.items) {
82+
if (item.type !== 'column_definition') {
83+
continue;
84+
}
85+
let column: string[] = [];
86+
column.push(INDENT, item.name.name);
87+
if (item.dataType?.type === 'named_data_type') {
88+
let dataTypeName = Array.isArray(item.dataType.nameKw)
89+
? item.dataType.nameKw[0]
90+
: item.dataType.nameKw;
91+
switch (dataTypeName.name) {
92+
case 'CHARACTER':
93+
column.push('TEXT');
94+
break;
95+
case 'JSONB':
96+
// TODO change this to 'BLOB' after we do the sqlite BLOB storage
97+
// support in CS-6668 for faster performance
98+
column.push('JSON');
99+
break;
100+
case 'BOOLEAN':
101+
column.push('BOOLEAN');
102+
break;
103+
case 'INTEGER':
104+
column.push('INTEGER');
105+
break;
106+
}
107+
}
108+
for (let constraint of item.constraints) {
109+
switch (constraint.type) {
110+
case 'constraint_not_null':
111+
column.push('NOT NULL');
112+
break;
113+
case 'constraint_primary_key':
114+
column.push('PRIMARY KEY');
115+
break;
116+
default:
117+
throw new Error(
118+
`Don't know how to serialize constraint ${constraint.type} for column '${item.name.name}'`,
119+
);
120+
}
121+
}
122+
123+
columns.push(column.join(' '));
124+
}
125+
sql.push([...columns, makePrimaryKeyConstraint(cst, tableName)].join(',\n'));
126+
}
127+
128+
function makePrimaryKeyConstraint(cst: Program, tableName: string): string {
129+
let alterTableStmts = cst.statements.filter(
130+
(s) =>
131+
s.type === 'alter_table_stmt' &&
132+
s.table.type === 'table_without_inheritance' &&
133+
s.table.table.type === 'member_expr' &&
134+
s.table.table.property.type === 'identifier' &&
135+
s.table.table.property.name === tableName,
136+
) as AlterTableStmt[];
137+
let pkConstraint: string[] = [];
138+
for (let alterTableStmt of alterTableStmts) {
139+
for (let item of alterTableStmt.actions.items) {
140+
if (item.type === 'alter_action_add_constraint') {
141+
switch (item.constraint.type) {
142+
case 'constraint_primary_key': {
143+
if (pkConstraint.length > 0) {
144+
throw new Error(
145+
`encountered multiple primary key constraints for table ${tableName}`,
146+
);
147+
}
148+
if (item.constraint.columns) {
149+
let columns: string[] = [];
150+
if (item.constraint.columns.type === 'paren_expr') {
151+
for (let column of item.constraint.columns.expr.items) {
152+
if (
153+
column.type === 'index_specification' &&
154+
column.expr.type === 'identifier'
155+
) {
156+
columns.push(column.expr.name);
157+
}
158+
}
159+
} else {
160+
throw new Error(
161+
`Don't know how to serialize constraint ${item.constraint.type} for table '${tableName}'`,
162+
);
163+
}
164+
if (columns.length > 0) {
165+
pkConstraint.push(
166+
INDENT,
167+
'PRIMARY KEY (',
168+
columns.join(', '),
169+
')',
170+
);
171+
}
172+
}
173+
break;
174+
}
175+
default:
176+
throw new Error(
177+
`Don't know how to serialize constraint ${item.constraint.type} for table '${tableName}'`,
178+
);
179+
}
180+
}
181+
}
182+
}
183+
return pkConstraint.join(' ');
184+
}
185+
186+
// This strips out all the things that our SQL AST chokes on (it's still in an
187+
// experimental phase for postgresql)
188+
function prepareDump(sql: string): string {
189+
let result = sql
190+
.replace(/\s*SET\s[^;].*;/gm, '')
191+
.replace(/\s*CREATE\sTYPE\s[^;]*;/gm, '');
192+
return result;
193+
}
194+
195+
function getSchemaFilename(): string {
196+
let files = readdirSync(migrationsDir);
197+
let lastFile = files
198+
.filter((f) => f !== '.eslintrc.js')
199+
.sort()
200+
.pop()!;
201+
return `${lastFile.replace(/_.*/, '')}_schema.sql`;
202+
}

Diff for: packages/realm-server/scripts/ensure-db-exists.sh

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/sh
2+
3+
if docker exec boxel-pg psql -U postgres -w -lqt | cut -d \| -f 1 | grep -qw "$PGDATABASE"; then
4+
echo "Database $PGDATABASE exists"
5+
else
6+
docker exec boxel-pg psql -U postgres -w -c "CREATE DATABASE $PGDATABASE"
7+
echo "created database $PGDATABASE"
8+
fi

Diff for: packages/realm-server/scripts/schema-dump.sh

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/bin/sh
2+
3+
tmpFile='./schema_tmp.sql'
4+
5+
docker exec boxel-pg pg_dump \
6+
-U postgres -w --schema-only \
7+
--exclude-table-and-children=job_statuses \
8+
--exclude-table-and-children=pgmigrations \
9+
--exclude-table-and-children=jobs \
10+
--no-tablespaces \
11+
--no-table-access-method \
12+
--no-owner \
13+
--no-acl \
14+
boxel >$tmpFile
15+
16+
ts-node --transpileOnly ./scripts/convert-to-sqlite.ts $tmpFile
17+
rm $tmpFile

Diff for: pnpm-lock.yaml

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)