11import * as Kysely from "kysely" ;
2- import { mapObject , objectToEntries , ReadonlyRecord } from "../Object.js" ;
2+ import {
3+ createRecord ,
4+ getProperty ,
5+ mapObject ,
6+ ReadonlyRecord ,
7+ } from "../Object.js" ;
38import { ok , Result } from "../Result.js" ;
49import {
510 SafeSql ,
@@ -30,6 +35,8 @@ import {
3035 omit ,
3136 optional ,
3237 OptionalType ,
38+ record ,
39+ set ,
3340 String ,
3441 TableId ,
3542 Type ,
@@ -42,6 +49,7 @@ import { AppOwner, OwnerId } from "./Owner.js";
4249import { Query , Row } from "./Query.js" ;
4350import type { CrdtMessage , DbChange } from "./Storage.js" ;
4451import { Timestamp , TimestampBytes } from "./Timestamp.js" ;
52+ import { readonly } from "../Function.js" ;
4553
4654/**
4755 * Defines the schema of an Evolu database.
@@ -169,12 +177,10 @@ export const evoluSchemaToDbSchema = (
169177 schema : EvoluSchema ,
170178 indexesConfig ?: IndexesConfig ,
171179) : DbSchema => {
172- const tables = objectToEntries ( schema ) . map ( ( [ tableName , table ] ) => ( {
173- name : tableName ,
174- columns : objectToEntries ( table )
175- . filter ( ( [ k ] ) => k !== "id" )
176- . map ( ( [ k ] ) => k ) ,
177- } ) ) ;
180+ const tables = mapObject (
181+ schema ,
182+ ( table ) => new Set ( Object . keys ( table ) . filter ( ( k ) => k !== "id" ) ) ,
183+ ) ;
178184
179185 const indexes = indexesConfig
180186 ? indexesConfig ( createIndex ) . map (
@@ -206,6 +212,13 @@ export type CreateQuery<S extends EvoluSchema> = <R extends Row>(
206212 readonly column : string ;
207213 readonly value : SqliteValue ;
208214 } ;
215+ readonly evolu_message_quarantine : {
216+ readonly timestamp : TimestampBytes ;
217+ readonly table : string ;
218+ readonly id : IdBytes ;
219+ readonly column : string ;
220+ readonly value : SqliteValue ;
221+ } ;
209222 }
210223 > ,
211224 "selectFrom" | "fn" | "with" | "withRecursive"
@@ -231,7 +244,11 @@ export const SystemColumns = object({
231244} ) ;
232245export type SystemColumns = typeof SystemColumns . Type ;
233246
234- export const systemColumns = Object . keys ( SystemColumns . props ) ;
247+ export const systemColumns = readonly (
248+ new Set ( Object . keys ( SystemColumns . props ) ) ,
249+ ) ;
250+
251+ export const systemColumnsWithId = readonly ( [ ...systemColumns , "id" ] ) ;
235252
236253export type MutationKind = "insert" | "update" | "upsert" ;
237254
@@ -443,21 +460,17 @@ export type InferColumnErrors<
443460 > ;
444461} [ keyof MutationMapping < T , M > ] ;
445462
446- export const DbTable = object ( {
447- name : String ,
448- columns : array ( String ) ,
449- } ) ;
450- export type DbTable = typeof DbTable . Type ;
451-
452463export const DbIndex = object ( { name : String , sql : String } ) ;
453464export type DbIndex = typeof DbIndex . Type ;
454465
455466export const DbSchema = object ( {
456- tables : array ( DbTable ) ,
467+ tables : record ( String , set ( String ) ) ,
457468 indexes : array ( DbIndex ) ,
458469} ) ;
459470export type DbSchema = typeof DbSchema . Type ;
460471
472+ // TODO: Use a ref and update dbSchema on hot reloading to support
473+ // development workflows where schema changes without full app restart.
461474export interface DbSchemaDep {
462475 readonly dbSchema : DbSchema ;
463476}
@@ -469,7 +482,7 @@ export const getDbSchema =
469482 DbSchema ,
470483 SqliteError
471484 > => {
472- const map = new Map < string , Array < string > > ( ) ;
485+ const tables = createRecord < string , Set < string > > ( ) ;
473486
474487 const tableAndColumnInfoRows = deps . sqlite . exec ( sql `
475488 select
@@ -487,12 +500,9 @@ export const getDbSchema =
487500 tableName : string ;
488501 columnName : string ;
489502 } ;
490- if ( ! map . has ( tableName ) ) map . set ( tableName , [ ] ) ;
491- map . get ( tableName ) ?. push ( columnName ) ;
503+ ( tables [ tableName ] ??= new Set ( ) ) . add ( columnName ) ;
492504 } ) ;
493505
494- const tables = Array . from ( map , ( [ name , columns ] ) => ( { name, columns } ) ) ;
495-
496506 const indexesRows = deps . sqlite . exec (
497507 allIndexes
498508 ? sql `
@@ -546,23 +556,19 @@ export const ensureDbSchema =
546556 currentSchema = dbSchema . value ;
547557 }
548558
549- newSchema . tables . forEach ( ( newTable ) => {
550- const currentTable = currentSchema . tables . find (
551- ( t ) => t . name === newTable . name ,
552- ) ;
553- if ( ! currentTable ) {
554- queries . push ( createAppTable ( newTable ) ) ;
559+ for ( const [ tableName , newColumns ] of Object . entries ( newSchema . tables ) ) {
560+ const currentColumns = getProperty ( currentSchema . tables , tableName ) ;
561+ if ( ! currentColumns ) {
562+ queries . push ( createAppTable ( tableName , newColumns ) ) ;
555563 } else {
556- newTable . columns
557- . filter ( ( newColumn ) => ! currentTable . columns . includes ( newColumn ) )
558- . forEach ( ( newColumn ) => {
559- queries . push ( sql `
560- alter table ${ sql . identifier ( newTable . name ) }
561- add column ${ sql . identifier ( newColumn ) } blob;
562- ` ) ;
563- } ) ;
564+ for ( const newColumn of newColumns . difference ( currentColumns ) ) {
565+ queries . push ( sql `
566+ alter table ${ sql . identifier ( tableName ) }
567+ add column ${ sql . identifier ( newColumn ) } any;
568+ ` ) ;
569+ }
564570 }
565- } ) ;
571+ }
566572
567573 // Remove current indexes that are not in the newSchema.
568574 currentSchema . indexes
@@ -595,19 +601,19 @@ export const ensureDbSchema =
595601 return ok ( ) ;
596602 } ;
597603
598- const createAppTable = ( table : DbTable ) => sql `
599- create table ${ sql . identifier ( table . name ) } (
604+ const createAppTable = ( tableName : string , columns : ReadonlySet < string > ) => sql `
605+ create table ${ sql . identifier ( tableName ) } (
600606 "id" text,
601607 ${ sql . raw (
602- `${ systemColumns
603- . concat ( table . columns )
604- . filter ( ( c ) => c !== "id" )
608+ `${ [ ...systemColumns , ...columns ]
605609 // With strict tables and any type, data is preserved exactly as received
606610 // without any type affinity coercion. This allows storing any data type
607611 // while maintaining strict null enforcement for primary key columns.
612+ // TODO: Use proper SQLite types for system columns (text for createdAt,
613+ // updatedAt, ownerId, integer for isDeleted) instead of "any".
608614 . map ( ( name ) => `${ sql . identifier ( name ) . sql } any` )
609- . join ( ", " ) } `,
610- ) } ,
615+ . join ( ", " ) } , `,
616+ ) }
611617 primary key ("ownerId", "id")
612618 )
613619 without rowid, strict;
0 commit comments