Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 0 additions & 161 deletions .github/workflows/moodle-plugin-ci.yml

This file was deleted.

68 changes: 68 additions & 0 deletions MSSQL_JSON_SUPPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
MSSQL JSON support examples for mod_booking

This document contains example SQL snippets for SQL Server (MSSQL / sqlsrv) that correspond to changes added in the codebase.

1) Extract scalar from JSON object (JSON_VALUE)

Example: extract `price` from a JSON object stored in column `json`:

SELECT JSON_VALUE(json, '$.price') AS price

Cast to numeric:
SELECT CAST(JSON_VALUE(json, '$.price') AS FLOAT) AS price

2) Extract key from JSON array element at index (JSON_VALUE)

Example: extract `key` from first element of JSON array in column `json`:

SELECT JSON_VALUE(json, '$[0].key') AS first_key

3) Expand payments array and extract fields (OPENJSON + JSON_VALUE)

Assume `sch.json` contains {"installments": {"payments": [ {...}, {...} ] }}

SELECT
bo.id AS optionid,
sch.userid,
CAST(JSON_VALUE(pay.[value], '$.timestamp') AS INT) AS datefield,
CAST(JSON_VALUE(pay.[value], '$.paid') AS FLOAT) AS paid,
CAST(JSON_VALUE(pay.[value], '$.price') AS FLOAT) AS price,
CAST(JSON_VALUE(pay.[value], '$.id') AS INT) AS payment_id
FROM local_shopping_cart_history sch
CROSS APPLY OPENJSON(sch.json, '$.installments.payments') AS pay
WHERE CAST(JSON_VALUE(pay.[value], '$.paid') AS FLOAT) = 0

4) Check membership in JSON array (OPENJSON)

Assume `json` contains { "sharedplaceswithoptions": [ 1, 2, 3 ] }

SELECT id FROM booking_options
WHERE EXISTS (
SELECT 1 FROM OPENJSON(json, '$.sharedplaceswithoptions') AS sp
WHERE sp.[value] = '42' -- checks membership of option id 42
)

5) Aggregate JSON objects per user+option (FOR JSON PATH)

Create an aggregated JSON array of certificate objects per user + optionid:

SELECT t.userid,
CAST(JSON_VALUE(t.data, '$.bookingoptionid') AS INT) AS optionid,
(
SELECT t2.id, t2.code, t2.expires, t2.data, t2.timecreated
FROM tool_certificate_issues t2
WHERE t2.userid = t.userid
AND CAST(JSON_VALUE(t2.data, '$.bookingoptionid') AS INT) = CAST(JSON_VALUE(t.data, '$.bookingoptionid') AS INT)
FOR JSON PATH
) AS certificate
FROM tool_certificate_issues t
GROUP BY t.userid, CAST(JSON_VALUE(t.data, '$.bookingoptionid') AS INT)

Notes
- SQL Server functions used: JSON_VALUE, JSON_QUERY, OPENJSON, FOR JSON PATH.
- OPENJSON returns rows with columns `key`, `value`, `type`. For arrays of scalars, `value` contains the scalar value.
- JSON_VALUE returns NVARCHAR; cast when numeric types are required.

Testing tips
- Use `SELECT JSON_QUERY('[{"id":1,"price":10}]', '$[0]')` to inspect JSON fragments.
- Validate that the stored JSON is valid nvarchar (not stored as binary) and accessible by JSON functions.
19 changes: 18 additions & 1 deletion classes/bo_availability/bo_info.php
Original file line number Diff line number Diff line change
Expand Up @@ -1383,6 +1383,11 @@ public static function check_for_sqljson_key_in_array(string $dbcolumn, string $
case 'mysql':
// MySQL: Extract key from JSON array element at specified index.
return "JSON_UNQUOTE(JSON_EXTRACT($dbcolumn, '$[$index]." . addslashes($jsonkey) . "'))";
case 'mssql':
case 'sqlsrv':
// MSSQL / SQL Server: Use JSON_VALUE with array index path.
// Example: JSON_VALUE(column, '$[0].key')
return "JSON_VALUE($dbcolumn, '$[$index]." . addslashes($jsonkey) . "')";
default:
throw new \moodle_exception('Unsupported database type for JSON key extraction.');
}
Expand Down Expand Up @@ -1417,7 +1422,19 @@ public static function check_for_sqljson_key_in_object(string $dbcolumn, string
} else {
return "CAST(JSON_EXTRACT($dbcolumn, '$." . addslashes($jsonkey) . "') AS CHAR)";
}

case 'mssql':
case 'sqlsrv':
// MSSQL / SQL Server: Use JSON_VALUE for scalar extraction.
// Map common types to SQL Server types.
$lt = strtolower($type);
if (in_array($lt, ['int', 'integer', 'bigint'], true)) {
return "CAST(JSON_VALUE($dbcolumn, '$." . addslashes($jsonkey) . "') AS INT)";
} elseif (in_array($lt, ['float', 'double', 'decimal'], true)) {
return "CAST(JSON_VALUE($dbcolumn, '$." . addslashes($jsonkey) . "') AS FLOAT)";
} else {
// Default: return text value. JSON_VALUE already returns NVARCHAR.
return "JSON_VALUE($dbcolumn, '$." . addslashes($jsonkey) . "')";
}
default:
throw new moodle_exception('Unsupported database type for JSON key extraction.');
}
Expand Down
43 changes: 43 additions & 0 deletions classes/bo_availability/conditions/enrolledincohorts.php
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,49 @@ public function return_sql(int $userid = 0, &$params = []): array {
)
)";
return ['', '', '', $params, $where];
} else if ($databasetype == 'mssql' || $databasetype == 'sqlsrv') {
// SQL Server: Use OPENJSON and JSON_VALUE to evaluate array elements.
$cohorts = [];
foreach ($usercohorts as $cohort) {
$cohorts[] = "'$cohort->id'";
}
$appendwhere2 = implode(', ', $cohorts);

$where = "
availability IS NOT NULL
AND ((
(NOT EXISTS (
SELECT 1
FROM OPENJSON(availability) WITH (sqlfilter nvarchar(10) '$.sqlfilter') jt
WHERE jt.sqlfilter = '1'
))
)
OR (
id IN (
SELECT id
FROM (
SELECT id,
JSON_VALUE(availability, '$[0].cohortidsoperator') AS operator,
(
SELECT COUNT(1)
FROM OPENJSON(availability) AS obj
CROSS APPLY OPENJSON(obj.[value], '$.cohortids') AS cid
) AS length,
(
SELECT SUM(CASE WHEN cid2.[value] IN ($appendwhere2) THEN 1 ELSE 0 END)
FROM OPENJSON(availability) AS obj2
CROSS APPLY OPENJSON(obj2.[value], '$.cohortids') AS cid2
) AS true_conditions_count
FROM {booking_options}
WHERE availability IS NOT NULL
) s1
WHERE (
CASE WHEN operator LIKE 'AND' THEN length = true_conditions_count ELSE true_conditions_count > 0 END
)
)
)
)";
return ['', '', '', $params, $where];
} else {
return ['', '', '', $params, ''];
}
Expand Down
25 changes: 25 additions & 0 deletions classes/booking.php
Original file line number Diff line number Diff line change
Expand Up @@ -1715,6 +1715,31 @@ public static function get_sql_for_fieldofstudy(string $dbname, array $courses)
) bos1
WHERE bos1.boavailid = '" . MOD_BOOKING_BO_COND_JSON_ENROLLEDINCOURSE . "'"
. $where . " ) bo";
case 'sqlsrv_native_moodle_database':
case 'mssql_native_moodle_database':
// SQL Server: use OPENJSON to expand availability array and extract id/courseids.
$where = '';
$wherearray = [];
foreach ($courses as $courseid) {
$wherearray[] = " EXISTS (
SELECT 1 FROM OPENJSON(bos1.boscourseids) AS cid WHERE cid.[value] = '" . $courseid . "') ";
}
if (count($courses) > 0) {
$where = " AND ( ( " . implode(" ) OR ( ", $wherearray) . " ) ) ";
}

return "
FROM (
SELECT bos1.*
FROM (
SELECT bo.*, elem.[value] AS elemvalue,
JSON_VALUE(elem.[value], '$.id') AS boavailid,
JSON_QUERY(elem.[value], '$.courseids') AS boscourseids
FROM {booking_options} bo
CROSS APPLY OPENJSON(bo.availability) AS elem
) bos1
WHERE bos1.boavailid = '" . MOD_BOOKING_BO_COND_JSON_ENROLLEDINCOURSE . "'"
. $where . " ) bo";
}
}

Expand Down
43 changes: 43 additions & 0 deletions classes/booking_rules/conditions/select_user_shopping_cart.php
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,49 @@ public function execute(
>= ( :nowparam + (86400 * :numberofdays ))";
}

break;
case 'mssql':
case 'sqlsrv':
// SQL Server: expand JSON payments array using OPENJSON and extract fields via JSON_VALUE.
// Use $DB->sql_concat to build unique id consistently.
$concatparts = [];
$concatparts[] = "bo.id";
if (strpos($sql->select, 'optiondateid') !== false) {
$concatparts[] = "bod.id";
}
$concatparts[] = "JSON_VALUE(payments_info.[value], '$.id')";
$concatparts[] = "JSON_VALUE(payments_info.[value], '$.timestamp')";
$concat = $DB->sql_concat(...array_map(function($p){ return $p; }, $concatparts));

$sql->select = "$concat AS uniquid,
bo.id optionid,
cm.id cmid,
sch.userid,
CAST(JSON_VALUE(payments_info.[value], '$.timestamp') AS INT) AS datefield,
CAST(JSON_VALUE(payments_info.[value], '$.paid') AS FLOAT) AS paid,
CAST(JSON_VALUE(payments_info.[value], '$.price') AS FLOAT) AS price,
CAST(JSON_VALUE(payments_info.[value], '$.id') AS INT) AS payment_id
";

$sql->from .= " RIGHT JOIN {local_shopping_cart_history} sch
ON sch.itemid = bo.id AND sch.componentname = :componentname AND sch.area = :area
CROSS APPLY OPENJSON(sch.json, '$.installments.payments') AS payments_info";

$sql->where = "CAST(JSON_VALUE(payments_info.[value], '$.paid') AS FLOAT) = 0
AND sch.installments > 0
AND sch.paymentstatus = :paymentstatus
AND sch.json IS NOT NULL
AND sch.json <> ''";

if ($testmode) {
$sql->where .= " AND sch.userid = :userid ";
$nextruntime = $nextruntime + $params['numberofdays'] * 86400;
$sql->where .= " AND CAST(JSON_VALUE(payments_info.[value], '$.timestamp') AS INT) = :nextruntime ";
$params['nextruntime'] = $nextruntime;
} else {
$sql->where .= " AND CAST(JSON_VALUE(payments_info.[value], '$.timestamp') AS INT)
>= ( :nowparam + (86400 * :numberofdays ))";
}
break;
}
}
Expand Down
Loading