Skip to content

Commit

Permalink
feat(DAL): support for booleans (#542)
Browse files Browse the repository at this point in the history
This PR introduces support for booleans in the DAL.

---------

Co-authored-by: David Martos <[email protected]>
  • Loading branch information
kevin-dp and davidmartos96 authored Oct 30, 2023
1 parent 3ae3f30 commit 318b26d
Show file tree
Hide file tree
Showing 34 changed files with 1,295 additions and 273 deletions.
6 changes: 6 additions & 0 deletions .changeset/hot-paws-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"electric-sql": patch
"@electric-sql/prisma-generator": patch
---

Adds client-side support for booleans.
11 changes: 11 additions & 0 deletions clients/typescript/src/client/conversions/datatypes/boolean.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Serialises a boolean to a number (0 for false and 1 for true)
export function serialiseBoolean(v: boolean): number {
return v ? 1 : 0
}

// Deserialises a SQLite boolean (i.e. 0 or 1) into a boolean value
export function deserialiseBoolean(v: number): boolean {
if (v === 0) return false
else if (v === 1) return true
else throw new Error(`Could not parse boolean. Value is not 0 or 1: ${v}`)
}
76 changes: 76 additions & 0 deletions clients/typescript/src/client/conversions/datatypes/date.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { PgDateType } from '../types'

// Serialises a `Date` object into a SQLite compatible date string
export function serialiseDate(v: Date, pgType: PgDateType): string {
switch (pgType) {
case PgDateType.PG_TIMESTAMP:
// Returns local timestamp
return ignoreTimeZone(v).toISOString().replace('T', ' ').replace('Z', '')

case PgDateType.PG_TIMESTAMPTZ:
// Returns UTC timestamp
return v.toISOString().replace('T', ' ')

case PgDateType.PG_DATE:
// Returns the local date
return extractDateAndTime(ignoreTimeZone(v)).date

case PgDateType.PG_TIME:
// Returns the local time
return extractDateAndTime(ignoreTimeZone(v)).time

case PgDateType.PG_TIMETZ:
// Returns UTC time
return extractDateAndTime(v).time
}
}

// Deserialises a SQLite compatible date string into a `Date` object
export function deserialiseDate(v: string, pgType: PgDateType): Date {
const parse = (v: any) => {
const millis = Date.parse(v)
if (isNaN(millis))
throw new Error(`Could not parse date, invalid format: ${v}`)
else return new Date(millis)
}

switch (pgType) {
case PgDateType.PG_TIMESTAMP:
case PgDateType.PG_TIMESTAMPTZ:
case PgDateType.PG_DATE:
return parse(v)

case PgDateType.PG_TIME:
// interpret as local time
return parse(`1970-01-01 ${v}`)

case PgDateType.PG_TIMETZ:
// interpret as UTC time
return parse(`1970-01-01 ${v}+00`)
}
}

/**
* Corrects the provided `Date` such that
* the current date is set as UTC date.
* e.g. if it is 3PM in GMT+2 then it is 1PM UTC.
* This function would return a date in which it is 3PM UTC.
*/
function ignoreTimeZone(v: Date): Date {
// `v.toISOString` returns the UTC time but we want the time in this timezone
// so we get the timezone offset and subtract it from the current time in order to
// compensate for the timezone correction done by `toISOString`
const offsetInMs = 1000 * 60 * v.getTimezoneOffset()
return new Date(v.getTime() - offsetInMs)
}

type ExtractedDateTime = { date: string; time: string }
function extractDateAndTime(v: Date): ExtractedDateTime {
const regex = /([0-9-]*)T([0-9:.]*)Z/g
const [_, date, time] = regex.exec(v.toISOString())! as unknown as [
string,
string,
string
]
return { date, time }
}
3 changes: 2 additions & 1 deletion clients/typescript/src/client/conversions/input.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import mapValues from 'lodash.mapvalues'
import { FieldName, Fields } from '../model/schema'
import { PgDateType, PgType, fromSqlite, toSqlite } from './sqlite'
import { fromSqlite, toSqlite } from './sqlite'
import { InvalidArgumentError } from '../validation/errors/invalidArgumentError'
import { mapObject } from '../util/functions'
import { PgDateType, PgType } from './types'

export enum Transformation {
Js2Sqlite,
Expand Down
104 changes: 8 additions & 96 deletions clients/typescript/src/client/conversions/sqlite.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { InvalidArgumentError } from '../validation/errors/invalidArgumentError'
import { deserialiseBoolean, serialiseBoolean } from './datatypes/boolean'
import { deserialiseDate, serialiseDate } from './datatypes/date'
import { PgBasicType, PgDateType, PgType } from './types'

/**
* This module takes care of converting TypeScript values for Postgres-specific types to a SQLite storeable value and back.
Expand All @@ -8,27 +11,6 @@ import { InvalidArgumentError } from '../validation/errors/invalidArgumentError'
* When reading from the SQLite database, the string can be parsed back into a `Date` object.
*/

export enum PgBasicType {
PG_BOOL = 'BOOLEAN',
PG_SMALLINT = 'INT2',
PG_INT = 'INT4',
PG_FLOAT = 'FLOAT8',
PG_TEXT = 'TEXT',
}

/**
* Union type of all Pg types that are represented by a `Date` in JS/TS.
*/
export enum PgDateType {
PG_TIMESTAMP = 'TIMESTAMP',
PG_TIMESTAMPTZ = 'TIMESTAMPTZ',
PG_DATE = 'DATE',
PG_TIME = 'TIME',
PG_TIMETZ = 'TIMETZ',
}

export type PgType = PgBasicType | PgDateType

export function toSqlite(v: any, pgType: PgType): any {
if (v === null) {
// don't transform null values
Expand All @@ -40,6 +22,8 @@ export function toSqlite(v: any, pgType: PgType): any {
)

return serialiseDate(v, pgType as PgDateType)
} else if (pgType === PgBasicType.PG_BOOL) {
return serialiseBoolean(v)
} else {
return v
}
Expand All @@ -52,86 +36,14 @@ export function fromSqlite(v: any, pgType: PgType): any {
} else if (isPgDateType(pgType)) {
// it's a serialised date
return deserialiseDate(v, pgType as PgDateType)
} else if (pgType === PgBasicType.PG_BOOL) {
// it's a serialised boolean
return deserialiseBoolean(v)
} else {
return v
}
}

// Serialises a `Date` object into a SQLite compatible date string
function serialiseDate(v: Date, pgType: PgDateType): string {
switch (pgType) {
case PgDateType.PG_TIMESTAMP:
// Returns local timestamp
return ignoreTimeZone(v).toISOString().replace('T', ' ').replace('Z', '')

case PgDateType.PG_TIMESTAMPTZ:
// Returns UTC timestamp
return v.toISOString().replace('T', ' ')

case PgDateType.PG_DATE:
// Returns the local date
return extractDateAndTime(ignoreTimeZone(v)).date

case PgDateType.PG_TIME:
// Returns the local time
return extractDateAndTime(ignoreTimeZone(v)).time

case PgDateType.PG_TIMETZ:
// Returns UTC time
return extractDateAndTime(v).time
}
}

// Deserialises a SQLite compatible date string into a `Date` object
function deserialiseDate(v: string, pgType: PgDateType): Date {
switch (pgType) {
case PgDateType.PG_DATE:
case PgDateType.PG_TIMESTAMP:
case PgDateType.PG_TIMESTAMPTZ:
return parseDate(v)

case PgDateType.PG_TIME:
// interpret as local time
return parseDate(`1970-01-01 ${v}`)

case PgDateType.PG_TIMETZ:
// interpret as UTC time
return parseDate(`1970-01-01 ${v}+00`)
}
}

function parseDate(v: string) {
const millis = Date.parse(v)
if (isNaN(millis))
throw new Error(`Could not parse date, invalid format: ${v}`)
else return new Date(millis)
}

/**
* Corrects the provided `Date` such that
* the current date is set as UTC date.
* e.g. if it is 3PM in GMT+2 then it is 1PM UTC.
* This function would return a date in which it is 3PM UTC.
*/
function ignoreTimeZone(v: Date): Date {
// `v.toISOString` returns the UTC time but we want the time in this timezone
// so we get the timezone offset and subtract it from the current time in order to
// compensate for the timezone correction done by `toISOString`
const offsetInMs = 1000 * 60 * v.getTimezoneOffset()
return new Date(v.getTime() - offsetInMs)
}

type ExtractedDateTime = { date: string; time: string }
function extractDateAndTime(v: Date): ExtractedDateTime {
const regex = /([0-9-]*)T([0-9:.]*)Z/g
const [_, date, time] = regex.exec(v.toISOString())! as unknown as [
string,
string,
string
]
return { date, time }
}

function isPgDateType(pgType: PgType): boolean {
return (Object.values(PgDateType) as Array<string>).includes(pgType)
}
28 changes: 28 additions & 0 deletions clients/typescript/src/client/conversions/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export enum PgBasicType {
PG_BOOL = 'BOOL',
PG_INT = 'INT',
PG_INT2 = 'INT2',
PG_INT4 = 'INT4',
PG_INT8 = 'INT8',
PG_INTEGER = 'INTEGER',
PG_REAL = 'REAL',
PG_FLOAT4 = 'FLOAT4',
PG_FLOAT8 = 'FLOAT8',
PG_TEXT = 'TEXT',
PG_VARCHAR = 'VARCHAR',
PG_CHAR = 'CHAR',
PG_UUID = 'UUID',
}

/**
* Union type of all Pg types that are represented by a `Date` in JS/TS.
*/
export enum PgDateType {
PG_TIMESTAMP = 'TIMESTAMP',
PG_TIMESTAMPTZ = 'TIMESTAMPTZ',
PG_DATE = 'DATE',
PG_TIME = 'TIME',
PG_TIMETZ = 'TIMETZ',
}

export type PgType = PgBasicType | PgDateType
6 changes: 5 additions & 1 deletion clients/typescript/src/client/model/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { DeleteInput, DeleteManyInput } from '../input/deleteInput'
import { HKT } from '../util/hkt'
import groupBy from 'lodash.groupby'
import { Migration } from '../../migrators'
import { PgType } from '../conversions/sqlite'
import { PgType } from '../conversions/types'

export type Arity = 'one' | 'many'

Expand Down Expand Up @@ -166,6 +166,10 @@ export class DbSchema<T extends TableSchemas> {
return obj
}

hasTable(table: TableName): boolean {
return Object.keys(this.extendedTables).includes(table)
}

getTableDescription(
table: TableName
): ExtendedTableSchema<any, any, any, any, any, any, any, any, any, HKT> {
Expand Down
1 change: 1 addition & 0 deletions clients/typescript/src/electric/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export const electrify = async <DB extends DbSchema<any>>(

const satellite = await registry.ensureStarted(
dbName,
dbDescription,
adapter,
migrator,
notifier,
Expand Down
Loading

0 comments on commit 318b26d

Please sign in to comment.