How to dynamic, handle nested WHERE AND/OR queries using Rails and SQL - sql

I'm currently building a feature that requires me to loop over an hash, and for each key in the hash, dynamically modify an SQL query.
The actual SQL query should look something like this:
select * from space_dates d
inner join space_prices p on p.space_date_id = d.id
where d.space_id = ?
and d.date between ? and ?
and (
(p.price_type = 'monthly' and p.price_cents <> 9360) or
(p.price_type = 'daily' and p.price_cents <> 66198) or
(p.price_type = 'hourly' and p.price_cents <> 66198) # This part should be added in dynamically
)
The last and query is to be added dynamically, as you can see, I basically need only one of the conditions to be true but not all.
query = space.dates
.joins(:price)
.where('date between ? and ?', start_date, end_date)
# We are looping over the rails enum (hash) and getting the key for each key value pair, alongside the index
SpacePrice.price_types.each_with_index do |(price_type, _), index|
amount_cents = space.send("#{price_type}_price").price_cents
query = if index.positive? # It's not the first item so we want to chain it as an 'OR'
query.or(
space.dates
.joins(:price)
.where('space_prices.price_type = ?', price_type)
.where('space_prices.price_cents <> ?', amount_cents)
)
else
query # It's the first item, chain it as an and
.where('space_prices.price_type = ?', price_type)
.where('space_prices.price_cents <> ?', amount_cents)
end
end
The output of this in rails is:
SELECT "space_dates".* FROM "space_dates"
INNER JOIN "space_prices" ON "space_prices"."space_date_id" = "space_dates"."id"
WHERE "space_dates"."space_id" = $1 AND (
(
(date between '2020-06-11' and '2020-06-11') AND
(space_prices.price_type = 'hourly') AND (space_prices.price_cents <> 9360) OR
(space_prices.price_type = 'daily') AND (space_prices.price_cents <> 66198)) OR
(space_prices.price_type = 'monthly') AND (space_prices.price_cents <> 5500)
) LIMIT $2
Which isn't as expected. I need to wrap the last few lines in another set of round brackets in order to produce the same output. I'm not sure how to go about this using ActiveRecord.
It's not possible for me to use find_by_sql since this would be dynamically generated SQL too.

So, I managed to solve this in about an hour using Arel with rails
dt = SpaceDate.arel_table
pt = SpacePrice.arel_table
combined_clauses = SpacePrice.price_types.map do |price_type, _|
amount_cents = space.send("#{price_type}_price").price_cents
pt[:price_type]
.eq(price_type)
.and(pt[:price_cents].not_eq(amount_cents))
end.reduce(&:or)
space.dates
.joins(:price)
.where(dt[:date].between(start_date..end_date).and(combined_clauses))
end
And the SQL output is:
SELECT "space_dates".* FROM "space_dates"
INNER JOIN "space_prices" ON "space_prices"."space_date_id" = "space_dates"."id"
WHERE "space_dates"."space_id" = $1
AND "space_dates"."date" BETWEEN '2020-06-11' AND '2020-06-15'
AND (
("space_prices"."price_type" = 'hourly'
AND "space_prices"."price_cents" != 9360
OR "space_prices"."price_type" = 'daily'
AND "space_prices"."price_cents" != 66198)
OR "space_prices"."price_type" = 'monthly'
AND "space_prices"."price_cents" != 5500
) LIMIT $2
What I ended up doing was:
Creating an array of clauses based on the enum key and the price_cents
Reduced the clauses and joined them using or
Added this to the main query with an and operator and the combined_clauses

Related

Case Statement that runs sql

I am applying a mask to data and believe the best way is to use a Case Statement. However, I need the case statement to run a sub query. When I pull data, it will either be a number or appear as 99999999999v999b:99999999999v999-
Using
TO_NUMBER(REGEXP_REPLACE(RD.subm_quantity, '^(\d+)(-)?$', '\2\1'))/1000 as "Submitted_Quantity"
This will convert it to a number. So if 00000000100000 is present, it will convert to 100
However, I need a case to not divide when not needed. To determine if I need to divide, I need to add a rule in the below sql:
if the result is 99999999999v999b:99999999999v999-, apply the conversion;
if not, just output RD.subm_quantity.
How can I get a case statement to run a query?
Running in TOAD for Oracle:
select m.mask
FROM Valiuser.ivd_mapping m,
Valiuser.ivd_mappingset s,
Valiuser.ivd_mapping_record r,
Valiuser.ivd_transaction_file tf,
VALIUSER.ivd_transaction_record_details RD
WHERE s.mappingset_id = r.mappingset_id
AND r.mapping_record_id = m.mapping_record_ID
AND m.repository_column_id = '34'
AND s.mappingset_id = tf.MAPPINGSET_ID
AND rd.file_id = tf.file_id
AND rd.TRANSACTION_RECORD_ID =
If the mask and original subm_quantity are both available from the query you showed, which seems to be the same as that includes the rd table you're referencing in the conversion, then I think you want something like this:
case when m.mask = '99999999999v999b:99999999999v999-'
then TO_NUMBER(REGEXP_REPLACE(rd.subm_quantity, '^(\d+)(-)?$', '\2\1')) / 1000
else rd.subm_quantity
end as "Submitted_Quantity"
rather than a subquery. So plugged into your current query that would make it:
SELECT
case when m.mask = '99999999999v999b:99999999999v999-'
then TO_NUMBER(REGEXP_REPLACE(rd.subm_quantity, '^(\d+)(-)?$', '\2\1')) / 1000
else rd.subm_quantity
end as "Submitted_Quantity"
FROM
Valiuser.ivd_mapping m,
Valiuser.ivd_mappingset s,
Valiuser.ivd_mapping_record r,
Valiuser.ivd_transaction_file tf,
Valiuser.ivd_transaction_record_details rd
WHERE
s.mappingset_id = r.mappingset_id
AND r.mapping_record_id = m.mapping_record_ID
AND m.repository_column_id = '34'
AND s.mappingset_id = tf.mappingset_id
AND rd.file_id = tf.file_id
AND rd.Transaction_Record_Id = <?>
or with modern join syntax instead of the old version, something like:
SELECT
case when m.mask = '99999999999v999b:99999999999v999-'
then TO_NUMBER(REGEXP_REPLACE(rd.subm_quantity, '^(\d+)(-)?$', '\2\1')) / 1000
else rd.subm_quantity
end as "Submitted_Quantity"
FROM Valiuser.ivd_mapping m
JOIN Valiuser.ivd_mapping_record r ON r.mapping_record_id = m.mapping_record_ID
JOIN Valiuser.ivd_mappingset s ON s.mappingset_id = r.mappingset_id
JOIN Valiuser.ivd_transaction_file tf ON tf.mappingset_id = s.mappingset_id
JOIN Valiuser.ivd_transaction_record_details rd ON rd.file_id = tf.file_id
WHERE m.repository_column_id = '34'
AND rd.transaction_record_id = <?>

change where condition based on column value

I faced the following requirement. The following query is called by a procedure. The value p_pac_code is the input parameter of the procedure.
The requirement is the query should have an additional condition sp_sbsp.SUBSPCLTY_CODE!='C430 if the p_pac_code value is '008'.
For any other p_pac_code value, it should run as it is below. Is there way to do this by adding an additional condition in the WHERE clause?
As for now, I have done this using IF.....ELSE using the query two times separately depending on p_pac_code value. But I am required to find a way to do with just adding a condition to this single query.
SELECT ptg.group_cid
FROM PRVDR_TYPE_X_SPCLTY_SUBSPCLTY ptxss,
PT_X_SP_SSP_STATUS pxsst ,
pt_sp_ssp_x_group ptg,
group_x_group_store gg,
specialty_subspecialty sp_sbsp,
treatment_type tt,
provider_type pt
WHERE
pt.PRVDR_TYPE_CODE = ptxss.PRVDR_TYPE_CODE
AND tt.TRTMNT_TYPE_CODE = pxsst.TRTMNT_TYPE_CODE
AND ptxss.PRVDR_TYPE_X_SPCLTY_SID = pxsst.PRVDR_TYPE_X_SPCLTY_SID
AND tt.TRTMNT_TYPE_CODE = p_pac_code
AND TRUNC(SYSDATE) BETWEEN TRUNC(PXSST.FROM_DATE) AND TRUNC(PXSST.TO_DATE)
AND ptg.prvdr_type_code =ptxss.prvdr_type_code
AND ptg.spclty_subspclty_sid = ptxss.spclty_subspclty_sid
AND ptxss.spclty_subspclty_sid = sp_sbsp.spclty_subspclty_sid
AND ptg.spclty_subspclty_sid = sp_sbsp.spclty_subspclty_sid
AND ptg.status_cid = 2
AND ptg.group_cid = gg.group_cid
AND gg.group_store_cid = 16
AND gg.status_cid = 2;
Thanks in advance.
You can simply add a condition like this:
... and (
( sp_sbsp.SUBSPCLTY_CODE!='C430' and p_pac_code = '008')
OR
NVL(p_pac_code, '-') != '008'
)
This can be re-written in different ways, this one is quite self-explanatory
Just add:
AND NOT ( NVL( sp_sbsp.SUBSPCLTY_CODE, 'x' ) = 'C430'
AND NVL( p_pac_code value, 'x' ) = '008' )
to the where clause.
The NVL function is used so that it will match NULL values (if they exist in your data); otherwise, even though NULL does not match C430 you will still find that NULL = 'C430' and NULL <> 'C430' and NOT( NULL = 'C430' ) will all return false.
Quite easy. Add the following condition:
AND (sp_sbsp.SUBSPCLTY_CODE != 'C430' OR p_pac_code value != '008')
(don't forget the parenthesis)
Just sharing
SELECT ptg.group_cid
FROM PRVDR_TYPE_X_SPCLTY_SUBSPCLTY ptxss
WHERE
pt.PRVDR_TYPE_CODE = ptxss.PRVDR_TYPE_CODE
&&((tt.TRTMNT_TYPE_CODE = pxsst.TRTMNT_TYPE_CODE)
or (tt.TRTMNT_TYPE_CODE = pxsst.TRTMNT_TYPE_CODE))
just use the parenthesis to specify where the conditionmust implement.

Trying to get better at understand sql queries

I'm a new developer in a corporate IT group. I feel completely lost looking at alot of the sql that's written in our code. There are many queries that are literally hundreds of lines long. Here's an example of a "smaller" one (the table/field names have been modified, but the structure is the same). Do I just suck at this or is it really difficult to understand? How would you approach rewriting it to be more manageable/understandable?
SELECT pd.commission_header_id COMMISSION_HEADER_ID
,pd.sales_rep_id DIRECT_SALESREP_ID
,pd.org_id ORG_ID
,LEAST(100, innview.spilt_percent) SPLIT_PCT
,pd.source SOURCE
,pd.application APPLICATION
,pd.plan PLANE
,pd.pay_freq pay_freq
,pd.emp_app_count EMP_APP_COUNT
,pd.client_name CLIENT_NAME
,pd.fed_id FED_ID
,pd.off_nbr OFF_NBR
,pd.payroll_num payroll_num
,TO_DATE(pd.product_start_date,'DD-MON-YY') PRODUCT_START_DATE
,pd.loss_date LOSS_DATE
,pd.acquisitions_ind ACQUISITIONS_IND
,pd.source_of_business SOURCE_OF_BUSINESS
,pd.prev_br_clt_nbr PREV_BR_CLT_NBR
,pd.previous_method PREVIOUS_METHOD
,pd.natl_acct_number NATL_ACCT_NUMBER
,pd.product_name PRODUCT_NAME
,pd.lost_reason LOST_REASON
,pd.client_id CLIENT_ID
,pd.lead_nbr LEAD_NBR
,pd.processed_date PROCESSED_DATE
,pd.rep_type REP_TYPE
,pd.order_number ORDER_NUMBER
,pd.conversion_type CONVERSION_TYPE
,SUBSTR(original_billing_info,1,INSTR(original_billing_info,'|',1,1)-1) BILLING_APPLICATION
,SUBSTR(original_billing_info,INSTR(original_billing_info,'|',1,1)+1,INSTR(original_billing_info,'|',1,2)-INSTR(original_billing_info,'|',1,1)-1) BILLING_CODE
,SUBSTR(original_billing_info,INSTR(original_billing_info,'|',1,2)+1,INSTR(original_billing_info,'|',1,3)-INSTR(original_billing_info,'|',1,2)-1) BILLING_PROD_GRP_NM
,SUBSTR(original_billing_info,INSTR(original_billing_info,'|',1,3)+1) BILLING_FIELD_FOR_CHRGS
,SUBSTR(original_discount_info,1,INSTR(original_discount_info,'|',1,1)-1) DISCOUNT_APPLICATION
,SUBSTR(original_discount_info,INSTR(original_discount_info,'|',1,1)+1,INSTR(original_discount_info,'|',1,2)-INSTR(original_discount_info,'|',1,1)-1) DISCOUNT_CODE
,CASE WHEN (NVL(innview.trans,0)) = 0
THEN 0
ELSE ( NVL(innview.comm,0)) / ( NVL(innview.trans,0))
END ORIGINAL_COMM_PCT
,pd.record_type RECORD_TYPE
,innview.trans PAR_SUM
,NVL(ocp.oic_clients_pk, pxmis.oic_client_products_pk_seq.nextval) CLIENT_PRODUCT_ID
,pd.associated_with ASSOCIATED_WITH_ID
,pd.refferal_sale refferal_sale
,pd.parent_client_nbr PARENT_CLIENT_NBR
FROM pxmis.pd_commissions pd
JOIN pxmis.oic_lookback_months olm ON (pd.application = olm.application
AND pd.plan = olm.plan)
LEFT OUTER JOIN pxmis.oic_client_products ocp ON (pd.off_nbr = ocp.off_nbr
AND pd.payroll_num = ocp.payroll_num
AND pd.application = ocp.application
AND pd.plan = ocp.plan
AND pd.sales_rep_id = ocp.direct_salesrep_id
AND pd.source = ocp.source
AND ocp.record_type in (:1,:2)
AND NVL(pd.refferal_sale,'NON REF') = NVL(ocp.refferal_sale,'NON REF'))
JOIN (SELECT SUM(NVL(transaction_amount,0)) as trans
,SUM(commission_amount) as comm
,SUM(pd2.oic_split_percent) as spilt_percent
,pd2.application as application
,pd2.plan as code
,pd2.sales_rep_id as sales_rep_id
,pd2.payroll_num as client_nbr
,LPAD(pd2.off_nbr,4,0) as off_nbr
FROM pxmis.pd_commissions pd2
JOIN pxmis.oic_lookback_months olm2 ON (pd2.application = olm2.application
AND pd2.plan = olm2.plan)
WHERE CASE WHEN (pd2.record_type IN ('SAPBA','ASAPBA'))
THEN pd2.SUB_RECORD_TYPE
ELSE 'Eligible'
END = 'Eligible'
AND pd2.record_type IN ('SDBDA', 'ASDBDA',
decode(:3,'CHCST','SAPBA',NULL),
decode(:4,'CHCST','ASAPBA',NULL),
'SAPBP','ASAPBP')
AND pd2.processed_date BETWEEN ADD_MONTHS(:5,(olm2.lookback_months_high - 1) * -1) AND ADD_MONTHS(:6, (olm2.lookback_months_low - 2) * -1) - 1
and :7 between olm2.effective_start_date and olm2.effective_end_date
and olm2.function_type = :8
GROUP BY pd2.application
,pd2.plan
,pd2.sales_rep_id
,pd2.payroll_num
,LPAD(pd2.off_nbr,4,0)) innview on (innview.application = pd.application
AND innview.code = pd.plan
AND innview.sales_rep_id = pd.sales_rep_id
AND innview.client_nbr = pd.payroll_num
AND innview.off_nbr = LPAD(pd.off_nbr,4,0))
WHERE :9 BETWEEN olm.effective_start_date AND olm.effective_end_date
AND pd.sales_rep_id <> -3
AND pd.record_type IN ('SAPBP','ASAPBP')
AND NOT EXISTS(SELECT 'X'
FROM pxmis.pd_commissions pd3
WHERE pd3.application = pd.application
AND pd3.plan = pd.plan
AND pd3.sales_rep_id = pd.sales_rep_id
AND pd3.payroll_num = pd.payroll_num
AND (pd3.record_type in ('CHCST','ACHCST')
or ( pd3.record_type = 'SAPBA'
and pd3.sub_record_type = 'Chargeback')))
AND pd.commission_header_id = (SELECT MAX(pd4.commission_header_id)
FROM pxmis.pd_commissions pd4
WHERE pd4.application = pd.application
AND pd4.plan = pd.plan
AND pd4.sales_rep_id = pd.sales_rep_id
AND pd4.payroll_num = pd.payroll_num
AND pd4.record_type = pd.record_type)
AND olm.function_type = :10
AND pd.processed_date BETWEEN ADD_MONTHS(:11,(olm.lookback_months_high - 1) * -1) AND ADD_MONTHS(:12, (olm.lookback_months_low - 2) * -1) - 1
AND pd.source = :13
AND pay_freq IS NOT NULL
AND pd.core_conversion_revenue IS NULL;
Sidenote: (Due to my reputation I can not make comments:)
This is my first time answering. Hopefully it will help.
I do not know if it might help, but most of the SQL script (maybe all of it), can be rewritten in other languages. For example you can use JOINS in C# .NET LINQ.
But a way I usually separate the SQL script, is to look for the keywords and see what they do. Do not confuse the big letters from the small ones.
The SELECT function selects all of the datas.
The TO_DATE function says that it will convert the a string to date (source: http://www.techonthenet.com/oracle/functions/to_date.php)
The LEAST function returns the smallest number between the variables you have as parameters (Source: http://www.techonthenet.com/oracle/functions/least.php)
You can do this for every function, and try and grasp what the script does.
I think you could end the script here: FROM pxmis.pd_commissions pd
Also I have seen that you do a JOIN.
Here you could properly make a new script and run that code.
Right now I might not be able to make it more readable. (Mostly because I work with .NET C# linqs).
Again hopefully it helped in some way.

Rails Mysql2::Error: Operand should contain 1 column(s)

I wanted to take this method:
self.spend_on_scholarships_for_african_people = training_programs.scholarships.joins(:participants).where('participants.race' => AFRICAN_RACES).sum('participants.spend')
and now want break it into different genders. We capture gender within the participants table. so i tried using active records array conditions within the query to do so like this:
self.bursary_spend_on_potential_female_employees = training_programs.scholarships.joins(:participants).where('participants.race = ? AND participants.gender = ?', AFRICAN_RACES, 'Female').sum('participants.spend')
self.bursary_spend_on_potential_male_employees = training_programs.scholarships.joins(:participants).where('participants.race = ? AND participants.gender = ?', AFRICAN_RACES, 'Male').sum('participants.spend')
But i keep getting the following error:
ActiveRecord::StatementInvalid - Mysql2::Error: Operand should contain 1 column(s): SELECT SUM(participants.spend) AS sum_id FROM `training_programs` INNER JOIN `participants` ON `participants`.`training_program_id` = `training_programs`.`id` WHERE `training_programs`.`skills_development_id` IS NULL AND `training_programs`.`skills_development_type` = 'SkillsDevelopment' AND `training_programs`.`scholarship` = 1 AND (participants.race = 'African','Coloured','Indian' AND participants.gender = 'Female'):
I have cut up my query and i can't seem to find what i have done wrong?
participants.race = 'African','Coloured','Indian'
should be
participants.race IN ('African','Coloured','Indian')
To get that SQL result, change the way you build your query to something like:
participants.race IN (?)

Where is the "LEFT" operator in LINQ?

Using SQL Server 2005 I've run a query like this
SELECT *
FROM mytable
WHERE (LEFT (title, 1) BETWEEN #PREFIXFROM AND #PREFIXTO)
I use this to do alphabet filtering, so for example
PREFIXFROM = a
PREFIXTO = c
and I get all the items in mytable between a and c (inclusive)
How do I do this in linq?
Selecting all the records fine.. but
1) how do I do the "LEFT" operation
and 2) How do I do a <= type operator with a non numeric field?
Any ideas appreciated!
Don't think of the SQL - think of what you're trying to achieve, and how you'd do it in .NET. So you're trying to get the first character, and work out whether it's between 'a' and 'c':
var query = from row in mytable
where row.title[0] >= 'a' && row.title[0] <= 'c'
select row;
or in dot notation:
var query = mytable.Where(row => row.title[0] >= 'a' && row.title[0] <= 'c');
Alternatively:
var query = mytable.Where(row => row.title.Substring(0, 1).CompareTo("a") >= 0 &&
row.title.Substring(0, 1).CompareTo("c") <= 0));