morphismtech / squeal

Squeal, a deep embedding of SQL in Haskell
350 stars 32 forks source link

A way to define `Definition` divorced from initial schema #151

Open ilyakooo0 opened 5 years ago

ilyakooo0 commented 5 years ago

Currently the types of definition functions like createTable constrain the resulting schema, rather than deriving it from the initial schema and the definition itself, which kind of hinders the ability to decompose a schema definition into independent Definitions.

echatav commented 5 years ago

Can you give an example of what you mean by hinder? I guess you're talking about the constraint

schemas1 ~ Alter sch (Create tab ('Table (constraints :=> columns)) schema0) schemas0

in createTable. It's an equality constraint so it doesn't act much differently from inlining, but schemas1 is used in 2 places so it makes the type a little clearer I think.

ilyakooo0 commented 5 years ago

I mean that there isn't a way (at least I couldn't think of it) to describe the Definition of two different tables separately and then compose them into one Definition.

If the two tables are independent, then they will both come from an empty schema. in this case the composition would have to be roughly:

cat a b -> cat a c -> cat a (b + c) 

Which is more akin to a special case of &&& from Arrow.

Furthermore, a table definition could depend on a different specific table being defined.

echatav commented 5 years ago

You can use the DDL type families to be polymorphic in the initial schema.

createUser
  :: Has "public" db schema
  => Definition db (Alter "public" (Create "user" UserTable schema) db)
ilyakooo0 commented 4 years ago

I think providing something akin to this might be useful (of course not hard-coded to the "public" schema):

type SchemumCreation (s :: Symbol) (table :: SchemumType) =
  forall db schema.
  Has "public" db schema =>
  Definition db (Alter "public" (Create s table schema) db)
echatav commented 4 years ago

I think that's less clear. It wouldn't be as obvious createTable is a Definition. And it's not a very general tool, whereas the DDL type families are very useful and general.

ilyakooo0 commented 4 years ago

Related: I am getting Could not deduce: AllNotNull with the SchemumCreation from the previous comment.

I think what I have written might not be strict enough somehow.

(I have a primaryKey constraint)

echatav commented 4 years ago

If you have a foreign key in your definition then the column(s) it references must be unique and not null. You should specify that your primary key in the other table is not null.

ilyakooo0 commented 4 years ago
type TasksTable =
  'Table
    ( '[ "pk_task_payload" :=> PrimaryKey '["task", "payload"]
       ]
        :=> '[ "task" ::: 'NoDef :=> 'NotNull (PG Text),
               "payload" ::: 'NoDef :=> 'NotNull (PG (Jsonb Value)),
               "start_time" ::: 'NoDef :=> 'NotNull (PG UTCTime),
               "initial_start_time" ::: 'NoDef :=> 'NotNull (PG UTCTime)
             ]
    )

createTasksTable ::
  Has "public" db schema =>
  Definition db (Alter "public" (Create "tasks" TasksTable schema) db)
createTasksTable =
  createTable
    #tasks
    ( notNullable text `as` #task
        :* notNullable jsonb `as` #payload
        :* notNullable timestampWithTimeZone `as` #start_time
        :* notNullable timestampWithTimeZone `as` #initial_start_time
    )
    ( primaryKey (#task :* #payload) `as` #pk_task_payload
    )

This gives me:

    • Could not deduce: AllNotNull
                          '["task" ::: field1, "payload" ::: field]
ilyakooo0 commented 4 years ago

The exact same code works when it is placed in a constant of type

Definition (Public '[]) (Public '[ "tasks" ::: TasksTable ])
echatav commented 4 years ago

interesting...clearly GHC's having trouble inferring field1 and field2. It should be able to infer them from the HasAll constraint but it can't because it's not fully applying the type families yet. Is there a big reason to want the definitions split up and polymorphic?

ilyakooo0 commented 4 years ago

Just decomposition.

Having one multi-thousand line file with every table and view definition and migration doesn't feel like a terribly good practice and feels like it will hinder maintainability and composability. (Compared to every definition/migration defining its own dependancies and separated into module).

For example define two executables with their own sets of tables, which overlap. Can't think of a good way except for copy-pasting.

ilyakooo0 commented 4 years ago

In an ideal scenario every function could explicitly specify which tables it relies on and not depend on the whole schema.

echatav commented 4 years ago

You can still decompose monomorphically though. In my project I have modules like V1.hs, V2.hs, etc. Each has their own Schemas type and a migration from the previous one.

ilyakooo0 commented 4 years ago

You mean explicitly defining some midpoint schema and splitting the definition along that schema?

echatav commented 4 years ago

basically, yes. Although I don't do one for each table, but just introduce a new Vn.hs each time I need to change the schema. So the first schema I defined was relatively large

ilyakooo0 commented 4 years ago

Not ideal, but seems reasonable.

Thanks

echatav commented 4 years ago

You can of course break apart that first one however you like. As for maintainability though, I never change the old Vn.hss, just introduce a new one.