4 +--------------------------------------------------------------------+
5 | Copyright CiviCRM LLC. All rights reserved. |
7 | This work is published under the GNU AGPLv3 license with some |
8 | permitted exceptions and without any warranty. For full license |
9 | and copyright information, see https://civicrm.org/licensing |
10 +--------------------------------------------------------------------+
16 * @copyright CiviCRM LLC https://civicrm.org/licensing
20 namespace api\v
4\Action
;
22 use api\v
4\Api4TestBase
;
23 use Civi\Api4\Activity
;
24 use Civi\Api4\Contact
;
25 use Civi\Api4\Contribution
;
26 use Civi\Test\TransactionalInterface
;
31 class SqlFunctionTest
extends Api4TestBase
implements TransactionalInterface
{
33 public function testGetFunctions() {
34 $functions = array_column(\CRM_Api4_Page_Api4Explorer
::getSqlFunctions(), NULL, 'name');
35 $this->assertArrayHasKey('SUM', $functions);
36 $this->assertArrayNotHasKey('', $functions);
37 $this->assertArrayNotHasKey('SqlFunction', $functions);
38 $this->assertEquals(1, $functions['MAX']['params'][0]['min_expr']);
39 $this->assertEquals(1, $functions['MAX']['params'][0]['max_expr']);
42 public function testGroupAggregates() {
43 $cid = Contact
::create(FALSE)->addValue('first_name', 'bill')->execute()->first()['id'];
44 Contribution
::save(FALSE)
45 ->setDefaults(['contact_id' => $cid, 'financial_type_id:name' => 'Donation'])
47 ['total_amount' => 100, 'receive_date' => '2020-01-01'],
48 ['total_amount' => 200, 'receive_date' => '2020-01-01'],
49 ['total_amount' => 300, 'receive_date' => '2020-01-01', 'financial_type_id:name' => 'Member Dues'],
50 ['total_amount' => 400, 'receive_date' => '2020-01-01', 'financial_type_id:name' => 'Event Fee'],
54 // Test AVG, SUM, MAX, MIN, COUNT
55 $agg = Contribution
::get(FALSE)
56 ->addGroupBy('contact_id')
57 ->addWhere('contact_id', '=', $cid)
58 ->addSelect('AVG(total_amount) AS average')
59 ->addSelect('SUM(total_amount)')
60 ->addSelect('MAX(total_amount)')
61 ->addSelect('MIN(total_amount)')
62 ->addSelect('COUNT(*) AS count')
63 ->addOrderBy('average')
66 $this->assertTrue(250.0 === $agg['average']);
67 $this->assertTrue(1000.0 === $agg['SUM:total_amount']);
68 $this->assertTrue(400.0 === $agg['MAX:total_amount']);
69 $this->assertTrue(100.0 === $agg['MIN:total_amount']);
70 $this->assertTrue(4 === $agg['count']);
73 $agg = Contribution
::get(FALSE)
74 ->addGroupBy('contact_id')
75 ->addWhere('contact_id', '=', $cid)
76 ->addSelect('GROUP_CONCAT(financial_type_id:name)')
77 ->addSelect('GROUP_CONCAT(financial_type_id)')
78 ->addSelect('COUNT(*) AS count')
82 $this->assertTrue(4 === $agg['count']);
83 $this->assertContains('Donation', $agg['GROUP_CONCAT:financial_type_id:name']);
84 foreach ($agg['GROUP_CONCAT:financial_type_id'] as $type) {
85 $this->assertTrue(is_int($type));
88 // Test GROUP_CONCAT with a CONCAT as well
89 $agg = Contribution
::get(FALSE)
90 ->addGroupBy('contact_id')
91 ->addWhere('contact_id', '=', $cid)
92 ->addSelect("GROUP_CONCAT(CONCAT(financial_type_id, ', ', contact_id, ', ', total_amount))")
93 ->addSelect('COUNT(*) AS count')
97 $this->assertTrue(4 === $agg['count']);
98 $this->assertContains('1, ' . $cid . ', 100.00', $agg['GROUP_CONCAT:financial_type_id_contact_id_total_amount']);
101 public function testGroupHaving() {
102 $cid = Contact
::create(FALSE)->addValue('first_name', 'donor')->execute()->first()['id'];
103 Contribution
::save(FALSE)
104 ->setDefaults(['contact_id' => $cid, 'financial_type_id' => 1])
106 ['total_amount' => 100, 'receive_date' => '2020-02-02'],
107 ['total_amount' => 200, 'receive_date' => '2020-02-02'],
108 ['total_amount' => 300, 'receive_date' => '2020-03-03'],
109 ['total_amount' => 400, 'receive_date' => '2020-04-04'],
112 $result = Contribution
::get(FALSE)
113 ->addGroupBy('contact_id')
114 ->addGroupBy('receive_date')
115 ->addSelect('contact_id')
116 ->addSelect('receive_date')
117 ->addSelect('AVG(total_amount) AS average')
118 ->addSelect('SUM(total_amount)')
119 ->addSelect('MAX(total_amount)')
120 ->addSelect('MIN(total_amount)')
121 ->addSelect('COUNT(*) AS count')
122 ->addOrderBy('receive_date')
123 ->addHaving('contact_id', '=', $cid)
124 ->addHaving('receive_date', '<', '2020-04-01')
126 $this->assertCount(2, $result);
127 $this->assertEquals(150, $result[0]['average']);
128 $this->assertEquals(300, $result[1]['average']);
129 $this->assertEquals(300, $result[0]['SUM:total_amount']);
130 $this->assertEquals(300, $result[1]['SUM:total_amount']);
131 $this->assertEquals(200, $result[0]['MAX:total_amount']);
132 $this->assertEquals(100, $result[0]['MIN:total_amount']);
133 $this->assertEquals(2, $result[0]['count']);
134 $this->assertEquals(1, $result[1]['count']);
136 $result = Contribution
::get(FALSE)
137 ->addGroupBy('contact_id')
138 ->addGroupBy('receive_date')
139 ->addSelect('contact_id')
140 ->addSelect('receive_date')
141 ->addSelect('SUM(total_amount)')
142 ->addOrderBy('receive_date')
143 ->addWhere('contact_id', '=', $cid)
144 ->addHaving('SUM(total_amount)', '>', 300)
146 $this->assertCount(1, $result);
147 $this->assertStringStartsWith('2020-04-04', $result[0]['receive_date']);
148 $this->assertEquals(400, $result[0]['SUM:total_amount']);
151 public function testComparisonFunctions() {
152 $cid = Contact
::create(FALSE)
153 ->addValue('first_name', 'hello')
154 ->execute()->first()['id'];
156 ['subject' => 'abc', 'activity_type_id:name' => 'Meeting', 'source_contact_id' => $cid, 'duration' => 123, 'location' => 'abc'],
157 ['subject' => 'xyz', 'activity_type_id:name' => 'Meeting', 'source_contact_id' => $cid, 'location' => 'abc', 'is_deleted' => 1],
158 ['subject' => 'def', 'activity_type_id:name' => 'Meeting', 'source_contact_id' => $cid, 'duration' => 456, 'location' => 'abc'],
160 $aids = Activity
::save(FALSE)
161 ->setRecords($sampleData)
162 ->execute()->column('id');
164 $result = Activity
::get(FALSE)
165 ->addWhere('id', 'IN', $aids)
166 ->addSelect('IF(is_deleted, "Trash", "No Trash") AS trashed')
167 ->addSelect('NULLIF(subject, location) AS subject_is_location')
168 ->addSelect('NULLIF(duration, 456) AS duration_not_456')
169 ->addSelect('COALESCE(duration, location) AS duration_or_location')
170 ->addSelect('GREATEST(duration, 0200) AS duration_or_200')
171 ->addSelect('LEAST(duration, 300) AS 300_or_duration')
172 ->addSelect('ISNULL(duration) AS duration_isnull')
174 ->execute()->indexBy('id');
176 $this->assertCount(3, $result);
177 $this->assertEquals('No Trash', $result[$aids[0]]['trashed']);
178 $this->assertEquals('Trash', $result[$aids[1]]['trashed']);
179 $this->assertEquals('No Trash', $result[$aids[2]]['trashed']);
180 $this->assertEquals(NULL, $result[$aids[0]]['subject_is_location']);
181 $this->assertEquals('xyz', $result[$aids[1]]['subject_is_location']);
182 $this->assertEquals('def', $result[$aids[2]]['subject_is_location']);
183 $this->assertEquals(123, $result[$aids[0]]['duration_not_456']);
184 $this->assertEquals(NULL, $result[$aids[1]]['duration_not_456']);
185 $this->assertEquals(NULL, $result[$aids[2]]['duration_not_456']);
186 $this->assertEquals('123', $result[$aids[0]]['duration_or_location']);
187 $this->assertEquals('abc', $result[$aids[1]]['duration_or_location']);
188 $this->assertEquals(123, $result[$aids[0]]['300_or_duration']);
189 $this->assertEquals(300, $result[$aids[2]]['300_or_duration']);
190 $this->assertEquals(FALSE, $result[$aids[0]]['duration_isnull']);
191 $this->assertEquals(TRUE, $result[$aids[1]]['duration_isnull']);
192 $this->assertEquals(FALSE, $result[$aids[2]]['duration_isnull']);
195 public function testStringFunctions() {
197 ['first_name' => 'abc', 'middle_name' => 'Q', 'last_name' => 'tester1', 'source' => '123'],
199 $cid = Contact
::save(FALSE)
200 ->setRecords($sampleData)
201 ->execute()->first()['id'];
203 $result = Contact
::get(FALSE)
204 ->addWhere('id', '=', $cid)
205 ->addSelect('CONCAT_WS("|", first_name, middle_name, last_name) AS concat_ws')
206 ->addSelect('REPLACE(first_name, "c", "cdef") AS new_first')
207 ->addSelect('UPPER(first_name)')
208 ->addSelect('LOWER(middle_name)')
209 ->execute()->first();
211 $this->assertEquals('abc|Q|tester1', $result['concat_ws']);
212 $this->assertEquals('abcdef', $result['new_first']);
213 $this->assertEquals('ABC', $result['UPPER:first_name']);
214 $this->assertEquals('q', $result['LOWER:middle_name']);
217 public function testIncorrectNumberOfArguments() {
220 ->addSelect('IF(is_deleted) AS whoops')
222 $this->fail('Api should have thrown exception');
224 catch (\API_Exception
$e) {
225 $this->assertEquals('Missing param 2 for SQL function IF', $e->getMessage());
230 ->addSelect('NULLIF(is_deleted, 1, 2) AS whoops')
232 $this->fail('Api should have thrown exception');
234 catch (\API_Exception
$e) {
235 $this->assertEquals('Too many arguments given for SQL function NULLIF', $e->getMessage());
240 ->addSelect('CONCAT_WS(",", ) AS whoops')
242 $this->fail('Api should have thrown exception');
244 catch (\API_Exception
$e) {
245 $this->assertEquals('Too few arguments to param 2 for SQL function CONCAT_WS', $e->getMessage());
249 public function testCurrentDate() {
250 $lastName = uniqid(__FUNCTION__
);
252 ['first_name' => 'abc', 'last_name' => $lastName, 'birth_date' => 'now'],
253 ['first_name' => 'def', 'last_name' => $lastName, 'birth_date' => 'now - 1 year'],
254 ['first_name' => 'def', 'last_name' => $lastName, 'birth_date' => 'now - 10 year'],
257 ->setRecords($sampleData)
260 $result = Contact
::get(FALSE)
261 ->addWhere('last_name', '=', $lastName)
262 ->addWhere('birth_date', '=', 'CURDATE()', TRUE)
265 $this->assertCount(1, $result);
267 $result = Contact
::get(FALSE)
268 ->addWhere('last_name', '=', $lastName)
269 ->addWhere('birth_date', '<', 'DATE(NOW())', TRUE)
272 $this->assertCount(2, $result);
275 public function testRandFunction() {
277 ->setRecords(array_fill(0, 6, []))
280 $result = Contact
::get(FALSE)
281 ->addSelect('RAND() AS rand')
282 ->addOrderBy('RAND()')
287 // Random numbers should have been ordered from least to greatest
288 $this->assertGreaterThanOrEqual($result[0]['rand'], $result[1]['rand']);
289 $this->assertGreaterThanOrEqual($result[1]['rand'], $result[2]['rand']);
290 $this->assertGreaterThanOrEqual($result[2]['rand'], $result[3]['rand']);
291 $this->assertGreaterThanOrEqual($result[3]['rand'], $result[4]['rand']);
292 $this->assertGreaterThanOrEqual($result[4]['rand'], $result[5]['rand']);
295 public function testDateInWhereClause() {
296 $lastName = uniqid(__FUNCTION__
);
298 ['first_name' => 'abc', 'last_name' => $lastName, 'birth_date' => '2009-11-11'],
299 ['first_name' => 'def', 'last_name' => $lastName, 'birth_date' => '2009-01-01'],
300 ['first_name' => 'def', 'last_name' => $lastName, 'birth_date' => '2010-01-01'],
303 ->setRecords($sampleData)
306 // Should work with isExpression=FALSE
307 $result = Contact
::get(FALSE)
308 ->addWhere('last_name', '=', $lastName)
309 ->addWhere('YEAR(birth_date)', '=', 2009)
312 $this->assertCount(2, $result);
314 // Should work with isExpression=TRUE
315 $result = Contact
::get(FALSE)
316 ->addWhere('last_name', '=', $lastName)
317 ->addWhere('YEAR(birth_date)', '=', 2009, TRUE)
320 $this->assertCount(2, $result);
322 // Try an expression in the value
323 $result = Contact
::get(FALSE)
324 ->addWhere('last_name', '=', $lastName)
325 ->addWhere('MONTH(birth_date)', '=', 'MONTH("2030-11-12")', TRUE)
326 ->addSelect('birth_date')
327 ->execute()->single();
328 $this->assertEquals('2009-11-11', $result['birth_date']);