Skip to content

Commit

Permalink
Add 5000 movies
Browse files Browse the repository at this point in the history
  • Loading branch information
leerob committed Dec 8, 2024
1 parent 23477a1 commit 9067e1f
Show file tree
Hide file tree
Showing 11 changed files with 4,978 additions and 60 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
# AWS Aurora DQSL Movies Demo

> **Note:** DSQL [does not support extensions](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/working-with-postgresql-compatibility-unsupported-features.html) currently.
6 changes: 5 additions & 1 deletion app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,17 @@ async function Movies({ searchParams }: Props) {
const { filter } = await searchParams;
const cookieStore = await cookies();
const sessionId = cookieStore.get('sessionId')?.value;
const { movies, queryTimeMs } = await getMovies(sessionId);
const { movies, totalRecords, queryTimeMs } = await getMovies(
sessionId,
filter,
);

return (
<MovieVoting
movies={movies}
highlight={filter || ''}
queryTimeMs={queryTimeMs}
totalRecords={totalRecords}
/>
);
}
11 changes: 10 additions & 1 deletion components/explanation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,16 @@ export default function Explanation() {
<span className="pl-1">How does this work?</span>
</summary>
<p className="p-5 mt-2 bg-gray-100 dark:bg-gray-800">
Movies are fetched from the Postgres database when the page loads.
Movies are fetched from the Postgres database when the page loads. It
has been seeded with the top 5000 movies from{' '}
<Link
href="https://www.kaggle.com/datasets/tmdb/tmdb-movie-metadata"
target="_blank"
rel="noreferrer"
className="text-gray-900 dark:text-white border-b border-gray-900 dark:border-white hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
>
TMDB.
</Link>{' '}
When a user votes on a movie,{' '}
<code className="bg-gray-200 dark:bg-gray-700 px-1">
useOptimistic
Expand Down
2 changes: 1 addition & 1 deletion components/loading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default function Loading() {
disabled
/>
<ul className="space-y-2">
{[...Array(10)].map((_, index) => (
{[...Array(8)].map((_, index) => (
<li
key={index}
className="flex items-center justify-between p-2 bg-gray-100 rounded"
Expand Down
5 changes: 4 additions & 1 deletion components/movie-voting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ interface MovieVotingProps {
movies: Movie[];
highlight: string;
queryTimeMs: string;
totalRecords: number;
}

function formatTimeAgo(date: Date) {
Expand All @@ -44,6 +45,7 @@ export function MovieVoting({
movies: initialMovies,
highlight,
queryTimeMs,
totalRecords,
}: MovieVotingProps) {
const [_, startTransition] = useTransition();

Expand Down Expand Up @@ -148,7 +150,8 @@ export function MovieVoting({
<p className="text-center text-gray-500">No movies found</p>
) : (
<p className="text-xs italic">
Fetched {initialMovies.length} movies in {queryTimeMs}
Fetched {totalRecords} movie{totalRecords == 1 ? '' : 's'} in{' '}
{queryTimeMs}
ms
</p>
)}
Expand Down
4,804 changes: 4,804 additions & 0 deletions lib/db/movies.csv

Large diffs are not rendered by default.

79 changes: 56 additions & 23 deletions lib/db/queries.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getConnection } from './drizzle';
import { movies, votes } from './schema';
import { and, eq, sql } from 'drizzle-orm';
import { and, eq, ilike, sql } from 'drizzle-orm';
import { performance } from 'perf_hooks';

export interface Movie {
Expand All @@ -13,35 +13,68 @@ export interface Movie {

export interface MoviesResult {
movies: Movie[];
totalRecords: number;
queryTimeMs: string;
}

export async function getMovies(sessionId?: string) {
export async function getMovies(
sessionId?: string,
filter?: string,
): Promise<MoviesResult> {
const db = await getConnection();
const startTime = performance.now();
const conditions = [];

const hasVoted = sessionId
? sql<boolean>`CASE WHEN COUNT(${votes.id}) > 0 THEN true ELSE false END`
: sql<boolean>`false`;
if (filter) {
conditions.push(ilike(movies.title, `%${filter}%`));
}

const startTime = performance.now();
const whereCondition = conditions.length > 0 ? and(...conditions) : undefined;

const moviesWithVotes = await db
.select({
id: movies.id,
title: movies.title,
score: movies.score,
lastVoteTime: movies.lastVoteTime,
hasVoted,
})
const totalRecordsResult = await db
.select({ count: sql<number>`COUNT(*)` })
.from(movies)
.leftJoin(
votes,
and(
eq(votes.movieId, movies.id),
sessionId ? eq(votes.sessionId, sql`${sessionId}::uuid`) : undefined,
),
)
.groupBy(movies.id, movies.title, movies.score, movies.lastVoteTime);
.where(whereCondition);

const totalRecords = totalRecordsResult[0]?.count || 0;

let moviesWithVotes;

if (sessionId) {
moviesWithVotes = await db
.select({
id: movies.id,
title: movies.title,
score: movies.score,
lastVoteTime: movies.lastVoteTime,
hasVoted: sql<boolean>`BOOL_OR(votes.session_id = ${sessionId}::uuid)`,
})
.from(movies)
.leftJoin(
votes,
and(
eq(votes.movieId, movies.id),
eq(votes.sessionId, sql`${sessionId}::uuid`),
),
)
.where(whereCondition)
.groupBy(movies.id, movies.title, movies.score, movies.lastVoteTime)
.orderBy(sql`${movies.score} DESC`)
.limit(8);
} else {
moviesWithVotes = await db
.select({
id: movies.id,
title: movies.title,
score: movies.score,
lastVoteTime: movies.lastVoteTime,
hasVoted: sql<boolean>`false`,
})
.from(movies)
.where(whereCondition)
.orderBy(sql`${movies.score} DESC`)
.limit(8);
}

const endTime = performance.now();
const queryTimeMs = (endTime - startTime).toFixed(2);
Expand All @@ -53,5 +86,5 @@ export async function getMovies(sessionId?: string) {
hasVoted: Boolean(movie.hasVoted),
}));

return { movies: data, queryTimeMs };
return { movies: data, totalRecords, queryTimeMs };
}
56 changes: 39 additions & 17 deletions lib/db/schema.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,47 @@
import { integer, text, timestamp, pgTable, uuid } from 'drizzle-orm/pg-core';
import {
pgTable,
integer,
timestamp,
uuid,
uniqueIndex,
text,
} from 'drizzle-orm/pg-core';

export const movies = pgTable('movies', {
id: integer('id').primaryKey(),
title: text('title').notNull(),
score: integer('score').notNull(),
lastVoteTime: timestamp('last_vote_time').notNull(),
});
export const movies = pgTable(
'movies',
{
id: integer('id').primaryKey(),
title: text('title').notNull(),
score: integer('score').notNull(),
lastVoteTime: timestamp('last_vote_time').notNull(),
},
(table) => ({
scoreIdx: uniqueIndex('movies_score_idx').on(table.score),
}),
);

export const sessions = pgTable('sessions', {
id: uuid('id').primaryKey().defaultRandom(),
createdAt: timestamp('created_at').notNull().defaultNow(),
expiresAt: timestamp('expires_at').notNull(),
});

export const votes = pgTable('votes', {
id: uuid('id').primaryKey().defaultRandom(),
sessionId: uuid('session_id')
.notNull()
.references(() => sessions.id),
movieId: integer('movie_id')
.notNull()
.references(() => movies.id),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
export const votes = pgTable(
'votes',
{
id: uuid('id').primaryKey().defaultRandom(),
sessionId: uuid('session_id')
.notNull()
.references(() => sessions.id),
movieId: integer('movie_id')
.notNull()
.references(() => movies.id),
createdAt: timestamp('created_at').notNull().defaultNow(),
},
(table) => ({
movieSessionIdx: uniqueIndex('votes_movieId_sessionId_idx').on(
table.movieId,
table.sessionId,
),
}),
);
55 changes: 39 additions & 16 deletions lib/db/seed.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,40 @@
import fs from 'fs';
import path from 'path';
import csv from 'csv-parser';
import { seed } from 'drizzle-seed';
import { closeConnection, getConnection } from './drizzle';
import { movies } from './schema';
import { config } from 'dotenv';

const movieTitles = [
'The Shawshank Redemption',
'The Godfather',
'The Dark Knight',
'12 Angry Men',
"Schindler's List",
'The Lord of the Rings: The Return of the King',
'Pulp Fiction',
'The Good, the Bad and the Ugly',
'Forrest Gump',
'Inception',
];
config();

async function readMovieTitlesFromCSV(): Promise<string[]> {
const movieTitles: string[] = [];
const csvFilePath = path.resolve(__dirname, 'movies.csv');

return new Promise((resolve, reject) => {
fs.createReadStream(csvFilePath)
.pipe(csv())
.on('data', (row) => {
const title = row.title?.trim();
if (title) {
movieTitles.push(title);
}
})
.on('end', () => {
console.log(`Parsed ${movieTitles.length} movies from CSV.`);
resolve(movieTitles);
})
.on('error', (error) => {
console.error('Error reading CSV file:', error);
reject(error);
});
});
}

async function main() {
const db = await getConnection();
const movieTitles = await readMovieTitlesFromCSV();

await seed(db, { movies }).refine((f) => ({
movies: {
Expand All @@ -26,12 +44,14 @@ async function main() {
values: movieTitles,
isUnique: true,
}),
seed: f.int({
score: f.int({
minValue: 0,
maxValue: 100,
maxValue: 0,
isUnique: false,
}),
lastVoteTime: f.datetime(),
lastVoteTime: f.default({
defaultValue: new Date('2024-12-07'),
}),
},
count: movieTitles.length,
},
Expand All @@ -41,4 +61,7 @@ async function main() {
process.exit();
}

main();
main().catch((error) => {
console.error('Seeding failed:', error);
process.exit(1);
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@types/node": "^22.10.1",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.1",
"csv-parser": "^3.0.0",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.16",
"typescript": "^5.7.2"
Expand Down
17 changes: 17 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 9067e1f

Please sign in to comment.