From be44117c7039501d40f099a85152827bb4b0856a Mon Sep 17 00:00:00 2001 From: ionica21 Date: Tue, 23 Feb 2021 10:18:02 +1300 Subject: [PATCH 01/13] Quote fields in PG query assembler --- src/granite/query/assemblers/pg.cr | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/granite/query/assemblers/pg.cr b/src/granite/query/assemblers/pg.cr index db3a43c4..540cc693 100644 --- a/src/granite/query/assemblers/pg.cr +++ b/src/granite/query/assemblers/pg.cr @@ -4,6 +4,12 @@ module Granite::Query::Assembler class Pg(Model) < Base(Model) @placeholder = "$" + def field_list + # Override this method to quote the fields as upper case characters + # get converted to lower case in PG, which we do not want. + [Model.fields].flatten.map{ |field| "\"#{field}\"" }.join ", " + end + def add_parameter(value : Granite::Columns::Type) : String @numbered_parameters << value "$#{@numbered_parameters.size}" From 47cc00b462a390138bf79a9ade15b424aee69f28 Mon Sep 17 00:00:00 2001 From: ionica21 Date: Tue, 23 Feb 2021 10:19:47 +1300 Subject: [PATCH 02/13] Created converter for PG Enums that are returned as bytes --- src/granite/converters.cr | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/granite/converters.cr b/src/granite/converters.cr index bd4d102b..1f508d65 100644 --- a/src/granite/converters.cr +++ b/src/granite/converters.cr @@ -107,4 +107,17 @@ module Granite::Converters result.read(::PG::Numeric?).try &.to_f end end + + # Converts a `Slice(UInt8)/Bytes` value into a `String`. Usually for PG Enums + module EnumSlice + extend self + + def self.to_db(value) : Granite::Columns::Type + value + end + + def self.from_rs(result : ::DB::ResultSet) : String + String.new result.read Slice(UInt8) + end + end end From ee3ecf1e6244be43dea9d58edc2e72d0f18c2ff3 Mon Sep 17 00:00:00 2001 From: ionica21 Date: Tue, 23 Feb 2021 10:22:14 +1300 Subject: [PATCH 03/13] Quote table names in adapter methods only if not already quoted --- src/adapter/base.cr | 13 +++++++++++-- src/adapter/pg.cr | 10 +++++----- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/adapter/base.cr b/src/adapter/base.cr index aa0395e7..57f32cc0 100644 --- a/src/adapter/base.cr +++ b/src/adapter/base.cr @@ -43,7 +43,7 @@ abstract class Granite::Adapter::Base clause = ensure_clause_template(clause) statement = query.custom ? "#{query.custom} #{clause}" : String.build do |stmt| stmt << "SELECT " - stmt << query.fields.map { |name| "#{quote(query.table_name)}.#{quote(name)}" }.join(", ") + stmt << query.fields.map { |name| "#{quote_if_required(query.table_name)}.#{quote(name)}" }.join(", ") stmt << " FROM #{quote(query.table_name)} #{clause}" end @@ -60,7 +60,7 @@ abstract class Granite::Adapter::Base # Returns `true` if a record exists that matches *criteria*, otherwise `false`. def exists?(table_name : String, criteria : String, params = [] of Granite::Columns::Type) : Bool - statement = "SELECT EXISTS(SELECT 1 FROM #{table_name} WHERE #{ensure_clause_template(criteria)})" + statement = "SELECT EXISTS(SELECT 1 FROM #{quote_if_required(table_name)} WHERE #{ensure_clause_template(criteria)})" exists = false elapsed_time = Time.measure do @@ -104,6 +104,15 @@ abstract class Granite::Adapter::Base # Use macro in order to read a constant defined in each subclasses. macro inherited + # quote the string only if it hasn't already been quoted + def quote_if_required(name : String) : String + if name[0] != QUOTING_CHAR && name[name.size - 1] != QUOTING_CHAR + quote(name) + else + name + end + end + # quotes table and column names def quote(name : String) : String String.build do |str| diff --git a/src/adapter/pg.cr b/src/adapter/pg.cr index 5d017d7d..e6d09e30 100644 --- a/src/adapter/pg.cr +++ b/src/adapter/pg.cr @@ -28,7 +28,7 @@ class Granite::Adapter::Pg < Granite::Adapter::Base # remove all rows from a table and reset the counter on the id. def clear(table_name : String) - statement = "DELETE FROM #{quote(table_name)}" + statement = "DELETE FROM #{quote_if_required(table_name)}" elapsed_time = Time.measure do open do |db| @@ -41,7 +41,7 @@ class Granite::Adapter::Pg < Granite::Adapter::Base def insert(table_name : String, fields, params, lastval) : Int64 statement = String.build do |stmt| - stmt << "INSERT INTO #{quote(table_name)} (" + stmt << "INSERT INTO #{quote_if_required(table_name)} (" stmt << fields.map { |name| "#{quote(name)}" }.join(", ") stmt << ") VALUES (" stmt << fields.map { |name| "$#{fields.index(name).not_nil! + 1}" }.join(", ") @@ -75,7 +75,7 @@ class Granite::Adapter::Pg < Granite::Adapter::Base statement = String.build do |stmt| stmt << "INSERT" - stmt << " INTO #{quote(table_name)} (" + stmt << " INTO #{quote_if_required(table_name)} (" stmt << fields.map { |field| quote(field) }.join(", ") stmt << ") VALUES " @@ -115,7 +115,7 @@ class Granite::Adapter::Pg < Granite::Adapter::Base # This will update a row in the database. def update(table_name : String, primary_name : String, fields, params) statement = String.build do |stmt| - stmt << "UPDATE #{quote(table_name)} SET " + stmt << "UPDATE #{quote_if_required(table_name)} SET " stmt << fields.map { |name| "#{quote(name)}=$#{fields.index(name).not_nil! + 1}" }.join(", ") stmt << " WHERE #{quote(primary_name)}=$#{fields.size + 1}" end @@ -131,7 +131,7 @@ class Granite::Adapter::Pg < Granite::Adapter::Base # This will delete a row from the database. def delete(table_name : String, primary_name : String, value) - statement = "DELETE FROM #{quote(table_name)} WHERE #{quote(primary_name)}=$1" + statement = "DELETE FROM #{quote_if_required(table_name)} WHERE #{quote(primary_name)}=$1" elapsed_time = Time.measure do open do |db| From 94f1d3a2b7696413bdff7509822cf7401a17348c Mon Sep 17 00:00:00 2001 From: ionica21 Date: Tue, 23 Feb 2021 11:41:33 +1300 Subject: [PATCH 04/13] Check if table/column string has already been quoted --- src/adapter/base.cr | 24 ++++++++++-------------- src/adapter/pg.cr | 10 +++++----- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/adapter/base.cr b/src/adapter/base.cr index 57f32cc0..a9cb1a48 100644 --- a/src/adapter/base.cr +++ b/src/adapter/base.cr @@ -43,7 +43,7 @@ abstract class Granite::Adapter::Base clause = ensure_clause_template(clause) statement = query.custom ? "#{query.custom} #{clause}" : String.build do |stmt| stmt << "SELECT " - stmt << query.fields.map { |name| "#{quote_if_required(query.table_name)}.#{quote(name)}" }.join(", ") + stmt << query.fields.map { |name| "#{quote(query.table_name)}.#{quote(name)}" }.join(", ") stmt << " FROM #{quote(query.table_name)} #{clause}" end @@ -60,7 +60,7 @@ abstract class Granite::Adapter::Base # Returns `true` if a record exists that matches *criteria*, otherwise `false`. def exists?(table_name : String, criteria : String, params = [] of Granite::Columns::Type) : Bool - statement = "SELECT EXISTS(SELECT 1 FROM #{quote_if_required(table_name)} WHERE #{ensure_clause_template(criteria)})" + statement = "SELECT EXISTS(SELECT 1 FROM #{quote(table_name)} WHERE #{ensure_clause_template(criteria)})" exists = false elapsed_time = Time.measure do @@ -104,24 +104,20 @@ abstract class Granite::Adapter::Base # Use macro in order to read a constant defined in each subclasses. macro inherited - # quote the string only if it hasn't already been quoted - def quote_if_required(name : String) : String + # quotes table and column names + def quote(name : String) : String + # only quote the string if it isn't already quoted if name[0] != QUOTING_CHAR && name[name.size - 1] != QUOTING_CHAR - quote(name) + String.build do |str| + str << QUOTING_CHAR + str << name + str << QUOTING_CHAR + end else name end end - # quotes table and column names - def quote(name : String) : String - String.build do |str| - str << QUOTING_CHAR - str << name - str << QUOTING_CHAR - end - end - # converts the crystal class to database type of this adapter def self.schema_type?(key : String) : String? Schema::TYPES[key]? || Granite::Adapter::Base::Schema::TYPES[key]? diff --git a/src/adapter/pg.cr b/src/adapter/pg.cr index e6d09e30..5d017d7d 100644 --- a/src/adapter/pg.cr +++ b/src/adapter/pg.cr @@ -28,7 +28,7 @@ class Granite::Adapter::Pg < Granite::Adapter::Base # remove all rows from a table and reset the counter on the id. def clear(table_name : String) - statement = "DELETE FROM #{quote_if_required(table_name)}" + statement = "DELETE FROM #{quote(table_name)}" elapsed_time = Time.measure do open do |db| @@ -41,7 +41,7 @@ class Granite::Adapter::Pg < Granite::Adapter::Base def insert(table_name : String, fields, params, lastval) : Int64 statement = String.build do |stmt| - stmt << "INSERT INTO #{quote_if_required(table_name)} (" + stmt << "INSERT INTO #{quote(table_name)} (" stmt << fields.map { |name| "#{quote(name)}" }.join(", ") stmt << ") VALUES (" stmt << fields.map { |name| "$#{fields.index(name).not_nil! + 1}" }.join(", ") @@ -75,7 +75,7 @@ class Granite::Adapter::Pg < Granite::Adapter::Base statement = String.build do |stmt| stmt << "INSERT" - stmt << " INTO #{quote_if_required(table_name)} (" + stmt << " INTO #{quote(table_name)} (" stmt << fields.map { |field| quote(field) }.join(", ") stmt << ") VALUES " @@ -115,7 +115,7 @@ class Granite::Adapter::Pg < Granite::Adapter::Base # This will update a row in the database. def update(table_name : String, primary_name : String, fields, params) statement = String.build do |stmt| - stmt << "UPDATE #{quote_if_required(table_name)} SET " + stmt << "UPDATE #{quote(table_name)} SET " stmt << fields.map { |name| "#{quote(name)}=$#{fields.index(name).not_nil! + 1}" }.join(", ") stmt << " WHERE #{quote(primary_name)}=$#{fields.size + 1}" end @@ -131,7 +131,7 @@ class Granite::Adapter::Pg < Granite::Adapter::Base # This will delete a row from the database. def delete(table_name : String, primary_name : String, value) - statement = "DELETE FROM #{quote_if_required(table_name)} WHERE #{quote(primary_name)}=$1" + statement = "DELETE FROM #{quote(table_name)} WHERE #{quote(primary_name)}=$1" elapsed_time = Time.measure do open do |db| From 064bcc78a6cd383d502e0a61631b0afd9e4e65b2 Mon Sep 17 00:00:00 2001 From: ionica21 Date: Tue, 2 Mar 2021 16:58:44 +1300 Subject: [PATCH 05/13] Quote foreign key --- src/granite/association_collection.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/granite/association_collection.cr b/src/granite/association_collection.cr index c7abfbc4..940206ba 100644 --- a/src/granite/association_collection.cr +++ b/src/granite/association_collection.cr @@ -36,7 +36,7 @@ class Granite::AssociationCollection(Owner, Target) private def query if through.nil? - "WHERE #{Target.table_name}.#{@foreign_key} = ?" + "WHERE #{Target.table_name}.#{Target.quote(@foreign_key.to_s)} = ?" else "JOIN #{through} ON #{through}.#{Target.to_s.underscore}_id = #{Target.table_name}.#{Target.primary_name} " \ "WHERE #{through}.#{@foreign_key} = ?" From 2d701bbe7e36761ef30d8aab9902c786afc9d9f4 Mon Sep 17 00:00:00 2001 From: ionica21 Date: Tue, 2 Mar 2021 16:59:23 +1300 Subject: [PATCH 06/13] Quote fields in query assemblers --- src/granite/query/assemblers/base.cr | 29 ++++++++++++++++++++------ src/granite/query/assemblers/mysql.cr | 1 + src/granite/query/assemblers/pg.cr | 3 ++- src/granite/query/assemblers/sqlite.cr | 1 + 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/granite/query/assemblers/base.cr b/src/granite/query/assemblers/base.cr index 59a48a68..0606bd56 100644 --- a/src/granite/query/assemblers/base.cr +++ b/src/granite/query/assemblers/base.cr @@ -36,6 +36,23 @@ module Granite::Query::Assembler clauses.compact!.join " " end + # Use macro in order to read a constant defined in each subclasses. + macro inherited + # quotes table and column names + def quote(name : String) : String + # only quote the string if it isn't already quoted + if name[0] != QUOTING_CHAR && name[name.size - 1] != QUOTING_CHAR + String.build do |str| + str << QUOTING_CHAR + str << name + str << QUOTING_CHAR + end + else + name + end + end + end + def where return @where if @where @@ -60,7 +77,7 @@ module Granite::Query::Assembler add_aggregate_field expression[:field] if expression[:value].nil? - clauses << "#{expression[:field]} IS NULL" + clauses << "#{quote(expression[:field])} IS NULL" elsif expression[:value].is_a?(Array) in_stmt = String.build do |str| str << '(' @@ -72,9 +89,9 @@ module Granite::Query::Assembler end str << ')' end - clauses << "#{expression[:field]} #{sql_operator(expression[:operator])} #{in_stmt}" + clauses << "#{quote(expression[:field])} #{sql_operator(expression[:operator])} #{in_stmt}" else - clauses << "#{expression[:field]} #{sql_operator(expression[:operator])} #{add_parameter expression[:value]}" + clauses << "#{quote(expression[:field])} #{sql_operator(expression[:operator])} #{add_parameter expression[:value]}" end end end @@ -101,9 +118,9 @@ module Granite::Query::Assembler add_aggregate_field expression[:field] if expression[:direction] == Builder::Sort::Ascending - "#{expression[:field]} ASC" + "#{quote(expression[:field])} ASC" else - "#{expression[:field]} DESC" + "#{quote(expression[:field])} DESC" end end @@ -115,7 +132,7 @@ module Granite::Query::Assembler group_fields = @query.group_fields return nil if group_fields.none? group_clauses = group_fields.map do |expression| - "#{expression[:field]}" + "#{quote(expression[:field])}" end @group_by = "GROUP BY #{group_clauses.join ", "}" diff --git a/src/granite/query/assemblers/mysql.cr b/src/granite/query/assemblers/mysql.cr index 65897a82..ccd03ef3 100644 --- a/src/granite/query/assemblers/mysql.cr +++ b/src/granite/query/assemblers/mysql.cr @@ -2,6 +2,7 @@ # This will likely require adapter specific subclassing :[. module Granite::Query::Assembler class Mysql(Model) < Base(Model) + QUOTING_CHAR = '`' @placeholder = "?" def add_parameter(value : Granite::Columns::Type) : String diff --git a/src/granite/query/assemblers/pg.cr b/src/granite/query/assemblers/pg.cr index 540cc693..1f94d0a9 100644 --- a/src/granite/query/assemblers/pg.cr +++ b/src/granite/query/assemblers/pg.cr @@ -2,12 +2,13 @@ # This will likely require adapter specific subclassing :[. module Granite::Query::Assembler class Pg(Model) < Base(Model) + QUOTING_CHAR = '"' @placeholder = "$" def field_list # Override this method to quote the fields as upper case characters # get converted to lower case in PG, which we do not want. - [Model.fields].flatten.map{ |field| "\"#{field}\"" }.join ", " + [Model.fields].flatten.map{ |field| "#{quote(field)}" }.join ", " end def add_parameter(value : Granite::Columns::Type) : String diff --git a/src/granite/query/assemblers/sqlite.cr b/src/granite/query/assemblers/sqlite.cr index 2f9bd5d1..48fb117a 100644 --- a/src/granite/query/assemblers/sqlite.cr +++ b/src/granite/query/assemblers/sqlite.cr @@ -1,5 +1,6 @@ module Granite::Query::Assembler class Sqlite(Model) < Base(Model) + QUOTING_CHAR = '"' @placeholder = "?" def add_parameter(value : Granite::Columns::Type) : String From 33ceb9ae356b0753f8975b90dce84f88714029a5 Mon Sep 17 00:00:00 2001 From: Richard Howell-Peak Date: Tue, 16 Mar 2021 16:04:33 +1300 Subject: [PATCH 07/13] added some docs --- docs/graphql.md | 16 ++++++++++++++++ docs/postgresql.md | 18 ++++++++++++++++++ docs/readme.md | 2 ++ src/test.cr | 12 ++++++++++++ 4 files changed, 48 insertions(+) create mode 100644 docs/graphql.md create mode 100644 docs/postgresql.md create mode 100644 src/test.cr diff --git a/docs/graphql.md b/docs/graphql.md new file mode 100644 index 00000000..c7b3bd02 --- /dev/null +++ b/docs/graphql.md @@ -0,0 +1,16 @@ +# GraphQL + +To integrate Granite with GraphQL, you can use the 'column_and_field' macros. + +```crystal +@[GraphQL::Object] +class Backtest < Granite::Base + include GraphQL::ObjectType + + + def self.table_name + "\"schemaName\".\"TableName\"" + end + +end +``` diff --git a/docs/postgresql.md b/docs/postgresql.md new file mode 100644 index 00000000..26b2413c --- /dev/null +++ b/docs/postgresql.md @@ -0,0 +1,18 @@ +# Postgresql + +In field names are case-sensitive. If this is a pure Crystal project then this does not matter as all variable and table names will be snake_case. + +However, if you need to integrate your Crystal project with another, pre-existing technology (such as a NodeJS application), then you might find that column or table names are now in camelCase. + +As such you can create a class method which overrides the Granite default naming convention: + +```crystal +class MyModel < Granite::Base + + + def self.table_name + "\"schemaName\".\"TableName\"" + end + +end +``` diff --git a/docs/readme.md b/docs/readme.md index f1533a0a..75f1f001 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -72,3 +72,5 @@ end [Migrations](./migrations.md) [Imports](./imports.md) + +[Postgresql](./postgresql.md) diff --git a/src/test.cr b/src/test.cr new file mode 100644 index 00000000..8dc42d96 --- /dev/null +++ b/src/test.cr @@ -0,0 +1,12 @@ +class Bob + + def initialize + @a = 4 + @b = 5 + end + +end + +b = Bob.new + +puts b.instance_vars \ No newline at end of file From b5aca1ce8b9096abb7f8df3d860cb9717f6b5c34 Mon Sep 17 00:00:00 2001 From: Richard Howell-Peak Date: Tue, 11 May 2021 20:47:08 +1200 Subject: [PATCH 08/13] proper indentation --- src/granite/converters.cr | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/granite/converters.cr b/src/granite/converters.cr index 1f508d65..aa59fd63 100644 --- a/src/granite/converters.cr +++ b/src/granite/converters.cr @@ -108,16 +108,17 @@ module Granite::Converters end end - # Converts a `Slice(UInt8)/Bytes` value into a `String`. Usually for PG Enums - module EnumSlice - extend self - - def self.to_db(value) : Granite::Columns::Type - value - end - - def self.from_rs(result : ::DB::ResultSet) : String - String.new result.read Slice(UInt8) - end + # Converts a `Slice(UInt8)/Bytes` value into a `String`. Usually for PG Enums + module EnumSlice + extend self + + def self.to_db(value) : Granite::Columns::Type + value end + + def self.from_rs(result : ::DB::ResultSet) : String + String.new result.read Slice(UInt8) + end + end + end From 8cce63b1050dd79bea9c8607636fad8b25ec8df6 Mon Sep 17 00:00:00 2001 From: Richard Howell-Peak Date: Tue, 11 May 2021 20:47:14 +1200 Subject: [PATCH 09/13] remove work from another pr --- docs/graphql.md | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 docs/graphql.md diff --git a/docs/graphql.md b/docs/graphql.md deleted file mode 100644 index c7b3bd02..00000000 --- a/docs/graphql.md +++ /dev/null @@ -1,16 +0,0 @@ -# GraphQL - -To integrate Granite with GraphQL, you can use the 'column_and_field' macros. - -```crystal -@[GraphQL::Object] -class Backtest < Granite::Base - include GraphQL::ObjectType - - - def self.table_name - "\"schemaName\".\"TableName\"" - end - -end -``` From 221c7b3ea5b53c664af77777791ff1a457efdd82 Mon Sep 17 00:00:00 2001 From: Richard Howell-Peak Date: Tue, 11 May 2021 20:48:59 +1200 Subject: [PATCH 10/13] fixed typo --- docs/postgresql.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/postgresql.md b/docs/postgresql.md index 26b2413c..ef46060e 100644 --- a/docs/postgresql.md +++ b/docs/postgresql.md @@ -1,6 +1,6 @@ # Postgresql -In field names are case-sensitive. If this is a pure Crystal project then this does not matter as all variable and table names will be snake_case. +In Postgres field names are case-sensitive. If this is a pure Crystal project then this does not matter as all variable and table names will be snake_case. However, if you need to integrate your Crystal project with another, pre-existing technology (such as a NodeJS application), then you might find that column or table names are now in camelCase. From 19d60b1b101c79d42d5cd20ba0edfcf58d73bfe5 Mon Sep 17 00:00:00 2001 From: Richard Howell-Peak Date: Tue, 11 May 2021 20:51:04 +1200 Subject: [PATCH 11/13] remove work from another pr --- src/granite/converters.cr | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/granite/converters.cr b/src/granite/converters.cr index aa59fd63..b864fc71 100644 --- a/src/granite/converters.cr +++ b/src/granite/converters.cr @@ -107,18 +107,5 @@ module Granite::Converters result.read(::PG::Numeric?).try &.to_f end end - - # Converts a `Slice(UInt8)/Bytes` value into a `String`. Usually for PG Enums - module EnumSlice - extend self - - def self.to_db(value) : Granite::Columns::Type - value - end - - def self.from_rs(result : ::DB::ResultSet) : String - String.new result.read Slice(UInt8) - end - end end From 5862186aed281ed8982728880d919d2f11384298 Mon Sep 17 00:00:00 2001 From: Richard Howell-Peak Date: Tue, 11 May 2021 20:52:18 +1200 Subject: [PATCH 12/13] remove spurious file --- src/test.cr | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 src/test.cr diff --git a/src/test.cr b/src/test.cr deleted file mode 100644 index 8dc42d96..00000000 --- a/src/test.cr +++ /dev/null @@ -1,12 +0,0 @@ -class Bob - - def initialize - @a = 4 - @b = 5 - end - -end - -b = Bob.new - -puts b.instance_vars \ No newline at end of file From b266d6d5f107d8d9aa46557479c4d396b8b9d12a Mon Sep 17 00:00:00 2001 From: Richard Howell-Peak Date: Tue, 11 May 2021 20:54:45 +1200 Subject: [PATCH 13/13] remove extra new line char --- src/granite/converters.cr | 1 - 1 file changed, 1 deletion(-) diff --git a/src/granite/converters.cr b/src/granite/converters.cr index b864fc71..bd4d102b 100644 --- a/src/granite/converters.cr +++ b/src/granite/converters.cr @@ -107,5 +107,4 @@ module Granite::Converters result.read(::PG::Numeric?).try &.to_f end end - end