SkillDevs / electric_dart

A Dart implementation for Electric (electric-sql.com).
Apache License 2.0
104 stars 9 forks source link

Bad state: No element when generating tables. #20

Closed MichalNemec closed 7 months ago

MichalNemec commented 7 months ago
dart run electricsql_cli generate       
Generating the Electric client code...
Service URL: http://localhost:5133
Proxy URL: postgresql://prisma:********@IP:65432/electric
✓ Prisma CLI installed (1.4s)
✓ Database introspected (0.6s)
✗ Generating Drift DB schema (14ms)
generate command failed: Bad state: No element
Unhandled exception:
Bad state: No element
#0      ListBase.firstWhere (dart:collection/list.dart:132:5)
#1      _getPrismaRelationValue (package:electricsql_cli/src/commands/generate/prisma.dart:421:25)
#2      _extractOutgoindRelation (package:electricsql_cli/src/commands/generate/prisma.dart:364:24)
#3      _extractFromModel (package:electricsql_cli/src/commands/generate/prisma.dart:289:11)
#4      extractInfoFromPrismaSchema.<anonymous closure> (package:electricsql_cli/src/commands/generate/prisma.dart:172:25)
#5      MappedListIterable.elementAt (dart:_internal/iterable.dart:425:31)
#6      ListIterator.moveNext (dart:_internal/iterable.dart:354:26)
#7      new _GrowableList._ofEfficientLengthIterable (dart:core-patch/growable_array.dart:189:27)
#8      new _GrowableList.of (dart:core-patch/growable_array.dart:150:28)
#9      new List.of (dart:core-patch/array_patch.dart:39:18)
#10     ListIterable.toList (dart:_internal/iterable.dart:224:7)
#11     extractInfoFromPrismaSchema (package:electricsql_cli/src/commands/generate/prisma.dart:186:6)
#12     _generateClient (package:electricsql_cli/src/commands/generate/command.dart:370:22)
<asynchronous suspension>
#13     wrapWithProgress (package:electricsql_cli/src/util.dart:142:17)
<asynchronous suspension>
#14     _runGeneratorInner (package:electricsql_cli/src/commands/generate/command.dart:338:5)
<asynchronous suspension>
#15     _runGenerator (package:electricsql_cli/src/commands/generate/command.dart:275:5)
<asynchronous suspension>
#16     runElectricCodeGeneration (package:electricsql_cli/src/commands/generate/command.dart:154:3)
<asynchronous suspension>
#17     GenerateElectricClientCommand.run (package:electricsql_cli/src/commands/generate/command.dart:95:5)
<asynchronous suspension>
#18     CommandRunner.runCommand (package:args/command_runner.dart:212:13)
<asynchronous suspension>
#19     ElectricCliCommandRunner.runCommand (package:electricsql_cli/src/command_runner.dart:124:18)
<asynchronous suspension>
#20     ElectricCliCommandRunner.run (package:electricsql_cli/src/command_runner.dart:65:14)
<asynchronous suspension>
#21     main (file:///Users/user/.pub-cache/hosted/pub.dev/electricsql_cli-0.6.0/bin/electricsql_cli.dart:6:24)

Version 0.6.0

davidmartos96 commented 7 months ago

@MichalNemec Could you run the generate command with the --debug flag and paste the <file>.schema file here?

MichalNemec commented 7 months ago

schema.prisma

datasource db {
  provider = "postgresql"
  url      = "postgresql://prisma:proxy@192.168.0.117:65432/electric"
}

model membership {
  id            String    @id(map: "PK_membership") @db.Uuid
  user_id       String    @db.Uuid
  room_id       String?   @db.Uuid
  space_id      String?   @db.Uuid
  status        Int
  joined_at_utc DateTime? @db.Timestamptz(6)
  read_at_utc   DateTime? @db.Timestamptz(6)
  room          room?     @relation(fields: [room_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "FK_membership_room_room_id")
  space         space?    @relation(fields: [space_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "FK_membership_space_space_id")
  user          user      @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "FK_membership_user_user_id")

  @@index([room_id], map: "IX_membership_room_id")
  @@index([space_id], map: "IX_membership_space_id")
  @@index([user_id], map: "IX_membership_user_id")
}

model message {
  id             String     @id(map: "PK_message") @db.Uuid
  room_id        String     @db.Uuid
  sender_id      String     @db.Uuid
  parent_id      String?    @db.Uuid
  message_type   Int?
  content        String
  created_at_utc DateTime   @db.Timestamptz(6)
  updated_at_utc DateTime?  @db.Timestamptz(6)
  redacted       Boolean?
  system_type    Int?
  message        message?   @relation("messageTomessage", fields: [parent_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "FK_message_message_parent_id")
  other_message  message[]  @relation("messageTomessage")
  room           room       @relation(fields: [room_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "FK_message_room_room_id")
  user           user       @relation(fields: [sender_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "FK_message_user_sender_id")
  reaction       reaction[]

  @@index([id], map: "IX_message_id")
  @@index([parent_id], map: "IX_message_parent_id")
  @@index([room_id], map: "IX_message_room_id")
  @@index([sender_id], map: "IX_message_sender_id")
}

model presence {
  id             String   @id(map: "PK_presence") @db.Uuid
  user_id        String   @db.Uuid
  platform       String
  status         Int
  updated_at_utc DateTime @db.Timestamptz(6)
  user           user     @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "FK_presence_user_user_id")

  @@index([user_id, platform], map: "IX_presence_user_id_platform")
}

model reaction {
  id             String   @id(map: "PK_reaction") @db.Uuid
  message_id     String   @db.Uuid
  user_id        String   @db.Uuid
  content        String
  created_at_utc DateTime @db.Timestamptz(6)
  message        message  @relation(fields: [message_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "FK_reaction_message_message_id")
  user           user     @relation(fields: [user_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "FK_reaction_user_user_id")

  @@index([message_id], map: "IX_reaction_message_id")
  @@index([user_id], map: "IX_reaction_user_id")
}

model room {
  id          String       @id(map: "PK_room") @db.Uuid
  creator_id  String       @db.Uuid
  room_type   Int
  name        String?
  avatar      String?
  motd        String?
  description String?
  space_id    String?      @db.Uuid
  membership  membership[]
  message     message[]
  space       space?       @relation(fields: [space_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "FK_room_space_space_id")
  user        user         @relation(fields: [creator_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "FK_room_user_creator_id")

  @@index([creator_id], map: "IX_room_creator_id")
  @@index([id], map: "IX_room_id")
  @@index([space_id], map: "IX_room_space_id")
}

model space {
  id          String       @id(map: "PK_space") @db.Uuid
  name        String?
  description String?
  avatar      String?
  membership  membership[]
  room        room[]

  @@index([id], map: "IX_space_id")
}

model user {
  id           String       @id(map: "PK_user") @db.Uuid
  display_name String
  avatar       String?
  membership   membership[]
  message      message[]
  presence     presence[]
  reaction     reaction[]
  room         room[]

  @@index([id], map: "IX_user_id")
}
davidmartos96 commented 7 months ago

@MichalNemec Thanks! Will take a look at it tomorrow, and see if I can reproduce it with that schema.

MichalNemec commented 7 months ago

sweet, thank you!

davidmartos96 commented 7 months ago

@MichalNemec Could you share the real Postgres schema (CREATE TABLE) for the message table? The problem appears to be that the other_message @relation doesn't contain the fields section, and the code expects it to have it, to know through which field/column the relation goes.

other_message message[] @relation("messageTomessage")

MichalNemec commented 7 months ago

I hope electric stuff does not matter, hehe

-- Table: public.message

-- DROP TABLE IF EXISTS public.message;

CREATE TABLE IF NOT EXISTS public.message
(
    id uuid NOT NULL,
    room_id uuid NOT NULL,
    sender_id uuid NOT NULL,
    parent_id uuid,
    message_type integer,
    content text COLLATE pg_catalog."default" NOT NULL,
    created_at_utc timestamp with time zone NOT NULL,
    updated_at_utc timestamp with time zone,
    redacted boolean,
    system_type integer,
    CONSTRAINT "PK_message" PRIMARY KEY (id),
    CONSTRAINT "FK_message_message_parent_id" FOREIGN KEY (parent_id)
        REFERENCES public.message (id) MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE NO ACTION,
    CONSTRAINT "FK_message_room_room_id" FOREIGN KEY (room_id)
        REFERENCES public.room (id) MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE NO ACTION,
    CONSTRAINT "FK_message_user_sender_id" FOREIGN KEY (sender_id)
        REFERENCES public."user" (id) MATCH SIMPLE
        ON UPDATE NO ACTION
        ON DELETE CASCADE
)

TABLESPACE pg_default;

ALTER TABLE IF EXISTS public.message
    OWNER to postgres;
-- Index: IX_message_id

-- DROP INDEX IF EXISTS public."IX_message_id";

CREATE INDEX IF NOT EXISTS "IX_message_id"
    ON public.message USING btree
    (id ASC NULLS LAST)
    TABLESPACE pg_default;
-- Index: IX_message_parent_id

-- DROP INDEX IF EXISTS public."IX_message_parent_id";

CREATE INDEX IF NOT EXISTS "IX_message_parent_id"
    ON public.message USING btree
    (parent_id ASC NULLS LAST)
    TABLESPACE pg_default;
-- Index: IX_message_room_id

-- DROP INDEX IF EXISTS public."IX_message_room_id";

CREATE INDEX IF NOT EXISTS "IX_message_room_id"
    ON public.message USING btree
    (room_id ASC NULLS LAST)
    TABLESPACE pg_default;
-- Index: IX_message_sender_id

-- DROP INDEX IF EXISTS public."IX_message_sender_id";

CREATE INDEX IF NOT EXISTS "IX_message_sender_id"
    ON public.message USING btree
    (sender_id ASC NULLS LAST)
    TABLESPACE pg_default;

-- Trigger: as_first__save_deleted_rows_to_tombstone_table

-- DROP TRIGGER IF EXISTS as_first__save_deleted_rows_to_tombstone_table ON public.message;

CREATE OR REPLACE TRIGGER as_first__save_deleted_rows_to_tombstone_table
    AFTER DELETE
    ON public.message
    FOR EACH ROW
    EXECUTE FUNCTION electric.generate_tombstone_entry___public__message();

ALTER TABLE public.message
    ENABLE ALWAYS TRIGGER as_first__save_deleted_rows_to_tombstone_table;

-- Trigger: postgres_write__delete_generate_shadow_rows

-- DROP TRIGGER IF EXISTS postgres_write__delete_generate_shadow_rows ON public.message;

CREATE OR REPLACE TRIGGER postgres_write__delete_generate_shadow_rows
    BEFORE DELETE
    ON public.message
    FOR EACH ROW
    WHEN (electric.__session_replication_role() <> 'replica'::text)
    EXECUTE FUNCTION electric.update_shadow_row_from_delete___public__message();

-- Trigger: postgres_write__upsert_generate_shadow_rows

-- DROP TRIGGER IF EXISTS postgres_write__upsert_generate_shadow_rows ON public.message;

CREATE OR REPLACE TRIGGER postgres_write__upsert_generate_shadow_rows
    BEFORE INSERT OR UPDATE 
    ON public.message
    FOR EACH ROW
    WHEN (electric.__session_replication_role() <> 'replica'::text)
    EXECUTE FUNCTION electric.create_shadow_row_from_upsert___public__message();

-- Trigger: satellite_write__save_operation_for_reordering

-- DROP TRIGGER IF EXISTS satellite_write__save_operation_for_reordering ON public.message;

CREATE OR REPLACE TRIGGER satellite_write__save_operation_for_reordering
    BEFORE INSERT OR UPDATE 
    ON public.message
    FOR EACH ROW
    WHEN (electric.__session_replication_role() = 'replica'::text AND pg_trigger_depth() < 1)
    EXECUTE FUNCTION electric.reorder_main_op___public__message();

ALTER TABLE public.message
    ENABLE ALWAYS TRIGGER satellite_write__save_operation_for_reordering;

-- Trigger: send_self_and_references_on_insert

-- DROP TRIGGER IF EXISTS send_self_and_references_on_insert ON public.message;

CREATE OR REPLACE TRIGGER send_self_and_references_on_insert
    AFTER INSERT
    ON public.message
    REFERENCING NEW TABLE AS new_table
    FOR EACH STATEMENT
    EXECUTE FUNCTION electric.send_self_trigger___public__message();

ALTER TABLE public.message
    ENABLE ALWAYS TRIGGER send_self_and_references_on_insert;

-- Trigger: send_self_and_references_on_update

-- DROP TRIGGER IF EXISTS send_self_and_references_on_update ON public.message;

CREATE OR REPLACE TRIGGER send_self_and_references_on_update
    AFTER UPDATE 
    ON public.message
    REFERENCING NEW TABLE AS new_table
    FOR EACH STATEMENT
    EXECUTE FUNCTION electric.send_self_trigger___public__message();

ALTER TABLE public.message
    ENABLE ALWAYS TRIGGER send_self_and_references_on_update;
davidmartos96 commented 7 months ago

@MichalNemec I've found the cause of the problem. It was failing when the schema contained foreign keys that referenced the same table. In this case, messageToMessage.

You can try the fix with:

dependency_overrides:
  electricsql_cli:
    git:
      url: https://github.com/SkillDevs/electric_dart
      ref: 32e493e5bd7bba910b0cfa656a6bd5230c577bb2
      path: packages/electricsql_cli
MichalNemec commented 7 months ago

That worked, thank you!