nalgeon / redka

Redis re-implemented with SQLite

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Adding Set commands

gozeloglu opened this issue · comments

Hi @nalgeon.

I just discovered your project and loved it. I want to contribute to the project by adding the set commands you listed in README: https://github.com/nalgeon/redka?tab=readme-ov-file#sets. What do you think? Is it okay to implement from me? I can implement SADD, SCARD, SDIFF, SMEMBER, SPOP, and SREM. Or, maybe all of them 😃

Hi Gökhan, thank you for your interest in Redka! I think it's a great idea :)

I suggest we start by deciding on SQL schema changes and method signatures right here in the issue. That way we reduce the risk of further debate in the PR phase. Once we agree on schema and API, you can move on to implementation.

Hi @nalgeon,

I understand your point. You are right. Let me introduce the structure for Set commands. We can discuss and enhance them.

SADD

func (d *DB) Sadd(key string, members ...any) (int, error) {}

We return an error if len(members) == 0. Or, as an alternative approach, we can implement as follows:

func (d *DB) Sadd(key string, member any, members ...any) (int, error) {}

In that way, we guarantee that there will be at least 1 member.

SCARD

// Scard returns number of elements of the set stored at key.
func (d *DB) Scard(key string) int {}

SDIFF

func (d *DB) Sdiff(key string, keys ...string) []any {}

SMEMBER

func (d *DB) Smember(key string) []any {}

SPOP

func (d *DB) Spop(key string, count int) ([]any, error) {}

SREM

func (d *DB) Srem(key string, member any, members ...any) (int, error) {}

SQL Schema

create table if not exists
rset (
    key_id integer not null,
    member text not null,  -- this can be blob as well. I am not sure

    foreign key (key_id) references rkey (id)
	  on delete cascade
);

I am open to discuss the method signatures and SQL Schama. In SQL part, there might be missing parts because I had limited time to create this draft. Also, I can define method signatures for rest of the commands. I took only the commands I mentioned above.

I think it's better to start with a limited-scope PR. I suggest implementing SADD, SREM, and SISMEMBER. And only at the Go API level (without touching the command package, which may be the scope of the next PR).

I think the schema is fine (with a blob). However, you also need a unique index and a view.

As for method signatures, there is no need for the S prefix, since they are implemented in a separate package rset. Also, it's better to keep the naming consistent, so I'd go with the following:

Add(key string, elems ...any) (int, error)
Delete(key string, elems ...any) (int, error)
Exists(key string, elem any) (bool, error)

If you agree with the above, could you please also write SQL queries for the three methods mentioned?

I agree that starting with limited commands is a good way. Function/method signatures look fine except for naming. Our command names are Add, Rem(Remove), and IsMember. However, you suggested Delete and Exists instead of real names. I think choosing real command names will be more appropriate for naming convention and consistent naming scope. That's why I suggest using Remove and IsMember as function names. I know I need to write SQL queries for creating unique index and view. Please give me a little more time to create queries since I am a little bit busy.

Sorry, I'm not ready to go with Rem and IsMember. Redis naming is really inconsistent, the same things are called differently all the time (Exists/IsMember, Card/Len/Count, Del/Rem etc). I'm sure Salvatore had his reasons for doing this, and of course I will keep the command names for the RESP API. But I don't want this inconsistency in the Go API.

What do you think about the SQL queries?

create table if not exists
    rset (
             key_id integer not null,
             member blob not null,

             foreign key (key_id) references rkey (id)
    on delete cascade
    );

create unique index if not exists
rset_pk_idx on rset (key_id, member);

create index if not exists
rset_key_id_idx on rset (key_id);

create view if not exists
vset as
    select
        rkey.id as key_id, rkey.key, rset.member
        datetime(etime/1000, 'unixepoch') as etime,
        datetime(mtime/1000, 'unixepoch') as mtime
    from rkey join rset on rkey.id = rset.key_id
    where rkey.type = 3
        and (rkey.etime is null or rkey.etime > unixepoch('subsec'))

I think it's mostly fine. The rset_key_id_idx is unnecessary, and member is better called elem, so it'll probably be like this:

create table if not exists
rset (
    key_id integer not null,
    elem blob not null,

    foreign key (key_id) references rkey (id)
      on delete cascade
);

create unique index if not exists
rset_pk_idx on rset (key_id, elem);

create view if not exists
vset as
select
    rkey.id as key_id, rkey.key, rset.elem
    datetime(etime/1000, 'unixepoch') as etime,
    datetime(mtime/1000, 'unixepoch') as mtime
from rkey join rset on rkey.id = rset.key_id
where rkey.type = 3
    and (rkey.etime is null or rkey.etime > unixepoch('subsec'))

Okay, this didn't work out. I'm closing the issue and the associated PR.