Skip to content

Commit

Permalink
Merge pull request #367 from evoluhq/generateSql
Browse files Browse the repository at this point in the history
Indexes
  • Loading branch information
steida authored Mar 20, 2024
2 parents e607f0f + e4e1ef6 commit 2a041e5
Show file tree
Hide file tree
Showing 33 changed files with 2,256 additions and 1,667 deletions.
33 changes: 33 additions & 0 deletions .changeset/yellow-lobsters-promise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
"@evolu/common": minor
---

Indexes (or indices, we don't judge)

This release brings SQLite indexes support to Evolu with two helpful options for `evolu.createQuery` functions.

```ts
const indexes = [
createIndex("indexTodoCreatedAt").on("todo").column("createdAt"),
];

const evolu = createEvolu(Database, {
// Try to remove/re-add indexes with `logExplainQueryPlan`.
indexes,
});

const allTodos = evolu.createQuery(
(db) => db.selectFrom("todo").orderBy("createdAt").selectAll(),
{
logExecutionTime: true,
// logExplainQueryPlan: false,
},
);
```

Indexes are not necessary for development but are required for production.

Before adding an index, use `logExecutionTime` and `logExplainQueryPlan`
createQuery options.

SQLite has [a tool](https://sqlite.org/cli.html#index_recommendations_sqlite_expert_) for index recommendations.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ Start developing and watch for code changes:
pnpm dev
```

Start iOS developing:

```
pnpm ios
```

Lint and tests:

```
Expand Down
74 changes: 55 additions & 19 deletions apps/native/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
String,
cast,
createEvolu,
createIndex,
database,
id,
jsonArrayFrom,
Expand Down Expand Up @@ -37,19 +38,26 @@ import {
} from "react-native";
import RNPickerSelect from "react-native-picker-select";

// Let's start with the database schema.

// Every table needs Id. It's defined as a branded type.
// Branded types make database types super safe.
const TodoId = id("Todo");
type TodoId = S.Schema.Type<typeof TodoId>;

const TodoCategoryId = id("TodoCategory");
type TodoCategoryId = S.Schema.Type<typeof TodoCategoryId>;

// This branded type ensures a string must be validated before being put
// into the database.
const NonEmptyString50 = String.pipe(
S.minLength(1),
S.maxLength(50),
S.brand("NonEmptyString50"),
);
type NonEmptyString50 = S.Schema.Type<typeof NonEmptyString50>;

// Now we can define tables.
const TodoTable = table({
id: TodoId,
title: NonEmptyString1000,
Expand All @@ -58,6 +66,7 @@ const TodoTable = table({
});
type TodoTable = S.Schema.Type<typeof TodoTable>;

// Evolu tables can contain typed JSONs.
const SomeJson = S.struct({ foo: S.string, bar: S.boolean });
type SomeJson = S.Schema.Type<typeof SomeJson>;

Expand All @@ -68,13 +77,34 @@ const TodoCategoryTable = table({
});
type TodoCategoryTable = S.Schema.Type<typeof TodoCategoryTable>;

// Now, we can define the database schema.
const Database = database({
todo: TodoTable,
todoCategory: TodoCategoryTable,
});
type Database = S.Schema.Type<typeof Database>;

/**
* Indexes
*
* Indexes are not necessary for development but are required for production.
*
* Before adding an index, use `logExecutionTime` and `logExplainQueryPlan`
* createQuery options.
*
* SQLite has a tool for Index Recommendations (SQLite Expert)
* https://sqlite.org/cli.html#index_recommendations_sqlite_expert_
*/
const indexes = [
createIndex("indexTodoCreatedAt").on("todo").column("createdAt"),

createIndex("indexTodoCategoryCreatedAt")
.on("todoCategory")
.column("createdAt"),
];

const evolu = createEvolu(Database, {
indexes,
...(process.env.NODE_ENV === "development" && {
syncUrl: "http://localhost:4000",
}),
Expand Down Expand Up @@ -184,25 +214,31 @@ const OwnerActions: FC = () => {
);
};

const todosWithCategories = evolu.createQuery((db) =>
db
.selectFrom("todo")
.select(["id", "title", "isCompleted", "categoryId"])
.where("isDeleted", "is not", cast(true))
// Filter null value and ensure non-null type.
.where("title", "is not", null)
.$narrowType<{ title: NotNull }>()
.orderBy("createdAt")
// https://kysely.dev/docs/recipes/relations
.select((eb) => [
jsonArrayFrom(
eb
.selectFrom("todoCategory")
.select(["todoCategory.id", "todoCategory.name"])
.where("isDeleted", "is not", cast(true))
.orderBy("createdAt"),
).as("categories"),
]),
// Evolu queries should be collocated. If necessary, they can be preloaded.
const todosWithCategories = evolu.createQuery(
(db) =>
db
.selectFrom("todo")
.select(["id", "title", "isCompleted", "categoryId"])
.where("isDeleted", "is not", cast(true))
// Filter null value and ensure non-null type.
.where("title", "is not", null)
.$narrowType<{ title: NotNull }>()
.orderBy("createdAt")
// https://kysely.dev/docs/recipes/relations
.select((eb) => [
jsonArrayFrom(
eb
.selectFrom("todoCategory")
.select(["todoCategory.id", "todoCategory.name"])
.where("isDeleted", "is not", cast(true))
.orderBy("createdAt"),
).as("categories"),
]),
{
// logQueryExecutionTime: true,
// logExplainQueryPlan: true,
},
);

const Todos: FC = () => {
Expand Down
20 changes: 10 additions & 10 deletions apps/native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,34 @@
"clean": "rm -rf .turbo .expo node_modules dist"
},
"dependencies": {
"@effect/schema": "^0.64.5",
"@effect/schema": "^0.64.9",
"@evolu/common": "workspace:*",
"@evolu/common-react": "workspace:*",
"@evolu/react-native": "workspace:*",
"@react-native-community/netinfo": "11.1.0",
"babel-plugin-module-resolver": "^5.0.0",
"buffer": "^6.0.3",
"crypto-browserify": "^3.12.0",
"effect": "2.4.7",
"effect": "2.4.9",
"events": "^3.3.0",
"expo": "^50.0.11",
"expo-sqlite": "~13.3.0",
"expo": "^50.0.14",
"expo-sqlite": "~13.4.0",
"expo-status-bar": "~1.11.1",
"fast-text-encoding": "^1.0.6",
"react": "^18.2.0",
"react-native": "0.73.5",
"react-native": "0.73.6",
"react-native-get-random-values": "~1.8.0",
"react-native-picker-select": "^9.0.0",
"stream": "^0.0.2"
},
"devDependencies": {
"@babel/core": "^7.23.3",
"@babel/plugin-transform-dynamic-import": "^7.23.4",
"@babel/plugin-transform-private-methods": "^7.23.3",
"@types/react": "^18.2.66",
"@babel/core": "^7.24.3",
"@babel/plugin-transform-dynamic-import": "^7.24.1",
"@babel/plugin-transform-private-methods": "^7.24.1",
"@types/react": "^18.2.67",
"eslint": "^8.57.0",
"eslint-config-evolu": "workspace:*",
"prettier": "^3.2.5",
"typescript": "^5.4.2"
"typescript": "^5.4.3"
}
}
4 changes: 2 additions & 2 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
},
"devDependencies": {
"@evolu/tsconfig": "workspace:*",
"@types/node": "^20.11.28",
"@types/node": "^20.11.30",
"ts-node": "^10.9.1",
"typescript": "^5.4.2"
"typescript": "^5.4.3"
},
"engines": {
"node": ">=16.15"
Expand Down
75 changes: 56 additions & 19 deletions apps/web/components/NextJsExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
canUseDom,
cast,
createEvolu,
createIndex,
database,
id,
jsonArrayFrom,
Expand All @@ -31,19 +32,26 @@ import {
useState,
} from "react";

// Let's start with the database schema.

// Every table needs Id. It's defined as a branded type.
// Branded types make database types super safe.
const TodoId = id("Todo");
type TodoId = S.Schema.Type<typeof TodoId>;

const TodoCategoryId = id("TodoCategory");
type TodoCategoryId = S.Schema.Type<typeof TodoCategoryId>;

// This branded type ensures a string must be validated before being put
// into the database. Check the prompt function to see Schema validation.
const NonEmptyString50 = String.pipe(
S.minLength(1),
S.maxLength(50),
S.brand("NonEmptyString50"),
);
type NonEmptyString50 = S.Schema.Type<typeof NonEmptyString50>;

// Now we can define tables.
const TodoTable = table({
id: TodoId,
title: NonEmptyString1000,
Expand All @@ -52,23 +60,46 @@ const TodoTable = table({
});
type TodoTable = S.Schema.Type<typeof TodoTable>;

// Evolu tables can contain typed JSONs.
const SomeJson = S.struct({ foo: S.string, bar: S.boolean });
type SomeJson = S.Schema.Type<typeof SomeJson>;

// Let's make a table with JSON value.
const TodoCategoryTable = table({
id: TodoCategoryId,
name: NonEmptyString50,
json: S.nullable(SomeJson),
});
type TodoCategoryTable = S.Schema.Type<typeof TodoCategoryTable>;

// Now, we can define the database schema.
const Database = database({
todo: TodoTable,
todoCategory: TodoCategoryTable,
});
type Database = S.Schema.Type<typeof Database>;

/**
* Indexes
*
* Indexes are not necessary for development but are required for production.
*
* Before adding an index, use `logExecutionTime` and `logExplainQueryPlan`
* createQuery options.
*
* SQLite has a tool for Index Recommendations (SQLite Expert)
* https://sqlite.org/cli.html#index_recommendations_sqlite_expert_
*/
const indexes = [
createIndex("indexTodoCreatedAt").on("todo").column("createdAt"),

createIndex("indexTodoCategoryCreatedAt")
.on("todoCategory")
.column("createdAt"),
];

const evolu = createEvolu(Database, {
indexes,
reloadUrl: "/examples/nextjs",
...(process.env.NODE_ENV === "development" && {
syncUrl: "http://localhost:4000",
Expand Down Expand Up @@ -158,25 +189,31 @@ const NotificationBar: FC = () => {
);
};

const todosWithCategories = evolu.createQuery((db) =>
db
.selectFrom("todo")
.select(["id", "title", "isCompleted", "categoryId"])
.where("isDeleted", "is not", cast(true))
// Filter null value and ensure non-null type.
.where("title", "is not", null)
.$narrowType<{ title: NotNull }>()
.orderBy("createdAt")
// https://kysely.dev/docs/recipes/relations
.select((eb) => [
jsonArrayFrom(
eb
.selectFrom("todoCategory")
.select(["todoCategory.id", "todoCategory.name"])
.where("isDeleted", "is not", cast(true))
.orderBy("createdAt"),
).as("categories"),
]),
// Evolu queries should be collocated. If necessary, they can be preloaded.
const todosWithCategories = evolu.createQuery(
(db) =>
db
.selectFrom("todo")
.select(["id", "title", "isCompleted", "categoryId"])
.where("isDeleted", "is not", cast(true))
// Filter null value and ensure non-null type.
.where("title", "is not", null)
.$narrowType<{ title: NotNull }>()
.orderBy("createdAt")
// https://kysely.dev/docs/recipes/relations
.select((eb) => [
jsonArrayFrom(
eb
.selectFrom("todoCategory")
.select(["todoCategory.id", "todoCategory.name"])
.where("isDeleted", "is not", cast(true))
.orderBy("createdAt"),
).as("categories"),
]),
{
// logQueryExecutionTime: true,
// logExplainQueryPlan: true,
},
);

type TodosWithCategoriesRow = ExtractRow<typeof todosWithCategories>;
Expand Down
Loading

0 comments on commit 2a041e5

Please sign in to comment.