-
-
Notifications
You must be signed in to change notification settings - Fork 161
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
Decouple attribute alias from type definition #512
Conversation
Could you elaborate a bit more here? |
Well, I guess this test existed in order to be sure than aliasing an attribute didn't change the But maybe I'm missing something 🙂 |
This test exists because it checks we can still access attributes using canonical names, and |
f591200
to
3f46447
Compare
Makes sense. I readded the test, fixed rubocop issues and forced the push rebasing from master. There is still more to review, however. Besides the points I mentioned in the first comment, we should also decide about the |
We could do: def inspect
%(#<#{self.class}[#{type.name}] #{meta.merge(options).map { |k, v| "#{k}=#{v.inspect}" }.join(' ')}>)
end |
I added a commit for each change. We can squash them later if needed.
As with attribute |
Please open a PR in rom-sql which changes this, preferably with tests. You could temporary target this branch in the rom-sql's Gemfile, once it's merged we can go back to depending on master. |
Before I needed to push here the update to the schema DSL. I added some comments to the code to make it clear some intentions. |
I'm stuck with the last change needed in order to have the new feature available. We need the following to work: schema(infer: true) do
attribute :first_name, alias: :name
end Attribute inferrer checks whether an attribute has been already defined by the user comparing names. However, |
Yes, this makes perfect sense. My understanding is that first and foremost we're moving attribute-related options to the new options hash, instead of type meta. So |
Lines 140 to 148 in a9a9f2d
Do you think that backward compatibility issues with plugins could arise? Changing |
Not sure if I follow, wdym by |
I pushed a commit with the changes. I'll try to explain it in a review, alongside other comments.
So, do you think we should move everything from meta to options? |
# @return [Hash] A hash with `:type` and `:options` keys. | ||
# | ||
# @api private | ||
def self.build_attribute_info(type, options) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Defining this method here, because it seems that the attribute class is decoupled from the attribute representation (Schema.define
accepts the attribute class as argument). Otherwise, maybe we could work with Attribute#ast
, which would be neater.
spec/sql_overrides.rb
Outdated
|
||
if type | ||
if args.empty? | ||
::ROM::SQL::Function.new(type, name: :not_used, schema: schema) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be honest, not sure what is happening here. But seems that name:
is not needed at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm specially interested in this line. I'm trying to adapt rom-sql
to the new changes, and I see that attributes without a name are created in a lot of places. So, should we make :name
optional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
:name
is required, in case of functions it's just added later, because you create a function first, then you give it a name via #as
, ie str::upper(:name).as(:first_name)
. This is not pretty from a design pov of course, because we should probably introduce an intermediate object that represents a wip-function, and its #as
method would return the actual function. This is a refactoring that could be done later if we decide it's a good idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. I guess that in the meantime, before facing that refactor, the only possibility is to have :name
as optional in its dry-initializer
definition.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now let's do this:
# FIXME: refactor so that it returns an intermediate function representation which
# needs to a name
::ROM::SQL::Function.new(type, name: :__anonymous__, schema: schema)
I'll make sure this gets addressed before the release.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@solnic I was now re-adapting rom-sql
to new changes, and I was adding name: :__anonymous
everywhere a function is created without name. The problem is that AS "__anonymous__"
gets added to a lot of resulting SQL queries.
Maybe we should tackle that refactor first... or just stick with nil
names in the meanwhile.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh damn :/ My bad, apologies, I didn't think this through. Could you try redefining option :name, optional: true, type: Types::Symbol
and see if that works?
The previous commit added the end goal feature of being able to do: schema(infer: true) do
attribute :name, alias: :username
end Besides any change required to the API or the implementation, it would just remain to be done moving other |
spec/sql_overrides.rb
Outdated
|
||
if type | ||
if args.empty? | ||
::ROM::SQL::Function.new(type, name: :not_used, schema: schema) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
:name
is required, in case of functions it's just added later, because you create a function first, then you give it a name via #as
, ie str::upper(:name).as(:first_name)
. This is not pretty from a design pov of course, because we should probably introduce an intermediate object that represents a wip-function, and its #as
method would return the actual function. This is a refactoring that could be done later if we decide it's a good idea.
Let's move the rest in a separate PR. This is getting big enough already. |
Ok, I re-adapted Tell me if you want some other codeclimate offense to be addressed. About the Also in this method and in general for all Besides any other more than welcome code review, I think this PR is ready to go 😄 |
428fcbe
to
9cc5e8d
Compare
@@ -25,7 +25,7 @@ | |||
|
|||
it 'supports custom types' do | |||
schema_dsl.use :timestamps, type: ROM::Types::Date | |||
dt_attribute = -> name { ROM::Attribute.new(ROM::Types::Date.meta(name: name, source: relation)) } | |||
dt_attribute = -> name { ROM::Attribute.new(ROM::Types::Date.meta(source: relation), name: name) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Metrics/LineLength: Line is too long. [102/100]
@@ -8,7 +8,7 @@ | |||
subject(:schema) { schema_dsl.call } | |||
|
|||
it 'adds timestamp attributes' do | |||
ts_attribute = -> name { ROM::Attribute.new(ROM::Types::Time.meta(name: name, source: relation)) } | |||
ts_attribute = -> name { ROM::Attribute.new(ROM::Types::Time.meta(source: relation), name: name) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Metrics/LineLength: Line is too long. [102/100]
|
||
types.each do |type| | ||
specify do | ||
expect(to_attr.(type).to_ast).to eql([:attribute, [:id, type.to_ast, {}]]) | ||
expect(to_attr.(type).to_ast).to eql([:attribute, [:id, type.to_ast, {alias: nil}]]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Layout/SpaceInsideHashLiteralBraces: Space inside { missing.
Layout/SpaceInsideHashLiteralBraces: Space inside } missing.
type: define_type(id, source: source), | ||
options: { name: name } | ||
} | ||
end) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Layout/MultilineMethodCallBraceLayout: Closing method call brace must be on the line after the last argument when opening brace is on a separate line from the first argument.
@@ -27,7 +27,7 @@ | |||
insert(id: 2, name: 'Joe') | |||
insert(id: 1, name: 'Jane') | |||
|
|||
order(*rel_klass.schema.primary_key.map { |t| t.meta[:name] }) | |||
order(*rel_klass.schema.primary_key.map { |t| t.name }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style/SymbolProc: Pass &:name as an argument to map instead of a block.
@@ -76,4 +78,4 @@ def warn(str) | |||
end | |||
|
|||
config.include(MapperRegistry) | |||
end | |||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Layout/TrailingBlankLines: Final newline missing.
{ | ||
user_id: define_attr_info(:Integer, { name: :user_id }, primary_key: true), | ||
group_id: define_attr_info(:Integer, { name: :group_id }, primary_key: true), | ||
name_id: define_attr_info(:String, name: :name ), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Layout/SpaceInsideParens: Space inside parentheses detected.
Style/TrailingCommaInHashLiteral: Avoid comma after the last item of a hash.
[[:attribute, [:id, [:nominal, [Integer, {}]], {}]], | ||
[:attribute, [:name, [:nominal, [String, {}]], {}]]]]]) | ||
[[:attribute, [:id, [:nominal, [Integer, {}]], {alias: nil}]], | ||
[:attribute, [:name, [:nominal, [String, {}]], {alias: nil}]]]]]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Layout/SpaceInsideHashLiteralBraces: Space inside { missing.
Layout/SpaceInsideHashLiteralBraces: Space inside } missing.
Layout/MultilineArrayBraceLayout: Closing array brace must be on the line after the last array element when opening brace is on a separate line from the first array element.
schema = ROM::Schema.define(:name, attributes: attrs.values) | ||
|
||
expect(schema.to_ast). | ||
to eql([:schema, [ | ||
:name, | ||
[[:attribute, [:id, [:nominal, [Integer, {}]], {}]], | ||
[:attribute, [:name, [:nominal, [String, {}]], {}]]]]]) | ||
[[:attribute, [:id, [:nominal, [Integer, {}]], {alias: nil}]], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Layout/SpaceInsideHashLiteralBraces: Space inside { missing.
Layout/SpaceInsideHashLiteralBraces: Space inside } missing.
schema = ROM::Schema.define(:name, attributes: attrs.values) | ||
|
||
expect(schema.to_h).to eql(attrs) | ||
expect(schema.to_h).to eql({ id: ROM::Attribute.new(ROM::Types::Integer, name: :id), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style/BracesAroundHashParameters: Redundant curly braces around a hash parameter.
@@ -11,7 +11,7 @@ | |||
|
|||
it 'returns projected schema with renamed attributes using provided prefix' do | |||
expect(wrapped.map(&:alias)).to eql(%i[users_id users_name]) | |||
expect(wrapped.map { |attr| attr.meta[:name] }).to eql(%i[id name]) | |||
expect(wrapped.map { |attr| attr.name }).to eql(%i[id name]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style/SymbolProc: Pass &:name as an argument to map instead of a block.
@@ -11,6 +11,6 @@ | |||
|
|||
it 'returns projected schema with renamed attributes using provided prefix' do | |||
expect(prefixed.map(&:alias)).to eql(%i[user_id user_name]) | |||
expect(prefixed.map { |attr| attr.meta[:name] }).to eql(%i[id name]) | |||
expect(prefixed.map { |attr| attr.name }).to eql(%i[id name]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style/SymbolProc: Pass &:name as an argument to map instead of a block.
@@ -30,12 +30,13 @@ | |||
end | |||
|
|||
let(:attributes_inferrer) do | |||
proc { [ [define_attribute(:name, :String)], %i(id age) ] } | |||
proc { [ [define_attribute(:String, name: :name)], %i(id age) ] } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Layout/SpaceInsideArrayLiteralBrackets: Do not use space inside array brackets.
Style/PercentLiteralDelimiters: %i-literals should be delimited by [ and ].
end | ||
|
||
specify do | ||
expect(attr.inspect).to eql("#<ROM::Attribute[TrueClass | FalseClass] name=:admin alias=:adm>") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.
Metrics/LineLength: Line is too long. [103/100]
end | ||
|
||
specify do | ||
expect(attr.inspect).to eql("#<ROM::Attribute[TrueClass | FalseClass] name=:admin alias=nil>") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.
Metrics/LineLength: Line is too long. [102/100]
end | ||
end | ||
|
||
schema = Test::Users.schema_proc.call |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Layout/IndentationConsistency: Inconsistent indentation detected.
schema do | ||
attribute :name, alias: :username | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Layout/EndAlignment: end at 106, 4 is not aligned with class at 102, 5.
|
||
it 'allows setting attribute options while still leaving type undefined' do | ||
class Test::Users < ROM::Relation[:memory] | ||
schema do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Layout/IndentationWidth: Use 2 (not 1) spaces for indentation.
end | ||
|
||
it 'allows setting attribute options while still leaving type undefined' do | ||
class Test::Users < ROM::Relation[:memory] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Layout/IndentationWidth: Use 2 (not 3) spaces for indentation.
Style/Documentation: Missing top-level class documentation comment.
Style/ClassAndModuleChildren: Use nested module/class definitions instead of compact style.
@@ -83,6 +86,31 @@ class Test::Posts < ROM::Relation[:memory] | |||
expect(schema.foreign_key(:users)).to be(schema[:author_id]) | |||
end | |||
|
|||
it 'allows setting attribute options' do | |||
class Test::Users < ROM::Relation[:memory] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style/Documentation: Missing top-level class documentation comment.
Style/ClassAndModuleChildren: Use nested module/class definitions instead of compact style.
@waiting-for-dev thanks again for working on this. I rebased it and cleaned up commits. I'll merge once CI is green. |
This adds the following feature to the schema dsl: ``` schema(infer: true) do attribute :name, alias: :username end ``` `:name` will still be inferred while keeping the `alias:` option.
Fow now, functions are created without a name, and it is later provided with `as(:name)`.
Hey @solnic I was going to tackle CI offenses right now, but it seems you just merged it at the same exact time? |
@waiting-for-dev feel free to push such fixes directly to master |
Ok @solnic , thanks for your feedback and your support with this PR. |
This PR is created because of this thread in the forums:
https://discourse.rom-rb.org/t/ability-to-alias-an-attribute-but-keeping-type-inferred/289
Basically, it decouples an attribute alias from its type, so it will be
possible to define an alias but still make use of schema inference.
This is still a WIP, but I'm looking for some feedback in order not to do extra work.
Some considerations I'd like to have your thoughts:
The DSL is still not adapted. I haven't tested it, but I guess it won't work
defining an
attribute
without specifying its type, because the main purposeof this PR is to be able to do a simple:
attribute :foo, alias: :bar
The option to define an alias is called
alias:
. This is becauseas:
can'tbe freely used because it is already a method name and
dry-initializer
creates methods from option names. We could use method renaming feature in
dry-initializer
for that, but then we have to juggle with the renaming inthe rest of code, like in
aliased
andmeta
methods, and it is a bit ugly.The handicap is that in other places of the API
as:
is used for similarpurposes, like in association aliasing.
alias
method has been removed because it is defined out of the box bydry-initializer
.The attribute
ast
will now always returnalias: nil
when an alias is notdefined. This is different from before, where
alias
key was not present inthose situations. In my opinion, it makes more sense to always return all the keys.
The test for returning aliased attribute identified by canonincal name has
been removed, because I think that, being now an option and not part of the
type, it makes no sense.
I have redefined
ROM::SQL::Function#name
method in the repositoryintegration tests. This should be changed in
rom-sql
project. How would yogo with it?
I guess that it would also make sense to move
wrapped
from themeta
to anoption
. Would you like to change it also in this PR?What do you think about
primary_key
meta? Should we consider it part of thetype (
meta
) or also an option? Even if it related with the value (the valuebeing unique), it is so only considering the rest of values. It is like
dynamic typing. Anyway, it would be more convenient to have it as option, I
guess.
Thanks!