Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(electric): Enable introspection of electrified enum types in the proxy #670

Merged
merged 7 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dirty-hairs-mate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@core/electric": patch
---

[VAX-1040] [VAX-1041] [VAX-1042] Add support for user-defined enum types in electrified tables.
6 changes: 6 additions & 0 deletions .changeset/moody-carpets-allow.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 enumerations.
1 change: 1 addition & 0 deletions clients/typescript/src/satellite/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,7 @@ function deserializeColumnData(
case PgDateType.PG_TIMETZ:
return typeDecoder.timetz(column)
default:
// also covers user-defined enumeration types
return typeDecoder.text(column)
}
}
Expand Down
52 changes: 42 additions & 10 deletions clients/typescript/test/satellite/serialization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ test('serialize/deserialize row data', async (t) => {
{ name: 'bool1', type: 'BOOL', isNullable: true },
{ name: 'bool2', type: 'BOOL', isNullable: true },
{ name: 'bool3', type: 'BOOL', isNullable: true },
// bundled migrations contain type 'TEXT' for enums
{ name: 'enum1', type: 'TEXT', isNullable: true },
{ name: 'enum2', type: 'TEXT', isNullable: true },
],
}

Expand All @@ -46,6 +49,9 @@ test('serialize/deserialize row data', async (t) => {
['bool1', PgBasicType.PG_BOOL],
['bool2', PgBasicType.PG_BOOL],
['bool3', PgBasicType.PG_BOOL],
// enum types are transformed to text type by our generator
['enum1', PgBasicType.PG_TEXT],
['enum2', PgBasicType.PG_TEXT],
]),
relations: [],
} as unknown as TableSchema<
Expand Down Expand Up @@ -76,12 +82,28 @@ test('serialize/deserialize row data', async (t) => {
bool1: 1,
bool2: 0,
bool3: null,
enum1: 'red',
enum2: null,
}

const s_row = serializeRow(record, rel, dbDescription)
t.deepEqual(
s_row.values.map((bytes) => new TextDecoder().decode(bytes)),
['Hello', 'World!', '', '1', '-30', '1', '-30.3', '5e+234', 't', 'f', '']
[
'Hello',
'World!',
'',
'1',
'-30',
'1',
'-30.3',
'5e+234',
't',
'f',
'',
'red',
'',
]
)

const d_row = deserializeRow(s_row, rel, dbDescription)
Expand All @@ -100,6 +122,8 @@ test('serialize/deserialize row data', async (t) => {
bool1: null,
bool2: null,
bool3: null,
enum1: 'red',
enum2: null,
}

const s_row2 = serializeRow(record2, rel, dbDescription)
Expand All @@ -117,6 +141,8 @@ test('serialize/deserialize row data', async (t) => {
'',
'',
'',
'red',
'',
]
)

Expand Down Expand Up @@ -282,23 +308,29 @@ test('Use incoming Relation types if not found in the schema', async (t) => {
schema: 'schema',
table: 'new_table',
tableType: SatRelation_RelationType.TABLE,
columns: [{ name: 'value', type: 'INTEGER', isNullable: true }],
columns: [
{ name: 'value', type: 'INTEGER', isNullable: true },
{ name: 'color', type: 'COLOR', isNullable: true }, // at runtime, incoming SatRelation messages contain the name of the enum type
],
}

const satOpRow = serializeRow(
{ value: 6 },
newTableRelation,
testDbDescription
)
const row = {
value: 6,
color: 'red',
}

const satOpRow = serializeRow(row, newTableRelation, testDbDescription)

// Encoded values ["6"]
t.deepEqual(satOpRow.values, [new Uint8Array(['6'.charCodeAt(0)])])
t.deepEqual(
satOpRow.values.map((bytes) => new TextDecoder().decode(bytes)),
['6', 'red']
)

const deserializedRow = deserializeRow(
satOpRow,
newTableRelation,
testDbDescription
)

t.deepEqual(deserializedRow, { value: 6 })
t.deepEqual(deserializedRow, row)
})
35 changes: 22 additions & 13 deletions components/electric/lib/electric/postgres/proxy/prisma/query.ex
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ end

defmodule Electric.Postgres.Proxy.Prisma.Query.NamespaceVersionV5_2 do
@moduledoc """
SELECT
SELECT
EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = $1),
version(),
current_setting('server_version_num')::integer as numeric_version;
Expand Down Expand Up @@ -312,7 +312,7 @@ defmodule Electric.Postgres.Proxy.Prisma.Query.ConstraintV5_2 do
AND contype NOT IN ('p', 'u', 'f')
ORDER BY namespace, table_name, constr.contype, constraint_name;

Lists:
Lists:

- check constraints
- constraint trigger
Expand Down Expand Up @@ -489,9 +489,14 @@ defmodule Electric.Postgres.Proxy.Prisma.Query.TypeV4_8 do
]
end

# we don't support custom types
def data_rows([_nspname], _schema_version, _config) do
[]
def data_rows([nspname_array], schema_version, _config) do
namespaces = Electric.Postgres.Proxy.Prisma.Query.parse_name_array(nspname_array)

for %{name: %{name: name, schema: schema}} = enum <- schema_version.schema.enums,
schema in namespaces,
value <- enum.values do
[name, value, schema]
end
end
end

Expand Down Expand Up @@ -530,9 +535,13 @@ defmodule Electric.Postgres.Proxy.Prisma.Query.TypeV5_2 do
]
end

# we don't support custom types
def data_rows([_nspname], _schema_version, _config) do
[]
def data_rows([nspname_array], schema_version, config) do
Electric.Postgres.Proxy.Prisma.Query.TypeV4_8.data_rows(
[nspname_array],
schema_version,
config
)
|> Enum.map(fn [name, value, namespace] -> [name, value, namespace, nil] end)
end
end

Expand Down Expand Up @@ -560,11 +569,11 @@ defmodule Electric.Postgres.Proxy.Prisma.Query.ColumnV4_8 do
FROM pg_class
JOIN pg_namespace on pg_namespace.oid = pg_class.relnamespace
AND pg_namespace.nspname = ANY ( $1 )
) as oid on oid.oid = att.attrelid
) as oid on oid.oid = att.attrelid
AND relname = info.table_name
AND namespace = info.table_schema
LEFT OUTER JOIN pg_attrdef attdef ON attdef.adrelid = att.attrelid AND attdef.adnum = att.attnum AND table_schema = namespace
WHERE table_schema = ANY ( $1 )
WHERE table_schema = ANY ( $1 )
ORDER BY namespace, table_name, ordinal_position;
"""
@behaviour Electric.Postgres.Proxy.Prisma.Query
Expand Down Expand Up @@ -1039,9 +1048,9 @@ defmodule Electric.Postgres.Proxy.Prisma.Query.ForeignKeyV4_8 do
conname AS constraint_name,
child,
parent,
table_name,
table_name,
namespace
FROM (SELECT
FROM (SELECT
ns.nspname AS \"namespace\",
unnest(con1.conkey) AS \"parent\",
unnest(con1.confkey) AS \"child\",
Expand Down Expand Up @@ -1340,7 +1349,7 @@ defmodule Electric.Postgres.Proxy.Prisma.Query.IndexV4_8 do
@moduledoc """
WITH rawindex AS (
SELECT
indrelid,
indrelid,
indexrelid,
indisunique,
indisprimary,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ defmodule Electric.Postgres.Proxy.Prisma.QueryTest do
["with_constraint", "public", <<0>>, <<0>>, <<0>>, nil, nil],
["checked", "public", <<0>>, <<0>>, <<0>>, nil, nil],
["interesting", "public", <<0>>, <<0>>, <<0>>, nil, nil],
["manuals", "public", <<0>>, <<0>>, <<0>>, nil, nil],
["pointy", "public", <<0>>, <<0>>, <<0>>, nil, nil],
["pointy2", "public", <<0>>, <<0>>, <<0>>, nil, nil]
])
Expand Down Expand Up @@ -82,7 +83,11 @@ defmodule Electric.Postgres.Proxy.Prisma.QueryTest do
end

test "TypeV5_2", cxt do
[] = Query.TypeV5_2.data_rows([@public], cxt.schema, config())
assert [
["oses", "linux", "public", nil],
["oses", "macos", "public", nil],
["oses", "windows", "public", nil]
] == Query.TypeV5_2.data_rows([@public], cxt.schema, config())
end

test "ColumnV5_2", cxt do
Expand Down Expand Up @@ -312,6 +317,60 @@ defmodule Electric.Postgres.Proxy.Prisma.QueryTest do
nil,
nil
],
[
"public",
"manuals",
"id",
"text",
nil,
nil,
nil,
nil,
"text",
"pg_catalog",
"text",
nil,
"NO",
"NO",
nil,
nil
],
[
"public",
"manuals",
"manual_url",
"text",
nil,
nil,
nil,
nil,
"text",
"pg_catalog",
"text",
nil,
"NO",
"NO",
nil,
nil
],
[
"public",
"manuals",
"os",
"oses",
nil,
nil,
nil,
nil,
"oses",
"pg_catalog",
"oses",
nil,
"NO",
"NO",
nil,
nil
],
[
"public",
"with_constraint",
Expand Down Expand Up @@ -498,7 +557,7 @@ defmodule Electric.Postgres.Proxy.Prisma.QueryTest do
test "ForeignKeyV5_2", cxt do
data_rows = Query.ForeignKeyV5_2.data_rows([@public], cxt.schema, config())

# can't do an equality check as the actual oids
# can't do an equality check as the actual oids
assert Enum.sort(data_rows) ==
Enum.sort([
[
Expand Down Expand Up @@ -555,7 +614,7 @@ defmodule Electric.Postgres.Proxy.Prisma.QueryTest do
test "IndexV5_2", cxt do
data_rows = Query.IndexV5_2.data_rows([@public], cxt.schema, config())

# can't do an equality check as the actual oids
# can't do an equality check as the actual oids
assert Enum.sort(data_rows) ==
Enum.sort([
[
Expand Down Expand Up @@ -654,6 +713,22 @@ defmodule Electric.Postgres.Proxy.Prisma.QueryTest do
<<0>>,
<<0>>
],
[
"public",
"manuals_pkey",
"manuals",
"id",
<<1>>,
<<1>>,
<<0, 0, 0, 0>>,
"text_ops",
<<1>>,
"btree",
"ASC",
<<0>>,
<<0>>,
<<0>>
],
[
"public",
"pointy_pkey",
Expand Down
7 changes: 7 additions & 0 deletions components/electric/test/support/prisma/002_query_test.sql
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,10 @@ CREATE TABLE public.pointy2 (
);


CREATE TYPE oses AS ENUM ('linux', 'macos', 'windows');

CREATE TABLE public.manuals (
id text PRIMARY KEY,
os oses NOT NULL,
manual_url text NOT NULL
);
4 changes: 1 addition & 3 deletions components/electric/test/support/prisma/003_query_test.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@

ALTER TABLE public.with_constraint ENABLE ELECTRIC;
ALTER TABLE public.checked ENABLE ELECTRIC;
-- NOTE: `interesting` can't currently be electrified as it contains columns of
-- types we currently (as of 09/2023) don't support
ALTER TABLE public.interesting ENABLE ELECTRIC;
ALTER TABLE public.pointy ENABLE ELECTRIC;
ALTER TABLE public.pointy2 ENABLE ELECTRIC;

ALTER TABLE public.oses ENABLE ELECTRIC;
2 changes: 1 addition & 1 deletion e2e/tests/03.20_node_satellite_can_sync_enums.lux
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
[invoke node_get_enum "row3" null]

[shell pg_1]
[invoke wait-for "SELECT * FROM public.enums;" "row1" 10 $psql]
[invoke wait-for "SELECT * FROM public.enums;" "row2" 10 $psql]

!SELECT * FROM public.enums;
??row1 | RED
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ function pgType(field: ExtendedDMMFField, modelName: string): string {
case 'Json':
return jsonToPg(attributes)
default:
if (field.kind === 'enum') return 'TEXT' // treat enums as TEXT such that the ts-client correctly serializes/deserialises them as text
return 'UNRECOGNIZED PRISMA TYPE'
}
}
Expand Down
Loading