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

Minimally useful query syntax #132

Merged
merged 18 commits into from
Apr 27, 2018
Merged

Minimally useful query syntax #132

merged 18 commits into from
Apr 27, 2018

Conversation

robacarp
Copy link
Member

@robacarp robacarp commented Jan 31, 2018

There's been some chatter about building a query syntax for granite. I've been tinkering with this, added to my Amber project directly. I've included it here as a granite extension for simplicity.

Goals:

  • I really like how easy it is to get to the Granite raw SQL, and I don't want to hide that.
  • The basic ORM is working great.
  • I'm also seeing several of the reasons (besides database interoperability) why builder syntax is handy for efficiency and also for readability.
  • I think Granite should, where possible and efficient, stay out of macrospace.
  • My example tries to provide good separation of concerns. Several classes which each address a very specific problem. Comments welcome.
  • I really would like the ability to pass around partial query objects without mangling strings, so an intermediate class which holds a pending/partial query which can be appended to is necessary.
  • I don't want to handle caching here. Granite would benefit from that, but it's a different problem, for a different day.
  • I don't want to handle any relationships here. Eventually it'd be great to be able to build multiple models out of one joined query, but: crawl, walk, run.
  • Most importantly, this functionality should be expressly opt-in, at least for now. Easy to include into a single model, not monkeypatching the base. At least for now.

Current status

This works:

  • Model.where(field: "value").count
  • Model.where(field: "value").first(n) / .first / .any?
  • Model.where(field: "value").order(field: :desc)
  • Model.where(field: "value").delete
  • Model.where(...).raw_sql

Currently there is only a query builder for Postgres, but all query generation logic is contained with Assembler::Postgres, so adding a Mysql and Sqlite compliant version shouldn't be too difficult. 🍌

I'd really like to work together on this. Granite has a great history of working together over time! If the current code is a viable starting point, let's get it in and everyone can contribute.

🥑

@robacarp robacarp changed the title First stab at a query syntax Minimally useful query syntax Feb 2, 2018
Copy link

@marksiemers marksiemers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple inline comments.

My only concern here is the ability to eventually insert JOINs to handle associations along with the query syntax.

Have we looked at Lucky's query builder to see if it could be compatible and if it does what is needed?

self
end

def _build_order

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these underscore methods meant to indicate internal use? If so, make them private.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes and no. They can't be private because they're called by the Query::Compiled class. I think this is evidence of an architectural mis-step in this design, but I can't quite see what the correction would be yet.

Copy link

@marksiemers marksiemers Feb 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't really change things, but I like it a little more:

def order
  set_default_order if @order.none?
  build_order
end

private def build_order
  @order.map { |expression| "#{expression[:field]} #{expression[:direction]}" }.join(", ")
end

private def set_default_order
  @order = [{ field: T.primary_name, direction: "ASC" }]
end

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

protected might be what you are looking for

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, technically, _build_order is returning an order clause, right? so:

def order - def order_clause : String
private def build_order -> private def build_order_clause : String

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eliasjpr - There isn't any inheritance, an instance of this class is used in class he mentioned.

Some of these classes could potentially be made into module mixins, just going off names:
Query - noun, class
A query you can build, compile, and run - those could be modules that the Query mixes in

def initialize(@query : Compiled(T))
end

def log(*args)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will need to hook into a logger eventually, right? So it doesn't log in production.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, it's just a stub for now

@robacarp
Copy link
Member Author

robacarp commented Feb 2, 2018

@marksiemers I haven't done anything thing in here yet with it, but I'm trying to keep joins in mind for sure. Thanks for looking!

Copy link
Contributor

@eliasjpr eliasjpr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good start. Some comments.

self
end

def order(field : Symbol)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about using a tuple instead order(updated_at: :asc)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other overload does exactly that. This method is meant to be called: .order(:id) and assumes :asc.

self
end

def _build_order
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

protected might be what you are looking for

#
# - Model.where(field: value).or( Model.where(field3: value3) )
# or
# - Model.where(field: value).or { whehre(field3: value3) }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why no Model.where(field: value).or(field3: value3)

@robacarp
Copy link
Member Author

  • Added postgres where(field: nil) correct handling
  • Added builder.raw_sql:
> amber exec
User.where(email: nil).raw_sql
  SELECT id, name, email, crypted_password, admin, created_at, updated_at
    FROM users
    WHERE email IS NULL
    ORDER BY id DESC

There are a couple tests now, but they're written with some project specific requires where I'm using this query language in my amber project. Changing the require statements will be required to get the suite to pass.

@faustinoaq
Copy link
Contributor

Fixes #158

@drujensen
Copy link
Member

@robacarp what is the status on this? I really like the work done and think the foundation provides more additional features down the road. Let me know if I can help work on some of it.

@robacarp
Copy link
Member Author

robacarp commented Apr 2, 2018

@drujensen Thanks for looking. It's status is "pending". I'd like to move the tests into place for the granite repo, so that it can pass specs, and then it can be merged in relatively short order so others can contribute. There are some big components missing, but I think it's the beginnings of a usable foundation.

So long as it continues to be opt-in, I'd be fine having it in master. At some point when it's more stable, it can be included by default. Is that a sane way to open the doors for more collaboration?

@drujensen
Copy link
Member

@robacarp yeah, we can include this in master as long as it doesn't break the existing stuff and then add to it until its full featured. We can keep it as an undocumented until we feel comfortable its ready for prime time. I think this is a great start!

If we do document it, let's mark it as experimental because more than likely going to change over time as we iterate over it. WDYT?

@robacarp
Copy link
Member Author

robacarp commented Apr 2, 2018

Agreed

@robacarp
Copy link
Member Author

robacarp commented Apr 9, 2018

@drujensen @eliasjpr I've moved tests into the right place to run them, so I think this is ready for a merge so others can start collaborating on it.

@faustinoaq
Copy link
Contributor

faustinoaq commented Apr 9, 2018

@robacarp I like this 👍

Just one question, does this conflict with #176 in some way?

@robacarp
Copy link
Member Author

robacarp commented Apr 9, 2018

@faustinoaq good question, no. None of this takes effect in a granite model without explicitly including it, like this:

class MyModel < Granite::ORM::Base
  include Granite::Query::BuilderMethods
end

It does share some of the same functionality, which I hope will merge down the line, but it's independent.

end

def log(*stuff)
puts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use granite logger?

@robacarp robacarp merged commit ea226b4 into amberframework:master Apr 27, 2018
@robacarp robacarp deleted the query_syntax_proposal branch April 27, 2018 22:13
@faustinoaq
Copy link
Contributor

Hi, Is this documented in Granite's README? 😅

@robacarp
Copy link
Member Author

@faustinoaq no, it's not. I'm not sure it's ready for the readme yet. I do want to ping @noahlh that this code exists though, because I think it might be relevant to the custom query work he's doing.

@faustinoaq
Copy link
Contributor

@robacarp Oh, ok, I just opened a tracking issue for this 👍 , see: #216

@noahlh
Copy link
Contributor

noahlh commented May 23, 2018

@robacarp thank you for the ping! I have to apologize - I totally missed this PR when I started diving into my query macro (I saw this code but didn't know how WIP it was). Would love to help out / make sure I'm not duplicating any of your work or causing more complexity. I'll dive in to better understand this plugin - it looks super cool and makes a lot of sense. LMK thoughts.

@robacarp
Copy link
Member Author

@noahlh no apology necessary. This is pretty minimal, but I think it'd benefit from the way you're thinking about things as well. Happy to hop on a pairing session to see how we can make it all work together

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants