From d0444765c419f739db8df7e0d8dff3a1588fb530 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Thu, 21 Jul 2022 20:48:53 -0400 Subject: [PATCH 001/254] adds tiny text --- src/masoniteorm/schema/Blueprint.py | 18 ++++++++++++++++++ .../schema/platforms/MSSQLPlatform.py | 1 + .../schema/platforms/MySQLPlatform.py | 1 + .../schema/platforms/PostgresPlatform.py | 1 + .../schema/platforms/SQLitePlatform.py | 1 + .../mssql/schema/test_mssql_schema_builder.py | 12 ++++++++++++ .../mysql/schema/test_mysql_schema_builder.py | 14 +++++++++++++- .../schema/test_postgres_schema_builder.py | 12 ++++++++++++ .../schema/test_sqlite_schema_builder.py | 12 ++++++++++++ 9 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/masoniteorm/schema/Blueprint.py b/src/masoniteorm/schema/Blueprint.py index 3b20464de..80a116f99 100644 --- a/src/masoniteorm/schema/Blueprint.py +++ b/src/masoniteorm/schema/Blueprint.py @@ -483,6 +483,24 @@ def text(self, column, length=None, nullable=False): ) return self + def tiny_text(self, column, length=None, nullable=False): + """Sets a column to be the text representation for the table. + + Arguments: + column {string} -- The column name. + + Keyword Arguments: + length {int} -- The length of the column if any. (default: {False}) + nullable {bool} -- Whether the column is nullable. (default: {False}) + + Returns: + self + """ + self._last_column = self.table.add_column( + column, "tiny_text", length=length, nullable=nullable + ) + return self + def long_text(self, column, length=None, nullable=False): """Sets a column to be the long_text representation for the table. diff --git a/src/masoniteorm/schema/platforms/MSSQLPlatform.py b/src/masoniteorm/schema/platforms/MSSQLPlatform.py index 7f3ba591c..94ef50f1e 100644 --- a/src/masoniteorm/schema/platforms/MSSQLPlatform.py +++ b/src/masoniteorm/schema/platforms/MSSQLPlatform.py @@ -34,6 +34,7 @@ class MSSQLPlatform(Platform): "double": "DOUBLE", "enum": "VARCHAR", "text": "TEXT", + "tiny_text": "TINYTEXT", "float": "FLOAT", "geometry": "GEOMETRY", "json": "JSON", diff --git a/src/masoniteorm/schema/platforms/MySQLPlatform.py b/src/masoniteorm/schema/platforms/MySQLPlatform.py index 9c8de30fb..580e70a96 100644 --- a/src/masoniteorm/schema/platforms/MySQLPlatform.py +++ b/src/masoniteorm/schema/platforms/MySQLPlatform.py @@ -29,6 +29,7 @@ class MySQLPlatform(Platform): "double": "DOUBLE", "enum": "ENUM", "text": "TEXT", + "tiny_text": "TINYTEXT", "float": "FLOAT", "geometry": "GEOMETRY", "json": "JSON", diff --git a/src/masoniteorm/schema/platforms/PostgresPlatform.py b/src/masoniteorm/schema/platforms/PostgresPlatform.py index 4c8fc0978..d15b1fdcf 100644 --- a/src/masoniteorm/schema/platforms/PostgresPlatform.py +++ b/src/masoniteorm/schema/platforms/PostgresPlatform.py @@ -40,6 +40,7 @@ class PostgresPlatform(Platform): "double": "DOUBLE PRECISION", "enum": "VARCHAR", "text": "TEXT", + "tiny_text": "TEXT", "float": "FLOAT", "geometry": "GEOMETRY", "json": "JSON", diff --git a/src/masoniteorm/schema/platforms/SQLitePlatform.py b/src/masoniteorm/schema/platforms/SQLitePlatform.py index d73684fb5..7806d6bba 100644 --- a/src/masoniteorm/schema/platforms/SQLitePlatform.py +++ b/src/masoniteorm/schema/platforms/SQLitePlatform.py @@ -35,6 +35,7 @@ class SQLitePlatform(Platform): "double": "DOUBLE", "enum": "VARCHAR", "text": "TEXT", + "tiny_text": "TEXT", "float": "FLOAT", "geometry": "GEOMETRY", "json": "JSON", diff --git a/tests/mssql/schema/test_mssql_schema_builder.py b/tests/mssql/schema/test_mssql_schema_builder.py index e46c6efb5..34775cde0 100644 --- a/tests/mssql/schema/test_mssql_schema_builder.py +++ b/tests/mssql/schema/test_mssql_schema_builder.py @@ -29,6 +29,18 @@ def test_can_add_columns(self): ["CREATE TABLE [users] ([name] VARCHAR(255) NOT NULL, [age] INT NOT NULL)"], ) + def test_can_add_tiny_text(self): + with self.schema.create("users") as blueprint: + blueprint.tiny_text("description") + + self.assertEqual(len(blueprint.table.added_columns), 1) + self.assertEqual( + blueprint.to_sql(), + [ + 'CREATE TABLE [users] ([description] TINYTEXT NOT NULL)' + ], + ) + def test_can_add_columns_with_constaint(self): with self.schema.create("users") as blueprint: blueprint.string("name") diff --git a/tests/mysql/schema/test_mysql_schema_builder.py b/tests/mysql/schema/test_mysql_schema_builder.py index f2019e9b6..690ef0cd6 100644 --- a/tests/mysql/schema/test_mysql_schema_builder.py +++ b/tests/mysql/schema/test_mysql_schema_builder.py @@ -1,7 +1,7 @@ import os import unittest -from masoniteorm import Model +from src.masoniteorm import Model from tests.integrations.config.database import DATABASES from src.masoniteorm.connections import MySQLConnection from src.masoniteorm.schema import Schema @@ -37,6 +37,18 @@ def test_can_add_columns1(self): ], ) + def test_can_add_tiny_text(self): + with self.schema.create("users") as blueprint: + blueprint.tiny_text("description") + + self.assertEqual(len(blueprint.table.added_columns), 1) + self.assertEqual( + blueprint.to_sql(), + [ + 'CREATE TABLE `users` (`description` TINYTEXT NOT NULL)' + ], + ) + def test_can_create_table_if_not_exists(self): with self.schema.create_table_if_not_exists("users") as blueprint: blueprint.string("name") diff --git a/tests/postgres/schema/test_postgres_schema_builder.py b/tests/postgres/schema/test_postgres_schema_builder.py index 9c7751b7c..28fdea56a 100644 --- a/tests/postgres/schema/test_postgres_schema_builder.py +++ b/tests/postgres/schema/test_postgres_schema_builder.py @@ -31,6 +31,18 @@ def test_can_add_columns(self): ], ) + def test_can_add_tiny_text(self): + with self.schema.create("users") as blueprint: + blueprint.tiny_text("description") + + self.assertEqual(len(blueprint.table.added_columns), 1) + self.assertEqual( + blueprint.to_sql(), + [ + 'CREATE TABLE "users" ("description" TEXT NOT NULL)' + ], + ) + def test_can_create_table_if_not_exists(self): with self.schema.create_table_if_not_exists("users") as blueprint: blueprint.string("name") diff --git a/tests/sqlite/schema/test_sqlite_schema_builder.py b/tests/sqlite/schema/test_sqlite_schema_builder.py index 6b37303d9..7c87cc4c6 100644 --- a/tests/sqlite/schema/test_sqlite_schema_builder.py +++ b/tests/sqlite/schema/test_sqlite_schema_builder.py @@ -30,6 +30,18 @@ def test_can_add_columns(self): ], ) + def test_can_add_tiny_text(self): + with self.schema.create("users") as blueprint: + blueprint.tiny_text("description") + + self.assertEqual(len(blueprint.table.added_columns), 1) + self.assertEqual( + blueprint.to_sql(), + [ + 'CREATE TABLE "users" ("description" TEXT NOT NULL)' + ], + ) + def test_can_create_table_if_not_exists(self): with self.schema.create_table_if_not_exists("users") as blueprint: blueprint.string("name") From 651295e1f5b9cd9056e8c5aabeafb46df832307c Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Thu, 21 Jul 2022 22:11:04 -0400 Subject: [PATCH 002/254] added unsigned decimal --- src/masoniteorm/schema/Blueprint.py | 32 ++++++++++++++++--- src/masoniteorm/schema/Column.py | 20 ++++++++++++ src/masoniteorm/schema/Table.py | 2 ++ .../schema/platforms/MySQLPlatform.py | 9 ++++-- src/masoniteorm/schema/platforms/Platform.py | 5 +++ .../schema/platforms/SQLitePlatform.py | 3 +- .../mssql/schema/test_mssql_schema_builder.py | 12 +++++++ .../mysql/schema/test_mysql_schema_builder.py | 24 ++++++++++---- .../schema/test_postgres_schema_builder.py | 12 +++++++ .../schema/test_sqlite_schema_builder.py | 16 ++++++++-- 10 files changed, 118 insertions(+), 17 deletions(-) diff --git a/src/masoniteorm/schema/Blueprint.py b/src/masoniteorm/schema/Blueprint.py index 80a116f99..83c21b32d 100644 --- a/src/masoniteorm/schema/Blueprint.py +++ b/src/masoniteorm/schema/Blueprint.py @@ -396,6 +396,7 @@ def decimal(self, column, length=17, precision=6, nullable=False): Returns: self """ + self._last_column = self.table.add_column( column, "decimal", @@ -501,6 +502,28 @@ def tiny_text(self, column, length=None, nullable=False): ) return self + def unsigned_decimal(self, column, length=17, precision=6, nullable=False): + """Sets a column to be the text representation for the table. + + Arguments: + column {string} -- The column name. + + Keyword Arguments: + length {int} -- The length of the column if any. (default: {False}) + nullable {bool} -- Whether the column is nullable. (default: {False}) + + Returns: + self + """ + self._last_column = self.table.add_column( + column, + "decimal", + length="{length}, {precision}".format(length=length, precision=precision), + nullable=nullable, + ).unsigned() + return self + return self + def long_text(self, column, length=None, nullable=False): """Sets a column to be the long_text representation for the table. @@ -661,13 +684,12 @@ def unsigned(self, column=None, length=None, nullable=False): self """ if not column: - self._last_column.column_type += "_unsigned" - self._last_column.length = None + self._last_column.unsigned() return self self._last_column = self.table.add_column( column, "unsigned", length=length, nullable=nullable - ) + ).unsigned() return self def unsigned_integer(self, column, nullable=False): @@ -683,8 +705,8 @@ def unsigned_integer(self, column, nullable=False): self """ self._last_column = self.table.add_column( - column, "integer_unsigned", nullable=nullable - ) + column, "integer", nullable=nullable + ).unsigned() return self def morphs(self, column, nullable=False, indexes=True): diff --git a/src/masoniteorm/schema/Column.py b/src/masoniteorm/schema/Column.py index 63783a2cd..8f1a6c07f 100644 --- a/src/masoniteorm/schema/Column.py +++ b/src/masoniteorm/schema/Column.py @@ -9,6 +9,7 @@ def __init__( values=None, nullable=False, default=None, + signed=None, default_is_raw=False, column_python_type=str, ): @@ -21,6 +22,7 @@ def __init__( self._after = None self.old_column = "" self.default = default + self._signed = signed self.default_is_raw = default_is_raw self.primary = False self.comment = None @@ -34,6 +36,24 @@ def nullable(self): self.is_null = True return self + def signed(self): + """Sets this column to be nullable + + Returns: + self + """ + self._signed = "signed" + return self + + def unsigned(self): + """Sets this column to be nullable + + Returns: + self + """ + self._signed = "unsigned" + return self + def not_nullable(self): """Sets this column to be not nullable diff --git a/src/masoniteorm/schema/Table.py b/src/masoniteorm/schema/Table.py index 75d0d413a..b64f33d15 100644 --- a/src/masoniteorm/schema/Table.py +++ b/src/masoniteorm/schema/Table.py @@ -25,6 +25,7 @@ def add_column( values=None, nullable=False, default=None, + signed=None, default_is_raw=False, primary=False, column_python_type=str, @@ -36,6 +37,7 @@ def add_column( nullable=nullable, values=values or [], default=default, + signed=signed, default_is_raw=default_is_raw, column_python_type=column_python_type, ) diff --git a/src/masoniteorm/schema/platforms/MySQLPlatform.py b/src/masoniteorm/schema/platforms/MySQLPlatform.py index 580e70a96..f3e9d154d 100644 --- a/src/masoniteorm/schema/platforms/MySQLPlatform.py +++ b/src/masoniteorm/schema/platforms/MySQLPlatform.py @@ -56,6 +56,11 @@ class MySQLPlatform(Platform): "null": " DEFAULT NULL", } + signed = { + "unsigned": "UNSIGNED", + "signed": "SIGNED" + } + def columnize(self, columns): sql = [] for name, column in columns.items(): @@ -88,7 +93,6 @@ def columnize(self, columns): if column.column_type == "enum": values = ", ".join(f"'{x}'" for x in column.values) column_constraint = f"({values})" - sql.append( self.columnize_string() .format( @@ -99,6 +103,7 @@ def columnize(self, columns): constraint=constraint, nullable=self.premapped_nulls.get(column.is_null) or "", default=default, + signed=" " + self.signed.get(column._signed, "") or "", comment="COMMENT '" + column.comment + "'" if column.comment else "", @@ -337,7 +342,7 @@ def rename_column_string(self): return "CHANGE {old} {to}" def columnize_string(self): - return "{name} {data_type}{length}{column_constraint} {nullable}{default} {constraint}{comment}" + return "{name} {data_type}{length}{column_constraint}{signed} {nullable}{default} {constraint}{comment}" def constraintize(self, constraints, table): sql = [] diff --git a/src/masoniteorm/schema/platforms/Platform.py b/src/masoniteorm/schema/platforms/Platform.py index 25e5a4865..d04eb06b8 100644 --- a/src/masoniteorm/schema/platforms/Platform.py +++ b/src/masoniteorm/schema/platforms/Platform.py @@ -9,6 +9,11 @@ class Platform: "default": "SET DEFAULT", } + signed = { + "signed": "SIGNED", + "unsigned": "UNSIGNED" + } + def columnize(self, columns): sql = [] for name, column in columns.items(): diff --git a/src/masoniteorm/schema/platforms/SQLitePlatform.py b/src/masoniteorm/schema/platforms/SQLitePlatform.py index 7806d6bba..7298018d8 100644 --- a/src/masoniteorm/schema/platforms/SQLitePlatform.py +++ b/src/masoniteorm/schema/platforms/SQLitePlatform.py @@ -134,6 +134,7 @@ def columnize(self, columns): data_type=self.type_map.get(column.column_type, ""), column_constraint=column_constraint, length=length, + signed=" "+self.signed.get(column._signed) or "", constraint=constraint, nullable=self.premapped_nulls.get(column.is_null) or "", default=default, @@ -289,7 +290,7 @@ def create_column_length(self, column_type): return "({length})" def columnize_string(self): - return "{name} {data_type}{length}{column_constraint} {nullable}{default} {constraint}" + return "{name} {data_type}{length}{column_constraint}{signed} {nullable}{default} {constraint}" def get_unique_constraint_string(self): return "UNIQUE({columns})" diff --git a/tests/mssql/schema/test_mssql_schema_builder.py b/tests/mssql/schema/test_mssql_schema_builder.py index 34775cde0..15ec7c783 100644 --- a/tests/mssql/schema/test_mssql_schema_builder.py +++ b/tests/mssql/schema/test_mssql_schema_builder.py @@ -41,6 +41,18 @@ def test_can_add_tiny_text(self): ], ) + def test_can_add_unsigned_decimal(self): + with self.schema.create("users") as blueprint: + blueprint.unsigned_decimal("amount", 19, 4) + + self.assertEqual(len(blueprint.table.added_columns), 1) + self.assertEqual( + blueprint.to_sql(), + [ + 'CREATE TABLE [users] ([amount] DECIMAL(19, 4) NOT NULL)' + ], + ) + def test_can_add_columns_with_constaint(self): with self.schema.create("users") as blueprint: blueprint.string("name") diff --git a/tests/mysql/schema/test_mysql_schema_builder.py b/tests/mysql/schema/test_mysql_schema_builder.py index 690ef0cd6..b8dccedca 100644 --- a/tests/mysql/schema/test_mysql_schema_builder.py +++ b/tests/mysql/schema/test_mysql_schema_builder.py @@ -49,6 +49,18 @@ def test_can_add_tiny_text(self): ], ) + def test_can_add_unsigned_decimal(self): + with self.schema.create("users") as blueprint: + blueprint.unsigned_decimal("amount", 19, 4) + + self.assertEqual(len(blueprint.table.added_columns), 1) + self.assertEqual( + blueprint.to_sql(), + [ + 'CREATE TABLE `users` (`amount` DECIMAL(19, 4) UNSIGNED NOT NULL)' + ], + ) + def test_can_create_table_if_not_exists(self): with self.schema.create_table_if_not_exists("users") as blueprint: blueprint.string("name") @@ -290,13 +302,13 @@ def test_can_have_unsigned_columns(self): blueprint.to_sql(), [ "CREATE TABLE `users` (" - "`profile_id` INT UNSIGNED NOT NULL, " - "`big_profile_id` BIGINT UNSIGNED NOT NULL, " - "`tiny_profile_id` TINYINT UNSIGNED NOT NULL, " - "`small_profile_id` SMALLINT UNSIGNED NOT NULL, " - "`medium_profile_id` MEDIUMINT UNSIGNED NOT NULL, " + "`profile_id` INT(11) UNSIGNED NOT NULL, " + "`big_profile_id` BIGINT(32) UNSIGNED NOT NULL, " + "`tiny_profile_id` TINYINT(1) UNSIGNED NOT NULL, " + "`small_profile_id` SMALLINT(5) UNSIGNED NOT NULL, " + "`medium_profile_id` MEDIUMINT(7) UNSIGNED NOT NULL, " "`unsigned_profile_id` INT UNSIGNED NOT NULL, " - "`unsigned_big_profile_id` BIGINT UNSIGNED NOT NULL)" + "`unsigned_big_profile_id` BIGINT(32) UNSIGNED NOT NULL)" ], ) diff --git a/tests/postgres/schema/test_postgres_schema_builder.py b/tests/postgres/schema/test_postgres_schema_builder.py index 28fdea56a..46f8d0b6b 100644 --- a/tests/postgres/schema/test_postgres_schema_builder.py +++ b/tests/postgres/schema/test_postgres_schema_builder.py @@ -43,6 +43,18 @@ def test_can_add_tiny_text(self): ], ) + def test_can_add_unsigned_decimal(self): + with self.schema.create("users") as blueprint: + blueprint.unsigned_decimal("amount", 19, 4) + + self.assertEqual(len(blueprint.table.added_columns), 1) + self.assertEqual( + blueprint.to_sql(), + [ + 'CREATE TABLE "users" ("amount" DECIMAL(19, 4) NOT NULL)' + ], + ) + def test_can_create_table_if_not_exists(self): with self.schema.create_table_if_not_exists("users") as blueprint: blueprint.string("name") diff --git a/tests/sqlite/schema/test_sqlite_schema_builder.py b/tests/sqlite/schema/test_sqlite_schema_builder.py index 7c87cc4c6..4aac895eb 100644 --- a/tests/sqlite/schema/test_sqlite_schema_builder.py +++ b/tests/sqlite/schema/test_sqlite_schema_builder.py @@ -42,6 +42,18 @@ def test_can_add_tiny_text(self): ], ) + def test_can_add_unsigned_decimal(self): + with self.schema.create("users") as blueprint: + blueprint.unsigned_decimal("amount", 19, 4) + + self.assertEqual(len(blueprint.table.added_columns), 1) + self.assertEqual( + blueprint.to_sql(), + [ + 'CREATE TABLE "users" ("amount" DECIMAL(19, 4) NOT NULL)' + ], + ) + def test_can_create_table_if_not_exists(self): with self.schema.create_table_if_not_exists("users") as blueprint: blueprint.string("name") @@ -313,13 +325,11 @@ def test_can_have_unsigned_columns(self): blueprint.small_integer("small_profile_id").unsigned() blueprint.medium_integer("medium_profile_id").unsigned() - print(blueprint.to_sql()) - self.assertEqual( blueprint.to_sql(), [ """CREATE TABLE "users" (""" - """"profile_id" INT UNSIGNED NOT NULL, """ + """"profile_id" INTEGER UNSIGNED NOT NULL, """ """"big_profile_id" BIGINT UNSIGNED NOT NULL, """ """"tiny_profile_id" TINYINT UNSIGNED NOT NULL, """ """"small_profile_id" SMALLINT UNSIGNED NOT NULL, """ From 97a88811cd6d8b226b7ef2b9c04011db63b4c489 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Thu, 21 Jul 2022 22:11:25 -0400 Subject: [PATCH 003/254] formatted --- src/masoniteorm/schema/Blueprint.py | 6 ++++-- src/masoniteorm/schema/platforms/MySQLPlatform.py | 5 +---- src/masoniteorm/schema/platforms/Platform.py | 5 +---- src/masoniteorm/schema/platforms/SQLitePlatform.py | 2 +- tests/mssql/schema/test_mssql_schema_builder.py | 8 ++------ tests/mysql/schema/test_mysql_schema_builder.py | 8 ++------ tests/postgres/schema/test_postgres_schema_builder.py | 9 ++------- tests/sqlite/schema/test_sqlite_schema_builder.py | 9 ++------- 8 files changed, 15 insertions(+), 37 deletions(-) diff --git a/src/masoniteorm/schema/Blueprint.py b/src/masoniteorm/schema/Blueprint.py index 83c21b32d..a96275eaf 100644 --- a/src/masoniteorm/schema/Blueprint.py +++ b/src/masoniteorm/schema/Blueprint.py @@ -940,9 +940,11 @@ def foreign_id_for(self, model, column=None): """ clm = column if column else model.get_foreign_key() - return self.foreign_id(clm)\ - if model.get_primary_key_type() == 'int'\ + return ( + self.foreign_id(clm) + if model.get_primary_key_type() == "int" else self.foreign_uuid(column) + ) def references(self, column): """Sets the other column on the foreign table that the local column will use to reference. diff --git a/src/masoniteorm/schema/platforms/MySQLPlatform.py b/src/masoniteorm/schema/platforms/MySQLPlatform.py index f3e9d154d..ad52cc929 100644 --- a/src/masoniteorm/schema/platforms/MySQLPlatform.py +++ b/src/masoniteorm/schema/platforms/MySQLPlatform.py @@ -56,10 +56,7 @@ class MySQLPlatform(Platform): "null": " DEFAULT NULL", } - signed = { - "unsigned": "UNSIGNED", - "signed": "SIGNED" - } + signed = {"unsigned": "UNSIGNED", "signed": "SIGNED"} def columnize(self, columns): sql = [] diff --git a/src/masoniteorm/schema/platforms/Platform.py b/src/masoniteorm/schema/platforms/Platform.py index d04eb06b8..8f0b2d968 100644 --- a/src/masoniteorm/schema/platforms/Platform.py +++ b/src/masoniteorm/schema/platforms/Platform.py @@ -9,10 +9,7 @@ class Platform: "default": "SET DEFAULT", } - signed = { - "signed": "SIGNED", - "unsigned": "UNSIGNED" - } + signed = {"signed": "SIGNED", "unsigned": "UNSIGNED"} def columnize(self, columns): sql = [] diff --git a/src/masoniteorm/schema/platforms/SQLitePlatform.py b/src/masoniteorm/schema/platforms/SQLitePlatform.py index 7298018d8..e98e2d16e 100644 --- a/src/masoniteorm/schema/platforms/SQLitePlatform.py +++ b/src/masoniteorm/schema/platforms/SQLitePlatform.py @@ -134,7 +134,7 @@ def columnize(self, columns): data_type=self.type_map.get(column.column_type, ""), column_constraint=column_constraint, length=length, - signed=" "+self.signed.get(column._signed) or "", + signed=" " + self.signed.get(column._signed) or "", constraint=constraint, nullable=self.premapped_nulls.get(column.is_null) or "", default=default, diff --git a/tests/mssql/schema/test_mssql_schema_builder.py b/tests/mssql/schema/test_mssql_schema_builder.py index 15ec7c783..16813a3ad 100644 --- a/tests/mssql/schema/test_mssql_schema_builder.py +++ b/tests/mssql/schema/test_mssql_schema_builder.py @@ -36,9 +36,7 @@ def test_can_add_tiny_text(self): self.assertEqual(len(blueprint.table.added_columns), 1) self.assertEqual( blueprint.to_sql(), - [ - 'CREATE TABLE [users] ([description] TINYTEXT NOT NULL)' - ], + ["CREATE TABLE [users] ([description] TINYTEXT NOT NULL)"], ) def test_can_add_unsigned_decimal(self): @@ -48,9 +46,7 @@ def test_can_add_unsigned_decimal(self): self.assertEqual(len(blueprint.table.added_columns), 1) self.assertEqual( blueprint.to_sql(), - [ - 'CREATE TABLE [users] ([amount] DECIMAL(19, 4) NOT NULL)' - ], + ["CREATE TABLE [users] ([amount] DECIMAL(19, 4) NOT NULL)"], ) def test_can_add_columns_with_constaint(self): diff --git a/tests/mysql/schema/test_mysql_schema_builder.py b/tests/mysql/schema/test_mysql_schema_builder.py index b8dccedca..ee755f7da 100644 --- a/tests/mysql/schema/test_mysql_schema_builder.py +++ b/tests/mysql/schema/test_mysql_schema_builder.py @@ -44,9 +44,7 @@ def test_can_add_tiny_text(self): self.assertEqual(len(blueprint.table.added_columns), 1) self.assertEqual( blueprint.to_sql(), - [ - 'CREATE TABLE `users` (`description` TINYTEXT NOT NULL)' - ], + ["CREATE TABLE `users` (`description` TINYTEXT NOT NULL)"], ) def test_can_add_unsigned_decimal(self): @@ -56,9 +54,7 @@ def test_can_add_unsigned_decimal(self): self.assertEqual(len(blueprint.table.added_columns), 1) self.assertEqual( blueprint.to_sql(), - [ - 'CREATE TABLE `users` (`amount` DECIMAL(19, 4) UNSIGNED NOT NULL)' - ], + ["CREATE TABLE `users` (`amount` DECIMAL(19, 4) UNSIGNED NOT NULL)"], ) def test_can_create_table_if_not_exists(self): diff --git a/tests/postgres/schema/test_postgres_schema_builder.py b/tests/postgres/schema/test_postgres_schema_builder.py index 46f8d0b6b..d5804e811 100644 --- a/tests/postgres/schema/test_postgres_schema_builder.py +++ b/tests/postgres/schema/test_postgres_schema_builder.py @@ -37,10 +37,7 @@ def test_can_add_tiny_text(self): self.assertEqual(len(blueprint.table.added_columns), 1) self.assertEqual( - blueprint.to_sql(), - [ - 'CREATE TABLE "users" ("description" TEXT NOT NULL)' - ], + blueprint.to_sql(), ['CREATE TABLE "users" ("description" TEXT NOT NULL)'] ) def test_can_add_unsigned_decimal(self): @@ -50,9 +47,7 @@ def test_can_add_unsigned_decimal(self): self.assertEqual(len(blueprint.table.added_columns), 1) self.assertEqual( blueprint.to_sql(), - [ - 'CREATE TABLE "users" ("amount" DECIMAL(19, 4) NOT NULL)' - ], + ['CREATE TABLE "users" ("amount" DECIMAL(19, 4) NOT NULL)'], ) def test_can_create_table_if_not_exists(self): diff --git a/tests/sqlite/schema/test_sqlite_schema_builder.py b/tests/sqlite/schema/test_sqlite_schema_builder.py index 4aac895eb..c4f90f9cb 100644 --- a/tests/sqlite/schema/test_sqlite_schema_builder.py +++ b/tests/sqlite/schema/test_sqlite_schema_builder.py @@ -36,10 +36,7 @@ def test_can_add_tiny_text(self): self.assertEqual(len(blueprint.table.added_columns), 1) self.assertEqual( - blueprint.to_sql(), - [ - 'CREATE TABLE "users" ("description" TEXT NOT NULL)' - ], + blueprint.to_sql(), ['CREATE TABLE "users" ("description" TEXT NOT NULL)'] ) def test_can_add_unsigned_decimal(self): @@ -49,9 +46,7 @@ def test_can_add_unsigned_decimal(self): self.assertEqual(len(blueprint.table.added_columns), 1) self.assertEqual( blueprint.to_sql(), - [ - 'CREATE TABLE "users" ("amount" DECIMAL(19, 4) NOT NULL)' - ], + ['CREATE TABLE "users" ("amount" DECIMAL(19, 4) NOT NULL)'], ) def test_can_create_table_if_not_exists(self): From 5ce2c80087dfd9b019e76fc9bf839ad952e88cd6 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Thu, 21 Jul 2022 22:14:25 -0400 Subject: [PATCH 004/254] fixed signed --- src/masoniteorm/schema/platforms/SQLitePlatform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/masoniteorm/schema/platforms/SQLitePlatform.py b/src/masoniteorm/schema/platforms/SQLitePlatform.py index e98e2d16e..13b4d7ba0 100644 --- a/src/masoniteorm/schema/platforms/SQLitePlatform.py +++ b/src/masoniteorm/schema/platforms/SQLitePlatform.py @@ -134,7 +134,7 @@ def columnize(self, columns): data_type=self.type_map.get(column.column_type, ""), column_constraint=column_constraint, length=length, - signed=" " + self.signed.get(column._signed) or "", + signed=" " + self.signed.get(column._signed, "") or "", constraint=constraint, nullable=self.premapped_nulls.get(column.is_null) or "", default=default, From f7687256fb20f04202f33d22c5a75b2f93e56ebb Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Thu, 21 Jul 2022 22:16:51 -0400 Subject: [PATCH 005/254] fixed signed again --- src/masoniteorm/schema/platforms/MySQLPlatform.py | 2 +- src/masoniteorm/schema/platforms/SQLitePlatform.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/masoniteorm/schema/platforms/MySQLPlatform.py b/src/masoniteorm/schema/platforms/MySQLPlatform.py index ad52cc929..1cf926cd8 100644 --- a/src/masoniteorm/schema/platforms/MySQLPlatform.py +++ b/src/masoniteorm/schema/platforms/MySQLPlatform.py @@ -100,7 +100,7 @@ def columnize(self, columns): constraint=constraint, nullable=self.premapped_nulls.get(column.is_null) or "", default=default, - signed=" " + self.signed.get(column._signed, "") or "", + signed=" " + self.signed.get(column._signed) if column._signed else "", comment="COMMENT '" + column.comment + "'" if column.comment else "", diff --git a/src/masoniteorm/schema/platforms/SQLitePlatform.py b/src/masoniteorm/schema/platforms/SQLitePlatform.py index 13b4d7ba0..301280c5e 100644 --- a/src/masoniteorm/schema/platforms/SQLitePlatform.py +++ b/src/masoniteorm/schema/platforms/SQLitePlatform.py @@ -134,7 +134,7 @@ def columnize(self, columns): data_type=self.type_map.get(column.column_type, ""), column_constraint=column_constraint, length=length, - signed=" " + self.signed.get(column._signed, "") or "", + signed=" " + self.signed.get(column._signed) if column._signed else "", constraint=constraint, nullable=self.premapped_nulls.get(column.is_null) or "", default=default, From 9e7582f2330c44014ddf86845c1971ae875dbceb Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Thu, 21 Jul 2022 22:54:43 -0400 Subject: [PATCH 006/254] fixed tests --- src/masoniteorm/schema/Blueprint.py | 4 ++-- src/masoniteorm/schema/platforms/MySQLPlatform.py | 3 ++- src/masoniteorm/schema/platforms/SQLitePlatform.py | 9 +++++++-- tests/mysql/schema/test_mysql_schema_builder.py | 2 +- tests/sqlite/schema/test_sqlite_schema_builder.py | 4 ++-- tests/sqlite/schema/test_sqlite_schema_builder_alter.py | 4 ++-- 6 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/masoniteorm/schema/Blueprint.py b/src/masoniteorm/schema/Blueprint.py index a96275eaf..b2865c54a 100644 --- a/src/masoniteorm/schema/Blueprint.py +++ b/src/masoniteorm/schema/Blueprint.py @@ -724,8 +724,8 @@ def morphs(self, column, nullable=False, indexes=True): _columns = [] _columns.append( self.table.add_column( - "{}_id".format(column), "integer_unsigned", nullable=nullable - ) + "{}_id".format(column), "integer", nullable=nullable + ).unsigned() ) _columns.append( self.table.add_column( diff --git a/src/masoniteorm/schema/platforms/MySQLPlatform.py b/src/masoniteorm/schema/platforms/MySQLPlatform.py index 1cf926cd8..4190f691d 100644 --- a/src/masoniteorm/schema/platforms/MySQLPlatform.py +++ b/src/masoniteorm/schema/platforms/MySQLPlatform.py @@ -183,6 +183,7 @@ def compile_alter_sql(self, table): constraint="PRIMARY KEY" if column.primary else "", nullable="NULL" if column.is_null else "NOT NULL", default=default, + signed=" " + self.signed.get(column._signed) if column._signed else "", after=(" AFTER " + self.wrap_column(column._after)) if column._after else "", @@ -327,7 +328,7 @@ def compile_alter_sql(self, table): return sql def add_column_string(self): - return "ADD {name} {data_type}{length} {nullable}{default}{after}{comment}" + return "ADD {name} {data_type}{length}{signed} {nullable}{default}{after}{comment}" def drop_column_string(self): return "DROP COLUMN {name}" diff --git a/src/masoniteorm/schema/platforms/SQLitePlatform.py b/src/masoniteorm/schema/platforms/SQLitePlatform.py index 301280c5e..15979c607 100644 --- a/src/masoniteorm/schema/platforms/SQLitePlatform.py +++ b/src/masoniteorm/schema/platforms/SQLitePlatform.py @@ -13,6 +13,10 @@ class SQLitePlatform(Platform): "medium_integer", ] + types_without_signs = [ + "decimal", + ] + type_map = { "string": "VARCHAR", "char": "CHAR", @@ -134,7 +138,7 @@ def columnize(self, columns): data_type=self.type_map.get(column.column_type, ""), column_constraint=column_constraint, length=length, - signed=" " + self.signed.get(column._signed) if column._signed else "", + signed=" " + self.signed.get(column._signed) if column.column_type not in self.types_without_signs and column._signed else "", constraint=constraint, nullable=self.premapped_nulls.get(column.is_null) or "", default=default, @@ -173,12 +177,13 @@ def compile_alter_sql(self, diff): constraint = f" REFERENCES {self.wrap_table(foreign_key.foreign_table)}({self.wrap_column(foreign_key.foreign_column)})" sql.append( - "ALTER TABLE {table} ADD COLUMN {name} {data_type} {nullable}{default}{constraint}".format( + "ALTER TABLE {table} ADD COLUMN {name} {data_type}{signed} {nullable}{default}{constraint}".format( table=self.wrap_table(diff.name), name=self.wrap_column(column.name), data_type=self.type_map.get(column.column_type, ""), nullable="NULL" if column.is_null else "NOT NULL", default=default, + signed=" " + self.signed.get(column._signed) if column.column_type not in self.types_without_signs and column._signed else "", constraint=constraint, ).strip() ) diff --git a/tests/mysql/schema/test_mysql_schema_builder.py b/tests/mysql/schema/test_mysql_schema_builder.py index ee755f7da..ccd338c41 100644 --- a/tests/mysql/schema/test_mysql_schema_builder.py +++ b/tests/mysql/schema/test_mysql_schema_builder.py @@ -223,7 +223,7 @@ def test_can_advanced_table_creation2(self): "CREATE TABLE `users` (`id` BIGINT UNSIGNED AUTO_INCREMENT NOT NULL, `name` VARCHAR(255) NOT NULL, " "`duration` VARCHAR(255) NOT NULL, `url` VARCHAR(255) NOT NULL, `last_address` VARCHAR(255) NULL, `route_origin` VARCHAR(255) NULL, `mac_address` VARCHAR(255) NULL, " "`published_at` DATETIME NOT NULL, `thumbnail` VARCHAR(255) NULL, " - "`premium` INT(11) NOT NULL, `author_id` INT UNSIGNED NULL, `description` TEXT NOT NULL, `created_at` DATETIME NULL DEFAULT CURRENT_TIMESTAMP, " + "`premium` INT(11) NOT NULL, `author_id` INT(11) UNSIGNED NULL, `description` TEXT NOT NULL, `created_at` DATETIME NULL DEFAULT CURRENT_TIMESTAMP, " "`updated_at` DATETIME NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT users_id_primary PRIMARY KEY (id), CONSTRAINT users_author_id_foreign FOREIGN KEY (`author_id`) REFERENCES `users`(`id`) ON DELETE CASCADE)" ], ) diff --git a/tests/sqlite/schema/test_sqlite_schema_builder.py b/tests/sqlite/schema/test_sqlite_schema_builder.py index c4f90f9cb..acaf1bc7b 100644 --- a/tests/sqlite/schema/test_sqlite_schema_builder.py +++ b/tests/sqlite/schema/test_sqlite_schema_builder.py @@ -133,7 +133,7 @@ def test_can_use_morphs_for_polymorphism_relationships(self): self.assertEqual(len(blueprint.table.added_columns), 2) sql = [ - 'CREATE TABLE "likes" ("record_id" INT UNSIGNED NOT NULL, "record_type" VARCHAR(255) NOT NULL)', + 'CREATE TABLE "likes" ("record_id" INTEGER UNSIGNED NOT NULL, "record_type" VARCHAR(255) NOT NULL)', 'CREATE INDEX likes_record_id_index ON "likes"(record_id)', 'CREATE INDEX likes_record_type_index ON "likes"(record_type)', ] @@ -270,7 +270,7 @@ def test_can_advanced_table_creation2(self): [ 'CREATE TABLE "users" ("id" BIGINT NOT NULL, "name" VARCHAR(255) NOT NULL, "duration" VARCHAR(255) NOT NULL, ' '"url" VARCHAR(255) NOT NULL, "payload" JSON NOT NULL, "birth" VARCHAR(4) NOT NULL, "last_address" VARCHAR(255) NULL, "route_origin" VARCHAR(255) NULL, "mac_address" VARCHAR(255) NULL, ' - '"published_at" DATETIME NOT NULL, "wakeup_at" TIME NOT NULL, "thumbnail" VARCHAR(255) NULL, "premium" INTEGER NOT NULL, "author_id" INT UNSIGNED NULL, "description" TEXT NOT NULL, ' + '"published_at" DATETIME NOT NULL, "wakeup_at" TIME NOT NULL, "thumbnail" VARCHAR(255) NULL, "premium" INTEGER NOT NULL, "author_id" INTEGER UNSIGNED NULL, "description" TEXT NOT NULL, ' '"created_at" DATETIME NULL DEFAULT CURRENT_TIMESTAMP, "updated_at" DATETIME NULL DEFAULT CURRENT_TIMESTAMP, ' 'CONSTRAINT users_id_primary PRIMARY KEY (id), CONSTRAINT users_author_id_foreign FOREIGN KEY ("author_id") REFERENCES "users"("id") ON DELETE SET NULL)' ] diff --git a/tests/sqlite/schema/test_sqlite_schema_builder_alter.py b/tests/sqlite/schema/test_sqlite_schema_builder_alter.py index fde74e94f..91c5c43d0 100644 --- a/tests/sqlite/schema/test_sqlite_schema_builder_alter.py +++ b/tests/sqlite/schema/test_sqlite_schema_builder_alter.py @@ -176,10 +176,10 @@ def test_alter_add_column_and_foreign_key(self): blueprint.table.from_table = table sql = [ - 'ALTER TABLE "users" ADD COLUMN "playlist_id" INT UNSIGNED NULL REFERENCES "playlists"("id")', + 'ALTER TABLE "users" ADD COLUMN "playlist_id" INTEGER UNSIGNED NULL REFERENCES "playlists"("id")', "CREATE TEMPORARY TABLE __temp__users AS SELECT age, email FROM users", 'DROP TABLE "users"', - 'CREATE TABLE "users" ("age" VARCHAR NOT NULL, "email" VARCHAR NOT NULL, "playlist_id" INT UNSIGNED NULL, ' + 'CREATE TABLE "users" ("age" VARCHAR NOT NULL, "email" VARCHAR NOT NULL, "playlist_id" INTEGER UNSIGNED NULL, ' 'CONSTRAINT users_playlist_id_foreign FOREIGN KEY ("playlist_id") REFERENCES "playlists"("id") ON DELETE CASCADE ON UPDATE SET NULL)', 'INSERT INTO "users" ("age", "email") SELECT age, email FROM __temp__users', "DROP TABLE __temp__users", From c315b5b5863b840f9f1f41c85146846c64e2a9cf Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Thu, 21 Jul 2022 22:55:02 -0400 Subject: [PATCH 007/254] formatted --- src/masoniteorm/schema/platforms/MySQLPlatform.py | 12 +++++++++--- src/masoniteorm/schema/platforms/SQLitePlatform.py | 14 +++++++++----- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/masoniteorm/schema/platforms/MySQLPlatform.py b/src/masoniteorm/schema/platforms/MySQLPlatform.py index 4190f691d..da956ca7b 100644 --- a/src/masoniteorm/schema/platforms/MySQLPlatform.py +++ b/src/masoniteorm/schema/platforms/MySQLPlatform.py @@ -100,7 +100,9 @@ def columnize(self, columns): constraint=constraint, nullable=self.premapped_nulls.get(column.is_null) or "", default=default, - signed=" " + self.signed.get(column._signed) if column._signed else "", + signed=" " + self.signed.get(column._signed) + if column._signed + else "", comment="COMMENT '" + column.comment + "'" if column.comment else "", @@ -183,7 +185,9 @@ def compile_alter_sql(self, table): constraint="PRIMARY KEY" if column.primary else "", nullable="NULL" if column.is_null else "NOT NULL", default=default, - signed=" " + self.signed.get(column._signed) if column._signed else "", + signed=" " + self.signed.get(column._signed) + if column._signed + else "", after=(" AFTER " + self.wrap_column(column._after)) if column._after else "", @@ -328,7 +332,9 @@ def compile_alter_sql(self, table): return sql def add_column_string(self): - return "ADD {name} {data_type}{length}{signed} {nullable}{default}{after}{comment}" + return ( + "ADD {name} {data_type}{length}{signed} {nullable}{default}{after}{comment}" + ) def drop_column_string(self): return "DROP COLUMN {name}" diff --git a/src/masoniteorm/schema/platforms/SQLitePlatform.py b/src/masoniteorm/schema/platforms/SQLitePlatform.py index 15979c607..0e32c1196 100644 --- a/src/masoniteorm/schema/platforms/SQLitePlatform.py +++ b/src/masoniteorm/schema/platforms/SQLitePlatform.py @@ -13,9 +13,7 @@ class SQLitePlatform(Platform): "medium_integer", ] - types_without_signs = [ - "decimal", - ] + types_without_signs = ["decimal"] type_map = { "string": "VARCHAR", @@ -138,7 +136,10 @@ def columnize(self, columns): data_type=self.type_map.get(column.column_type, ""), column_constraint=column_constraint, length=length, - signed=" " + self.signed.get(column._signed) if column.column_type not in self.types_without_signs and column._signed else "", + signed=" " + self.signed.get(column._signed) + if column.column_type not in self.types_without_signs + and column._signed + else "", constraint=constraint, nullable=self.premapped_nulls.get(column.is_null) or "", default=default, @@ -183,7 +184,10 @@ def compile_alter_sql(self, diff): data_type=self.type_map.get(column.column_type, ""), nullable="NULL" if column.is_null else "NOT NULL", default=default, - signed=" " + self.signed.get(column._signed) if column.column_type not in self.types_without_signs and column._signed else "", + signed=" " + self.signed.get(column._signed) + if column.column_type not in self.types_without_signs + and column._signed + else "", constraint=constraint, ).strip() ) From 55f23610dbb4782dfbd4c6572754dcf27abe64b4 Mon Sep 17 00:00:00 2001 From: Kieren Eaton Date: Fri, 22 Jul 2022 12:17:29 +0800 Subject: [PATCH 008/254] Sort incoming dicts by key to align column values --- src/masoniteorm/query/QueryBuilder.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 47179b856..9ccc8fa87 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -464,7 +464,12 @@ def bulk_create(self, creates, query=False): model = None self.set_action("bulk_create") - self._creates = creates + sorted_creates = [] + # sort the dicts by key so the values inserted align + # with the correct column + for unsorted_dict in creates: + sorted_creates.append(dict(sorted(unsorted_dict.items()))) + self._creates = sorted_creates if self._model: model = self._model From 4b170e05dab7a0a90703d03164c4b843fc67453d Mon Sep 17 00:00:00 2001 From: Kieren Eaton Date: Fri, 22 Jul 2022 12:34:31 +0800 Subject: [PATCH 009/254] Updated tests --- tests/mssql/grammar/test_mssql_insert_grammar.py | 4 ++-- tests/mysql/grammar/test_mysql_insert_grammar.py | 6 +++--- tests/postgres/grammar/test_insert_grammar.py | 4 ++-- tests/sqlite/grammar/test_sqlite_insert_grammar.py | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/mssql/grammar/test_mssql_insert_grammar.py b/tests/mssql/grammar/test_mssql_insert_grammar.py index a6147acb3..e08cbdf21 100644 --- a/tests/mssql/grammar/test_mssql_insert_grammar.py +++ b/tests/mssql/grammar/test_mssql_insert_grammar.py @@ -17,10 +17,10 @@ def test_can_compile_insert(self): def test_can_compile_bulk_create(self): to_sql = self.builder.bulk_create( - [{"name": "Joe"}, {"name": "Bill"}, {"name": "John"}], query=True + [{"name": "Joe", "age": 5}, {"age": 35, "name": "Bill"}, {"name": "John", "age": 10}], query=True ).to_sql() - sql = "INSERT INTO [users] ([name]) VALUES ('Joe'), ('Bill'), ('John')" + sql = "INSERT INTO [users] ([age], [name]) VALUES ('5', 'Joe'), ('35', 'Bill'), ('10', 'John')" self.assertEqual(to_sql, sql) def test_can_compile_bulk_create_qmark(self): diff --git a/tests/mysql/grammar/test_mysql_insert_grammar.py b/tests/mysql/grammar/test_mysql_insert_grammar.py index d246beff7..daf6f008d 100644 --- a/tests/mysql/grammar/test_mysql_insert_grammar.py +++ b/tests/mysql/grammar/test_mysql_insert_grammar.py @@ -27,7 +27,7 @@ def test_can_compile_insert_with_keywords(self): def test_can_compile_bulk_create(self): to_sql = self.builder.bulk_create( - [{"name": "Joe"}, {"name": "Bill"}, {"name": "John"}], query=True + [{"name": "Joe", "age": 5}, {"age": 35, "name": "Bill"}, {"name": "John", "age": 10}], query=True ).to_sql() sql = getattr( @@ -83,13 +83,13 @@ def can_compile_bulk_create(self): """ self.builder.create(name="Joe").to_sql() """ - return """INSERT INTO `users` (`name`) VALUES ('Joe'), ('Bill'), ('John')""" + return """INSERT INTO `users` (`age`, `name`) VALUES ('5', 'Joe'), ('35', 'Bill'), ('10', 'John')""" def can_compile_bulk_create_multiple(self): """ self.builder.create(name="Joe").to_sql() """ - return """INSERT INTO `users` (`name`, `active`) VALUES ('Joe', '1'), ('Bill', '1'), ('John', '1')""" + return """INSERT INTO `users` (`active`, `name`) VALUES ('1', 'Joe'), ('1', 'Bill'), ('1', 'John')""" def can_compile_bulk_create_qmark(self): """ diff --git a/tests/postgres/grammar/test_insert_grammar.py b/tests/postgres/grammar/test_insert_grammar.py index 68859fb32..4c3f7ef61 100644 --- a/tests/postgres/grammar/test_insert_grammar.py +++ b/tests/postgres/grammar/test_insert_grammar.py @@ -27,7 +27,7 @@ def test_can_compile_insert_with_keywords(self): def test_can_compile_bulk_create(self): to_sql = self.builder.bulk_create( - [{"name": "Joe"}, {"name": "Bill"}, {"name": "John"}], query=True + [{"name": "Joe", "age": 5}, {"age": 35, "name": "Bill"}, {"name": "John", "age": 10}], query=True ).to_sql() sql = getattr( @@ -68,7 +68,7 @@ def can_compile_bulk_create(self): """ self.builder.create(name="Joe").to_sql() """ - return """INSERT INTO "users" ("name") VALUES ('Joe'), ('Bill'), ('John') RETURNING *""" + return """INSERT INTO "users" ("age", "name") VALUES ('5', 'Joe'), ('35', 'Bill'), ('10', 'John') RETURNING *""" def can_compile_bulk_create_qmark(self): """ diff --git a/tests/sqlite/grammar/test_sqlite_insert_grammar.py b/tests/sqlite/grammar/test_sqlite_insert_grammar.py index 8b7017770..9e0c0c351 100644 --- a/tests/sqlite/grammar/test_sqlite_insert_grammar.py +++ b/tests/sqlite/grammar/test_sqlite_insert_grammar.py @@ -27,7 +27,7 @@ def test_can_compile_insert_with_keywords(self): def test_can_compile_bulk_create(self): to_sql = self.builder.bulk_create( - [{"name": "Joe"}, {"name": "Bill"}, {"name": "John"}], query=True + [{"name": "Joe", "age": 5}, {"age": 35, "name": "Bill"}, {"name": "John", "age": 10}], query=True ).to_sql() sql = getattr( @@ -83,13 +83,13 @@ def can_compile_bulk_create(self): """ self.builder.create(name="Joe").to_sql() """ - return """INSERT INTO "users" ("name") VALUES ('Joe'), ('Bill'), ('John')""" + return """INSERT INTO "users" ("age", "name") VALUES ('5', 'Joe'), ('35', 'Bill'), ('10', 'John')""" def can_compile_bulk_create_multiple(self): """ self.builder.create(name="Joe").to_sql() """ - return """INSERT INTO "users" ("name", "active") VALUES ('Joe', '1'), ('Bill', '1'), ('John', '1')""" + return """INSERT INTO "users" ("active", "name") VALUES ('1', 'Joe'), ('1', 'Bill'), ('1', 'John')""" def can_compile_bulk_create_qmark(self): """ From 51a6b58ca8485611ecd7916afcc6b1e32f48abb3 Mon Sep 17 00:00:00 2001 From: Kieren Eaton Date: Fri, 22 Jul 2022 12:42:58 +0800 Subject: [PATCH 010/254] Removed duplicate grammar creation line --- src/masoniteorm/query/QueryBuilder.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 9ccc8fa87..49c427fda 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -2089,7 +2089,6 @@ def to_qmark(self): for name, scope in self._global_scopes.get(self._action, {}).items(): scope(self) - grammar = self.get_grammar() sql = grammar.compile(self._action, qmark=True).to_sql() self._bindings = grammar._bindings From eb245247e997caf032dd2f7dc3004fca7011badc Mon Sep 17 00:00:00 2001 From: Kieren Eaton Date: Sat, 23 Jul 2022 10:23:21 +0800 Subject: [PATCH 011/254] added comment to note why column keys are not in the same order --- tests/mssql/grammar/test_mssql_insert_grammar.py | 1 + tests/mysql/grammar/test_mysql_insert_grammar.py | 1 + tests/postgres/grammar/test_insert_grammar.py | 1 + tests/sqlite/grammar/test_sqlite_insert_grammar.py | 1 + 4 files changed, 4 insertions(+) diff --git a/tests/mssql/grammar/test_mssql_insert_grammar.py b/tests/mssql/grammar/test_mssql_insert_grammar.py index e08cbdf21..3919f8dc5 100644 --- a/tests/mssql/grammar/test_mssql_insert_grammar.py +++ b/tests/mssql/grammar/test_mssql_insert_grammar.py @@ -17,6 +17,7 @@ def test_can_compile_insert(self): def test_can_compile_bulk_create(self): to_sql = self.builder.bulk_create( + # These keys are intentionally out of order to show column to value alignment works [{"name": "Joe", "age": 5}, {"age": 35, "name": "Bill"}, {"name": "John", "age": 10}], query=True ).to_sql() diff --git a/tests/mysql/grammar/test_mysql_insert_grammar.py b/tests/mysql/grammar/test_mysql_insert_grammar.py index daf6f008d..21996ec35 100644 --- a/tests/mysql/grammar/test_mysql_insert_grammar.py +++ b/tests/mysql/grammar/test_mysql_insert_grammar.py @@ -27,6 +27,7 @@ def test_can_compile_insert_with_keywords(self): def test_can_compile_bulk_create(self): to_sql = self.builder.bulk_create( + # These keys are intentionally out of order to show column to value alignment works [{"name": "Joe", "age": 5}, {"age": 35, "name": "Bill"}, {"name": "John", "age": 10}], query=True ).to_sql() diff --git a/tests/postgres/grammar/test_insert_grammar.py b/tests/postgres/grammar/test_insert_grammar.py index 4c3f7ef61..43fa784d8 100644 --- a/tests/postgres/grammar/test_insert_grammar.py +++ b/tests/postgres/grammar/test_insert_grammar.py @@ -27,6 +27,7 @@ def test_can_compile_insert_with_keywords(self): def test_can_compile_bulk_create(self): to_sql = self.builder.bulk_create( + # These keys are intentionally out of order to show column to value alignment works [{"name": "Joe", "age": 5}, {"age": 35, "name": "Bill"}, {"name": "John", "age": 10}], query=True ).to_sql() diff --git a/tests/sqlite/grammar/test_sqlite_insert_grammar.py b/tests/sqlite/grammar/test_sqlite_insert_grammar.py index 9e0c0c351..cd587dd6c 100644 --- a/tests/sqlite/grammar/test_sqlite_insert_grammar.py +++ b/tests/sqlite/grammar/test_sqlite_insert_grammar.py @@ -27,6 +27,7 @@ def test_can_compile_insert_with_keywords(self): def test_can_compile_bulk_create(self): to_sql = self.builder.bulk_create( + # These keys are intentionally out of order to show column to value alignment works [{"name": "Joe", "age": 5}, {"age": 35, "name": "Bill"}, {"name": "John", "age": 10}], query=True ).to_sql() From cad675c622dde38c69f9739068c7c955e659f65a Mon Sep 17 00:00:00 2001 From: Joseph Mancuso Date: Tue, 26 Jul 2022 21:28:23 -0400 Subject: [PATCH 012/254] Update BaseRelationship.py --- src/masoniteorm/relationships/BaseRelationship.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/masoniteorm/relationships/BaseRelationship.py b/src/masoniteorm/relationships/BaseRelationship.py index a9922c0b0..34037a016 100644 --- a/src/masoniteorm/relationships/BaseRelationship.py +++ b/src/masoniteorm/relationships/BaseRelationship.py @@ -1,4 +1,3 @@ -from distutils.command.build import build from ..collection import Collection From da41d341fc2604346261c695651ac71a346408ec Mon Sep 17 00:00:00 2001 From: Joseph Mancuso Date: Tue, 26 Jul 2022 21:28:38 -0400 Subject: [PATCH 013/254] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bb3caf5d1..ad96aa928 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version="2.18.1", + version="2.18.2", package_dir={"": "src"}, description="The Official Masonite ORM", long_description=long_description, From d05fd9d4139a28eebed9c5c9975831fc3a255ecb Mon Sep 17 00:00:00 2001 From: Felipe Hertzer Date: Wed, 27 Jul 2022 12:06:54 +1000 Subject: [PATCH 014/254] Fixed wrong select structure when using 'having' and 'order' --- src/masoniteorm/query/grammars/MSSQLGrammar.py | 3 +-- src/masoniteorm/query/grammars/MySQLGrammar.py | 3 +-- src/masoniteorm/query/grammars/PostgresGrammar.py | 3 +-- src/masoniteorm/query/grammars/SQLiteGrammar.py | 3 +-- tests/mssql/grammar/test_mssql_select_grammar.py | 11 +++++++++++ tests/mysql/grammar/test_mysql_select_grammar.py | 10 ++++++++++ tests/postgres/grammar/test_select_grammar.py | 10 ++++++++++ tests/sqlite/grammar/test_sqlite_select_grammar.py | 6 ++++++ 8 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/masoniteorm/query/grammars/MSSQLGrammar.py b/src/masoniteorm/query/grammars/MSSQLGrammar.py index 34c1315b5..90af2a237 100644 --- a/src/masoniteorm/query/grammars/MSSQLGrammar.py +++ b/src/masoniteorm/query/grammars/MSSQLGrammar.py @@ -10,7 +10,6 @@ class MSSQLGrammar(BaseGrammar): "MIN": "MIN", "AVG": "AVG", "COUNT": "COUNT", - "AVG": "AVG", } join_keywords = { @@ -37,7 +36,7 @@ def select_no_table(self): return "SELECT {columns}" def select_format(self): - return "SELECT {limit} {columns} FROM {table} {lock} {joins} {wheres} {group_by} {order_by} {offset} {having}" + return "SELECT {limit} {columns} FROM {table} {lock} {joins} {wheres} {group_by} {having} {order_by} {offset}" def update_format(self): return "UPDATE {table} SET {key_equals} {wheres}" diff --git a/src/masoniteorm/query/grammars/MySQLGrammar.py b/src/masoniteorm/query/grammars/MySQLGrammar.py index 2f5d89421..fd461b690 100644 --- a/src/masoniteorm/query/grammars/MySQLGrammar.py +++ b/src/masoniteorm/query/grammars/MySQLGrammar.py @@ -10,7 +10,6 @@ class MySQLGrammar(BaseGrammar): "MIN": "MIN", "AVG": "AVG", "COUNT": "COUNT", - "AVG": "AVG", } join_keywords = { @@ -50,7 +49,7 @@ class MySQLGrammar(BaseGrammar): locks = {"share": "LOCK IN SHARE MODE", "update": "FOR UPDATE"} def select_format(self): - return "SELECT {columns} FROM {table} {joins} {wheres} {group_by} {order_by} {limit} {offset} {having} {lock}" + return "SELECT {columns} FROM {table} {joins} {wheres} {group_by} {having} {order_by} {limit} {offset} {lock}" def select_no_table(self): return "SELECT {columns} {lock}" diff --git a/src/masoniteorm/query/grammars/PostgresGrammar.py b/src/masoniteorm/query/grammars/PostgresGrammar.py index eab71ca17..6c42d0ea4 100644 --- a/src/masoniteorm/query/grammars/PostgresGrammar.py +++ b/src/masoniteorm/query/grammars/PostgresGrammar.py @@ -11,7 +11,6 @@ class PostgresGrammar(BaseGrammar): "MIN": "MIN", "AVG": "AVG", "COUNT": "COUNT", - "AVG": "AVG", } join_keywords = { @@ -38,7 +37,7 @@ def select_no_table(self): return "SELECT {columns} {lock}" def select_format(self): - return "SELECT {columns} FROM {table} {joins} {wheres} {group_by} {order_by} {limit} {offset} {having} {lock}" + return "SELECT {columns} FROM {table} {joins} {wheres} {group_by} {having} {order_by} {limit} {offset} {lock}" def update_format(self): return "UPDATE {table} SET {key_equals} {wheres}" diff --git a/src/masoniteorm/query/grammars/SQLiteGrammar.py b/src/masoniteorm/query/grammars/SQLiteGrammar.py index f3d87ce52..fe82d56d5 100644 --- a/src/masoniteorm/query/grammars/SQLiteGrammar.py +++ b/src/masoniteorm/query/grammars/SQLiteGrammar.py @@ -11,7 +11,6 @@ class SQLiteGrammar(BaseGrammar): "MIN": "MIN", "AVG": "AVG", "COUNT": "COUNT", - "AVG": "AVG", } join_keywords = { @@ -35,7 +34,7 @@ class SQLiteGrammar(BaseGrammar): locks = {"share": "", "update": ""} def select_format(self): - return "SELECT {columns} FROM {table} {joins} {wheres} {group_by} {order_by} {limit} {offset} {having} {lock}" + return "SELECT {columns} FROM {table} {joins} {wheres} {group_by} {having} {order_by} {limit} {offset} {lock}" def select_no_table(self): return "SELECT {columns} {lock}" diff --git a/tests/mssql/grammar/test_mssql_select_grammar.py b/tests/mssql/grammar/test_mssql_select_grammar.py index 8591882fb..cfbd4acdc 100644 --- a/tests/mssql/grammar/test_mssql_select_grammar.py +++ b/tests/mssql/grammar/test_mssql_select_grammar.py @@ -246,6 +246,12 @@ def can_compile_having(self): """ return "SELECT SUM([users].[age]) AS age FROM [users] GROUP BY [users].[age] HAVING [users].[age]" + def can_compile_having_order(self): + """ + builder.sum('age').group_by('age').having('age').order_by('age', 'desc').to_sql() + """ + return "SELECT SUM([users].[age]) AS age FROM [users] GROUP BY [users].[age] HAVING [users].[age] ORDER [users].[age] DESC" + def can_compile_between(self): """ builder.between('age', 18, 21).to_sql() @@ -302,6 +308,11 @@ def test_can_compile_having_raw(self): to_sql = self.builder.select_raw("COUNT(*) as counts").having_raw("counts > 10").to_sql() self.assertEqual(to_sql, "SELECT COUNT(*) as counts FROM [users] HAVING counts > 10") + def test_can_compile_having_raw_order(self): + to_sql = self.builder.select_raw("COUNT(*) as counts").having_raw("counts > 10").order_by_raw( + 'counts DESC').to_sql() + self.assertEqual(to_sql, "SELECT COUNT(*) as counts FROM [users] HAVING counts > 10 ORDER BY counts DESC") + def test_can_compile_select_raw(self): to_sql = self.builder.select_raw("COUNT(*)").to_sql() sql = getattr( diff --git a/tests/mysql/grammar/test_mysql_select_grammar.py b/tests/mysql/grammar/test_mysql_select_grammar.py index 16f6f59bb..6b6635fed 100644 --- a/tests/mysql/grammar/test_mysql_select_grammar.py +++ b/tests/mysql/grammar/test_mysql_select_grammar.py @@ -248,6 +248,12 @@ def can_compile_having(self): """ return "SELECT SUM(`users`.`age`) AS age FROM `users` GROUP BY `users`.`age` HAVING `users`.`age`" + def can_compile_having_order(self): + """ + builder.sum('age').group_by('age').having('age').order_by('age', 'desc').to_sql() + """ + return "SELECT SUM(`users`.`age`) AS age FROM `users` GROUP BY `users`.`age` HAVING `users`.`age` ORDER `users`.`age` DESC" + def can_compile_having_with_expression(self): """ builder.sum('age').group_by('age').having('age', 10).to_sql() @@ -298,6 +304,10 @@ def test_can_compile_having_raw(self): to_sql = self.builder.select_raw("COUNT(*) as counts").having_raw("counts > 10").to_sql() self.assertEqual(to_sql, "SELECT COUNT(*) as counts FROM `users` HAVING counts > 10") + def test_can_compile_having_raw_order(self): + to_sql = self.builder.select_raw("COUNT(*) as counts").having_raw("counts > 10").order_by_raw('counts DESC').to_sql() + self.assertEqual(to_sql, "SELECT COUNT(*) as counts FROM `users` HAVING counts > 10 ORDER BY counts DESC") + def test_can_compile_select_raw(self): to_sql = self.builder.select_raw("COUNT(*)").to_sql() self.assertEqual(to_sql, "SELECT COUNT(*) FROM `users`") diff --git a/tests/postgres/grammar/test_select_grammar.py b/tests/postgres/grammar/test_select_grammar.py index 8df5492f9..aca8edb81 100644 --- a/tests/postgres/grammar/test_select_grammar.py +++ b/tests/postgres/grammar/test_select_grammar.py @@ -247,6 +247,12 @@ def can_compile_having(self): """ return """SELECT SUM("users"."age") AS age FROM "users" GROUP BY "users"."age" HAVING "users"."age\"""" + def can_compile_having_order(self): + """ + builder.sum('age').group_by('age').having('age').order_by('age', 'desc').to_sql() + """ + return """SELECT SUM("users"."age") AS age FROM "users" GROUP BY "users"."age" HAVING "users"."age" ORDER "users"."age" DESC""" + def can_compile_having_with_expression(self): """ builder.sum('age').group_by('age').having('age', 10).to_sql() @@ -303,6 +309,10 @@ def test_can_compile_having_raw(self): to_sql = self.builder.select_raw("COUNT(*) as counts").having_raw("counts > 10").to_sql() self.assertEqual(to_sql, """SELECT COUNT(*) as counts FROM "users" HAVING counts > 10""") + def test_can_compile_having_raw_order(self): + to_sql = self.builder.select_raw("COUNT(*) as counts").having_raw("counts > 10").order_by_raw('counts DESC').to_sql() + self.assertEqual(to_sql, """SELECT COUNT(*) as counts FROM "users" HAVING counts > 10 ORDER BY counts DESC""") + def test_can_compile_where_raw_and_where_with_multiple_bindings(self): query = self.builder.where_raw( """ "age" = '?' AND "is_admin" = '?'""", [18, True] diff --git a/tests/sqlite/grammar/test_sqlite_select_grammar.py b/tests/sqlite/grammar/test_sqlite_select_grammar.py index b1196975c..7e064812d 100644 --- a/tests/sqlite/grammar/test_sqlite_select_grammar.py +++ b/tests/sqlite/grammar/test_sqlite_select_grammar.py @@ -239,6 +239,12 @@ def can_compile_having(self): """ return """SELECT SUM("users"."age") AS age FROM "users" GROUP BY "users"."age" HAVING "users"."age\"""" + def can_compile_having_order(self): + """ + builder.sum('age').group_by('age').having('age').order_by('age', 'desc').to_sql() + """ + return """SELECT SUM("users"."age") AS age FROM "users" GROUP BY "users"."age" HAVING "users"."age\" ORDER "users"."age" DESC""" + def can_compile_having_raw(self): """ builder.select_raw("COUNT(*) as counts").having_raw("counts > 18").to_sql() From eea6dfe1455ca525fa397e379208cb35434c2560 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sat, 30 Jul 2022 20:54:31 -0400 Subject: [PATCH 015/254] made pivot record back to delete --- src/masoniteorm/relationships/BelongsToMany.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/masoniteorm/relationships/BelongsToMany.py b/src/masoniteorm/relationships/BelongsToMany.py index 36c7ec61b..33aa177ec 100644 --- a/src/masoniteorm/relationships/BelongsToMany.py +++ b/src/masoniteorm/relationships/BelongsToMany.py @@ -541,7 +541,7 @@ def detach(self, current_model, related_record): .table(self._table) .without_global_scopes() .where(data) - .update({self.foreign_key: None, self.local_key: None}) + .delete() ) def attach_related(self, current_model, related_record): From 27745666eefb7ec785b6d19a5508f0433f721944 Mon Sep 17 00:00:00 2001 From: Joseph Mancuso Date: Sat, 30 Jul 2022 21:01:33 -0400 Subject: [PATCH 016/254] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ad96aa928..3d937e211 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version="2.18.2", + version="2.18.3", package_dir={"": "src"}, description="The Official Masonite ORM", long_description=long_description, From 288dfc7972cbc95e82ebc8253ff9a4b86fd7dfbe Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sat, 30 Jul 2022 21:29:21 -0400 Subject: [PATCH 017/254] changed how collection is plucked in relatinoships --- src/masoniteorm/relationships/BaseRelationship.py | 2 +- src/masoniteorm/relationships/BelongsTo.py | 2 +- src/masoniteorm/relationships/BelongsToMany.py | 2 +- src/masoniteorm/relationships/HasManyThrough.py | 2 +- src/masoniteorm/relationships/HasOne.py | 2 +- src/masoniteorm/relationships/HasOneThrough.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/masoniteorm/relationships/BaseRelationship.py b/src/masoniteorm/relationships/BaseRelationship.py index 34037a016..6b9475f62 100644 --- a/src/masoniteorm/relationships/BaseRelationship.py +++ b/src/masoniteorm/relationships/BaseRelationship.py @@ -152,7 +152,7 @@ def get_related(self, query, relation, eagers=None, callback=None): if isinstance(relation, Collection): return builder.where_in( f"{builder.get_table_name()}.{self.foreign_key}", - relation.pluck(self.local_key, keep_nulls=False).unique(), + Collection(relation._get_value(self.local_key)).unique(), ).get() else: return builder.where( diff --git a/src/masoniteorm/relationships/BelongsTo.py b/src/masoniteorm/relationships/BelongsTo.py index 1339124ac..92c18ed3d 100644 --- a/src/masoniteorm/relationships/BelongsTo.py +++ b/src/masoniteorm/relationships/BelongsTo.py @@ -53,7 +53,7 @@ def get_related(self, query, relation, eagers=(), callback=None): if isinstance(relation, Collection): return builder.where_in( f"{builder.get_table_name()}.{self.foreign_key}", - relation.pluck(self.local_key, keep_nulls=False).unique(), + Collection(relation._get_value(self.local_key)).unique(), ).get() else: diff --git a/src/masoniteorm/relationships/BelongsToMany.py b/src/masoniteorm/relationships/BelongsToMany.py index 33aa177ec..1d55cafc9 100644 --- a/src/masoniteorm/relationships/BelongsToMany.py +++ b/src/masoniteorm/relationships/BelongsToMany.py @@ -237,7 +237,7 @@ def make_query(self, query, relation, eagers=None, callback=None): if isinstance(relation, Collection): return result.where_in( self.local_owner_key, - relation.pluck(self.local_owner_key, keep_nulls=False), + Collection(relation._get_value(self.local_owner_key)).unique(), ).get() else: return result.where( diff --git a/src/masoniteorm/relationships/HasManyThrough.py b/src/masoniteorm/relationships/HasManyThrough.py index 9a256fe64..ee7396e1d 100644 --- a/src/masoniteorm/relationships/HasManyThrough.py +++ b/src/masoniteorm/relationships/HasManyThrough.py @@ -113,7 +113,7 @@ def get_related(self, query, relation, eagers=None, callback=None): if isinstance(relation, Collection): return builder.where_in( f"{builder.get_table_name()}.{self.foreign_key}", - relation.pluck(self.local_key, keep_nulls=False).unique(), + Collection(relation._get_value(self.local_key)).unique(), ).get() else: return builder.where( diff --git a/src/masoniteorm/relationships/HasOne.py b/src/masoniteorm/relationships/HasOne.py index a1a76c403..c60056402 100644 --- a/src/masoniteorm/relationships/HasOne.py +++ b/src/masoniteorm/relationships/HasOne.py @@ -54,7 +54,7 @@ def get_related(self, query, relation, eagers=(), callback=None): if isinstance(relation, Collection): return builder.where_in( f"{builder.get_table_name()}.{self.foreign_key}", - relation.pluck(self.local_key, keep_nulls=False).unique(), + Collection(relation._get_value(self.local_key)).unique(), ).get() else: return builder.where( diff --git a/src/masoniteorm/relationships/HasOneThrough.py b/src/masoniteorm/relationships/HasOneThrough.py index 3840da32c..ae7c587b4 100644 --- a/src/masoniteorm/relationships/HasOneThrough.py +++ b/src/masoniteorm/relationships/HasOneThrough.py @@ -113,7 +113,7 @@ def get_related(self, query, relation, eagers=None, callback=None): if isinstance(relation, Collection): return builder.where_in( f"{builder.get_table_name()}.{self.foreign_key}", - relation.pluck(self.local_key, keep_nulls=False).unique(), + Collection(relation._get_value(self.local_key)).unique(), ).get() else: return builder.where( From 9728db9e00bbb2ba3fff70470cde6f9d0522f9cb Mon Sep 17 00:00:00 2001 From: Joseph Mancuso Date: Sat, 30 Jul 2022 21:32:15 -0400 Subject: [PATCH 018/254] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3d937e211..fbd455793 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version="2.18.3", + version="2.18.4", package_dir={"": "src"}, description="The Official Masonite ORM", long_description=long_description, From 7a688e78a8e2ac404b2c54de018ce2466094d65c Mon Sep 17 00:00:00 2001 From: LordDeveloper Date: Wed, 17 Aug 2022 22:51:48 +0430 Subject: [PATCH 019/254] Fixed null attribute error when creation --- src/masoniteorm/models/Model.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index 235e1bcff..c7750f6d0 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -780,12 +780,7 @@ def method(*args, **kwargs): if attribute in self.__dict__.get("_relationships", {}): return self.__dict__["_relationships"][attribute] - - if attribute not in self.__dict__: - name = self.__class__.__name__ - - raise AttributeError(f"class model '{name}' has no attribute {attribute}") - + return None def __setattr__(self, attribute, value): From 23b43826acd89b84d00632819ac34f249526a5fa Mon Sep 17 00:00:00 2001 From: Kieren Eaton Date: Fri, 26 Aug 2022 13:18:15 +0800 Subject: [PATCH 020/254] Fixed Morph_one should respect provided key and id --- src/masoniteorm/relationships/MorphOne.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/masoniteorm/relationships/MorphOne.py b/src/masoniteorm/relationships/MorphOne.py index b5595e30c..62e98a1d9 100644 --- a/src/masoniteorm/relationships/MorphOne.py +++ b/src/masoniteorm/relationships/MorphOne.py @@ -1,6 +1,6 @@ from ..collection import Collection -from .BaseRelationship import BaseRelationship from ..config import load_config +from .BaseRelationship import BaseRelationship class MorphOne(BaseRelationship): @@ -67,8 +67,8 @@ def apply_query(self, builder, instance): polymorphic_builder = self.polymorphic_builder return ( - polymorphic_builder.where("record_type", polymorphic_key) - .where("record_id", instance.get_primary_key_value()) + polymorphic_builder.where(self.morph_key, polymorphic_key) + .where(self.morph_id, instance.get_primary_key_value()) .first() ) From 57b5b64bf3151279013693bcfca685469f26c577 Mon Sep 17 00:00:00 2001 From: federicoparroni Date: Thu, 1 Sep 2022 13:22:21 +0200 Subject: [PATCH 021/254] Index scopes by class in which they are declared --- src/masoniteorm/models/Model.py | 2 +- src/masoniteorm/scopes/scope.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index 235e1bcff..ce3d8fc91 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -337,7 +337,7 @@ def get_builder(self): table=self.get_table_name(), connection_details=self.get_connection_details(), model=self, - scopes=self._scopes, + scopes=self._scopes.get(self.__class__), dry=self.__dry__, ) diff --git a/src/masoniteorm/scopes/scope.py b/src/masoniteorm/scopes/scope.py index 482a5e284..79c2cb81b 100644 --- a/src/masoniteorm/scopes/scope.py +++ b/src/masoniteorm/scopes/scope.py @@ -3,7 +3,10 @@ def __init__(self, callback, *params, **kwargs): self.fn = callback def __set_name__(self, cls, name): - cls._scopes.update({name: self.fn}) + if cls not in cls._scopes: + cls._scopes[cls] = {name: self.fn} + else: + cls._scopes[cls].update({name: self.fn}) self.cls = cls def __call__(self, *args, **kwargs): From 6752b3861aa5b632863c54581e024ab1d5aeacdf Mon Sep 17 00:00:00 2001 From: Javad Sadeghi <34248444+LordDeveloper@users.noreply.github.com> Date: Sat, 3 Sep 2022 20:58:38 +0430 Subject: [PATCH 022/254] key and value must be applied to _items --- src/masoniteorm/collection/Collection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/masoniteorm/collection/Collection.py b/src/masoniteorm/collection/Collection.py index efadd444e..0c3cb9c55 100644 --- a/src/masoniteorm/collection/Collection.py +++ b/src/masoniteorm/collection/Collection.py @@ -280,7 +280,7 @@ def push(self, value): self._items.append(value) def put(self, key, value): - self[key] = value + self._items[key] = value return self def random(self, count=None): From e8744d06de00a69fb2db78f3c1074ceebc748619 Mon Sep 17 00:00:00 2001 From: Javad Sadeghi <34248444+LordDeveloper@users.noreply.github.com> Date: Sun, 4 Sep 2022 01:16:26 +0430 Subject: [PATCH 023/254] It has been reverted for now --- src/masoniteorm/models/Model.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index c7750f6d0..235e1bcff 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -780,7 +780,12 @@ def method(*args, **kwargs): if attribute in self.__dict__.get("_relationships", {}): return self.__dict__["_relationships"][attribute] - + + if attribute not in self.__dict__: + name = self.__class__.__name__ + + raise AttributeError(f"class model '{name}' has no attribute {attribute}") + return None def __setattr__(self, attribute, value): From 199fec45b075dcd9d17098f7a44640df26dcb84f Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 11 Sep 2022 20:35:06 -0400 Subject: [PATCH 024/254] fixed qmark and scope order --- src/masoniteorm/query/QueryBuilder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 49c427fda..9ac9ddf14 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -2084,11 +2084,11 @@ def to_qmark(self): Returns: self """ - grammar = self.get_grammar() - for name, scope in self._global_scopes.get(self._action, {}).items(): scope(self) + grammar = self.get_grammar() + sql = grammar.compile(self._action, qmark=True).to_sql() self._bindings = grammar._bindings From 3c2358db8eb9c76330b8eca39d9a99d09e0cc08e Mon Sep 17 00:00:00 2001 From: Joseph Mancuso Date: Sun, 11 Sep 2022 20:37:31 -0400 Subject: [PATCH 025/254] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index fbd455793..0d1c6c1c7 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version="2.18.4", + version="2.18.5", package_dir={"": "src"}, description="The Official Masonite ORM", long_description=long_description, From e6efedbbab3ba050536aa2786daec4c9f3951356 Mon Sep 17 00:00:00 2001 From: yubarajshrestha Date: Wed, 21 Sep 2022 08:25:38 +0545 Subject: [PATCH 026/254] Adds oldest, latest methods to QueryBuilder, fixes #790 --- src/masoniteorm/models/Model.py | 2 ++ src/masoniteorm/query/QueryBuilder.py | 32 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index ce3d8fc91..b3fd6cc8e 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -255,6 +255,8 @@ class Model(TimeStampsMixin, ObservesEvents, metaclass=ModelMeta): "where_doesnt_have", "with_", "with_count", + "latest", + "oldest" ] __cast_map__ = {} diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 9ac9ddf14..52b358440 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -2266,3 +2266,35 @@ def get_schema(self): return Schema( connection=self.connection, connection_details=self._connection_details ) + + def latest(self, *fields): + """Gets the latest record. + + Returns: + querybuilder + """ + + if not fields: + fields = ("created_at",) + + table = self.get_table_name() + fields = map(lambda field: f"`{table}`.`{field}`", fields) + sql = " desc, ".join(fields) + " desc" + + return self.order_by_raw(sql) + + def oldest(self, *fields): + """Gets the oldest record. + + Returns: + querybuilder + """ + + if not fields: + fields = ("created_at",) + + table = self.get_table_name() + fields = map(lambda field: f"`{table}`.`{field}`", fields) + sql = " asc, ".join(fields) + " asc" + + return self.order_by_raw(sql) From 2f570b2ab79bb5fd3fe6e0df3a2024f8c2ddc798 Mon Sep 17 00:00:00 2001 From: Marlysson Date: Sun, 25 Sep 2022 12:10:32 -0300 Subject: [PATCH 027/254] Changing the way config_path is loaded. First from environment, after custom path, and so, set to environment. --- src/masoniteorm/config.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/masoniteorm/config.py b/src/masoniteorm/config.py index 6e37ef0cf..441025caf 100644 --- a/src/masoniteorm/config.py +++ b/src/masoniteorm/config.py @@ -11,9 +11,12 @@ def load_config(config_path=None): 1. try to load from DB_CONFIG_PATH environment variable 2. else try to load from default config_path: config/database """ - selected_config_path = ( - config_path or os.getenv("DB_CONFIG_PATH", None) or "config/database" - ) + + if not os.getenv("DB_CONFIG_PATH", None): + os.environ['DB_CONFIG_PATH'] = config_path or "config/database" + + selected_config_path = os.environ['DB_CONFIG_PATH'] + # format path as python module if needed selected_config_path = ( selected_config_path.replace("/", ".").replace("\\", ".").rstrip(".py") From 18de466233beafaa3ccbb5fdd8b95bdf3f0ad46c Mon Sep 17 00:00:00 2001 From: Marlysson Date: Sun, 25 Sep 2022 12:11:30 -0300 Subject: [PATCH 028/254] Removing second parameter from self.option(), because already define to default in ddeclaration. --- src/masoniteorm/commands/MigrateRefreshCommand.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/masoniteorm/commands/MigrateRefreshCommand.py b/src/masoniteorm/commands/MigrateRefreshCommand.py index b56e5d377..71a15aa4c 100644 --- a/src/masoniteorm/commands/MigrateRefreshCommand.py +++ b/src/masoniteorm/commands/MigrateRefreshCommand.py @@ -33,11 +33,11 @@ def handle(self): if self.option("seed") == "null": self.call( "seed:run", - f"None --directory {self.option('seed-directory')} --connection {self.option('connection', 'default')}", + f"None --directory {self.option('seed-directory')} --connection {self.option('connection')}", ) elif self.option("seed"): self.call( "seed:run", - f"{self.option('seed')} --directory {self.option('seed-directory')} --connection {self.option('connection', 'default')}", + f"{self.option('seed')} --directory {self.option('seed-directory')} --connection {self.option('connection')}", ) From 792227f3a220068c5ada0e73b75ad6146dd5a9ee Mon Sep 17 00:00:00 2001 From: Marlysson Date: Sun, 25 Sep 2022 12:24:42 -0300 Subject: [PATCH 029/254] Linting. --- src/masoniteorm/config.py | 8 ++++---- src/masoniteorm/observers/ObservesEvents.py | 6 ++---- src/masoniteorm/schema/Blueprint.py | 6 ++++-- src/masoniteorm/schema/Schema.py | 3 +-- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/masoniteorm/config.py b/src/masoniteorm/config.py index 441025caf..7895dc95f 100644 --- a/src/masoniteorm/config.py +++ b/src/masoniteorm/config.py @@ -13,10 +13,10 @@ def load_config(config_path=None): """ if not os.getenv("DB_CONFIG_PATH", None): - os.environ['DB_CONFIG_PATH'] = config_path or "config/database" - - selected_config_path = os.environ['DB_CONFIG_PATH'] - + os.environ["DB_CONFIG_PATH"] = config_path or "config/database" + + selected_config_path = os.environ["DB_CONFIG_PATH"] + # format path as python module if needed selected_config_path = ( selected_config_path.replace("/", ".").replace("\\", ".").rstrip(".py") diff --git a/src/masoniteorm/observers/ObservesEvents.py b/src/masoniteorm/observers/ObservesEvents.py index c9d21274d..ebc48c2b5 100644 --- a/src/masoniteorm/observers/ObservesEvents.py +++ b/src/masoniteorm/observers/ObservesEvents.py @@ -16,14 +16,12 @@ def observe(cls, observer): @classmethod def without_events(cls): - """Sets __has_events__ attribute on model to false. - """ + """Sets __has_events__ attribute on model to false.""" cls.__has_events__ = False return cls @classmethod def with_events(cls): - """Sets __has_events__ attribute on model to True. - """ + """Sets __has_events__ attribute on model to True.""" cls.__has_events__ = False return cls diff --git a/src/masoniteorm/schema/Blueprint.py b/src/masoniteorm/schema/Blueprint.py index 3b20464de..3c9c0b1bd 100644 --- a/src/masoniteorm/schema/Blueprint.py +++ b/src/masoniteorm/schema/Blueprint.py @@ -900,9 +900,11 @@ def foreign_id_for(self, model, column=None): """ clm = column if column else model.get_foreign_key() - return self.foreign_id(clm)\ - if model.get_primary_key_type() == 'int'\ + return ( + self.foreign_id(clm) + if model.get_primary_key_type() == "int" else self.foreign_uuid(column) + ) def references(self, column): """Sets the other column on the foreign table that the local column will use to reference. diff --git a/src/masoniteorm/schema/Schema.py b/src/masoniteorm/schema/Schema.py index 6140c8c24..812a48fbf 100644 --- a/src/masoniteorm/schema/Schema.py +++ b/src/masoniteorm/schema/Schema.py @@ -287,8 +287,7 @@ def truncate(self, table, foreign_keys=False): return bool(self.new_connection().query(sql, ())) def get_schema(self): - """Gets the schema set on the migration class - """ + """Gets the schema set on the migration class""" return self.schema or self.get_connection_information().get("full_details").get( "schema" ) From 3344b455257568d157f7882467de58bbefae3c3c Mon Sep 17 00:00:00 2001 From: Bader Almutairi Date: Tue, 4 Oct 2022 03:40:27 +0300 Subject: [PATCH 030/254] Update Model.py --- src/masoniteorm/models/Model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index ce3d8fc91..73f969da5 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -61,7 +61,7 @@ def get(self, value): if not isinstance(value, str): return json.dumps(value) - return value + return json.loads(value) def set(self, value): if isinstance(value, str): From dd87ef1c1e4404bc9b9376c66dbe97baf65256aa Mon Sep 17 00:00:00 2001 From: Bader Almutairi Date: Tue, 4 Oct 2022 03:45:45 +0300 Subject: [PATCH 031/254] Update Model.py --- src/masoniteorm/models/Model.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index 73f969da5..a6af41222 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -58,10 +58,10 @@ class JsonCast: """Casts a value to JSON""" def get(self, value): - if not isinstance(value, str): - return json.dumps(value) + if isinstance(value, str): + return json.loads(value) - return json.loads(value) + return value def set(self, value): if isinstance(value, str): From 72d0975e4d05180809ab3b35d9815c3063b22085 Mon Sep 17 00:00:00 2001 From: Bader Almutairi Date: Tue, 4 Oct 2022 09:58:01 +0300 Subject: [PATCH 032/254] Update misc.py --- src/masoniteorm/helpers/misc.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/masoniteorm/helpers/misc.py b/src/masoniteorm/helpers/misc.py index 72d71354c..8ff862c89 100644 --- a/src/masoniteorm/helpers/misc.py +++ b/src/masoniteorm/helpers/misc.py @@ -1,6 +1,15 @@ """Module for miscellaneous helper methods.""" import warnings +import json + + +def is_json(json_string): + try: + json.loads(json_string) + except ValueError as e: + return False + return True def deprecated(message): From c5850c2373136fc58a231e5f322b909e44ce4f32 Mon Sep 17 00:00:00 2001 From: Bader Almutairi Date: Tue, 4 Oct 2022 09:59:58 +0300 Subject: [PATCH 033/254] Update Model.py --- src/masoniteorm/models/Model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index a6af41222..c25bd008b 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -58,7 +58,7 @@ class JsonCast: """Casts a value to JSON""" def get(self, value): - if isinstance(value, str): + if is_json(value): return json.loads(value) return value From bd0c0d2b89477b5291a1a5f62c965a289e40380a Mon Sep 17 00:00:00 2001 From: Bader Almutairi Date: Tue, 4 Oct 2022 10:48:20 +0300 Subject: [PATCH 034/254] .. --- src/masoniteorm/helpers/misc.py | 10 +++++----- src/masoniteorm/models/Model.py | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/masoniteorm/helpers/misc.py b/src/masoniteorm/helpers/misc.py index 8ff862c89..165df4f14 100644 --- a/src/masoniteorm/helpers/misc.py +++ b/src/masoniteorm/helpers/misc.py @@ -5,11 +5,11 @@ def is_json(json_string): - try: - json.loads(json_string) - except ValueError as e: - return False - return True + try: + json.loads(json_string) + except ValueError as e: + return False + return True def deprecated(message): diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index c25bd008b..2e839ff61 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -8,6 +8,7 @@ import pendulum +from ..helpers.misc import is_json from ..query import QueryBuilder from ..collection import Collection from ..observers import ObservesEvents From 45c22a5e0d92443b554174edbdc0ac249844925a Mon Sep 17 00:00:00 2001 From: Bader Almutairi Date: Tue, 4 Oct 2022 10:53:23 +0300 Subject: [PATCH 035/254] Update Model.py --- src/masoniteorm/models/Model.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index 2e839ff61..102c46da9 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -59,7 +59,9 @@ class JsonCast: """Casts a value to JSON""" def get(self, value): - if is_json(value): + if not isinstance(value, str): + return json.dumps(value) + elif is_json(value): return json.loads(value) return value From c95d5ac72148d3cfa59a3f1aa4aa86735c03a7d1 Mon Sep 17 00:00:00 2001 From: Bader Almutairi Date: Tue, 4 Oct 2022 10:58:00 +0300 Subject: [PATCH 036/254] Update test_models.py --- tests/models/test_models.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/models/test_models.py b/tests/models/test_models.py index d55eab384..399c4a790 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -97,8 +97,7 @@ def test_model_can_cast_attributes(self): } ) - self.assertEqual(type(model.payload), str) - self.assertEqual(type(json.loads(model.payload)), list) + self.assertEqual(type(model.payload), list) self.assertEqual(type(model.x), int) self.assertEqual(type(model.f), float) self.assertEqual(type(model.is_vip), bool) From 092a16f540aa9256f32e1cddeaa26cc470fbe673 Mon Sep 17 00:00:00 2001 From: federicoparroni Date: Tue, 4 Oct 2022 17:45:59 +0200 Subject: [PATCH 037/254] Fix --- src/masoniteorm/models/Model.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index ce3d8fc91..83e6df7ca 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -160,7 +160,7 @@ class Model(TimeStampsMixin, ObservesEvents, metaclass=ModelMeta): """Pass through will pass any method calls to the model directly through to the query builder. Anytime one of these methods are called on the model it will actually be called on the query builder class. """ - __passthrough__ = [ + __passthrough__ = set( "add_select", "aggregate", "all", @@ -255,7 +255,7 @@ class Model(TimeStampsMixin, ObservesEvents, metaclass=ModelMeta): "where_doesnt_have", "with_", "with_count", - ] + ) __cast_map__ = {} @@ -366,7 +366,7 @@ def boot(self): self.append_passthrough(list(self.get_builder()._macros.keys())) def append_passthrough(self, passthrough): - self.__passthrough__ += passthrough + self.__passthrough__.update(passthrough) return self @classmethod From 2bd70b2dab16ea8239788dc3523a035ca6530f1d Mon Sep 17 00:00:00 2001 From: federicoparroni Date: Tue, 4 Oct 2022 17:50:32 +0200 Subject: [PATCH 038/254] Fix --- src/masoniteorm/models/Model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index 83e6df7ca..0ffdac8c7 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -160,7 +160,7 @@ class Model(TimeStampsMixin, ObservesEvents, metaclass=ModelMeta): """Pass through will pass any method calls to the model directly through to the query builder. Anytime one of these methods are called on the model it will actually be called on the query builder class. """ - __passthrough__ = set( + __passthrough__ = set(( "add_select", "aggregate", "all", @@ -255,7 +255,7 @@ class Model(TimeStampsMixin, ObservesEvents, metaclass=ModelMeta): "where_doesnt_have", "with_", "with_count", - ) + )) __cast_map__ = {} From 9493db514c2d4b9c43056d29dc15d0ccc4958c2e Mon Sep 17 00:00:00 2001 From: Bader Almutairi Date: Wed, 5 Oct 2022 04:03:12 +0300 Subject: [PATCH 039/254] fix lint --- src/masoniteorm/helpers/misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/masoniteorm/helpers/misc.py b/src/masoniteorm/helpers/misc.py index 165df4f14..7a9591c54 100644 --- a/src/masoniteorm/helpers/misc.py +++ b/src/masoniteorm/helpers/misc.py @@ -7,7 +7,7 @@ def is_json(json_string): try: json.loads(json_string) - except ValueError as e: + except ValueError: return False return True From 9f4e692db2c8f071add5bd23fae30dec330cfbf3 Mon Sep 17 00:00:00 2001 From: Bader Almutairi Date: Wed, 5 Oct 2022 14:18:34 +0300 Subject: [PATCH 040/254] added another test --- src/masoniteorm/helpers/misc.py | 2 ++ src/masoniteorm/models/Model.py | 4 +--- tests/models/test_models.py | 13 ++++++++++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/masoniteorm/helpers/misc.py b/src/masoniteorm/helpers/misc.py index 7a9591c54..91c7ae887 100644 --- a/src/masoniteorm/helpers/misc.py +++ b/src/masoniteorm/helpers/misc.py @@ -5,6 +5,8 @@ def is_json(json_string): + if not isinstance(json_string, str): + return False try: json.loads(json_string) except ValueError: diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index 102c46da9..2e839ff61 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -59,9 +59,7 @@ class JsonCast: """Casts a value to JSON""" def get(self, value): - if not isinstance(value, str): - return json.dumps(value) - elif is_json(value): + if is_json(value): return json.loads(value) return value diff --git a/tests/models/test_models.py b/tests/models/test_models.py index 399c4a790..a38b33179 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -30,7 +30,6 @@ def test_model_can_access_str_dates_as_pendulum(self): self.assertIsInstance(model.due_date, pendulum.now().__class__) def test_model_can_access_str_dates_as_pendulum_from_correct_datetimes(self): - model = ModelTest() self.assertEqual( @@ -111,13 +110,21 @@ def test_model_can_cast_dict_attributes(self): {"is_vip": 1, "payload": dictcasttest, "x": True, "f": "10.5"} ) - self.assertEqual(type(model.payload), str) - self.assertEqual(type(json.loads(model.payload)), dict) + self.assertEqual(type(model.payload), dict) self.assertEqual(type(model.x), int) self.assertEqual(type(model.f), float) self.assertEqual(type(model.is_vip), bool) self.assertEqual(type(model.serialize()["is_vip"]), bool) + def test_valid_json_cast(self): + model = ModelTest.hydrate({ + "valid_cast1": {"this": "dict", "is": "usable", "as": "json"}, + "valid_cast2": {'this': 'dict', 'is': 'invalid', 'as': 'json'} + }) + + self.assertEqual(type(model.valid_cast1), dict) + self.assertEqual(type(model.valid_cast2), dict) + def test_model_update_without_changes(self): model = ModelTest.hydrate( {"id": 1, "username": "joe", "name": "Joe", "admin": True} From d11e557214476c324fa17e734672bcd3da2792af Mon Sep 17 00:00:00 2001 From: Bader Almutairi Date: Wed, 5 Oct 2022 14:48:58 +0300 Subject: [PATCH 041/254] Update test_models.py --- tests/models/test_models.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/models/test_models.py b/tests/models/test_models.py index a38b33179..d97706dda 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -118,12 +118,23 @@ def test_model_can_cast_dict_attributes(self): def test_valid_json_cast(self): model = ModelTest.hydrate({ - "valid_cast1": {"this": "dict", "is": "usable", "as": "json"}, + "payload": {"this": "dict", "is": "usable", "as": "json"}, "valid_cast2": {'this': 'dict', 'is': 'invalid', 'as': 'json'} }) - self.assertEqual(type(model.valid_cast1), dict) - self.assertEqual(type(model.valid_cast2), dict) + self.assertEqual(type(model.payload), dict) + + model = ModelTest.hydrate({ + "payload": {'this': 'dict', 'is': 'invalid', 'as': 'json'} + }) + + self.assertEqual(type(model.payload), dict) + + model = ModelTest.hydrate({ + "payload": '{"this": "dict", "is": "usable", "as": "json"}' + }) + + self.assertEqual(type(model.payload), dict) def test_model_update_without_changes(self): model = ModelTest.hydrate( From 09ea24f7278b7da0f10d51e1d42fb9a1ba9f8205 Mon Sep 17 00:00:00 2001 From: Bader Almutairi Date: Wed, 5 Oct 2022 16:06:34 +0300 Subject: [PATCH 042/254] Update test_models.py --- tests/models/test_models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/models/test_models.py b/tests/models/test_models.py index d97706dda..007072a2a 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -119,7 +119,6 @@ def test_model_can_cast_dict_attributes(self): def test_valid_json_cast(self): model = ModelTest.hydrate({ "payload": {"this": "dict", "is": "usable", "as": "json"}, - "valid_cast2": {'this': 'dict', 'is': 'invalid', 'as': 'json'} }) self.assertEqual(type(model.payload), dict) From 04b93037e51bcc6f6c5c93647dd4d4e2814fa7bc Mon Sep 17 00:00:00 2001 From: Bader Almutairi Date: Wed, 5 Oct 2022 17:54:08 +0300 Subject: [PATCH 043/254] ValueError test --- src/masoniteorm/models/Model.py | 3 +++ tests/models/test_models.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index 2e839ff61..721bb9709 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -62,6 +62,9 @@ def get(self, value): if is_json(value): return json.loads(value) + if isinstance(value, str): + return None + return value def set(self, value): diff --git a/tests/models/test_models.py b/tests/models/test_models.py index 007072a2a..9bc642003 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -135,6 +135,22 @@ def test_valid_json_cast(self): self.assertEqual(type(model.payload), dict) + model = ModelTest.hydrate({ + "payload": '{"valid": "json", "int": 1}' + }) + + self.assertEqual(type(model.payload), dict) + + model = ModelTest.hydrate({ + "payload": "{'this': 'should', 'throw': 'error'}" + }) + + self.assertEqual(model.payload, None) + + with self.assertRaises(ValueError): + model.payload = "{'this': 'should', 'throw': 'error'}" + model.save() + def test_model_update_without_changes(self): model = ModelTest.hydrate( {"id": 1, "username": "joe", "name": "Joe", "admin": True} From 4fd2a25b3ac8971e37569be298df4d3b1d367266 Mon Sep 17 00:00:00 2001 From: Bader Almutairi Date: Fri, 7 Oct 2022 02:49:13 +0300 Subject: [PATCH 044/254] refactor --- src/masoniteorm/helpers/misc.py | 11 ----------- src/masoniteorm/models/Model.py | 9 ++++----- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/masoniteorm/helpers/misc.py b/src/masoniteorm/helpers/misc.py index 91c7ae887..72d71354c 100644 --- a/src/masoniteorm/helpers/misc.py +++ b/src/masoniteorm/helpers/misc.py @@ -1,17 +1,6 @@ """Module for miscellaneous helper methods.""" import warnings -import json - - -def is_json(json_string): - if not isinstance(json_string, str): - return False - try: - json.loads(json_string) - except ValueError: - return False - return True def deprecated(message): diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index 721bb9709..2dc08241e 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -8,7 +8,6 @@ import pendulum -from ..helpers.misc import is_json from ..query import QueryBuilder from ..collection import Collection from ..observers import ObservesEvents @@ -59,11 +58,11 @@ class JsonCast: """Casts a value to JSON""" def get(self, value): - if is_json(value): - return json.loads(value) - if isinstance(value, str): - return None + try: + return json.loads(value) + except ValueError: + return None return value From 565dc3b4d8f0ef71eeaee54dafca29b7179dbe29 Mon Sep 17 00:00:00 2001 From: Joseph Mancuso Date: Thu, 6 Oct 2022 20:46:13 -0400 Subject: [PATCH 045/254] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0d1c6c1c7..2c79ba54d 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version="2.18.5", + version="2.18.6", package_dir={"": "src"}, description="The Official Masonite ORM", long_description=long_description, From 89992b76c5719c7d9d19d36df08ef795e97627b8 Mon Sep 17 00:00:00 2001 From: yubarajshrestha Date: Sat, 8 Oct 2022 15:20:48 +0545 Subject: [PATCH 046/254] Changed raw query to order_by --- src/masoniteorm/query/QueryBuilder.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 52b358440..a1e127d65 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -2277,11 +2277,7 @@ def latest(self, *fields): if not fields: fields = ("created_at",) - table = self.get_table_name() - fields = map(lambda field: f"`{table}`.`{field}`", fields) - sql = " desc, ".join(fields) + " desc" - - return self.order_by_raw(sql) + return self.order_by(column=",".join(fields), direction="DESC") def oldest(self, *fields): """Gets the oldest record. @@ -2293,8 +2289,4 @@ def oldest(self, *fields): if not fields: fields = ("created_at",) - table = self.get_table_name() - fields = map(lambda field: f"`{table}`.`{field}`", fields) - sql = " asc, ".join(fields) + " asc" - - return self.order_by_raw(sql) + return self.order_by(column=",".join(fields), direction="ASC") From 7d0ee4de6df0b36329ec7bc6e0bd7aa243543967 Mon Sep 17 00:00:00 2001 From: yubarajshrestha Date: Mon, 10 Oct 2022 15:18:47 +0545 Subject: [PATCH 047/254] Added Latest/Oldest Test Cases --- .../mssql/builder/test_mssql_query_builder.py | 24 ++++++++++++++++ tests/mysql/builder/test_query_builder.py | 28 +++++++++++++++++++ .../builder/test_postgres_query_builder.py | 28 +++++++++++++++++++ .../builder/test_sqlite_query_builder.py | 28 +++++++++++++++++++ 4 files changed, 108 insertions(+) diff --git a/tests/mssql/builder/test_mssql_query_builder.py b/tests/mssql/builder/test_mssql_query_builder.py index a3b450792..3fbf671f0 100644 --- a/tests/mssql/builder/test_mssql_query_builder.py +++ b/tests/mssql/builder/test_mssql_query_builder.py @@ -411,3 +411,27 @@ def test_truncate_without_foreign_keys(self): builder = self.get_builder(dry=True) sql = builder.truncate(foreign_keys=True) self.assertEqual(sql, "TRUNCATE TABLE [users]") + + def test_latest(self): + builder = self.get_builder() + builder.latest("email") + self.assertEqual( + builder.to_sql(), "SELECT * FROM [users] ORDER BY [email] DESC" + ) + + def test_latest_multiple(self): + builder = self.get_builder() + builder.latest("email", "created_at") + self.assertEqual( + builder.to_sql(), "SELECT * FROM [users] ORDER BY [email] DESC, [created_at] DESC" + ) + + def test_oldest(self): + builder = self.get_builder() + builder.oldest("email") + self.assertEqual(builder.to_sql(), "SELECT * FROM [users] ORDER BY [email] ASC") + + def test_oldest_multiple(self): + builder = self.get_builder() + builder.oldest("email", "created_at") + self.assertEqual(builder.to_sql(), "SELECT * FROM [users] ORDER BY [email] ASC, [created_at] ASC") diff --git a/tests/mysql/builder/test_query_builder.py b/tests/mysql/builder/test_query_builder.py index 8ce5df4e8..cd1887878 100644 --- a/tests/mysql/builder/test_query_builder.py +++ b/tests/mysql/builder/test_query_builder.py @@ -917,3 +917,31 @@ def update_lock(self): builder.truncate() """ return "SELECT * FROM `users` WHERE `users`.`votes` >= '100' FOR UPDATE" + + def test_latest(self): + builder = self.get_builder() + builder.latest('email') + sql = getattr( + self, inspect.currentframe().f_code.co_name.replace("test_", "") + )() + self.assertEqual(builder.to_sql(), sql) + + def test_oldest(self): + builder = self.get_builder() + builder.oldest('email') + sql = getattr( + self, inspect.currentframe().f_code.co_name.replace("test_", "") + )() + self.assertEqual(builder.to_sql(), sql) + + def latest(self): + """ + builder.order_by('email', 'des') + """ + return "SELECT * FROM `users` ORDER BY `email` DESC" + + def oldest(self): + """ + builder.order_by('email', 'asc') + """ + return "SELECT * FROM `users` ORDER BY `email` ASC" \ No newline at end of file diff --git a/tests/postgres/builder/test_postgres_query_builder.py b/tests/postgres/builder/test_postgres_query_builder.py index d905e0d80..46a98ffbb 100644 --- a/tests/postgres/builder/test_postgres_query_builder.py +++ b/tests/postgres/builder/test_postgres_query_builder.py @@ -772,3 +772,31 @@ def shared_lock(self): builder.truncate() """ return """SELECT * FROM "users" WHERE "users"."votes" >= '100' FOR SHARE""" + + def test_latest(self): + builder = self.get_builder() + builder.latest('email') + sql = getattr( + self, inspect.currentframe().f_code.co_name.replace("test_", "") + )() + self.assertEqual(builder.to_sql(), sql) + + def test_oldest(self): + builder = self.get_builder() + builder.oldest('email') + sql = getattr( + self, inspect.currentframe().f_code.co_name.replace("test_", "") + )() + self.assertEqual(builder.to_sql(), sql) + + def oldest(self): + """ + builder.order_by('email', 'asc') + """ + return """SELECT * FROM "users" ORDER BY "email" ASC""" + + def latest(self): + """ + builder.order_by('email', 'des') + """ + return """SELECT * FROM "users" ORDER BY "email" DESC""" \ No newline at end of file diff --git a/tests/sqlite/builder/test_sqlite_query_builder.py b/tests/sqlite/builder/test_sqlite_query_builder.py index 7fa4a80d8..0449e0000 100644 --- a/tests/sqlite/builder/test_sqlite_query_builder.py +++ b/tests/sqlite/builder/test_sqlite_query_builder.py @@ -971,3 +971,31 @@ def truncate_without_foreign_keys(self): 'DELETE FROM "users"', "PRAGMA foreign_keys = ON", ] + + def test_latest(self): + builder = self.get_builder() + builder.latest('email') + sql = getattr( + self, inspect.currentframe().f_code.co_name.replace("test_", "") + )() + self.assertEqual(builder.to_sql(), sql) + + def test_oldest(self): + builder = self.get_builder() + builder.oldest('email') + sql = getattr( + self, inspect.currentframe().f_code.co_name.replace("test_", "") + )() + self.assertEqual(builder.to_sql(), sql) + + def oldest(self): + """ + builder.order_by('email', 'asc') + """ + return """SELECT * FROM "users" ORDER BY "email" ASC""" + + def latest(self): + """ + builder.order_by('email', 'des') + """ + return """SELECT * FROM "users" ORDER BY "email" DESC""" From ba9a5815faf9cfa3b16b8f4abfc9025587603fd6 Mon Sep 17 00:00:00 2001 From: Samuel Girardin Date: Thu, 13 Oct 2022 12:12:07 +0200 Subject: [PATCH 048/254] fix force_delete() method --- src/masoniteorm/scopes/SoftDeleteScope.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/masoniteorm/scopes/SoftDeleteScope.py b/src/masoniteorm/scopes/SoftDeleteScope.py index c7a6abc8a..294f66540 100644 --- a/src/masoniteorm/scopes/SoftDeleteScope.py +++ b/src/masoniteorm/scopes/SoftDeleteScope.py @@ -35,7 +35,7 @@ def _only_trashed(self, model, builder): return builder.where_not_null(self.deleted_at_column) def _force_delete(self, model, builder): - return builder.remove_global_scope(self).set_action("delete") + return builder.remove_global_scope(self).delete(self) def _restore(self, model, builder): return builder.remove_global_scope(self).update({self.deleted_at_column: None}) From 222cc65aa82269a9a4f5ad0e8dff5d356ec52611 Mon Sep 17 00:00:00 2001 From: Samuel Girardin Date: Thu, 13 Oct 2022 12:27:48 +0200 Subject: [PATCH 049/254] add missing separator for increment/decrement methods --- src/masoniteorm/query/grammars/MSSQLGrammar.py | 4 ++-- src/masoniteorm/query/grammars/MySQLGrammar.py | 4 ++-- src/masoniteorm/query/grammars/PostgresGrammar.py | 4 ++-- src/masoniteorm/query/grammars/SQLiteGrammar.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/masoniteorm/query/grammars/MSSQLGrammar.py b/src/masoniteorm/query/grammars/MSSQLGrammar.py index eb71bfc11..4c466d804 100644 --- a/src/masoniteorm/query/grammars/MSSQLGrammar.py +++ b/src/masoniteorm/query/grammars/MSSQLGrammar.py @@ -143,10 +143,10 @@ def offset_string(self): return "OFFSET {offset} ROWS FETCH NEXT {limit} ROWS ONLY" def increment_string(self): - return "{column} = {column} + '{value}'" + return "{column} = {column} + '{value}'{separator}" def decrement_string(self): - return "{column} = {column} - '{value}'" + return "{column} = {column} - '{value}'{separator}" def aggregate_string_with_alias(self): return "{aggregate_function}({column}) AS {alias}" diff --git a/src/masoniteorm/query/grammars/MySQLGrammar.py b/src/masoniteorm/query/grammars/MySQLGrammar.py index 8174227d2..80212992a 100644 --- a/src/masoniteorm/query/grammars/MySQLGrammar.py +++ b/src/masoniteorm/query/grammars/MySQLGrammar.py @@ -139,10 +139,10 @@ def column_value_string(self): return "{column} = {value}{separator}" def increment_string(self): - return "{column} = {column} + '{value}'" + return "{column} = {column} + '{value}'{separator}" def decrement_string(self): - return "{column} = {column} - '{value}'" + return "{column} = {column} - '{value}'{separator}" def create_column_string(self): return "{column} {data_type}{length}{nullable}{default_value}, " diff --git a/src/masoniteorm/query/grammars/PostgresGrammar.py b/src/masoniteorm/query/grammars/PostgresGrammar.py index ea18e22f2..ddcde4603 100644 --- a/src/masoniteorm/query/grammars/PostgresGrammar.py +++ b/src/masoniteorm/query/grammars/PostgresGrammar.py @@ -100,10 +100,10 @@ def column_value_string(self): return "{column} = {value}{separator}" def increment_string(self): - return "{column} = {column} + '{value}'" + return "{column} = {column} + '{value}'{separator}" def decrement_string(self): - return "{column} = {column} - '{value}'" + return "{column} = {column} - '{value}'{separator}" def create_column_string(self): return "{column} {data_type}{length}{nullable}, " diff --git a/src/masoniteorm/query/grammars/SQLiteGrammar.py b/src/masoniteorm/query/grammars/SQLiteGrammar.py index 9e659a1e2..5b905b704 100644 --- a/src/masoniteorm/query/grammars/SQLiteGrammar.py +++ b/src/masoniteorm/query/grammars/SQLiteGrammar.py @@ -97,10 +97,10 @@ def column_value_string(self): return "{column} = {value}{separator}" def increment_string(self): - return "{column} = {column} + '{value}'" + return "{column} = {column} + '{value}'{separator}" def decrement_string(self): - return "{column} = {column} - '{value}'" + return "{column} = {column} - '{value}'{separator}" def column_exists_string(self): return "SELECT column_name FROM information_schema.columns WHERE table_name='{clean_table}' and column_name={value}" From 510221b8219c8099868e48fd3c0b7f95a9df8a96 Mon Sep 17 00:00:00 2001 From: Samuel Girardin Date: Thu, 13 Oct 2022 12:42:20 +0200 Subject: [PATCH 050/254] Fix import to relative package import --- tests/mysql/schema/test_mysql_schema_builder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/mysql/schema/test_mysql_schema_builder.py b/tests/mysql/schema/test_mysql_schema_builder.py index f2019e9b6..33771240e 100644 --- a/tests/mysql/schema/test_mysql_schema_builder.py +++ b/tests/mysql/schema/test_mysql_schema_builder.py @@ -1,12 +1,12 @@ import os import unittest -from masoniteorm import Model -from tests.integrations.config.database import DATABASES +from src.masoniteorm.models import Model from src.masoniteorm.connections import MySQLConnection from src.masoniteorm.schema import Schema from src.masoniteorm.schema.platforms import MySQLPlatform +from tests.integrations.config.database import DATABASES class Discussion(Model): pass From 412a8462f5c4b2daa633d81f4b0791b66fe36ef0 Mon Sep 17 00:00:00 2001 From: Jarriq Rolle Date: Thu, 13 Oct 2022 07:46:17 -0400 Subject: [PATCH 051/254] added delete_quietly method on Model --- src/masoniteorm/models/Model.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index 3bc9af5ec..c794ed202 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -1053,6 +1053,21 @@ def detach(self, relation, related_record): return related.detach(self, related_record) + def delete_quietly(self): + """This method calls the delete method on a model without firing the delete & deleting observer events. + Instead of calling: + + User().delete(...) + + you can use this: + + User.delete_quietly(...) + + Returns: + self + """ + return self.without_events().delete() + def attach_related(self, relation, related_record): related = getattr(self.__class__, relation) From 26856fe596d42552ec86b5d3890faac62236d9c7 Mon Sep 17 00:00:00 2001 From: Jarriq Rolle Date: Thu, 13 Oct 2022 07:53:29 -0400 Subject: [PATCH 052/254] added save_quietly method on Model --- src/masoniteorm/models/Model.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index 3bc9af5ec..e1aa7c99d 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -1053,6 +1053,23 @@ def detach(self, relation, related_record): return related.detach(self, related_record) + def save_quietly(self): + """This method calls the save method on a model without firing the saved & saving observer events. Saved/Saving + are toggled back on once save_quietly has been ran. + + Instead of calling: + + User().save(...) + + you can use this: + + User.save_quietly(...) + + Returns: + self + """ + return self.without_events().save().with_events() + def attach_related(self, relation, related_record): related = getattr(self.__class__, relation) From 529377b69b3115412a5f61752f2dccdcc5a9bc0a Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Thu, 13 Oct 2022 20:03:49 -0400 Subject: [PATCH 053/254] fixed putting events back on --- src/masoniteorm/models/Model.py | 4 +++- src/masoniteorm/observers/ObservesEvents.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index c794ed202..8848ce33a 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -1066,7 +1066,9 @@ def delete_quietly(self): Returns: self """ - return self.without_events().delete() + delete = self.without_events().where(self.get_primary_key(), self.get_primary_key_value()).delete() + self.with_events() + return delete def attach_related(self, relation, related_record): related = getattr(self.__class__, relation) diff --git a/src/masoniteorm/observers/ObservesEvents.py b/src/masoniteorm/observers/ObservesEvents.py index ebc48c2b5..3e2cb0406 100644 --- a/src/masoniteorm/observers/ObservesEvents.py +++ b/src/masoniteorm/observers/ObservesEvents.py @@ -23,5 +23,5 @@ def without_events(cls): @classmethod def with_events(cls): """Sets __has_events__ attribute on model to True.""" - cls.__has_events__ = False + cls.__has_events__ = True return cls From c7643ae5e6d749f4982806ab5b467cf826b47690 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Thu, 13 Oct 2022 20:31:37 -0400 Subject: [PATCH 054/254] fixed save quietly --- src/masoniteorm/models/Model.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index 563fdf6fb..d34f9571b 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -1065,7 +1065,10 @@ def save_quietly(self): User.save_quietly(...) """ - return self.without_events().save().with_events() + self.without_events() + saved = self.save() + self.with_events() + return saved def delete_quietly(self): """This method calls the delete method on a model without firing the delete & deleting observer events. From 3d21236d334b30dd9312401985b4982b560c77b7 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Thu, 13 Oct 2022 20:59:55 -0400 Subject: [PATCH 055/254] fixed force delete --- src/masoniteorm/scopes/SoftDeleteScope.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/masoniteorm/scopes/SoftDeleteScope.py b/src/masoniteorm/scopes/SoftDeleteScope.py index 294f66540..4273dd175 100644 --- a/src/masoniteorm/scopes/SoftDeleteScope.py +++ b/src/masoniteorm/scopes/SoftDeleteScope.py @@ -35,7 +35,7 @@ def _only_trashed(self, model, builder): return builder.where_not_null(self.deleted_at_column) def _force_delete(self, model, builder): - return builder.remove_global_scope(self).delete(self) + return builder.remove_global_scope(self).delete() def _restore(self, model, builder): return builder.remove_global_scope(self).update({self.deleted_at_column: None}) From d70be29d3bef0902e66311243d66de71bdff50b0 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Thu, 13 Oct 2022 21:05:50 -0400 Subject: [PATCH 056/254] fixed force delete scope --- src/masoniteorm/scopes/SoftDeleteScope.py | 4 +++- tests/mysql/scopes/test_soft_delete.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/masoniteorm/scopes/SoftDeleteScope.py b/src/masoniteorm/scopes/SoftDeleteScope.py index 4273dd175..affc45fed 100644 --- a/src/masoniteorm/scopes/SoftDeleteScope.py +++ b/src/masoniteorm/scopes/SoftDeleteScope.py @@ -34,7 +34,9 @@ def _only_trashed(self, model, builder): builder.remove_global_scope("_where_null", action="select") return builder.where_not_null(self.deleted_at_column) - def _force_delete(self, model, builder): + def _force_delete(self, model, builder, query=False): + if query: + return builder.remove_global_scope(self).set_action("delete").to_sql() return builder.remove_global_scope(self).delete() def _restore(self, model, builder): diff --git a/tests/mysql/scopes/test_soft_delete.py b/tests/mysql/scopes/test_soft_delete.py index f405defbe..1615a68c9 100644 --- a/tests/mysql/scopes/test_soft_delete.py +++ b/tests/mysql/scopes/test_soft_delete.py @@ -44,7 +44,7 @@ def test_with_trashed(self): def test_force_delete(self): sql = "DELETE FROM `users`" builder = self.get_builder().set_global_scope(SoftDeleteScope()) - self.assertEqual(sql, builder.force_delete().to_sql()) + self.assertEqual(sql, builder.force_delete(query=True).to_sql()) def test_restore(self): sql = "UPDATE `users` SET `users`.`deleted_at` = 'None'" @@ -54,7 +54,7 @@ def test_restore(self): def test_force_delete_with_wheres(self): sql = "DELETE FROM `user_softs` WHERE `user_softs`.`active` = '1'" builder = self.get_builder().set_global_scope(SoftDeleteScope()) - self.assertEqual(sql, UserSoft.where("active", 1).force_delete().to_sql()) + self.assertEqual(sql, UserSoft.where("active", 1).force_delete(query=True).to_sql()) def test_that_trashed_users_are_not_returned_by_default(self): sql = "SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL" From 99d30b33aa3ea473472a216fa226c9795750409e Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Thu, 13 Oct 2022 21:08:22 -0400 Subject: [PATCH 057/254] linted --- src/masoniteorm/models/Model.py | 200 +++++++++++----------- src/masoniteorm/scopes/SoftDeleteScope.py | 2 +- tests/mysql/scopes/test_soft_delete.py | 4 +- 3 files changed, 107 insertions(+), 99 deletions(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index d34f9571b..e3f432327 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -163,102 +163,104 @@ class Model(TimeStampsMixin, ObservesEvents, metaclass=ModelMeta): """Pass through will pass any method calls to the model directly through to the query builder. Anytime one of these methods are called on the model it will actually be called on the query builder class. """ - __passthrough__ = set(( - "add_select", - "aggregate", - "all", - "avg", - "between", - "bulk_create", - "chunk", - "count", - "decrement", - "delete", - "distinct", - "doesnt_exist", - "doesnt_have", - "exists", - "find_or_404", - "find_or_fail", - "first_or_fail", - "first", - "first_where", - "first_or_create", - "force_update", - "from_", - "from_raw", - "get", - "get_table_schema", - "group_by_raw", - "group_by", - "has", - "having", - "having_raw", - "increment", - "in_random_order", - "join_on", - "join", - "joins", - "last", - "left_join", - "limit", - "lock_for_update", - "make_lock", - "max", - "min", - "new_from_builder", - "new", - "not_between", - "offset", - "on", - "or_where", - "or_where_null", - "order_by_raw", - "order_by", - "paginate", - "right_join", - "select_raw", - "select", - "set_global_scope", - "set_schema", - "shared_lock", - "simple_paginate", - "skip", - "statement", - "sum", - "table_raw", - "take", - "to_qmark", - "to_sql", - "truncate", - "update", - "when", - "where_between", - "where_column", - "where_date", - "or_where_doesnt_have", - "or_has", - "or_where_has", - "or_doesnt_have", - "or_where_not_exists", - "or_where_date", - "where_exists", - "where_from_builder", - "where_has", - "where_in", - "where_like", - "where_not_between", - "where_not_in", - "where_not_like", - "where_not_null", - "where_null", - "where_raw", - "without_global_scopes", - "where", - "where_doesnt_have", - "with_", - "with_count", - )) + __passthrough__ = set( + ( + "add_select", + "aggregate", + "all", + "avg", + "between", + "bulk_create", + "chunk", + "count", + "decrement", + "delete", + "distinct", + "doesnt_exist", + "doesnt_have", + "exists", + "find_or_404", + "find_or_fail", + "first_or_fail", + "first", + "first_where", + "first_or_create", + "force_update", + "from_", + "from_raw", + "get", + "get_table_schema", + "group_by_raw", + "group_by", + "has", + "having", + "having_raw", + "increment", + "in_random_order", + "join_on", + "join", + "joins", + "last", + "left_join", + "limit", + "lock_for_update", + "make_lock", + "max", + "min", + "new_from_builder", + "new", + "not_between", + "offset", + "on", + "or_where", + "or_where_null", + "order_by_raw", + "order_by", + "paginate", + "right_join", + "select_raw", + "select", + "set_global_scope", + "set_schema", + "shared_lock", + "simple_paginate", + "skip", + "statement", + "sum", + "table_raw", + "take", + "to_qmark", + "to_sql", + "truncate", + "update", + "when", + "where_between", + "where_column", + "where_date", + "or_where_doesnt_have", + "or_has", + "or_where_has", + "or_doesnt_have", + "or_where_not_exists", + "or_where_date", + "where_exists", + "where_from_builder", + "where_has", + "where_in", + "where_like", + "where_not_between", + "where_not_in", + "where_not_like", + "where_not_null", + "where_null", + "where_raw", + "without_global_scopes", + "where", + "where_doesnt_have", + "with_", + "with_count", + ) + ) __cast_map__ = {} @@ -1083,7 +1085,11 @@ def delete_quietly(self): Returns: self """ - delete = self.without_events().where(self.get_primary_key(), self.get_primary_key_value()).delete() + delete = ( + self.without_events() + .where(self.get_primary_key(), self.get_primary_key_value()) + .delete() + ) self.with_events() return delete diff --git a/src/masoniteorm/scopes/SoftDeleteScope.py b/src/masoniteorm/scopes/SoftDeleteScope.py index affc45fed..ac2b4788d 100644 --- a/src/masoniteorm/scopes/SoftDeleteScope.py +++ b/src/masoniteorm/scopes/SoftDeleteScope.py @@ -36,7 +36,7 @@ def _only_trashed(self, model, builder): def _force_delete(self, model, builder, query=False): if query: - return builder.remove_global_scope(self).set_action("delete").to_sql() + return builder.remove_global_scope(self).set_action("delete").to_sql() return builder.remove_global_scope(self).delete() def _restore(self, model, builder): diff --git a/tests/mysql/scopes/test_soft_delete.py b/tests/mysql/scopes/test_soft_delete.py index 1615a68c9..b48f829ef 100644 --- a/tests/mysql/scopes/test_soft_delete.py +++ b/tests/mysql/scopes/test_soft_delete.py @@ -54,7 +54,9 @@ def test_restore(self): def test_force_delete_with_wheres(self): sql = "DELETE FROM `user_softs` WHERE `user_softs`.`active` = '1'" builder = self.get_builder().set_global_scope(SoftDeleteScope()) - self.assertEqual(sql, UserSoft.where("active", 1).force_delete(query=True).to_sql()) + self.assertEqual( + sql, UserSoft.where("active", 1).force_delete(query=True).to_sql() + ) def test_that_trashed_users_are_not_returned_by_default(self): sql = "SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL" From 83adbbb61499dfd91d977e2df0722ba871369adf Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Thu, 13 Oct 2022 21:10:25 -0400 Subject: [PATCH 058/254] fixed scope --- src/masoniteorm/scopes/SoftDeleteScope.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/masoniteorm/scopes/SoftDeleteScope.py b/src/masoniteorm/scopes/SoftDeleteScope.py index ac2b4788d..ee79fcf51 100644 --- a/src/masoniteorm/scopes/SoftDeleteScope.py +++ b/src/masoniteorm/scopes/SoftDeleteScope.py @@ -36,7 +36,7 @@ def _only_trashed(self, model, builder): def _force_delete(self, model, builder, query=False): if query: - return builder.remove_global_scope(self).set_action("delete").to_sql() + return builder.remove_global_scope(self).set_action("delete") return builder.remove_global_scope(self).delete() def _restore(self, model, builder): From 226cc20402975b36795b4a883c510fe09a2605d1 Mon Sep 17 00:00:00 2001 From: Kieren Eaton Date: Thu, 20 Oct 2022 08:57:39 +0800 Subject: [PATCH 059/254] Force conversion to str for unhandled types --- src/masoniteorm/models/Model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index e3f432327..67ec544e1 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -72,7 +72,7 @@ def set(self, value): json.loads(value) return value - return json.dumps(value) + return json.dumps(value, default=str) class IntCast: From 8c87f480d0a328d5443a38b6eed97cb3efefe39c Mon Sep 17 00:00:00 2001 From: Maicol Battistini Date: Thu, 27 Oct 2022 11:53:16 +0200 Subject: [PATCH 060/254] fix: Belongs to many - Table can't be without an underscore (MasoniteFramework/orm#803) --- src/masoniteorm/relationships/BelongsToMany.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/masoniteorm/relationships/BelongsToMany.py b/src/masoniteorm/relationships/BelongsToMany.py index 1d55cafc9..897142e22 100644 --- a/src/masoniteorm/relationships/BelongsToMany.py +++ b/src/masoniteorm/relationships/BelongsToMany.py @@ -67,7 +67,7 @@ def apply_query(self, query, owner): self._table = "_".join(pivot_tables) self.foreign_key = self.foreign_key or f"{pivot_table_1}_id" self.local_key = self.local_key or f"{pivot_table_2}_id" - else: + elif self.local_key is None or self.foreign_key is None: pivot_table_1, pivot_table_2 = self._table.split("_", 1) self.foreign_key = self.foreign_key or f"{pivot_table_1}_id" self.local_key = self.local_key or f"{pivot_table_2}_id" @@ -185,7 +185,7 @@ def make_query(self, query, relation, eagers=None, callback=None): self._table = "_".join(pivot_tables) self.foreign_key = self.foreign_key or f"{pivot_table_1}_id" self.local_key = self.local_key or f"{pivot_table_2}_id" - else: + elif self.local_key is None or self.foreign_key is None: pivot_table_1, pivot_table_2 = self._table.split("_", 1) self.foreign_key = self.foreign_key or f"{pivot_table_1}_id" self.local_key = self.local_key or f"{pivot_table_2}_id" @@ -302,7 +302,7 @@ def relate(self, related_record): self._table = "_".join(pivot_tables) self.foreign_key = self.foreign_key or f"{pivot_table_1}_id" self.local_key = self.local_key or f"{pivot_table_2}_id" - else: + elif self.local_key is None or self.foreign_key is None: pivot_table_1, pivot_table_2 = self._table.split("_", 1) self.foreign_key = self.foreign_key or f"{pivot_table_1}_id" self.local_key = self.local_key or f"{pivot_table_2}_id" @@ -368,7 +368,7 @@ def joins(self, builder, clause=None): self._table = "_".join(pivot_tables) self.foreign_key = self.foreign_key or f"{pivot_table_1}_id" self.local_key = self.local_key or f"{pivot_table_2}_id" - else: + elif self.local_key is None or self.foreign_key is None: pivot_table_1, pivot_table_2 = self._table.split("_", 1) self.foreign_key = self.foreign_key or f"{pivot_table_1}_id" self.local_key = self.local_key or f"{pivot_table_2}_id" From c89d0fafdb5639d20cc5dfa6b4da76837cc98677 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Thu, 27 Oct 2022 21:31:58 -0400 Subject: [PATCH 061/254] fixed issue with sqlite not respecting the nullable values of columns --- src/masoniteorm/schema/platforms/SQLitePlatform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/masoniteorm/schema/platforms/SQLitePlatform.py b/src/masoniteorm/schema/platforms/SQLitePlatform.py index d73684fb5..cd8d3e3fb 100644 --- a/src/masoniteorm/schema/platforms/SQLitePlatform.py +++ b/src/masoniteorm/schema/platforms/SQLitePlatform.py @@ -144,7 +144,6 @@ def columnize(self, columns): def compile_alter_sql(self, diff): sql = [] - if diff.removed_indexes or diff.removed_unique_indexes: indexes = diff.removed_indexes indexes += diff.removed_unique_indexes @@ -363,6 +362,7 @@ def get_current_schema(self, connection, table_name, schema=None): column_python_type=Schema._type_hints_map.get(column_type, str), default=default, length=length, + nullable=int(column.get("notnull")) == 0, ) if column.get("pk") == 1: table.set_primary_key(column["name"]) From c54ac48e4f1f9c65f8392cc0c1a5dcee51d175f3 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Mon, 7 Nov 2022 18:22:19 -0500 Subject: [PATCH 062/254] added missing subquery_alias_string --- src/masoniteorm/query/grammars/MSSQLGrammar.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/masoniteorm/query/grammars/MSSQLGrammar.py b/src/masoniteorm/query/grammars/MSSQLGrammar.py index 4c466d804..4bb670b46 100644 --- a/src/masoniteorm/query/grammars/MSSQLGrammar.py +++ b/src/masoniteorm/query/grammars/MSSQLGrammar.py @@ -109,6 +109,9 @@ def aggregate_string(self): def subquery_string(self): return "({query})" + def subquery_alias_string(self): + return "AS {alias}" + def where_group_string(self): return "{keyword} {value}" From da1871b569984491e6f9c6fc7905752d1e42e1d9 Mon Sep 17 00:00:00 2001 From: Benjamin Stout Date: Thu, 17 Nov 2022 16:41:01 -0600 Subject: [PATCH 063/254] Fix fillable and guarded attribute behavior on mass-assignment --- src/masoniteorm/models/Model.py | 100 ++++++++----- src/masoniteorm/models/Model.pyi | 50 ++++++- src/masoniteorm/query/QueryBuilder.py | 172 +++++++++------------- tests/models/test_models.py | 36 ++++- tests/mysql/builder/test_query_builder.py | 13 +- 5 files changed, 220 insertions(+), 151 deletions(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index f10a24f09..d19a3617a 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -1,19 +1,21 @@ +import inspect import json -from datetime import datetime, date as datetimedate, time as datetimetime import logging +from datetime import date as datetimedate +from datetime import datetime +from datetime import time as datetimetime from decimal import Decimal - -from inflection import tableize, underscore -import inspect +from typing import Any, Dict import pendulum +from inflection import tableize, underscore -from ..query import QueryBuilder from ..collection import Collection -from ..observers import ObservesEvents -from ..scopes import TimeStampsMixin from ..config import load_config from ..exceptions import ModelNotFound +from ..observers import ObservesEvents +from ..query import QueryBuilder +from ..scopes import TimeStampsMixin """This is a magic class that will help using models like User.first() instead of having to instatiate a class like User().first() @@ -133,7 +135,7 @@ class Model(TimeStampsMixin, ObservesEvents, metaclass=ModelMeta): """ __fillable__ = ["*"] - __guarded__ = ["*"] + __guarded__ = [] __dry__ = False __table__ = None __connection__ = "default" @@ -160,6 +162,8 @@ class Model(TimeStampsMixin, ObservesEvents, metaclass=ModelMeta): date_created_at = "created_at" date_updated_at = "updated_at" + builder: QueryBuilder + """Pass through will pass any method calls to the model directly through to the query builder. Anytime one of these methods are called on the model it will actually be called on the query builder class. """ @@ -260,7 +264,7 @@ class Model(TimeStampsMixin, ObservesEvents, metaclass=ModelMeta): "with_", "with_count", "latest", - "oldest" + "oldest", ) ) @@ -366,6 +370,8 @@ def boot(self): if class_name.endswith("Mixin"): getattr(self, "boot_" + class_name)(self.get_builder()) + elif '__fillable__' in base_class.__dict__ and '__guarded__' in base_class.__dict__: + raise AttributeError(f'{type(self).__name__} must specify either __fillable__ or __guarded__ properties, but not both.') self._booted = True self.observe_events(self, "booted") @@ -526,45 +532,31 @@ def new_collection(cls, data): return Collection(data) @classmethod - def create(cls, dictionary=None, query=False, cast=False, **kwargs): + def create(cls, dictionary: Dict[str, Any]=None, query: bool=False, cast: bool=False, **kwargs): """Creates new records based off of a dictionary as well as data set on the model such as fillable values. Args: dictionary (dict, optional): [description]. Defaults to {}. query (bool, optional): [description]. Defaults to False. + cast (bool, optional): [description]. Whether or not to cast passed values. Returns: self: A hydrated version of a model """ - - if not dictionary: - dictionary = kwargs - - if cls.__fillable__ != ["*"]: - d = {} - for x in cls.__fillable__: - if x in dictionary: - if cast == True: - d.update({x: cls._set_casted_value(x, dictionary[x])}) - else: - d.update({x: dictionary[x]}) - dictionary = d - - if cls.__guarded__ != ["*"]: - for x in cls.__guarded__: - if x in dictionary: - dictionary.pop(x) - if query: return cls.builder.create( - dictionary, query=True, id_key=cls.__primary_key__ + dictionary, query=True, id_key=cls.__primary_key__, cast=cast, **kwargs ).to_sql() - return cls.builder.create(dictionary, id_key=cls.__primary_key__) + return cls.builder.create(dictionary, id_key=cls.__primary_key__, cast=cast, **kwargs) @classmethod - def _set_casted_value(cls, attribute, value): + def cast_value(cls, attribute: str, value: Any): + """ + Given an attribute name and a value, casts the value using the model's registered caster. + If no registered caster exists, returns the unmodified value. + """ cast_method = cls.__casts__.get(attribute) cast_map = cls.get_cast_map(cls) @@ -577,6 +569,15 @@ def _set_casted_value(cls, attribute, value): if cast_method: return cast_method(value) return value + + @classmethod + def cast_values(cls, dictionary: Dict[str, Any]) -> Dict[str, Any]: + """ + Runs provided dictionary through all model casters and returns the result. + + Does not mutate the passed dictionary. + """ + return {x: cls.cast_value(x, dictionary[x]) for x in dictionary} def fresh(self): return ( @@ -974,7 +975,8 @@ def get_new_date(self, _datetime=None): def get_new_datetime_string(self, _datetime=None): """ - Get the attributes that should be converted to dates. + Given an optional datetime value, constructs and returns a new datetime string. + If no datetime is specified, returns the current time. :rtype: list """ @@ -1104,3 +1106,35 @@ def attach_related(self, relation, related_record): related_record.save() return related.attach_related(self, related_record) + + @classmethod + def filter_fillable(cls, dictionary: Dict[str, Any]) -> Dict[str, Any]: + """ + Filters provided dictionary to only include fields specified in the model's __fillable__ property + + Passed dictionary is not mutated. + """ + if cls.__fillable__ != ["*"]: + dictionary = {x: dictionary[x] for x in cls.__fillable__ if x in dictionary} + return dictionary + + @classmethod + def filter_mass_assignment(cls, dictionary: Dict[str, Any]) -> Dict[str, Any]: + """ + Filters the provided dictionary in preparation for a mass-assignment operation + + Wrapper around filter_fillable() & filter_guarded(). Passed dictionary is not mutated. + """ + return cls.filter_guarded(cls.filter_fillable(dictionary)) + + @classmethod + def filter_guarded(cls, dictionary: Dict[str, Any]) -> Dict[str, Any]: + """ + Filters provided dictionary to exclude fields specified in the model's __guarded__ property + + Passed dictionary is not mutated. + """ + if cls.__guarded__ == ['*']: + # If all fields are guarded, all data should be filtered + return {} + return {f: dictionary[f] for f in dictionary if f not in cls.__guarded__} diff --git a/src/masoniteorm/models/Model.pyi b/src/masoniteorm/models/Model.pyi index d80c2d3bd..771e2fe8c 100644 --- a/src/masoniteorm/models/Model.pyi +++ b/src/masoniteorm/models/Model.pyi @@ -1,4 +1,5 @@ -from typing import Any +from typing import Any, Dict + from typing_extensions import Self from ..query.QueryBuilder import QueryBuilder @@ -53,6 +54,19 @@ class Model: pass def bulk_create(creates: dict, query: bool = False): pass + def cast_value(attribute: str, value: Any): + """ + Given an attribute name and a value, casts the value using the model's registered caster. + If no registered caster exists, returns the unmodified value. + """ + pass + def cast_values(dictionary: Dict[str, Any]) -> Dict[str, Any]: + """ + Runs provided dictionary through all model casters and returns the result. + + Does not mutate the passed dictionary. + """ + pass def chunk(chunk_amount: str | int): pass def count(column: str = None): @@ -65,6 +79,19 @@ class Model: self """ pass + def create(dictionary: Dict[str, Any]=None, query: bool=False, cast: bool=False, **kwargs): + """Creates new records based off of a dictionary as well as data set on the model + such as fillable values. + + Args: + dictionary (dict, optional): [description]. Defaults to {}. + query (bool, optional): [description]. Defaults to False. + cast (bool, optional): [description]. Whether or not to cast passed values. + + Returns: + self: A hydrated version of a model + """ + pass def decrement(column: str, value: int = 1): """Decrements a column's value. @@ -114,6 +141,27 @@ class Model: Bool - True or False """ pass + def filter_fillable(dictionary: Dict[str, Any]) -> Dict[str, Any]: + """ + Filters provided dictionary to only include fields specified in the model's __fillable__ property + + Passed dictionary is not mutated. + """ + pass + def filter_mass_assignment(dictionary: Dict[str, Any]) -> Dict[str, Any]: + """ + Filters the provided dictionary in preparation for a mass-assignment operation + + Wrapper around filter_fillable() & filter_guarded(). Passed dictionary is not mutated. + """ + pass + def filter_guarded(dictionary: Dict[str, Any]) -> Dict[str, Any]: + """ + Filters provided dictionary to exclude fields specified in the model's __guarded__ property + + Passed dictionary is not mutated. + """ + pass def find_or_404(record_id: str | int): """Finds a row by the primary key ID (Requires a model) or raise an 404 exception. diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index a1e127d65..7bb0ee024 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -1,38 +1,29 @@ import inspect from copy import deepcopy +from datetime import date as datetimedate +from datetime import datetime +from datetime import time as datetimetime +from typing import Any, Dict, List, Optional -from ..config import load_config -from ..collection.Collection import Collection -from ..expressions.expressions import ( - JoinClause, - SubGroupExpression, - SubSelectExpression, - SelectExpression, - BetweenExpression, - GroupByExpression, - AggregateExpression, - QueryExpression, - OrderByExpression, - UpdateQueryExpression, - HavingExpression, - FromTable, -) +import pendulum -from ..scopes import BaseScope -from ..schema import Schema +from ..collection.Collection import Collection +from ..config import load_config +from ..exceptions import (HTTP404, ConnectionNotRegistered, ModelNotFound, + MultipleRecordsFound) +from ..expressions.expressions import (AggregateExpression, BetweenExpression, + FromTable, GroupByExpression, + HavingExpression, JoinClause, + OrderByExpression, QueryExpression, + SelectExpression, SubGroupExpression, + SubSelectExpression, + UpdateQueryExpression) +from ..models import Model from ..observers import ObservesEvents -from ..exceptions import ( - ModelNotFound, - HTTP404, - ConnectionNotRegistered, - ModelNotFound, - MultipleRecordsFound, -) from ..pagination import LengthAwarePaginator, SimplePaginator -from .EagerRelation import EagerRelations -from datetime import datetime, date as datetimedate, time as datetimetime -import pendulum from ..schema import Schema +from ..scopes import BaseScope +from .EagerRelation import EagerRelations class QueryBuilder(ObservesEvents): @@ -46,7 +37,7 @@ def __init__( table=None, connection_details=None, connection_driver="default", - model=None, + model: Optional[Model]=None, scopes=None, schema=None, dry=False, @@ -460,22 +451,24 @@ def select_raw(self, query): def get_processor(self): return self.connection_class.get_default_post_processor()() - def bulk_create(self, creates, query=False): - model = None + def bulk_create(self, creates: List[Dict[str, Any]], query: bool=False, cast: bool = False): self.set_action("bulk_create") - - sorted_creates = [] - # sort the dicts by key so the values inserted align - # with the correct column - for unsorted_dict in creates: - sorted_creates.append(dict(sorted(unsorted_dict.items()))) - self._creates = sorted_creates - + model = None + if self._model: model = self._model + self._creates = [] + for unsorted_create in creates: + if model: + unsorted_create = model.filter_mass_assignment(unsorted_create) + if cast: + unsorted_create = model.cast_values(unsorted_create) + # sort the dicts by key so the values inserted align with the correct column + self._creates.append(dict(sorted(unsorted_create.items()))) + if query: - return self + return self.to_sql() if model: model = model.hydrate(self._creates) @@ -492,7 +485,7 @@ def bulk_create(self, creates, query=False): return processed_results - def create(self, creates=None, query=False, id_key="id", **kwargs): + def create(self, creates: Optional[Dict[str, Any]]=None, query: bool=False, id_key: str="id", cast: bool=False, **kwargs): """Specifies a dictionary that should be used to create new values. Arguments: @@ -501,21 +494,22 @@ def create(self, creates=None, query=False, id_key="id", **kwargs): Returns: self """ - self._creates = {} - - if not creates: - creates = kwargs - + self.set_action("insert") model = None + self._creates = creates if creates else kwargs if self._model: model = self._model - - self.set_action("insert") - self._creates.update(creates) + # Update values with related record's + self._creates.update(self._creates_related) + # Filter __fillable/__guarded__ fields + self._creates = model.filter_mass_assignment(self._creates) + # Cast values if necessary + if cast: + self._creates = model.cast_values(self._creates) if query: - return self + return self.to_sql() if model: model = model.hydrate(self._creates) @@ -527,18 +521,6 @@ def create(self, creates=None, query=False, id_key="id", **kwargs): if not self.dry: connection = self.new_connection() - if model: - d = {} - for x in self._creates: - if x in self._creates: - if kwargs.get("cast") == True: - d.update( - {x: self._model._set_casted_value(x, self._creates[x])} - ) - else: - d.update({x: self._creates[x]}) - d.update(self._creates_related) - self._creates = d query_result = connection.query(self.to_qmark(), self._bindings, results=1) if model: @@ -1385,11 +1367,14 @@ def skip(self, *args, **kwargs): """Alias for limit method""" return self.offset(*args, **kwargs) - def update(self, updates: dict, dry=False, force=False): + def update(self, updates: Dict[str, Any], dry: bool=False, force: bool=False, cast: bool=False): """Specifies columns and values to be updated. Arguments: updates {dictionary} -- A dictionary of columns and values to update. + dry {bool, optional}: Do everything except execute the query against the DB + force {bool, optional}: Force an update statement to be executed even if nothing was changed + cast {bool, optional}: Run all values through model's casters Keyword Arguments: dry {bool} -- Whether the query should be executed. (default: {False}) @@ -1403,6 +1388,9 @@ def update(self, updates: dict, dry=False, force=False): if self._model: model = self._model + # Filter __fillable/__guarded__ fields + updates = model.filter_mass_assignment(updates) + if model and model.is_loaded(): self.where(model.get_primary_key(), model.get_primary_key_value()) @@ -1410,25 +1398,29 @@ def update(self, updates: dict, dry=False, force=False): self.observe_events(model, "updating") - # update only attributes with changes if model and not model.__force_update__ and not force: - changes = {} - for attribute, value in updates.items(): + # Filter updates to only those with changes + updates = { + attr: value + for attr, value in updates.items() if ( - model.__original_attributes__.get(attribute, None) != value - or value is None - ): - changes.update({attribute: value}) - updates = changes + value is None + or model.__original_attributes__.get(attr, None) != value + ) + } + + # do not perform update query if no changes + if not updates: + return model if model else self if model and updates: + date_fields = model.get_dates() for key, value in updates.items(): - if key in model.get_dates(): - updates.update({key: model.get_new_datetime_string(value)}) - - # do not perform update query if no changes - if len(updates.keys()) == 0: - return model if model else self + if key in date_fields: + updates[key] = model.get_new_datetime_string(value) + # Cast value if necessary + if cast: + updates[key] = model.cast_value(value) self._updates = (UpdateQueryExpression(updates),) self.set_action("update") @@ -1588,32 +1580,6 @@ def count(self, column=None): else: return self - def cast_value(self, value): - - if isinstance(value, datetime): - return str(pendulum.instance(value)) - elif isinstance(value, datetimedate): - return str(pendulum.datetime(value.year, value.month, value.day)) - elif isinstance(value, datetimetime): - return str(pendulum.parse(f"{value.hour}:{value.minute}:{value.second}")) - - return value - - def cast_dates(self, result): - if isinstance(result, dict): - new_dict = {} - for key, value in result.items(): - new_dict.update({key: self.cast_value(value)}) - - return new_dict - elif isinstance(result, list): - new_list = [] - for res in result: - new_list.append(self.cast_dates(res)) - return new_list - - return result - def max(self, column): """Aggregates a columns values. diff --git a/tests/models/test_models.py b/tests/models/test_models.py index 9bc642003..4a45cae35 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -1,8 +1,10 @@ +import datetime import json import unittest -from src.masoniteorm.models import Model + import pendulum -import datetime + +from src.masoniteorm.models import Model class ModelTest(Model): @@ -15,7 +17,15 @@ class ModelTest(Model): "d": "decimal", } - +class InvalidFillableGuardedModelTest(Model): + __fillable__ = [ + 'due_date', + ] + __guarded__ = [ + 'is_vip', + 'payload', + ] + class ModelTestForced(Model): __table__ = "users" __force_update__ = True @@ -210,3 +220,23 @@ def test_model_using_or_where_and_chaining_wheres(self): sql, """SELECT * FROM `model_tests` WHERE `model_tests`.`name` = 'joe' OR (`model_tests`.`username` = 'Joseph' OR `model_tests`.`age` >= '18'))""", ) + + def test_both_fillable_and_guarded_attributes_raise(self): + # Both fillable and guarded props are populated on this class + with self.assertRaises(AttributeError): + InvalidFillableGuardedModelTest() + # Still shouldn't be allowed to define even if empty + InvalidFillableGuardedModelTest.__fillable__ = [] + with self.assertRaises(AttributeError): + InvalidFillableGuardedModelTest() + # Or wildcard + InvalidFillableGuardedModelTest.__fillable__ = ['*'] + with self.assertRaises(AttributeError): + InvalidFillableGuardedModelTest() + # Empty guarded attr still raises + InvalidFillableGuardedModelTest.__guarded__ = [] + with self.assertRaises(AttributeError): + InvalidFillableGuardedModelTest() + # Removing one of the props allows us to instantiate + delattr(InvalidFillableGuardedModelTest, '__guarded__') + InvalidFillableGuardedModelTest() diff --git a/tests/mysql/builder/test_query_builder.py b/tests/mysql/builder/test_query_builder.py index cd1887878..a280dfd32 100644 --- a/tests/mysql/builder/test_query_builder.py +++ b/tests/mysql/builder/test_query_builder.py @@ -1,14 +1,14 @@ +import datetime import inspect import unittest -from tests.integrations.config.database import DATABASES from src.masoniteorm.models import Model from src.masoniteorm.query import QueryBuilder from src.masoniteorm.query.grammars import MySQLGrammar from src.masoniteorm.relationships import has_many from src.masoniteorm.scopes import SoftDeleteScope +from tests.integrations.config.database import DATABASES from tests.utils import MockConnectionFactory -import datetime class Articles(Model): @@ -549,15 +549,6 @@ def test_update_lock(self): )() self.assertEqual(sql, sql_ref) - def test_cast_values(self): - builder = self.get_builder(dry=True) - result = builder.cast_dates({"created_at": datetime.datetime(2021, 1, 1)}) - self.assertEqual(result, {"created_at": "2021-01-01T00:00:00+00:00"}) - result = builder.cast_dates({"created_at": datetime.date(2021, 1, 1)}) - self.assertEqual(result, {"created_at": "2021-01-01T00:00:00+00:00"}) - result = builder.cast_dates([{"created_at": datetime.date(2021, 1, 1)}]) - self.assertEqual(result, [{"created_at": "2021-01-01T00:00:00+00:00"}]) - class MySQLQueryBuilderTest(BaseTestQueryBuilder, unittest.TestCase): grammar = MySQLGrammar From 7d13164dd429c95fa815d186e7780d472a0cc13a Mon Sep 17 00:00:00 2001 From: Benjamin Stout Date: Thu, 17 Nov 2022 16:41:54 -0600 Subject: [PATCH 064/254] Run formatting --- requirements.txt | 2 +- src/masoniteorm/models/Model.py | 35 ++++++++---- src/masoniteorm/models/Model.pyi | 28 ++++----- src/masoniteorm/query/QueryBuilder.py | 57 +++++++++++++------ tests/models/test_models.py | 44 +++++++------- .../mssql/builder/test_mssql_query_builder.py | 8 ++- .../grammar/test_mssql_insert_grammar.py | 7 ++- .../grammar/test_mssql_select_grammar.py | 13 ++++- tests/mysql/builder/test_query_builder.py | 6 +- .../grammar/test_mysql_insert_grammar.py | 7 ++- .../grammar/test_mysql_select_grammar.py | 12 +++- .../mysql/schema/test_mysql_schema_builder.py | 1 + .../builder/test_postgres_query_builder.py | 6 +- tests/postgres/grammar/test_insert_grammar.py | 7 ++- tests/postgres/grammar/test_select_grammar.py | 12 +++- .../builder/test_sqlite_query_builder.py | 4 +- .../grammar/test_sqlite_insert_grammar.py | 7 ++- 17 files changed, 171 insertions(+), 85 deletions(-) diff --git a/requirements.txt b/requirements.txt index 398264052..dd6a013d2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ flake8==3.7.9 -black==19.3b0 +black==22.10.0 faker pytest pytest-cov diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index d19a3617a..03d51a13e 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -370,8 +370,13 @@ def boot(self): if class_name.endswith("Mixin"): getattr(self, "boot_" + class_name)(self.get_builder()) - elif '__fillable__' in base_class.__dict__ and '__guarded__' in base_class.__dict__: - raise AttributeError(f'{type(self).__name__} must specify either __fillable__ or __guarded__ properties, but not both.') + elif ( + "__fillable__" in base_class.__dict__ + and "__guarded__" in base_class.__dict__ + ): + raise AttributeError( + f"{type(self).__name__} must specify either __fillable__ or __guarded__ properties, but not both." + ) self._booted = True self.observe_events(self, "booted") @@ -532,7 +537,13 @@ def new_collection(cls, data): return Collection(data) @classmethod - def create(cls, dictionary: Dict[str, Any]=None, query: bool=False, cast: bool=False, **kwargs): + def create( + cls, + dictionary: Dict[str, Any] = None, + query: bool = False, + cast: bool = False, + **kwargs, + ): """Creates new records based off of a dictionary as well as data set on the model such as fillable values. @@ -549,7 +560,9 @@ def create(cls, dictionary: Dict[str, Any]=None, query: bool=False, cast: bool=F dictionary, query=True, id_key=cls.__primary_key__, cast=cast, **kwargs ).to_sql() - return cls.builder.create(dictionary, id_key=cls.__primary_key__, cast=cast, **kwargs) + return cls.builder.create( + dictionary, id_key=cls.__primary_key__, cast=cast, **kwargs + ) @classmethod def cast_value(cls, attribute: str, value: Any): @@ -569,12 +582,12 @@ def cast_value(cls, attribute: str, value: Any): if cast_method: return cast_method(value) return value - + @classmethod def cast_values(cls, dictionary: Dict[str, Any]) -> Dict[str, Any]: """ Runs provided dictionary through all model casters and returns the result. - + Does not mutate the passed dictionary. """ return {x: cls.cast_value(x, dictionary[x]) for x in dictionary} @@ -1106,18 +1119,18 @@ def attach_related(self, relation, related_record): related_record.save() return related.attach_related(self, related_record) - + @classmethod def filter_fillable(cls, dictionary: Dict[str, Any]) -> Dict[str, Any]: """ Filters provided dictionary to only include fields specified in the model's __fillable__ property - + Passed dictionary is not mutated. """ if cls.__fillable__ != ["*"]: dictionary = {x: dictionary[x] for x in cls.__fillable__ if x in dictionary} return dictionary - + @classmethod def filter_mass_assignment(cls, dictionary: Dict[str, Any]) -> Dict[str, Any]: """ @@ -1131,10 +1144,10 @@ def filter_mass_assignment(cls, dictionary: Dict[str, Any]) -> Dict[str, Any]: def filter_guarded(cls, dictionary: Dict[str, Any]) -> Dict[str, Any]: """ Filters provided dictionary to exclude fields specified in the model's __guarded__ property - + Passed dictionary is not mutated. """ - if cls.__guarded__ == ['*']: + if cls.__guarded__ == ["*"]: # If all fields are guarded, all data should be filtered return {} return {f: dictionary[f] for f in dictionary if f not in cls.__guarded__} diff --git a/src/masoniteorm/models/Model.pyi b/src/masoniteorm/models/Model.pyi index 771e2fe8c..14efa3f38 100644 --- a/src/masoniteorm/models/Model.pyi +++ b/src/masoniteorm/models/Model.pyi @@ -6,8 +6,7 @@ from ..query.QueryBuilder import QueryBuilder class Model: def add_select(alias: str, callable: Any): - """Specifies a select subquery. - """ + """Specifies a select subquery.""" pass def aggregate(aggregate: str, column: str, alias: str): """Helper function to aggregate. @@ -63,7 +62,7 @@ class Model: def cast_values(dictionary: Dict[str, Any]) -> Dict[str, Any]: """ Runs provided dictionary through all model casters and returns the result. - + Does not mutate the passed dictionary. """ pass @@ -79,7 +78,12 @@ class Model: self """ pass - def create(dictionary: Dict[str, Any]=None, query: bool=False, cast: bool=False, **kwargs): + def create( + dictionary: Dict[str, Any] = None, + query: bool = False, + cast: bool = False, + **kwargs + ): """Creates new records based off of a dictionary as well as data set on the model such as fillable values. @@ -117,8 +121,7 @@ class Model: """ pass def distinct(boolean: bool = True): - """Species that the select query should be a SELECT DISTINCT query. - """ + """Species that the select query should be a SELECT DISTINCT query.""" pass def doesnt_exist() -> bool: """Determines if any rows exist for the current query. @@ -144,7 +147,7 @@ class Model: def filter_fillable(dictionary: Dict[str, Any]) -> Dict[str, Any]: """ Filters provided dictionary to only include fields specified in the model's __fillable__ property - + Passed dictionary is not mutated. """ pass @@ -158,7 +161,7 @@ class Model: def filter_guarded(dictionary: Dict[str, Any]) -> Dict[str, Any]: """ Filters provided dictionary to exclude fields specified in the model's __guarded__ property - + Passed dictionary is not mutated. """ pass @@ -506,8 +509,7 @@ class Model: def simple_paginate(per_page: int, page: int = 1): pass def skip(*args, **kwargs): - """Alias for limit method. - """ + """Alias for limit method.""" pass def statement(query: str, bindings: list = ()): pass @@ -568,8 +570,7 @@ class Model: def when(conditional: bool, callback: callable): pass def where_between(*args, **kwargs): - """Alias for between - """ + """Alias for between""" pass def where_column(column1: str, column2: str): """Specifies where two columns equal eachother. @@ -667,8 +668,7 @@ class Model: """ pass def where_not_between(*args: Any, **kwargs: Any): - """Alias for not_between - """ + """Alias for not_between""" pass def where_not_in(column: str, wheres: list = []): """Specifies where a column does not contain a list of a values. diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 7bb0ee024..24fde28c6 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -9,15 +9,26 @@ from ..collection.Collection import Collection from ..config import load_config -from ..exceptions import (HTTP404, ConnectionNotRegistered, ModelNotFound, - MultipleRecordsFound) -from ..expressions.expressions import (AggregateExpression, BetweenExpression, - FromTable, GroupByExpression, - HavingExpression, JoinClause, - OrderByExpression, QueryExpression, - SelectExpression, SubGroupExpression, - SubSelectExpression, - UpdateQueryExpression) +from ..exceptions import ( + HTTP404, + ConnectionNotRegistered, + ModelNotFound, + MultipleRecordsFound, +) +from ..expressions.expressions import ( + AggregateExpression, + BetweenExpression, + FromTable, + GroupByExpression, + HavingExpression, + JoinClause, + OrderByExpression, + QueryExpression, + SelectExpression, + SubGroupExpression, + SubSelectExpression, + UpdateQueryExpression, +) from ..models import Model from ..observers import ObservesEvents from ..pagination import LengthAwarePaginator, SimplePaginator @@ -37,7 +48,7 @@ def __init__( table=None, connection_details=None, connection_driver="default", - model: Optional[Model]=None, + model: Optional[Model] = None, scopes=None, schema=None, dry=False, @@ -451,10 +462,12 @@ def select_raw(self, query): def get_processor(self): return self.connection_class.get_default_post_processor()() - def bulk_create(self, creates: List[Dict[str, Any]], query: bool=False, cast: bool = False): + def bulk_create( + self, creates: List[Dict[str, Any]], query: bool = False, cast: bool = False + ): self.set_action("bulk_create") model = None - + if self._model: model = self._model @@ -485,7 +498,14 @@ def bulk_create(self, creates: List[Dict[str, Any]], query: bool=False, cast: bo return processed_results - def create(self, creates: Optional[Dict[str, Any]]=None, query: bool=False, id_key: str="id", cast: bool=False, **kwargs): + def create( + self, + creates: Optional[Dict[str, Any]] = None, + query: bool = False, + id_key: str = "id", + cast: bool = False, + **kwargs, + ): """Specifies a dictionary that should be used to create new values. Arguments: @@ -1367,7 +1387,13 @@ def skip(self, *args, **kwargs): """Alias for limit method""" return self.offset(*args, **kwargs) - def update(self, updates: Dict[str, Any], dry: bool=False, force: bool=False, cast: bool=False): + def update( + self, + updates: Dict[str, Any], + dry: bool = False, + force: bool = False, + cast: bool = False, + ): """Specifies columns and values to be updated. Arguments: @@ -1390,7 +1416,6 @@ def update(self, updates: Dict[str, Any], dry: bool=False, force: bool=False, ca model = self._model # Filter __fillable/__guarded__ fields updates = model.filter_mass_assignment(updates) - if model and model.is_loaded(): self.where(model.get_primary_key(), model.get_primary_key_value()) @@ -1408,7 +1433,7 @@ def update(self, updates: Dict[str, Any], dry: bool=False, force: bool=False, ca or model.__original_attributes__.get(attr, None) != value ) } - + # do not perform update query if no changes if not updates: return model if model else self diff --git a/tests/models/test_models.py b/tests/models/test_models.py index 4a45cae35..27d6c9c26 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -17,15 +17,17 @@ class ModelTest(Model): "d": "decimal", } + class InvalidFillableGuardedModelTest(Model): __fillable__ = [ - 'due_date', + "due_date", ] __guarded__ = [ - 'is_vip', - 'payload', + "is_vip", + "payload", ] - + + class ModelTestForced(Model): __table__ = "users" __force_update__ = True @@ -127,33 +129,31 @@ def test_model_can_cast_dict_attributes(self): self.assertEqual(type(model.serialize()["is_vip"]), bool) def test_valid_json_cast(self): - model = ModelTest.hydrate({ - "payload": {"this": "dict", "is": "usable", "as": "json"}, - }) + model = ModelTest.hydrate( + { + "payload": {"this": "dict", "is": "usable", "as": "json"}, + } + ) self.assertEqual(type(model.payload), dict) - model = ModelTest.hydrate({ - "payload": {'this': 'dict', 'is': 'invalid', 'as': 'json'} - }) + model = ModelTest.hydrate( + {"payload": {"this": "dict", "is": "invalid", "as": "json"}} + ) self.assertEqual(type(model.payload), dict) - model = ModelTest.hydrate({ - "payload": '{"this": "dict", "is": "usable", "as": "json"}' - }) + model = ModelTest.hydrate( + {"payload": '{"this": "dict", "is": "usable", "as": "json"}'} + ) self.assertEqual(type(model.payload), dict) - model = ModelTest.hydrate({ - "payload": '{"valid": "json", "int": 1}' - }) + model = ModelTest.hydrate({"payload": '{"valid": "json", "int": 1}'}) self.assertEqual(type(model.payload), dict) - model = ModelTest.hydrate({ - "payload": "{'this': 'should', 'throw': 'error'}" - }) + model = ModelTest.hydrate({"payload": "{'this': 'should', 'throw': 'error'}"}) self.assertEqual(model.payload, None) @@ -220,7 +220,7 @@ def test_model_using_or_where_and_chaining_wheres(self): sql, """SELECT * FROM `model_tests` WHERE `model_tests`.`name` = 'joe' OR (`model_tests`.`username` = 'Joseph' OR `model_tests`.`age` >= '18'))""", ) - + def test_both_fillable_and_guarded_attributes_raise(self): # Both fillable and guarded props are populated on this class with self.assertRaises(AttributeError): @@ -230,7 +230,7 @@ def test_both_fillable_and_guarded_attributes_raise(self): with self.assertRaises(AttributeError): InvalidFillableGuardedModelTest() # Or wildcard - InvalidFillableGuardedModelTest.__fillable__ = ['*'] + InvalidFillableGuardedModelTest.__fillable__ = ["*"] with self.assertRaises(AttributeError): InvalidFillableGuardedModelTest() # Empty guarded attr still raises @@ -238,5 +238,5 @@ def test_both_fillable_and_guarded_attributes_raise(self): with self.assertRaises(AttributeError): InvalidFillableGuardedModelTest() # Removing one of the props allows us to instantiate - delattr(InvalidFillableGuardedModelTest, '__guarded__') + delattr(InvalidFillableGuardedModelTest, "__guarded__") InvalidFillableGuardedModelTest() diff --git a/tests/mssql/builder/test_mssql_query_builder.py b/tests/mssql/builder/test_mssql_query_builder.py index 3fbf671f0..895f28f5e 100644 --- a/tests/mssql/builder/test_mssql_query_builder.py +++ b/tests/mssql/builder/test_mssql_query_builder.py @@ -423,7 +423,8 @@ def test_latest_multiple(self): builder = self.get_builder() builder.latest("email", "created_at") self.assertEqual( - builder.to_sql(), "SELECT * FROM [users] ORDER BY [email] DESC, [created_at] DESC" + builder.to_sql(), + "SELECT * FROM [users] ORDER BY [email] DESC, [created_at] DESC", ) def test_oldest(self): @@ -434,4 +435,7 @@ def test_oldest(self): def test_oldest_multiple(self): builder = self.get_builder() builder.oldest("email", "created_at") - self.assertEqual(builder.to_sql(), "SELECT * FROM [users] ORDER BY [email] ASC, [created_at] ASC") + self.assertEqual( + builder.to_sql(), + "SELECT * FROM [users] ORDER BY [email] ASC, [created_at] ASC", + ) diff --git a/tests/mssql/grammar/test_mssql_insert_grammar.py b/tests/mssql/grammar/test_mssql_insert_grammar.py index 3919f8dc5..dcac9f145 100644 --- a/tests/mssql/grammar/test_mssql_insert_grammar.py +++ b/tests/mssql/grammar/test_mssql_insert_grammar.py @@ -18,7 +18,12 @@ def test_can_compile_insert(self): def test_can_compile_bulk_create(self): to_sql = self.builder.bulk_create( # These keys are intentionally out of order to show column to value alignment works - [{"name": "Joe", "age": 5}, {"age": 35, "name": "Bill"}, {"name": "John", "age": 10}], query=True + [ + {"name": "Joe", "age": 5}, + {"age": 35, "name": "Bill"}, + {"name": "John", "age": 10}, + ], + query=True, ).to_sql() sql = "INSERT INTO [users] ([age], [name]) VALUES ('5', 'Joe'), ('35', 'Bill'), ('10', 'John')" diff --git a/tests/mssql/grammar/test_mssql_select_grammar.py b/tests/mssql/grammar/test_mssql_select_grammar.py index 469e55a08..a78161512 100644 --- a/tests/mssql/grammar/test_mssql_select_grammar.py +++ b/tests/mssql/grammar/test_mssql_select_grammar.py @@ -315,9 +315,16 @@ def test_can_compile_having_raw(self): ) def test_can_compile_having_raw_order(self): - to_sql = self.builder.select_raw("COUNT(*) as counts").having_raw("counts > 10").order_by_raw( - 'counts DESC').to_sql() - self.assertEqual(to_sql, "SELECT COUNT(*) as counts FROM [users] HAVING counts > 10 ORDER BY counts DESC") + to_sql = ( + self.builder.select_raw("COUNT(*) as counts") + .having_raw("counts > 10") + .order_by_raw("counts DESC") + .to_sql() + ) + self.assertEqual( + to_sql, + "SELECT COUNT(*) as counts FROM [users] HAVING counts > 10 ORDER BY counts DESC", + ) def test_can_compile_select_raw(self): to_sql = self.builder.select_raw("COUNT(*)").to_sql() diff --git a/tests/mysql/builder/test_query_builder.py b/tests/mysql/builder/test_query_builder.py index a280dfd32..88a7382b0 100644 --- a/tests/mysql/builder/test_query_builder.py +++ b/tests/mysql/builder/test_query_builder.py @@ -911,7 +911,7 @@ def update_lock(self): def test_latest(self): builder = self.get_builder() - builder.latest('email') + builder.latest("email") sql = getattr( self, inspect.currentframe().f_code.co_name.replace("test_", "") )() @@ -919,7 +919,7 @@ def test_latest(self): def test_oldest(self): builder = self.get_builder() - builder.oldest('email') + builder.oldest("email") sql = getattr( self, inspect.currentframe().f_code.co_name.replace("test_", "") )() @@ -935,4 +935,4 @@ def oldest(self): """ builder.order_by('email', 'asc') """ - return "SELECT * FROM `users` ORDER BY `email` ASC" \ No newline at end of file + return "SELECT * FROM `users` ORDER BY `email` ASC" diff --git a/tests/mysql/grammar/test_mysql_insert_grammar.py b/tests/mysql/grammar/test_mysql_insert_grammar.py index 21996ec35..be9c01b4a 100644 --- a/tests/mysql/grammar/test_mysql_insert_grammar.py +++ b/tests/mysql/grammar/test_mysql_insert_grammar.py @@ -28,7 +28,12 @@ def test_can_compile_insert_with_keywords(self): def test_can_compile_bulk_create(self): to_sql = self.builder.bulk_create( # These keys are intentionally out of order to show column to value alignment works - [{"name": "Joe", "age": 5}, {"age": 35, "name": "Bill"}, {"name": "John", "age": 10}], query=True + [ + {"name": "Joe", "age": 5}, + {"age": 35, "name": "Bill"}, + {"name": "John", "age": 10}, + ], + query=True, ).to_sql() sql = getattr( diff --git a/tests/mysql/grammar/test_mysql_select_grammar.py b/tests/mysql/grammar/test_mysql_select_grammar.py index 7155c3a6f..3f55cd162 100644 --- a/tests/mysql/grammar/test_mysql_select_grammar.py +++ b/tests/mysql/grammar/test_mysql_select_grammar.py @@ -311,8 +311,16 @@ def test_can_compile_having_raw(self): ) def test_can_compile_having_raw_order(self): - to_sql = self.builder.select_raw("COUNT(*) as counts").having_raw("counts > 10").order_by_raw('counts DESC').to_sql() - self.assertEqual(to_sql, "SELECT COUNT(*) as counts FROM `users` HAVING counts > 10 ORDER BY counts DESC") + to_sql = ( + self.builder.select_raw("COUNT(*) as counts") + .having_raw("counts > 10") + .order_by_raw("counts DESC") + .to_sql() + ) + self.assertEqual( + to_sql, + "SELECT COUNT(*) as counts FROM `users` HAVING counts > 10 ORDER BY counts DESC", + ) def test_can_compile_select_raw(self): to_sql = self.builder.select_raw("COUNT(*)").to_sql() diff --git a/tests/mysql/schema/test_mysql_schema_builder.py b/tests/mysql/schema/test_mysql_schema_builder.py index 33771240e..d4f460e69 100644 --- a/tests/mysql/schema/test_mysql_schema_builder.py +++ b/tests/mysql/schema/test_mysql_schema_builder.py @@ -8,6 +8,7 @@ from tests.integrations.config.database import DATABASES + class Discussion(Model): pass diff --git a/tests/postgres/builder/test_postgres_query_builder.py b/tests/postgres/builder/test_postgres_query_builder.py index 46a98ffbb..7571d5e48 100644 --- a/tests/postgres/builder/test_postgres_query_builder.py +++ b/tests/postgres/builder/test_postgres_query_builder.py @@ -775,7 +775,7 @@ def shared_lock(self): def test_latest(self): builder = self.get_builder() - builder.latest('email') + builder.latest("email") sql = getattr( self, inspect.currentframe().f_code.co_name.replace("test_", "") )() @@ -783,7 +783,7 @@ def test_latest(self): def test_oldest(self): builder = self.get_builder() - builder.oldest('email') + builder.oldest("email") sql = getattr( self, inspect.currentframe().f_code.co_name.replace("test_", "") )() @@ -799,4 +799,4 @@ def latest(self): """ builder.order_by('email', 'des') """ - return """SELECT * FROM "users" ORDER BY "email" DESC""" \ No newline at end of file + return """SELECT * FROM "users" ORDER BY "email" DESC""" diff --git a/tests/postgres/grammar/test_insert_grammar.py b/tests/postgres/grammar/test_insert_grammar.py index 43fa784d8..93f6e2e80 100644 --- a/tests/postgres/grammar/test_insert_grammar.py +++ b/tests/postgres/grammar/test_insert_grammar.py @@ -28,7 +28,12 @@ def test_can_compile_insert_with_keywords(self): def test_can_compile_bulk_create(self): to_sql = self.builder.bulk_create( # These keys are intentionally out of order to show column to value alignment works - [{"name": "Joe", "age": 5}, {"age": 35, "name": "Bill"}, {"name": "John", "age": 10}], query=True + [ + {"name": "Joe", "age": 5}, + {"age": 35, "name": "Bill"}, + {"name": "John", "age": 10}, + ], + query=True, ).to_sql() sql = getattr( diff --git a/tests/postgres/grammar/test_select_grammar.py b/tests/postgres/grammar/test_select_grammar.py index 14e300d8c..8d71eb1f8 100644 --- a/tests/postgres/grammar/test_select_grammar.py +++ b/tests/postgres/grammar/test_select_grammar.py @@ -316,8 +316,16 @@ def test_can_compile_having_raw(self): ) def test_can_compile_having_raw_order(self): - to_sql = self.builder.select_raw("COUNT(*) as counts").having_raw("counts > 10").order_by_raw('counts DESC').to_sql() - self.assertEqual(to_sql, """SELECT COUNT(*) as counts FROM "users" HAVING counts > 10 ORDER BY counts DESC""") + to_sql = ( + self.builder.select_raw("COUNT(*) as counts") + .having_raw("counts > 10") + .order_by_raw("counts DESC") + .to_sql() + ) + self.assertEqual( + to_sql, + """SELECT COUNT(*) as counts FROM "users" HAVING counts > 10 ORDER BY counts DESC""", + ) def test_can_compile_where_raw_and_where_with_multiple_bindings(self): query = self.builder.where_raw( diff --git a/tests/sqlite/builder/test_sqlite_query_builder.py b/tests/sqlite/builder/test_sqlite_query_builder.py index 0449e0000..bf3d4c653 100644 --- a/tests/sqlite/builder/test_sqlite_query_builder.py +++ b/tests/sqlite/builder/test_sqlite_query_builder.py @@ -974,7 +974,7 @@ def truncate_without_foreign_keys(self): def test_latest(self): builder = self.get_builder() - builder.latest('email') + builder.latest("email") sql = getattr( self, inspect.currentframe().f_code.co_name.replace("test_", "") )() @@ -982,7 +982,7 @@ def test_latest(self): def test_oldest(self): builder = self.get_builder() - builder.oldest('email') + builder.oldest("email") sql = getattr( self, inspect.currentframe().f_code.co_name.replace("test_", "") )() diff --git a/tests/sqlite/grammar/test_sqlite_insert_grammar.py b/tests/sqlite/grammar/test_sqlite_insert_grammar.py index cd587dd6c..cd2b99fad 100644 --- a/tests/sqlite/grammar/test_sqlite_insert_grammar.py +++ b/tests/sqlite/grammar/test_sqlite_insert_grammar.py @@ -28,7 +28,12 @@ def test_can_compile_insert_with_keywords(self): def test_can_compile_bulk_create(self): to_sql = self.builder.bulk_create( # These keys are intentionally out of order to show column to value alignment works - [{"name": "Joe", "age": 5}, {"age": 35, "name": "Bill"}, {"name": "John", "age": 10}], query=True + [ + {"name": "Joe", "age": 5}, + {"age": 35, "name": "Bill"}, + {"name": "John", "age": 10}, + ], + query=True, ).to_sql() sql = getattr( From 66421e5c30885efc399e8f7cae4883011651f8bb Mon Sep 17 00:00:00 2001 From: Benjamin Stout Date: Thu, 17 Nov 2022 16:49:54 -0600 Subject: [PATCH 065/254] Fix up dependencies --- makefile | 7 +++---- requirements.dev | 7 +++++++ requirements.txt | 8 -------- 3 files changed, 10 insertions(+), 12 deletions(-) create mode 100644 requirements.dev diff --git a/makefile b/makefile index 683335f7c..fc04f0f95 100644 --- a/makefile +++ b/makefile @@ -1,9 +1,8 @@ init: cp .env-example .env - pip install -r requirements.txt - pip install . - # Create MySQL Database - # Create Postgres Database + pip install -r requirements.txt -r requirements.dev +# Create MySQL Database +# Create Postgres Database test: python -m pytest tests ci: diff --git a/requirements.dev b/requirements.dev new file mode 100644 index 000000000..1e73c5a64 --- /dev/null +++ b/requirements.dev @@ -0,0 +1,7 @@ +flake8==3.7.9 +black +faker +pytest +pytest-cov +pymysql +isort \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index dd6a013d2..2e33f1d4f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,5 @@ -flake8==3.7.9 -black==22.10.0 -faker -pytest -pytest-cov -pymysql -isort inflection==0.3.1 psycopg2-binary -python-dotenv==0.14.0 pyodbc pendulum>=2.1,<2.2 cleo>=0.8.0,<0.9 \ No newline at end of file From d9a60d6c90bbb0336e5883f14927025e3db81741 Mon Sep 17 00:00:00 2001 From: Benjamin Stout Date: Thu, 17 Nov 2022 17:12:37 -0600 Subject: [PATCH 066/254] Remove `Model` typehint from `QueryBuilder` due to some pyi import oddities --- src/masoniteorm/query/QueryBuilder.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 24fde28c6..f94899418 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -1,12 +1,8 @@ import inspect from copy import deepcopy -from datetime import date as datetimedate from datetime import datetime -from datetime import time as datetimetime from typing import Any, Dict, List, Optional -import pendulum - from ..collection.Collection import Collection from ..config import load_config from ..exceptions import ( @@ -29,7 +25,6 @@ SubSelectExpression, UpdateQueryExpression, ) -from ..models import Model from ..observers import ObservesEvents from ..pagination import LengthAwarePaginator, SimplePaginator from ..schema import Schema @@ -48,7 +43,7 @@ def __init__( table=None, connection_details=None, connection_driver="default", - model: Optional[Model] = None, + model=None, scopes=None, schema=None, dry=False, From 43064fe996dacc5fddbf5674fc7a06b7ad2302de Mon Sep 17 00:00:00 2001 From: Benjamin Stout Date: Thu, 17 Nov 2022 17:17:39 -0600 Subject: [PATCH 067/254] Restore accidental removal of dotenv --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2e33f1d4f..a2188f679 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ inflection==0.3.1 psycopg2-binary pyodbc pendulum>=2.1,<2.2 -cleo>=0.8.0,<0.9 \ No newline at end of file +cleo>=0.8.0,<0.9 +python-dotenv==0.14.0 \ No newline at end of file From 79b918e221adeba0609c2535754cea1f2bed1fc7 Mon Sep 17 00:00:00 2001 From: Benjamin Stout Date: Thu, 17 Nov 2022 17:24:00 -0600 Subject: [PATCH 068/254] Try a fix for getting attrs on base class only --- src/masoniteorm/models/Model.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index 03d51a13e..ba529ebdb 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -370,13 +370,15 @@ def boot(self): if class_name.endswith("Mixin"): getattr(self, "boot_" + class_name)(self.get_builder()) - elif ( - "__fillable__" in base_class.__dict__ - and "__guarded__" in base_class.__dict__ - ): - raise AttributeError( - f"{type(self).__name__} must specify either __fillable__ or __guarded__ properties, but not both." - ) + else: + base_attrs = base_class.__class__.__dict__ + if ( + "__fillable__" in base_attrs + and "__guarded__" in base_attrs + ): + raise AttributeError( + f"{type(self).__name__} must specify either __fillable__ or __guarded__ properties, but not both." + ) self._booted = True self.observe_events(self, "booted") From 36e9faedd241736618efd5323b55bd30fa55c992 Mon Sep 17 00:00:00 2001 From: Benjamin Stout Date: Thu, 17 Nov 2022 17:30:59 -0600 Subject: [PATCH 069/254] Remove to_sql from QueryBuilder query=True args --- src/masoniteorm/query/QueryBuilder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index f94899418..e8c936cbc 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -476,7 +476,7 @@ def bulk_create( self._creates.append(dict(sorted(unsorted_create.items()))) if query: - return self.to_sql() + return self if model: model = model.hydrate(self._creates) @@ -524,7 +524,7 @@ def create( self._creates = model.cast_values(self._creates) if query: - return self.to_sql() + return self if model: model = model.hydrate(self._creates) From 4d0d2c24b87ec588662dda81a86a9de26fb322e7 Mon Sep 17 00:00:00 2001 From: Benjamin Stout Date: Mon, 21 Nov 2022 09:33:19 -0700 Subject: [PATCH 070/254] Add ability to run tests locally with test DB config file --- .gitignore | 3 ++- config/test-database.py | 35 +++++++++++++++++++++++++++++++++++ makefile | 8 ++++---- pytest.ini | 3 +++ requirements.dev | 1 + src/masoniteorm/config.py | 10 +++++++--- 6 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 config/test-database.py create mode 100644 pytest.ini diff --git a/.gitignore b/.gitignore index f593b05cf..3be9e1344 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ htmlcov/* coverage.xml .coverage *.log -build \ No newline at end of file +build +/orm.sqlite3 \ No newline at end of file diff --git a/config/test-database.py b/config/test-database.py new file mode 100644 index 000000000..a05058cbd --- /dev/null +++ b/config/test-database.py @@ -0,0 +1,35 @@ +from src.masoniteorm.connections import ConnectionResolver + +DATABASES = { + "default": "mysql", + "mysql": { + "host": "127.0.0.1", + "driver": "mysql", + "database": "masonite", + "user": "root", + "password": "", + "port": 3306, + "log_queries": False, + "options": { + # + } + }, + "postgres": { + "host": "127.0.0.1", + "driver": "postgres", + "database": "masonite", + "user": "root", + "password": "", + "port": 5432, + "log_queries": False, + "options": { + # + } + }, + "sqlite": { + "driver": "sqlite", + "database": "masonite.sqlite3", + } +} + +DB = ConnectionResolver().set_connection_details(DATABASES) diff --git a/makefile b/makefile index fc04f0f95..0cd571526 100644 --- a/makefile +++ b/makefile @@ -3,17 +3,17 @@ init: pip install -r requirements.txt -r requirements.dev # Create MySQL Database # Create Postgres Database -test: +test: init python -m pytest tests ci: make test -lint: +lint: format python -m flake8 src/masoniteorm/ --ignore=E501,F401,E203,E128,E402,E731,F821,E712,W503,F811 -format: +format: init black src/masoniteorm black tests/ make lint -sort: +sort: init isort tests isort src/masoniteorm coverage: diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 000000000..e86045349 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +env = + D:TESTING=true \ No newline at end of file diff --git a/requirements.dev b/requirements.dev index 1e73c5a64..15b093441 100644 --- a/requirements.dev +++ b/requirements.dev @@ -3,5 +3,6 @@ black faker pytest pytest-cov +pytest-env pymysql isort \ No newline at end of file diff --git a/src/masoniteorm/config.py b/src/masoniteorm/config.py index 7895dc95f..663a93996 100644 --- a/src/masoniteorm/config.py +++ b/src/masoniteorm/config.py @@ -12,10 +12,14 @@ def load_config(config_path=None): 2. else try to load from default config_path: config/database """ - if not os.getenv("DB_CONFIG_PATH", None): - os.environ["DB_CONFIG_PATH"] = config_path or "config/database" + if env_path := os.getenv("DB_CONFIG_PATH", None): + selected_config_path = env_path + elif os.getenv("TESTING", '').lower() == 'true': + selected_config_path = "config/test-database" + else: + selected_config_path = config_path or "config/database" - selected_config_path = os.environ["DB_CONFIG_PATH"] + os.environ["DB_CONFIG_PATH"] = selected_config_path # format path as python module if needed selected_config_path = ( From f172e68e97499647861a15ef0a8af4626180b054 Mon Sep 17 00:00:00 2001 From: Benjamin Stout Date: Mon, 21 Nov 2022 12:09:36 -0700 Subject: [PATCH 071/254] Fix model booting check --- src/masoniteorm/models/Model.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index ba529ebdb..542fb7758 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -370,15 +370,14 @@ def boot(self): if class_name.endswith("Mixin"): getattr(self, "boot_" + class_name)(self.get_builder()) - else: - base_attrs = base_class.__class__.__dict__ - if ( - "__fillable__" in base_attrs - and "__guarded__" in base_attrs - ): - raise AttributeError( - f"{type(self).__name__} must specify either __fillable__ or __guarded__ properties, but not both." - ) + elif ( + issubclass(base_class, self.__class__) + and "__fillable__" in base_class.__dict__ + and "__guarded__" in base_class.__dict__ + ): + raise AttributeError( + f"{type(self).__name__} must specify either __fillable__ or __guarded__ properties, but not both." + ) self._booted = True self.observe_events(self, "booted") From 961fab2be8c9b29cc1e755432b84b322a50b25a6 Mon Sep 17 00:00:00 2001 From: Benjamin Stout Date: Mon, 21 Nov 2022 12:21:54 -0700 Subject: [PATCH 072/254] Add `make check`, refactor makefile, lint fixes --- .gitignore | 3 ++- makefile | 14 ++++++++++---- src/masoniteorm/config.py | 5 +++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 3be9e1344..e226ea160 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ coverage.xml .coverage *.log build -/orm.sqlite3 \ No newline at end of file +/orm.sqlite3 +/.bootstrapped-pip \ No newline at end of file diff --git a/makefile b/makefile index 0cd571526..2d5e5c242 100644 --- a/makefile +++ b/makefile @@ -1,18 +1,24 @@ -init: - cp .env-example .env +init: .env .bootstrapped-pip + +.bootstrapped-pip: requirements.txt requirements.dev pip install -r requirements.txt -r requirements.dev + touch .bootstrapped-pip + +.env: + cp .env-example .env + # Create MySQL Database # Create Postgres Database test: init python -m pytest tests ci: make test -lint: format +check: format lint +lint: python -m flake8 src/masoniteorm/ --ignore=E501,F401,E203,E128,E402,E731,F821,E712,W503,F811 format: init black src/masoniteorm black tests/ - make lint sort: init isort tests isort src/masoniteorm diff --git a/src/masoniteorm/config.py b/src/masoniteorm/config.py index 663a93996..b4180824f 100644 --- a/src/masoniteorm/config.py +++ b/src/masoniteorm/config.py @@ -11,10 +11,11 @@ def load_config(config_path=None): 1. try to load from DB_CONFIG_PATH environment variable 2. else try to load from default config_path: config/database """ + env_path = os.getenv("DB_CONFIG_PATH", None) - if env_path := os.getenv("DB_CONFIG_PATH", None): + if env_path: selected_config_path = env_path - elif os.getenv("TESTING", '').lower() == 'true': + elif os.getenv("TESTING", "").lower() == "true": selected_config_path = "config/test-database" else: selected_config_path = config_path or "config/database" From 5aa8106f7b352aad284610107d65315aceef087e Mon Sep 17 00:00:00 2001 From: Benjamin Stout Date: Mon, 21 Nov 2022 15:25:18 -0700 Subject: [PATCH 073/254] Fix subclass check to exclude Model instances --- src/masoniteorm/models/Model.py | 3 ++- tests/models/test_models.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index 542fb7758..743c24832 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -371,7 +371,8 @@ def boot(self): if class_name.endswith("Mixin"): getattr(self, "boot_" + class_name)(self.get_builder()) elif ( - issubclass(base_class, self.__class__) + base_class != Model + and issubclass(base_class, Model) and "__fillable__" in base_class.__dict__ and "__guarded__" in base_class.__dict__ ): diff --git a/tests/models/test_models.py b/tests/models/test_models.py index 27d6c9c26..f46c88c9e 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -27,6 +27,15 @@ class InvalidFillableGuardedModelTest(Model): "payload", ] +class InvalidFillableGuardedChildModelTest(ModelTest): + __fillable__ = [ + "due_date", + ] + __guarded__ = [ + "is_vip", + "payload", + ] + class ModelTestForced(Model): __table__ = "users" @@ -225,6 +234,9 @@ def test_both_fillable_and_guarded_attributes_raise(self): # Both fillable and guarded props are populated on this class with self.assertRaises(AttributeError): InvalidFillableGuardedModelTest() + # Child that inherits from an intermediary class also fails + with self.assertRaises(AttributeError): + InvalidFillableGuardedChildModelTest() # Still shouldn't be allowed to define even if empty InvalidFillableGuardedModelTest.__fillable__ = [] with self.assertRaises(AttributeError): From 74744aac8ae4ce17abb24b4714d53d908b713ab2 Mon Sep 17 00:00:00 2001 From: Benjamin Stout Date: Mon, 21 Nov 2022 16:49:54 -0700 Subject: [PATCH 074/254] Add fillable/guarded tests, fix update method a bit --- src/masoniteorm/models/Model.pyi | 12 +- src/masoniteorm/query/QueryBuilder.py | 35 +- tests/models/test_models.py | 8 + tests/mysql/model/test_model.py | 551 ++++++++++++++++---------- 4 files changed, 369 insertions(+), 237 deletions(-) diff --git a/src/masoniteorm/models/Model.pyi b/src/masoniteorm/models/Model.pyi index 14efa3f38..1cb203636 100644 --- a/src/masoniteorm/models/Model.pyi +++ b/src/masoniteorm/models/Model.pyi @@ -552,16 +552,16 @@ class Model: pass def truncate(foreign_keys: bool = False): pass - def update(updates: dict, dry: bool = False, force: bool = False): + def update( + updates: dict, dry: bool = False, force: bool = False, cast: bool = False + ): """Specifies columns and values to be updated. Arguments: updates {dictionary} -- A dictionary of columns and values to update. - dry {bool} -- Whether a query should actually run - force {bool} -- Force the update even if there are no changes - - Keyword Arguments: - dry {bool} -- Whether the query should be executed. (default: {False}) + dry {bool, optional} -- Whether a query should actually run + force {bool, optional} -- Force the update even if there are no changes + cast {bool, optional} -- Run all values through model's casters Returns: self diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index e8c936cbc..2e98c5dcf 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -1397,9 +1397,6 @@ def update( force {bool, optional}: Force an update statement to be executed even if nothing was changed cast {bool, optional}: Run all values through model's casters - Keyword Arguments: - dry {bool} -- Whether the query should be executed. (default: {False}) - Returns: self """ @@ -1418,22 +1415,23 @@ def update( self.observe_events(model, "updating") - if model and not model.__force_update__ and not force: - # Filter updates to only those with changes - updates = { - attr: value - for attr, value in updates.items() - if ( - value is None - or model.__original_attributes__.get(attr, None) != value - ) - } + if model: + if not model.__force_update__ and not force: + # Filter updates to only those with changes + updates = { + attr: value + for attr, value in updates.items() + if ( + value is None + or model.__original_attributes__.get(attr, None) != value + ) + } - # do not perform update query if no changes - if not updates: - return model if model else self + # Do not execute query if no changes + if not updates: + return self if dry or self.dry else model - if model and updates: + # Cast date fields date_fields = model.get_dates() for key, value in updates.items(): if key in date_fields: @@ -1441,6 +1439,9 @@ def update( # Cast value if necessary if cast: updates[key] = model.cast_value(value) + elif not updates: + # Do not perform query if there are no updates + return self self._updates = (UpdateQueryExpression(updates),) self.set_action("update") diff --git a/tests/models/test_models.py b/tests/models/test_models.py index f46c88c9e..a473224cf 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -18,6 +18,13 @@ class ModelTest(Model): } +class FillableModelTest(Model): + __fillable__ = [ + "due_date", + "is_vip", + ] + + class InvalidFillableGuardedModelTest(Model): __fillable__ = [ "due_date", @@ -27,6 +34,7 @@ class InvalidFillableGuardedModelTest(Model): "payload", ] + class InvalidFillableGuardedChildModelTest(ModelTest): __fillable__ = [ "due_date", diff --git a/tests/mysql/model/test_model.py b/tests/mysql/model/test_model.py index 6711f98af..694b545ae 100644 --- a/tests/mysql/model/test_model.py +++ b/tests/mysql/model/test_model.py @@ -9,215 +9,342 @@ from src.masoniteorm.models import Model from tests.User import User -if os.getenv("RUN_MYSQL_DATABASE", False) == "True": - - class ProfileFillable(Model): - __fillable__ = ["name"] - __table__ = "profiles" - __timestamps__ = None - - class ProfileFillTimeStamped(Model): - __fillable__ = ["*"] - __table__ = "profiles" - - class ProfileFillAsterisk(Model): - __fillable__ = ["*"] - __table__ = "profiles" - __timestamps__ = None - - class ProfileGuarded(Model): - __guarded__ = ["email"] - __table__ = "profiles" - __timestamps__ = None - - class ProfileSerialize(Model): - __fillable__ = ["*"] - __table__ = "profiles" - __hidden__ = ["password"] - - class ProfileSerializeWithVisible(Model): - __fillable__ = ["*"] - __table__ = "profiles" - __visible__ = ["name", "email"] - - class ProfileSerializeWithVisibleAndHidden(Model): - __fillable__ = ["*"] - __table__ = "profiles" - __visible__ = ["name", "email"] - __hidden__ = ["password"] - - class Profile(Model): - pass - - class Company(Model): - pass - - class User(Model): - @property - def meta(self): - return {"is_subscribed": True} - - class ProductNames(Model): - pass - - class TestModel(unittest.TestCase): - def test_can_use_fillable(self): - sql = ProfileFillable.create( - {"name": "Joe", "email": "user@example.com"}, query=True - ) - - self.assertEqual( - sql, "INSERT INTO `profiles` (`profiles`.`name`) VALUES ('Joe')" - ) - - def test_can_use_fillable_asterisk(self): - sql = ProfileFillAsterisk.create( - {"name": "Joe", "email": "user@example.com"}, query=True - ) - - self.assertEqual( - sql, - "INSERT INTO `profiles` (`profiles`.`name`, `profiles`.`email`) VALUES ('Joe', 'user@example.com')", - ) - - def test_can_use_guarded(self): - sql = ProfileGuarded.create( - {"name": "Joe", "email": "user@example.com"}, query=True - ) - - self.assertEqual( - sql, "INSERT INTO `profiles` (`profiles`.`name`) VALUES ('Joe')" - ) - - def test_can_use_guarded_asterisk(self): - sql = ProfileFillAsterisk.create( - {"name": "Joe", "email": "user@example.com"}, query=True - ) - - self.assertEqual( - sql, - "INSERT INTO `profiles` (`profiles`.`name`, `profiles`.`email`) VALUES ('Joe', 'user@example.com')", - ) - - def test_can_touch(self): - profile = ProfileFillTimeStamped.hydrate({"name": "Joe", "id": 1}) - - sql = profile.touch("now", query=True) - - self.assertEqual( - sql, - "UPDATE `profiles` SET `profiles`.`updated_at` = 'now' WHERE `profiles`.`id` = '1'", - ) - - def test_table_name(self): - table_name = Profile.get_table_name() - self.assertEqual(table_name, "profiles") - - table_name = Company.get_table_name() - self.assertEqual(table_name, "companies") - - table_name = ProductNames.get_table_name() - self.assertEqual(table_name, "product_names") - def test_returns_correct_data_type(self): - self.assertIsInstance(User.all(), Collection) - # self.assertIsInstance(User.first(), User) - # self.assertIsInstance(User.first(), User) +class ProfileFillable(Model): + __fillable__ = ["name"] + __table__ = "profiles" + __timestamps__ = None + + +class ProfileFillTimeStamped(Model): + __fillable__ = ["*"] + __table__ = "profiles" + + +class ProfileFillAsterisk(Model): + __fillable__ = ["*"] + __table__ = "profiles" + __timestamps__ = None + + +class ProfileGuarded(Model): + __guarded__ = ["email"] + __table__ = "profiles" + __timestamps__ = None + + +class ProfileGuardedAsterisk(Model): + __guarded__ = ["*"] + __table__ = "profiles" + __timestamps__ = None + + +class ProfileSerialize(Model): + __fillable__ = ["*"] + __table__ = "profiles" + __hidden__ = ["password"] + + +class ProfileSerializeWithVisible(Model): + __fillable__ = ["*"] + __table__ = "profiles" + __visible__ = ["name", "email"] + + +class ProfileSerializeWithVisibleAndHidden(Model): + __fillable__ = ["*"] + __table__ = "profiles" + __visible__ = ["name", "email"] + __hidden__ = ["password"] - def test_serialize(self): - profile = ProfileFillAsterisk.hydrate({"name": "Joe", "id": 1}) - - self.assertEqual(profile.serialize(), {"name": "Joe", "id": 1}) - - def test_json(self): - profile = ProfileFillAsterisk.hydrate({"name": "Joe", "id": 1}) - - self.assertEqual(profile.to_json(), '{"name": "Joe", "id": 1}') - - def test_serialize_with_hidden(self): - profile = ProfileSerialize.hydrate( - {"name": "Joe", "id": 1, "password": "secret"} - ) - - self.assertTrue(profile.serialize().get("name")) - self.assertTrue(profile.serialize().get("id")) - self.assertFalse(profile.serialize().get("password")) - - def test_serialize_with_visible(self): - profile = ProfileSerializeWithVisible.hydrate( - { - "name": "Joe", - "id": 1, - "password": "secret", - "email": "joe@masonite.com", - } - ) - self.assertTrue( - {"name": "Joe", "email": "joe@masonite.com"}, profile.serialize() - ) - - def test_serialize_with_visible_and_hidden_raise_error(self): - profile = ProfileSerializeWithVisibleAndHidden.hydrate( - { - "name": "Joe", - "id": 1, - "password": "secret", - "email": "joe@masonite.com", - } - ) - with self.assertRaises(AttributeError): - profile.serialize() - - def test_serialize_with_on_the_fly_appends(self): - user = User.hydrate({"name": "Joe", "id": 1}) - - user.set_appends(["meta"]) - - serialized = user.serialize() - self.assertEqual(serialized["id"], 1) - self.assertEqual(serialized["name"], "Joe") - self.assertEqual(serialized["meta"]["is_subscribed"], True) - - def test_serialize_with_model_appends(self): - User.__appends__ = ["meta"] - user = User.hydrate({"name": "Joe", "id": 1}) - serialized = user.serialize() - self.assertEqual(serialized["id"], 1) - self.assertEqual(serialized["name"], "Joe") - self.assertEqual(serialized["meta"]["is_subscribed"], True) - - def test_serialize_with_date(self): - user = User.hydrate({"name": "Joe", "created_at": pendulum.now()}) - - self.assertTrue(json.dumps(user.serialize())) - - def test_set_as_date(self): - user = User.hydrate( - { - "name": "Joe", - "created_at": pendulum.now().add(days=10).to_datetime_string(), - } - ) - - self.assertTrue(user.created_at) - self.assertTrue(user.created_at.is_future()) - - def test_access_as_date(self): - user = User.hydrate( - { - "name": "Joe", - "created_at": datetime.datetime.now() + datetime.timedelta(days=1), - } - ) - - self.assertTrue(user.created_at) - self.assertTrue(user.created_at.is_future()) - - def test_hydrate_with_none(self): - profile = ProfileFillAsterisk.hydrate(None) - - self.assertEqual(profile, None) +class Profile(Model): + pass + + +class Company(Model): + pass + + +class User(Model): + @property + def meta(self): + return {"is_subscribed": True} + + +class ProductNames(Model): + pass + + +class TestModel(unittest.TestCase): + def test_create_can_use_fillable(self): + sql = ProfileFillable.create( + {"name": "Joe", "email": "user@example.com"}, query=True + ) + + self.assertEqual( + sql, "INSERT INTO `profiles` (`profiles`.`name`) VALUES ('Joe')" + ) + + def test_create_can_use_fillable_asterisk(self): + sql = ProfileFillAsterisk.create( + {"name": "Joe", "email": "user@example.com"}, query=True + ) + + self.assertEqual( + sql, + "INSERT INTO `profiles` (`profiles`.`name`, `profiles`.`email`) VALUES ('Joe', 'user@example.com')", + ) + + def test_create_can_use_guarded(self): + sql = ProfileGuarded.create( + {"name": "Joe", "email": "user@example.com"}, query=True + ) + + self.assertEqual( + sql, "INSERT INTO `profiles` (`profiles`.`name`) VALUES ('Joe')" + ) + + def test_create_can_use_guarded_asterisk(self): + sql = ProfileGuardedAsterisk.create( + {"name": "Joe", "email": "user@example.com"}, query=True + ) + + # An asterisk guarded attribute excludes all fields from mass-assignment. + # This would raise a DB error if there are any required fields. + self.assertEqual( + sql, + "INSERT INTO `profiles` (*) VALUES ()", + ) + + def test_bulk_create_can_use_fillable(self): + query_builder = ProfileFillable.bulk_create( + [ + {"name": "Joe", "email": "user@example.com"}, + {"name": "Joe II", "email": "userII@example.com"}, + ], + query=True, + ) + + self.assertEqual( + query_builder.to_sql(), + "INSERT INTO `profiles` (`name`) VALUES ('Joe'), ('Joe II')", + ) + + def test_bulk_create_can_use_fillable_asterisk(self): + query_builder = ProfileFillAsterisk.bulk_create( + [ + {"name": "Joe", "email": "user@example.com"}, + {"name": "Joe II", "email": "userII@example.com"}, + ], + query=True, + ) + + self.assertEqual( + query_builder.to_sql(), + "INSERT INTO `profiles` (`email`, `name`) VALUES ('user@example.com', 'Joe'), ('userII@example.com', 'Joe II')", + ) + + def test_bulk_create_can_use_guarded(self): + query_builder = ProfileGuarded.bulk_create( + [ + {"name": "Joe", "email": "user@example.com"}, + {"name": "Joe II", "email": "userII@example.com"}, + ], + query=True, + ) + + self.assertEqual( + query_builder.to_sql(), + "INSERT INTO `profiles` (`name`) VALUES ('Joe'), ('Joe II')", + ) + + def test_bulk_create_can_use_guarded_asterisk(self): + query_builder = ProfileGuardedAsterisk.bulk_create( + [ + {"name": "Joe", "email": "user@example.com"}, + {"name": "Joe II", "email": "userII@example.com"}, + ], + query=True, + ) + + # An asterisk guarded attribute excludes all fields from mass-assignment. + # This would obviously raise an invalid SQL syntax error. + # TODO: Raise a clearer error? + self.assertEqual( + query_builder.to_sql(), + "INSERT INTO `profiles` () VALUES (), ()", + ) + + def test_update_can_use_fillable(self): + query_builder = ProfileFillable().update( + {"name": "Joe", "email": "user@example.com"}, dry=True + ) + + self.assertEqual( + query_builder.to_sql(), "UPDATE `profiles` SET `profiles`.`name` = 'Joe'" + ) + + def test_update_can_use_fillable_asterisk(self): + query_builder = ProfileFillAsterisk().update( + {"name": "Joe", "email": "user@example.com"}, dry=True + ) + + self.assertEqual( + query_builder.to_sql(), + "UPDATE `profiles` SET `profiles`.`name` = 'Joe', `profiles`.`email` = 'user@example.com'", + ) + + def test_update_can_use_guarded(self): + query_builder = ProfileGuarded().update( + {"name": "Joe", "email": "user@example.com"}, dry=True + ) + + self.assertEqual( + query_builder.to_sql(), "UPDATE `profiles` SET `profiles`.`name` = 'Joe'" + ) + + def test_update_can_use_guarded_asterisk(self): + profile = ProfileGuardedAsterisk() + initial_sql = profile.get_builder().to_sql() + query_builder = profile.update( + {"name": "Joe", "email": "user@example.com"}, dry=True + ) + + # An asterisk guarded attribute excludes all fields from mass-assignment. + # The query builder's sql should not have been altered in any way. + self.assertEqual( + query_builder.to_sql(), + initial_sql, + ) + + def test_can_touch(self): + profile = ProfileFillTimeStamped.hydrate({"name": "Joe", "id": 1}) + + sql = profile.touch("now", query=True) + + self.assertEqual( + sql, + "UPDATE `profiles` SET `profiles`.`updated_at` = 'now' WHERE `profiles`.`id` = '1'", + ) + + def test_table_name(self): + table_name = Profile.get_table_name() + self.assertEqual(table_name, "profiles") + + table_name = Company.get_table_name() + self.assertEqual(table_name, "companies") + + table_name = ProductNames.get_table_name() + self.assertEqual(table_name, "product_names") + + def test_serialize(self): + profile = ProfileFillAsterisk.hydrate({"name": "Joe", "id": 1}) + + self.assertEqual(profile.serialize(), {"name": "Joe", "id": 1}) + + def test_json(self): + profile = ProfileFillAsterisk.hydrate({"name": "Joe", "id": 1}) + + self.assertEqual(profile.to_json(), '{"name": "Joe", "id": 1}') + + def test_serialize_with_hidden(self): + profile = ProfileSerialize.hydrate( + {"name": "Joe", "id": 1, "password": "secret"} + ) + + self.assertTrue(profile.serialize().get("name")) + self.assertTrue(profile.serialize().get("id")) + self.assertFalse(profile.serialize().get("password")) + + def test_serialize_with_visible(self): + profile = ProfileSerializeWithVisible.hydrate( + { + "name": "Joe", + "id": 1, + "password": "secret", + "email": "joe@masonite.com", + } + ) + self.assertTrue( + {"name": "Joe", "email": "joe@masonite.com"}, profile.serialize() + ) + + def test_serialize_with_visible_and_hidden_raise_error(self): + profile = ProfileSerializeWithVisibleAndHidden.hydrate( + { + "name": "Joe", + "id": 1, + "password": "secret", + "email": "joe@masonite.com", + } + ) + with self.assertRaises(AttributeError): + profile.serialize() + + def test_serialize_with_on_the_fly_appends(self): + user = User.hydrate({"name": "Joe", "id": 1}) + + user.set_appends(["meta"]) + + serialized = user.serialize() + self.assertEqual(serialized["id"], 1) + self.assertEqual(serialized["name"], "Joe") + self.assertEqual(serialized["meta"]["is_subscribed"], True) + + def test_serialize_with_model_appends(self): + User.__appends__ = ["meta"] + user = User.hydrate({"name": "Joe", "id": 1}) + serialized = user.serialize() + self.assertEqual(serialized["id"], 1) + self.assertEqual(serialized["name"], "Joe") + self.assertEqual(serialized["meta"]["is_subscribed"], True) + + def test_serialize_with_date(self): + user = User.hydrate({"name": "Joe", "created_at": pendulum.now()}) + + self.assertTrue(json.dumps(user.serialize())) + + def test_set_as_date(self): + user = User.hydrate( + { + "name": "Joe", + "created_at": pendulum.now().add(days=10).to_datetime_string(), + } + ) + + self.assertTrue(user.created_at) + self.assertTrue(user.created_at.is_future()) + + def test_access_as_date(self): + user = User.hydrate( + { + "name": "Joe", + "created_at": datetime.datetime.now() + datetime.timedelta(days=1), + } + ) + + self.assertTrue(user.created_at) + self.assertTrue(user.created_at.is_future()) + + def test_hydrate_with_none(self): + profile = ProfileFillAsterisk.hydrate(None) + + self.assertEqual(profile, None) + + def test_serialize_with_dirty_attribute(self): + profile = ProfileFillAsterisk.hydrate({"name": "Joe", "id": 1}) + + profile.age = 18 + self.assertEqual(profile.serialize(), {"age": 18, "name": "Joe", "id": 1}) + + def test_attribute_check_with_hasattr(self): + self.assertFalse(hasattr(Profile(), "__password__")) + + +if os.getenv("RUN_MYSQL_DATABASE", None).lower() == "true": + + class MysqlTestModel(unittest.TestCase): def test_can_find_first(self): profile = User.find(1) @@ -225,11 +352,7 @@ def test_find_or_fail_raise_an_exception_if_not_exists(self): with self.assertRaises(ModelNotFound): User.find(100) - def test_serialize_with_dirty_attribute(self): - profile = ProfileFillAsterisk.hydrate({"name": "Joe", "id": 1}) - - profile.age = 18 - self.assertEqual(profile.serialize(), {"age": 18, "name": "Joe", "id": 1}) - - def test_attribute_check_with_hasattr(self): - self.assertFalse(hasattr(Profile(), "__password__")) + def test_returns_correct_data_type(self): + self.assertIsInstance(User.all(), Collection) + # self.assertIsInstance(User.first(), User) + # self.assertIsInstance(User.first(), User) From f465806a0f142d56ff3dd605a7e97a4c2d12160e Mon Sep 17 00:00:00 2001 From: Benjamin Stout Date: Mon, 21 Nov 2022 16:50:37 -0700 Subject: [PATCH 075/254] Combine black formatting commands --- makefile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/makefile b/makefile index 2d5e5c242..4676fea99 100644 --- a/makefile +++ b/makefile @@ -17,8 +17,7 @@ check: format lint lint: python -m flake8 src/masoniteorm/ --ignore=E501,F401,E203,E128,E402,E731,F821,E712,W503,F811 format: init - black src/masoniteorm - black tests/ + black src/masoniteorm tests/ sort: init isort tests isort src/masoniteorm From c3123ac2fd1734e54c272f544b99f217f5c1b064 Mon Sep 17 00:00:00 2001 From: Benjamin Stout Date: Mon, 21 Nov 2022 16:52:10 -0700 Subject: [PATCH 076/254] combine isort commands, add sort to `make check` --- makefile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/makefile b/makefile index 4676fea99..137524336 100644 --- a/makefile +++ b/makefile @@ -13,14 +13,13 @@ test: init python -m pytest tests ci: make test -check: format lint +check: format sort lint lint: python -m flake8 src/masoniteorm/ --ignore=E501,F401,E203,E128,E402,E731,F821,E712,W503,F811 format: init black src/masoniteorm tests/ sort: init - isort tests - isort src/masoniteorm + isort src/masoniteorm tests/ coverage: python -m pytest --cov-report term --cov-report xml --cov=src/masoniteorm tests/ python -m coveralls From b6da63b91d9eddc915419379adf30a10b444dada Mon Sep 17 00:00:00 2001 From: Benjamin Stout Date: Mon, 21 Nov 2022 17:24:40 -0700 Subject: [PATCH 077/254] Move test_can_touch --- tests/mysql/model/test_model.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/tests/mysql/model/test_model.py b/tests/mysql/model/test_model.py index 694b545ae..4adfd010f 100644 --- a/tests/mysql/model/test_model.py +++ b/tests/mysql/model/test_model.py @@ -218,16 +218,6 @@ def test_update_can_use_guarded_asterisk(self): initial_sql, ) - def test_can_touch(self): - profile = ProfileFillTimeStamped.hydrate({"name": "Joe", "id": 1}) - - sql = profile.touch("now", query=True) - - self.assertEqual( - sql, - "UPDATE `profiles` SET `profiles`.`updated_at` = 'now' WHERE `profiles`.`id` = '1'", - ) - def test_table_name(self): table_name = Profile.get_table_name() self.assertEqual(table_name, "profiles") @@ -345,9 +335,20 @@ def test_attribute_check_with_hasattr(self): if os.getenv("RUN_MYSQL_DATABASE", None).lower() == "true": class MysqlTestModel(unittest.TestCase): + # TODO: these tests aren't getting run in CI... is that intentional? def test_can_find_first(self): profile = User.find(1) + def test_can_touch(self): + profile = ProfileFillTimeStamped.hydrate({"name": "Joe", "id": 1}) + + sql = profile.touch("now", query=True) + + self.assertEqual( + sql, + "UPDATE `profiles` SET `profiles`.`updated_at` = 'now' WHERE `profiles`.`id` = '1'", + ) + def test_find_or_fail_raise_an_exception_if_not_exists(self): with self.assertRaises(ModelNotFound): User.find(100) From 39f9b3f04dee70be50297c2f4b2e812a955fe63e Mon Sep 17 00:00:00 2001 From: Benjamin Stout Date: Mon, 2 Jan 2023 09:33:45 -0800 Subject: [PATCH 078/254] Remove TESTING env var --- pytest.ini | 2 +- src/masoniteorm/config.py | 14 +++++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/pytest.ini b/pytest.ini index e86045349..fb1fb3add 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,3 @@ [pytest] env = - D:TESTING=true \ No newline at end of file + D:DB_CONFIG_PATH=config/test-database \ No newline at end of file diff --git a/src/masoniteorm/config.py b/src/masoniteorm/config.py index b4180824f..52ce4eb37 100644 --- a/src/masoniteorm/config.py +++ b/src/masoniteorm/config.py @@ -2,7 +2,8 @@ import pydoc import urllib.parse as urlparse -from .exceptions import ConfigurationNotFound, InvalidUrlConfiguration +from .exceptions import ConfigurationNotFound +from .exceptions import InvalidUrlConfiguration def load_config(config_path=None): @@ -11,14 +12,9 @@ def load_config(config_path=None): 1. try to load from DB_CONFIG_PATH environment variable 2. else try to load from default config_path: config/database """ - env_path = os.getenv("DB_CONFIG_PATH", None) - - if env_path: - selected_config_path = env_path - elif os.getenv("TESTING", "").lower() == "true": - selected_config_path = "config/test-database" - else: - selected_config_path = config_path or "config/database" + selected_config_path = ( + os.getenv("DB_CONFIG_PATH", None) or config_path or "config/database" + ) os.environ["DB_CONFIG_PATH"] = selected_config_path From eab41dd2c0db1460a3a6c99fd732c3731e2c74ae Mon Sep 17 00:00:00 2001 From: Benjamin Stout Date: Mon, 2 Jan 2023 09:34:32 -0800 Subject: [PATCH 079/254] Add support for asdf + direnv --- .envrc | 2 ++ .gitignore | 3 ++- .tool-versions | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .envrc create mode 100644 .tool-versions diff --git a/.envrc b/.envrc new file mode 100644 index 000000000..118c1901d --- /dev/null +++ b/.envrc @@ -0,0 +1,2 @@ +use asdf +layout python diff --git a/.gitignore b/.gitignore index e226ea160..3dee2e72d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ venv +.direnv .python-version .vscode .pytest_* @@ -17,4 +18,4 @@ coverage.xml *.log build /orm.sqlite3 -/.bootstrapped-pip \ No newline at end of file +/.bootstrapped-pip diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 000000000..8b869bd71 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +python 3.8.10 From 6790ec3beaf96dd7fe17b2f41df58fd8ed8834d0 Mon Sep 17 00:00:00 2001 From: Benjamin Stout Date: Mon, 2 Jan 2023 09:49:58 -0800 Subject: [PATCH 080/254] Pin action to use ubuntu-20.04 instead of latest (see: https://github.com/actions/setup-python/issues/544) --- .github/workflows/pythonapp.yml | 4 ++-- .github/workflows/pythonpublish.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index 232f1e0d6..21ddd8d21 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -4,7 +4,7 @@ on: [push, pull_request] jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 services: postgres: @@ -58,7 +58,7 @@ jobs: python orm migrate --connection mysql make test lint: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 name: Lint steps: - uses: actions/checkout@v1 diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml index dc4afe8ef..662e29f05 100644 --- a/.github/workflows/pythonpublish.yml +++ b/.github/workflows/pythonpublish.yml @@ -6,7 +6,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 services: postgres: From 229f71e8ea20d383b67082e15a947f29910e24d5 Mon Sep 17 00:00:00 2001 From: Damien Bezborodow Date: Tue, 31 Jan 2023 18:30:43 +1030 Subject: [PATCH 081/254] Migrations should ignore dotfiles. https://github.com/MasoniteFramework/masonite/issues/743 --- src/masoniteorm/migrations/Migration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/masoniteorm/migrations/Migration.py b/src/masoniteorm/migrations/Migration.py index d7dd237b0..280b7fc09 100644 --- a/src/masoniteorm/migrations/Migration.py +++ b/src/masoniteorm/migrations/Migration.py @@ -60,7 +60,7 @@ def get_unran_migrations(self): all_migrations = [ f.replace(".py", "") for f in listdir(directory_path) - if isfile(join(directory_path, f)) and f != "__init__.py" + if isfile(join(directory_path, f)) and f != "__init__.py" and not f.startswith('.') ] all_migrations.sort() unran_migrations = [] @@ -107,7 +107,7 @@ def get_ran_migrations(self): all_migrations = [ f.replace(".py", "") for f in listdir(directory_path) - if isfile(join(directory_path, f)) and f != "__init__.py" + if isfile(join(directory_path, f)) and f != "__init__.py" and not f.startswith('.') ] all_migrations.sort() ran = [] From 21e653d47e1986a42854fda0464952dbc8066a3c Mon Sep 17 00:00:00 2001 From: Joseph Mancuso Date: Tue, 31 Jan 2023 20:51:39 -0500 Subject: [PATCH 082/254] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2c79ba54d..5b59446c5 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version="2.18.6", + version="2.18.7", package_dir={"": "src"}, description="The Official Masonite ORM", long_description=long_description, From 4560c1cee6586127fc6896368389c2c46b55ede8 Mon Sep 17 00:00:00 2001 From: Joseph Mancuso Date: Tue, 31 Jan 2023 20:53:28 -0500 Subject: [PATCH 083/254] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5b59446c5..d26156d95 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version="2.18.7", + version="2.19.0", package_dir={"": "src"}, description="The Official Masonite ORM", long_description=long_description, From 8197b55d8644abe395a81f90ebfb7ed05006d21d Mon Sep 17 00:00:00 2001 From: Jarriq Rolle Date: Wed, 1 Feb 2023 10:44:08 -0500 Subject: [PATCH 084/254] Retrieves record by primary key. If no record exists matching the given criteria, a user-defined callback function will be executed. --- src/masoniteorm/exceptions.py | 4 ++++ src/masoniteorm/query/QueryBuilder.py | 25 +++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/masoniteorm/exceptions.py b/src/masoniteorm/exceptions.py index e2aed15fb..9d4594db4 100644 --- a/src/masoniteorm/exceptions.py +++ b/src/masoniteorm/exceptions.py @@ -32,3 +32,7 @@ class InvalidUrlConfiguration(Exception): class MultipleRecordsFound(Exception): pass + + +class InvalidArgument(Exception): + pass diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 2e98c5dcf..5e09a2940 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -1,7 +1,7 @@ import inspect from copy import deepcopy from datetime import datetime -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Callable from ..collection.Collection import Collection from ..config import load_config @@ -9,7 +9,7 @@ HTTP404, ConnectionNotRegistered, ModelNotFound, - MultipleRecordsFound, + MultipleRecordsFound, InvalidArgument, ) from ..expressions.expressions import ( AggregateExpression, @@ -1789,6 +1789,27 @@ def find(self, record_id): return self.where(self._model.get_primary_key(), record_id).first() + def find_or(self, record_id: int, callback: Callable): + """Finds a row by the primary key ID (Requires a model) or raise a ModelNotFound exception. + + Arguments: + record_id {int} -- The ID of the primary key to fetch. + callback {Callable} -- The function to call if no record is found. + + Returns: + Model|Callable + """ + + if not callable(callback): + raise InvalidArgument("A callback must be callable.") + + result = self.find(record_id=record_id) + + if not result: + return callback() + + return result + def find_or_fail(self, record_id): """Finds a row by the primary key ID (Requires a model) or raise a ModelNotFound exception. From deea39e2d78ddcd3a5aa02dfd942d251b16cd092 Mon Sep 17 00:00:00 2001 From: Jarriq Rolle Date: Wed, 1 Feb 2023 11:39:00 -0500 Subject: [PATCH 085/254] Retrieves record by primary key. If no record exists matching the given criteria, a user-defined callback function will be executed. --- src/masoniteorm/models/Model.py | 1 + src/masoniteorm/query/QueryBuilder.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index 743c24832..14f7b8298 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -183,6 +183,7 @@ class Model(TimeStampsMixin, ObservesEvents, metaclass=ModelMeta): "doesnt_exist", "doesnt_have", "exists", + "find_or", "find_or_404", "find_or_fail", "first_or_fail", diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 5e09a2940..ef767b614 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -1789,7 +1789,7 @@ def find(self, record_id): return self.where(self._model.get_primary_key(), record_id).first() - def find_or(self, record_id: int, callback: Callable): + def find_or(self, record_id: int, callback: Callable, args: tuple|None = None): """Finds a row by the primary key ID (Requires a model) or raise a ModelNotFound exception. Arguments: @@ -1806,7 +1806,10 @@ def find_or(self, record_id: int, callback: Callable): result = self.find(record_id=record_id) if not result: - return callback() + if not args: + return callback() + else: + return callback(*args) return result From c813103e70ab591769e2a50c53974eff39c8b42f Mon Sep 17 00:00:00 2001 From: Jarriq Rolle Date: Wed, 1 Feb 2023 11:46:02 -0500 Subject: [PATCH 086/254] Fixed linting issue --- src/masoniteorm/query/QueryBuilder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index ef767b614..274e98cca 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -1789,7 +1789,7 @@ def find(self, record_id): return self.where(self._model.get_primary_key(), record_id).first() - def find_or(self, record_id: int, callback: Callable, args: tuple|None = None): + def find_or(self, record_id: int, callback: Callable, args: tuple | None = None): """Finds a row by the primary key ID (Requires a model) or raise a ModelNotFound exception. Arguments: From 7aa546f7280f3378288087480cf0013a876ff88e Mon Sep 17 00:00:00 2001 From: Jarriq Rolle Date: Wed, 1 Feb 2023 13:33:57 -0500 Subject: [PATCH 087/254] Get a single column's value from the first result of a query. --- src/masoniteorm/models/Model.py | 1 + src/masoniteorm/query/QueryBuilder.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index 743c24832..78c8f3d46 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -265,6 +265,7 @@ class Model(TimeStampsMixin, ObservesEvents, metaclass=ModelMeta): "with_count", "latest", "oldest", + "value" ) ) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 2e98c5dcf..7ae5ec889 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -2277,3 +2277,6 @@ def oldest(self, *fields): fields = ("created_at",) return self.order_by(column=",".join(fields), direction="ASC") + + def value(self, column: str): + return self.get().first()[column] From 7b6a269eac8afb5eaab4e6b24e8d70b3a722699d Mon Sep 17 00:00:00 2001 From: Jarriq Rolle Date: Wed, 1 Feb 2023 13:47:30 -0500 Subject: [PATCH 088/254] Get a single column's value from the first result of a query. --- src/masoniteorm/query/QueryBuilder.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 7ae5ec889..1ac3d4abc 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -1749,6 +1749,9 @@ def sole(self, query=False): return result.first() + def sole_value(self, column: str, query=False): + return self.sole()[column] + def first_where(self, column, *args): """Gets the first record with the given key / value pair""" if not args: From 09e91a95ab443ad943dac83916a4fb7c62cdab9e Mon Sep 17 00:00:00 2001 From: Jarriq Rolle <36413952+JarriqTheTechie@users.noreply.github.com> Date: Wed, 1 Feb 2023 13:54:06 -0500 Subject: [PATCH 089/254] Update QueryBuilder.py --- src/masoniteorm/query/QueryBuilder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 274e98cca..4d5cba32d 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -1789,7 +1789,7 @@ def find(self, record_id): return self.where(self._model.get_primary_key(), record_id).first() - def find_or(self, record_id: int, callback: Callable, args: tuple | None = None): + def find_or(self, record_id: int, callback: Callable, args=None): """Finds a row by the primary key ID (Requires a model) or raise a ModelNotFound exception. Arguments: From e261a07c5007af4d6204c022efb225b5e6e97740 Mon Sep 17 00:00:00 2001 From: Benjamin Stout Date: Wed, 15 Feb 2023 11:54:09 -0600 Subject: [PATCH 090/254] Use dynamic model variables for timestamp column names --- src/masoniteorm/scopes/TimeStampsScope.py | 11 +++++---- tests/mysql/model/test_model.py | 3 ++- tests/scopes/test_default_global_scopes.py | 27 ++++++++++++++++++++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/masoniteorm/scopes/TimeStampsScope.py b/src/masoniteorm/scopes/TimeStampsScope.py index d24ed9574..5e4551cc4 100644 --- a/src/masoniteorm/scopes/TimeStampsScope.py +++ b/src/masoniteorm/scopes/TimeStampsScope.py @@ -1,6 +1,5 @@ -from .BaseScope import BaseScope - from ..expressions.expressions import UpdateQueryExpression +from .BaseScope import BaseScope class TimeStampsScope(BaseScope): @@ -27,8 +26,8 @@ def set_timestamp_create(self, builder): builder._creates.update( { - "updated_at": builder._model.get_new_date().to_datetime_string(), - "created_at": builder._model.get_new_date().to_datetime_string(), + builder._model.date_updated_at: builder._model.get_new_date().to_datetime_string(), + builder._model.date_created_at: builder._model.get_new_date().to_datetime_string(), } ) @@ -38,6 +37,8 @@ def set_timestamp_update(self, builder): builder._updates += ( UpdateQueryExpression( - {"updated_at": builder._model.get_new_date().to_datetime_string()} + { + builder._model.date_updated_at: builder._model.get_new_date().to_datetime_string() + } ), ) diff --git a/tests/mysql/model/test_model.py b/tests/mysql/model/test_model.py index 4adfd010f..6bc7623d1 100644 --- a/tests/mysql/model/test_model.py +++ b/tests/mysql/model/test_model.py @@ -2,10 +2,11 @@ import json import os import unittest + import pendulum -from src.masoniteorm.exceptions import ModelNotFound from src.masoniteorm.collection import Collection +from src.masoniteorm.exceptions import ModelNotFound from src.masoniteorm.models import Model from tests.User import User diff --git a/tests/scopes/test_default_global_scopes.py b/tests/scopes/test_default_global_scopes.py index a12b53f0d..f890be6fd 100644 --- a/tests/scopes/test_default_global_scopes.py +++ b/tests/scopes/test_default_global_scopes.py @@ -28,6 +28,12 @@ class UserWithTimeStamps(Model, TimeStampsMixin): __dry__ = True +class UserWithCustomTimeStamps(Model, TimeStampsMixin): + __dry__ = True + date_updated_at = "updated_ts" + date_created_at = "created_ts" + + class UserSoft(Model, SoftDeletesMixin): __dry__ = True @@ -112,3 +118,24 @@ def test_timestamps_can_be_disabled(self): self.scope.set_timestamp_create(self.builder) self.assertNotIn("created_at", self.builder._creates) self.assertNotIn("updated_at", self.builder._creates) + + def test_uses_custom_timestamp_columns_on_create(self): + self.builder = MockBuilder(UserWithCustomTimeStamps) + self.scope.set_timestamp_create(self.builder) + created_column = UserWithCustomTimeStamps.date_created_at + updated_column = UserWithCustomTimeStamps.date_updated_at + self.assertNotIn("created_at", self.builder._creates) + self.assertNotIn("updated_at", self.builder._creates) + self.assertIn(created_column, self.builder._creates) + self.assertIn(updated_column, self.builder._creates) + self.assertIsInstance( + pendulum.parse(self.builder._creates[created_column]), pendulum.DateTime + ) + self.assertIsInstance( + pendulum.parse(self.builder._creates[updated_column]), pendulum.DateTime + ) + + def test_uses_custom_updated_column_on_update(self): + user = UserWithCustomTimeStamps.hydrate({"id": 1}) + sql = user.update({"id": 2}).to_sql() + self.assertTrue(UserWithCustomTimeStamps.date_updated_at in sql) From 03b23208c5ef7537da67c8e923d9f1541777972f Mon Sep 17 00:00:00 2001 From: Kieren Eaton Date: Thu, 16 Feb 2023 11:22:04 +0800 Subject: [PATCH 091/254] Fixed json serialization error if model contains pbjects that are not serializable Example std json non serializable object : Decimal() --- src/masoniteorm/models/Model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index 743c24832..323beeff2 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -683,7 +683,7 @@ def to_json(self): Returns: string """ - return json.dumps(self.serialize()) + return json.dumps(self.serialize(), default=str) @classmethod def first_or_create(cls, wheres, creates: dict = None): From 51260aa87f5733b9219fcf0c04bf97b9c081bc0d Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sat, 18 Feb 2023 20:09:23 -0500 Subject: [PATCH 092/254] Fixed mass assign issue on save --- src/masoniteorm/models/Model.py | 4 ++-- src/masoniteorm/query/QueryBuilder.py | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index 743c24832..c72bb7c55 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -863,7 +863,7 @@ def save(self, query=False): if not query: if self.is_loaded(): - result = builder.update(self.__dirty_attributes__) + result = builder.update(self.__dirty_attributes__, ignore_mass_assignment=True) else: result = self.create( self.__dirty_attributes__, @@ -876,7 +876,7 @@ def save(self, query=False): return result if self.is_loaded(): - result = builder.update(self.__dirty_attributes__, dry=query).to_sql() + result = builder.update(self.__dirty_attributes__, dry=query, ignore_mass_assignment=True).to_sql() else: result = self.create(self.__dirty_attributes__, query=query) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 2e98c5dcf..0454b6442 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -499,6 +499,7 @@ def create( query: bool = False, id_key: str = "id", cast: bool = False, + ignore_mass_assignment: bool = False, **kwargs, ): """Specifies a dictionary that should be used to create new values. @@ -518,7 +519,8 @@ def create( # Update values with related record's self._creates.update(self._creates_related) # Filter __fillable/__guarded__ fields - self._creates = model.filter_mass_assignment(self._creates) + if not ignore_mass_assignment: + self._creates = model.filter_mass_assignment(self._creates) # Cast values if necessary if cast: self._creates = model.cast_values(self._creates) @@ -1388,6 +1390,7 @@ def update( dry: bool = False, force: bool = False, cast: bool = False, + ignore_mass_assignment: bool = False ): """Specifies columns and values to be updated. @@ -1396,6 +1399,7 @@ def update( dry {bool, optional}: Do everything except execute the query against the DB force {bool, optional}: Force an update statement to be executed even if nothing was changed cast {bool, optional}: Run all values through model's casters + ignore_mass_assignment {bool, optional}: Whether the update should ignore mass assignment on the model Returns: self @@ -1407,7 +1411,8 @@ def update( if self._model: model = self._model # Filter __fillable/__guarded__ fields - updates = model.filter_mass_assignment(updates) + if not ignore_mass_assignment: + updates = model.filter_mass_assignment(updates) if model and model.is_loaded(): self.where(model.get_primary_key(), model.get_primary_key_value()) From c4fd7870f4e10df6e87bf11241ff39d6d9269ecb Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sat, 18 Feb 2023 20:12:41 -0500 Subject: [PATCH 093/254] Format --- src/masoniteorm/models/Model.py | 9 +++++++-- src/masoniteorm/query/QueryBuilder.py | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index c72bb7c55..f1155b78d 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -863,12 +863,15 @@ def save(self, query=False): if not query: if self.is_loaded(): - result = builder.update(self.__dirty_attributes__, ignore_mass_assignment=True) + result = builder.update( + self.__dirty_attributes__, ignore_mass_assignment=True + ) else: result = self.create( self.__dirty_attributes__, query=query, id_key=self.get_primary_key(), + ignore_mass_assignment=True, ) self.observe_events(self, "saved") self.fill(result.__attributes__) @@ -876,7 +879,9 @@ def save(self, query=False): return result if self.is_loaded(): - result = builder.update(self.__dirty_attributes__, dry=query, ignore_mass_assignment=True).to_sql() + result = builder.update( + self.__dirty_attributes__, dry=query, ignore_mass_assignment=True + ).to_sql() else: result = self.create(self.__dirty_attributes__, query=query) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 0454b6442..0c496bae7 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -1390,7 +1390,7 @@ def update( dry: bool = False, force: bool = False, cast: bool = False, - ignore_mass_assignment: bool = False + ignore_mass_assignment: bool = False, ): """Specifies columns and values to be updated. From eb18ce6f7bfffcb9a24f043a1eb0a169dcebed43 Mon Sep 17 00:00:00 2001 From: Joseph Mancuso Date: Sat, 18 Feb 2023 20:20:16 -0500 Subject: [PATCH 094/254] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d26156d95..4320978f7 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version="2.19.0", + version="2.19.1", package_dir={"": "src"}, description="The Official Masonite ORM", long_description=long_description, From 013ccca7f0f54aa800e90a00ea96482044f0b1e2 Mon Sep 17 00:00:00 2001 From: Kieren Eaton Date: Sat, 11 Mar 2023 10:15:39 +0800 Subject: [PATCH 095/254] Fixed UUID PK Mixin not creating primary key --- src/masoniteorm/scopes/UUIDPrimaryKeyScope.py | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/masoniteorm/scopes/UUIDPrimaryKeyScope.py b/src/masoniteorm/scopes/UUIDPrimaryKeyScope.py index ef091de78..00f814ad9 100644 --- a/src/masoniteorm/scopes/UUIDPrimaryKeyScope.py +++ b/src/masoniteorm/scopes/UUIDPrimaryKeyScope.py @@ -1,4 +1,5 @@ import uuid + from .BaseScope import BaseScope @@ -9,6 +10,9 @@ def on_boot(self, builder): builder.set_global_scope( "_UUID_primary_key", self.set_uuid_create, action="insert" ) + builder.set_global_scope( + "_UUID_primary_key", self.set_bulk_uuid_create, action="bulk_create" + ) def on_remove(self, builder): pass @@ -22,15 +26,21 @@ def generate_uuid(self, builder, uuid_version, bytes=False): return uuid_func(*args).bytes if bytes else str(uuid_func(*args)) + def build_uuid_pk(self, builder): + uuid_version = getattr(builder._model, "__uuid_version__", 4) + uuid_bytes = getattr(builder._model, "__uuid_bytes__", False) + return { + builder._model.__primary_key__: self.generate_uuid( + builder, uuid_version, uuid_bytes + ) + } + def set_uuid_create(self, builder): # if there is already a primary key, no need to set a new one if builder._model.__primary_key__ not in builder._creates: - uuid_version = getattr(builder._model, "__uuid_version__", 4) - uuid_bytes = getattr(builder._model, "__uuid_bytes__", False) - builder._creates.update( - { - builder._model.__primary_key__: self.generate_uuid( - builder, uuid_version, uuid_bytes - ) - } - ) + builder._creates.update(self.build_uuid_pk(builder)) + + def set_bulk_uuid_create(self, builder): + for idx, create_atts in enumerate(builder._creates): + if builder._model.__primary_key__ not in create_atts: + builder._creates[idx].update(self.build_uuid_pk(builder)) From e0be90038863a033c5af068d0ab6f775b161f4b9 Mon Sep 17 00:00:00 2001 From: sfinktah Date: Thu, 7 Sep 2023 23:19:41 +1000 Subject: [PATCH 096/254] fixed class model has no attribute id --- src/masoniteorm/models/Model.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index 822af6ecc..f231b2bba 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -562,8 +562,10 @@ def create( dictionary, query=True, id_key=cls.__primary_key__, cast=cast, **kwargs ).to_sql() + # when 'id_key' is already present in kwargs, the previous version raised + kwargs['id_key'] = cls.__primary_key__ return cls.builder.create( - dictionary, id_key=cls.__primary_key__, cast=cast, **kwargs + dictionary, cast=cast, **kwargs ) @classmethod @@ -711,9 +713,19 @@ def update_or_create(cls, wheres, updates): total.update(updates) total.update(wheres) if not record: - return self.create(total, id_key=cls.get_primary_key()) - - return self.where(wheres).update(total) + # if we don't return fresh, we don't get the primary_key that has been used, + # and we can't call it from outside the function lest we get a QueryBuilder. + # + # Without this we are reduced to performing a DIY update_or_create, e.g.: + # ebay_order = EbayOrder.where({'order_id': d['order_id']}).first() + # if not ebay_order: + # ebay_order = EbayOrder.create(d).fresh() + # else: + # ebay_order.save() + return self.create(total, id_key=cls.get_primary_key()).fresh() + + rv = self.where(wheres).update(total) + return self.where(wheres).first() def relations_to_dict(self): """Converts a models relationships to a dictionary From 2449f1960b8fc5a96a6086b2a9fa6efd5b9cd7af Mon Sep 17 00:00:00 2001 From: Felipe Hertzer Date: Tue, 12 Sep 2023 23:59:03 +1000 Subject: [PATCH 097/254] Close Cursor and Close DB when Commit MySQLConnection.py --- src/masoniteorm/connections/MySQLConnection.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/masoniteorm/connections/MySQLConnection.py b/src/masoniteorm/connections/MySQLConnection.py index 762e85e01..386145db6 100644 --- a/src/masoniteorm/connections/MySQLConnection.py +++ b/src/masoniteorm/connections/MySQLConnection.py @@ -105,6 +105,9 @@ def commit(self): """Transaction""" self._connection.commit() self.transaction_level -= 1 + if self.get_transaction_level() <= 0: + self.open = 0 + self._connection.close() def dry(self): """Transaction""" @@ -169,6 +172,7 @@ def query(self, query, bindings=(), results="*"): except Exception as e: raise QueryException(str(e)) from e finally: + self._cursor.close() if self.get_transaction_level() <= 0: self.open = 0 self._connection.close() From 9fa48b90a5135d6c9403972c663ee8e65ba5715c Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 24 Sep 2023 20:08:56 -0400 Subject: [PATCH 098/254] fixed duplicate id issue --- src/masoniteorm/models/Model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index 822af6ecc..bf34e0660 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -563,7 +563,7 @@ def create( ).to_sql() return cls.builder.create( - dictionary, id_key=cls.__primary_key__, cast=cast, **kwargs + dictionary, cast=cast, **kwargs ) @classmethod From 3e08b0765ee8294822637b56eacb3f9e68ef6ca9 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 24 Sep 2023 20:15:41 -0400 Subject: [PATCH 099/254] formatted --- src/masoniteorm/models/Model.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index bf34e0660..deff4ff8f 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -562,9 +562,7 @@ def create( dictionary, query=True, id_key=cls.__primary_key__, cast=cast, **kwargs ).to_sql() - return cls.builder.create( - dictionary, cast=cast, **kwargs - ) + return cls.builder.create(dictionary, cast=cast, **kwargs) @classmethod def cast_value(cls, attribute: str, value: Any): @@ -660,7 +658,6 @@ def serialize(self, exclude=None, include=None): remove_keys = [] for key, value in serialized_dictionary.items(): - if key in self.__hidden__: remove_keys.append(key) if hasattr(value, "serialize"): @@ -1020,7 +1017,6 @@ def set_appends(self, appends): return self def save_many(self, relation, relating_records): - if isinstance(relating_records, Model): raise ValueError( "Saving many records requires an iterable like a collection or a list of models and not a Model object. To attach a model, use the 'attach' method." @@ -1036,7 +1032,6 @@ def save_many(self, relation, relating_records): related.attach_related(self, related_record) def detach_many(self, relation, relating_records): - if isinstance(relating_records, Model): raise ValueError( "Detaching many records requires an iterable like a collection or a list of models and not a Model object. To detach a model, use the 'detach' method." From b2e6bcb6690168f18710089dcca71c7e1204ba99 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 24 Sep 2023 20:20:30 -0400 Subject: [PATCH 100/254] fixed id key again --- src/masoniteorm/models/Model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index deff4ff8f..4e397ee4c 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -559,7 +559,7 @@ def create( """ if query: return cls.builder.create( - dictionary, query=True, id_key=cls.__primary_key__, cast=cast, **kwargs + dictionary, query=True, cast=cast, **kwargs ).to_sql() return cls.builder.create(dictionary, cast=cast, **kwargs) From a1121ced6aada5002dbf5bc3e2a143319fd1877b Mon Sep 17 00:00:00 2001 From: Joseph Mancuso Date: Sun, 24 Sep 2023 20:22:32 -0400 Subject: [PATCH 101/254] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4320978f7..287a3b137 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version="2.19.1", + version="2.19.2", package_dir={"": "src"}, description="The Official Masonite ORM", long_description=long_description, From 0e0ead232a73e142deaa3a858733fd51066dea11 Mon Sep 17 00:00:00 2001 From: Marcio Antunes Date: Sat, 30 Sep 2023 07:54:54 -0300 Subject: [PATCH 102/254] Fix TypeError: 'NoneType' object is not iterable Fix TypeError: 'NoneType' object is not iterable Traceback (most recent call last): File ".../masoniteorm/connections/MSSQLConnection.py", line 156, in query return dict(zip(columnNames, result)) ^^^^^^^^^^^^^^^^^^^^^^^^ TypeError: 'NoneType' object is not iterable --- src/masoniteorm/connections/MSSQLConnection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/masoniteorm/connections/MSSQLConnection.py b/src/masoniteorm/connections/MSSQLConnection.py index f81aecc1c..a5fb7e4bc 100644 --- a/src/masoniteorm/connections/MSSQLConnection.py +++ b/src/masoniteorm/connections/MSSQLConnection.py @@ -152,7 +152,7 @@ def query(self, query, bindings=(), results="*"): return {} columnNames = [column[0] for column in cursor.description] result = cursor.fetchone() - return dict(zip(columnNames, result)) + return dict(zip(columnNames, result)) if result != None else {} else: if not cursor.description: return {} From 0b0ca187947df11397741e0c013e9a238147f012 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sat, 30 Sep 2023 15:42:20 -0400 Subject: [PATCH 103/254] linted --- src/masoniteorm/collection/Collection.py | 2 -- src/masoniteorm/commands/MigrateRefreshCommand.py | 1 - src/masoniteorm/commands/SeedRunCommand.py | 1 - src/masoniteorm/connections/BaseConnection.py | 1 - src/masoniteorm/connections/ConnectionResolver.py | 1 - src/masoniteorm/connections/MSSQLConnection.py | 3 +-- src/masoniteorm/connections/PostgresConnection.py | 1 - src/masoniteorm/factories/Factory.py | 1 - src/masoniteorm/migrations/Migration.py | 11 ++++++----- src/masoniteorm/models/MigrationModel.py | 1 - src/masoniteorm/models/Model.py | 2 +- src/masoniteorm/query/QueryBuilder.py | 3 --- src/masoniteorm/query/grammars/BaseGrammar.py | 3 --- src/masoniteorm/schema/Schema.py | 1 - src/masoniteorm/schema/platforms/MSSQLPlatform.py | 2 -- src/masoniteorm/schema/platforms/Platform.py | 1 - src/masoniteorm/schema/platforms/SQLitePlatform.py | 1 - src/masoniteorm/testing/BaseTestCaseSelectGrammar.py | 1 - tests/connections/test_base_connections.py | 2 -- tests/eagers/test_eager.py | 3 --- tests/mssql/builder/test_mssql_query_builder.py | 1 - .../builder/test_mssql_query_builder_relationships.py | 1 - tests/mssql/grammar/test_mssql_insert_grammar.py | 1 - tests/mssql/grammar/test_mssql_select_grammar.py | 1 - tests/mssql/grammar/test_mssql_update_grammar.py | 1 - tests/mssql/schema/test_mssql_schema_builder_alter.py | 1 - tests/mysql/builder/test_mysql_builder_transaction.py | 1 - .../connections/test_mysql_connection_selects.py | 1 - tests/mysql/grammar/test_mysql_delete_grammar.py | 1 - tests/mysql/grammar/test_mysql_insert_grammar.py | 1 - tests/mysql/grammar/test_mysql_qmark.py | 1 - tests/mysql/grammar/test_mysql_select_grammar.py | 1 - tests/mysql/grammar/test_mysql_update_grammar.py | 1 - tests/mysql/model/test_accessors_and_mutators.py | 2 -- tests/mysql/schema/test_mysql_schema_builder_alter.py | 1 - tests/mysql/scopes/test_can_use_global_scopes.py | 1 - tests/postgres/builder/test_postgres_query_builder.py | 2 -- tests/postgres/builder/test_postgres_transaction.py | 1 - tests/postgres/grammar/test_delete_grammar.py | 1 - tests/postgres/grammar/test_insert_grammar.py | 1 - tests/postgres/grammar/test_select_grammar.py | 1 - tests/postgres/grammar/test_update_grammar.py | 1 - .../relationships/test_postgres_relationships.py | 1 - .../schema/test_postgres_schema_builder_alter.py | 1 - tests/sqlite/builder/test_sqlite_builder_insert.py | 1 - .../sqlite/builder/test_sqlite_builder_pagination.py | 1 - tests/sqlite/builder/test_sqlite_query_builder.py | 3 --- .../test_sqlite_query_builder_eager_loading.py | 1 - .../test_sqlite_query_builder_relationships.py | 1 - tests/sqlite/builder/test_sqlite_transaction.py | 1 - tests/sqlite/grammar/test_sqlite_delete_grammar.py | 1 - tests/sqlite/grammar/test_sqlite_insert_grammar.py | 1 - tests/sqlite/grammar/test_sqlite_select_grammar.py | 1 - tests/sqlite/grammar/test_sqlite_update_grammar.py | 1 - tests/sqlite/models/test_observers.py | 1 - tests/sqlite/models/test_sqlite_model.py | 3 --- tests/sqlite/relationships/test_sqlite_polymorphic.py | 2 -- .../sqlite/relationships/test_sqlite_relationships.py | 4 ---- tests/sqlite/schema/test_table.py | 1 - 59 files changed, 8 insertions(+), 83 deletions(-) diff --git a/src/masoniteorm/collection/Collection.py b/src/masoniteorm/collection/Collection.py index 0c3cb9c55..44d84f349 100644 --- a/src/masoniteorm/collection/Collection.py +++ b/src/masoniteorm/collection/Collection.py @@ -351,7 +351,6 @@ def to_json(self, **kwargs): return json.dumps(self.serialize(), **kwargs) def group_by(self, key): - from itertools import groupby self.sort(key) @@ -410,7 +409,6 @@ def where(self, key, *args): return self.__class__(attributes) def where_in(self, key, args: list) -> "Collection": - attributes = [] for item in self._items: diff --git a/src/masoniteorm/commands/MigrateRefreshCommand.py b/src/masoniteorm/commands/MigrateRefreshCommand.py index 71a15aa4c..c65735643 100644 --- a/src/masoniteorm/commands/MigrateRefreshCommand.py +++ b/src/masoniteorm/commands/MigrateRefreshCommand.py @@ -17,7 +17,6 @@ class MigrateRefreshCommand(Command): """ def handle(self): - migration = Migration( command_class=self, connection=self.option("connection"), diff --git a/src/masoniteorm/commands/SeedRunCommand.py b/src/masoniteorm/commands/SeedRunCommand.py index 3ae6b585f..89e90359d 100644 --- a/src/masoniteorm/commands/SeedRunCommand.py +++ b/src/masoniteorm/commands/SeedRunCommand.py @@ -27,7 +27,6 @@ def handle(self): seeder_seeded = "Database Seeder" else: - table = self.argument("table") seeder_file = ( f"{underscore(table)}_table_seeder.{camelize(table)}TableSeeder" diff --git a/src/masoniteorm/connections/BaseConnection.py b/src/masoniteorm/connections/BaseConnection.py index ce8e83619..e6c6f8c6c 100644 --- a/src/masoniteorm/connections/BaseConnection.py +++ b/src/masoniteorm/connections/BaseConnection.py @@ -4,7 +4,6 @@ class BaseConnection: - _connection = None _cursor = None _dry = False diff --git a/src/masoniteorm/connections/ConnectionResolver.py b/src/masoniteorm/connections/ConnectionResolver.py index d518c36ad..f62eac302 100644 --- a/src/masoniteorm/connections/ConnectionResolver.py +++ b/src/masoniteorm/connections/ConnectionResolver.py @@ -2,7 +2,6 @@ class ConnectionResolver: - _connection_details = {} _connections = {} _morph_map = {} diff --git a/src/masoniteorm/connections/MSSQLConnection.py b/src/masoniteorm/connections/MSSQLConnection.py index a5fb7e4bc..121a2eff7 100644 --- a/src/masoniteorm/connections/MSSQLConnection.py +++ b/src/masoniteorm/connections/MSSQLConnection.py @@ -26,7 +26,6 @@ def __init__( full_details=None, name=None, ): - self.host = host if port: self.port = int(port) @@ -152,7 +151,7 @@ def query(self, query, bindings=(), results="*"): return {} columnNames = [column[0] for column in cursor.description] result = cursor.fetchone() - return dict(zip(columnNames, result)) if result != None else {} + return dict(zip(columnNames, result)) if result is not None else {} else: if not cursor.description: return {} diff --git a/src/masoniteorm/connections/PostgresConnection.py b/src/masoniteorm/connections/PostgresConnection.py index dccada024..3174933ed 100644 --- a/src/masoniteorm/connections/PostgresConnection.py +++ b/src/masoniteorm/connections/PostgresConnection.py @@ -26,7 +26,6 @@ def __init__( full_details=None, name=None, ): - self.host = host if port: self.port = int(port) diff --git a/src/masoniteorm/factories/Factory.py b/src/masoniteorm/factories/Factory.py index d3f7741a3..0aef9ba19 100644 --- a/src/masoniteorm/factories/Factory.py +++ b/src/masoniteorm/factories/Factory.py @@ -3,7 +3,6 @@ class Factory: - _factories = {} _after_creates = {} _faker = None diff --git a/src/masoniteorm/migrations/Migration.py b/src/masoniteorm/migrations/Migration.py index 280b7fc09..60a769c65 100644 --- a/src/masoniteorm/migrations/Migration.py +++ b/src/masoniteorm/migrations/Migration.py @@ -60,7 +60,9 @@ def get_unran_migrations(self): all_migrations = [ f.replace(".py", "") for f in listdir(directory_path) - if isfile(join(directory_path, f)) and f != "__init__.py" and not f.startswith('.') + if isfile(join(directory_path, f)) + and f != "__init__.py" + and not f.startswith(".") ] all_migrations.sort() unran_migrations = [] @@ -107,7 +109,9 @@ def get_ran_migrations(self): all_migrations = [ f.replace(".py", "") for f in listdir(directory_path) - if isfile(join(directory_path, f)) and f != "__init__.py" and not f.startswith('.') + if isfile(join(directory_path, f)) + and f != "__init__.py" + and not f.startswith(".") ] all_migrations.sort() ran = [] @@ -119,14 +123,12 @@ def get_ran_migrations(self): return ran def migrate(self, migration="all", output=False): - default_migrations = self.get_unran_migrations() migrations = default_migrations if migration == "all" else [migration] batch = self.get_last_batch_number() + 1 for migration in migrations: - try: migration_class = self.locate(migration) @@ -173,7 +175,6 @@ def migrate(self, migration="all", output=False): ) def rollback(self, migration="all", output=False): - default_migrations = self.get_rollback_migrations() migrations = default_migrations if migration == "all" else [migration] diff --git a/src/masoniteorm/models/MigrationModel.py b/src/masoniteorm/models/MigrationModel.py index d7677db4d..d915c5590 100644 --- a/src/masoniteorm/models/MigrationModel.py +++ b/src/masoniteorm/models/MigrationModel.py @@ -2,7 +2,6 @@ class MigrationModel(Model): - __table__ = "migrations" __fillable__ = ["migration", "batch"] __timestamps__ = None diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index 279482f6b..0f0e0587d 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -265,7 +265,7 @@ class Model(TimeStampsMixin, ObservesEvents, metaclass=ModelMeta): "with_count", "latest", "oldest", - "value" + "value", ) ) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 7e8bd81a9..fea2c6746 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -617,7 +617,6 @@ def where(self, column, *args): ) elif isinstance(column, dict): for key, value in column.items(): - self._wheres += ((QueryExpression(key, "=", value, "value")),) elif isinstance(value, QueryBuilder): self._wheres += ( @@ -898,7 +897,6 @@ def or_where_null(self, column): def chunk(self, chunk_amount): chunk_connection = self.new_connection() for result in chunk_connection.select_many(self.to_sql(), (), chunk_amount): - yield self.prepare_result(result) def where_not_null(self, column: str): @@ -2136,7 +2134,6 @@ def min(self, column): return self def _extract_operator_value(self, *args): - operators = [ "=", ">", diff --git a/src/masoniteorm/query/grammars/BaseGrammar.py b/src/masoniteorm/query/grammars/BaseGrammar.py index 4c25b7dee..5a0535765 100644 --- a/src/masoniteorm/query/grammars/BaseGrammar.py +++ b/src/masoniteorm/query/grammars/BaseGrammar.py @@ -292,7 +292,6 @@ def _compile_key_value_equals(self, qmark=False): """ sql = "" for update in self._updates: - if update.update_type == "increment": sql_string = self.increment_string() elif update.update_type == "decrement": @@ -304,7 +303,6 @@ def _compile_key_value_equals(self, qmark=False): value = update.value if isinstance(column, dict): for key, value in column.items(): - if hasattr(value, "expression"): sql += self.column_value_string().format( column=self._table_column_string(key), @@ -884,7 +882,6 @@ def _table_column_string(self, column, alias=None, separator=""): """ table = None if column and "." in column: - table, column = column.split(".") if column == "*": diff --git a/src/masoniteorm/schema/Schema.py b/src/masoniteorm/schema/Schema.py index 812a48fbf..817872e9a 100644 --- a/src/masoniteorm/schema/Schema.py +++ b/src/masoniteorm/schema/Schema.py @@ -6,7 +6,6 @@ class Schema: - _default_string_length = "255" _type_hints_map = { "string": str, diff --git a/src/masoniteorm/schema/platforms/MSSQLPlatform.py b/src/masoniteorm/schema/platforms/MSSQLPlatform.py index 7f3ba591c..c9a362770 100644 --- a/src/masoniteorm/schema/platforms/MSSQLPlatform.py +++ b/src/masoniteorm/schema/platforms/MSSQLPlatform.py @@ -3,7 +3,6 @@ class MSSQLPlatform(Platform): - types_without_lengths = [ "integer", "big_integer", @@ -120,7 +119,6 @@ def compile_alter_sql(self, table): if table.renamed_columns: for name, column in table.get_renamed_columns().items(): - sql.append( self.rename_column_string(table.name, name, column.name).strip() ) diff --git a/src/masoniteorm/schema/platforms/Platform.py b/src/masoniteorm/schema/platforms/Platform.py index 25e5a4865..4d69187e0 100644 --- a/src/masoniteorm/schema/platforms/Platform.py +++ b/src/masoniteorm/schema/platforms/Platform.py @@ -1,5 +1,4 @@ class Platform: - foreign_key_actions = { "cascade": "CASCADE", "set null": "SET NULL", diff --git a/src/masoniteorm/schema/platforms/SQLitePlatform.py b/src/masoniteorm/schema/platforms/SQLitePlatform.py index cd8d3e3fb..0b1639948 100644 --- a/src/masoniteorm/schema/platforms/SQLitePlatform.py +++ b/src/masoniteorm/schema/platforms/SQLitePlatform.py @@ -4,7 +4,6 @@ class SQLitePlatform(Platform): - types_without_lengths = [ "integer", "big_integer", diff --git a/src/masoniteorm/testing/BaseTestCaseSelectGrammar.py b/src/masoniteorm/testing/BaseTestCaseSelectGrammar.py index e2dd2e48c..0182e3c08 100644 --- a/src/masoniteorm/testing/BaseTestCaseSelectGrammar.py +++ b/src/masoniteorm/testing/BaseTestCaseSelectGrammar.py @@ -9,7 +9,6 @@ class MockConnection: - connection_details = {} def make_connection(self): diff --git a/tests/connections/test_base_connections.py b/tests/connections/test_base_connections.py index 7711af163..96fc2e3c3 100644 --- a/tests/connections/test_base_connections.py +++ b/tests/connections/test_base_connections.py @@ -6,7 +6,6 @@ class TestDefaultBehaviorConnections(unittest.TestCase): def test_should_return_connection_with_enabled_logs(self): - connection = DB.begin_transaction("dev") should_log_queries = connection.full_details.get("log_queries") DB.commit("dev") @@ -14,7 +13,6 @@ def test_should_return_connection_with_enabled_logs(self): self.assertTrue(should_log_queries) def test_should_disable_log_queries_in_connection(self): - connection = DB.begin_transaction("dev") connection.disable_query_log() diff --git a/tests/eagers/test_eager.py b/tests/eagers/test_eager.py index 97894f48d..482f21607 100644 --- a/tests/eagers/test_eager.py +++ b/tests/eagers/test_eager.py @@ -6,7 +6,6 @@ class TestEagerRelation(unittest.TestCase): def test_can_register_string_eager_load(self): - self.assertEqual( EagerRelations().register("profile").get_eagers(), [["profile"]] ) @@ -31,7 +30,6 @@ def test_can_register_string_eager_load(self): ) def test_can_register_tuple_eager_load(self): - self.assertEqual( EagerRelations().register(("profile",)).get_eagers(), [["profile"]] ) @@ -45,7 +43,6 @@ def test_can_register_tuple_eager_load(self): ) def test_can_register_list_eager_load(self): - self.assertEqual( EagerRelations().register(["profile"]).get_eagers(), [["profile"]] ) diff --git a/tests/mssql/builder/test_mssql_query_builder.py b/tests/mssql/builder/test_mssql_query_builder.py index 895f28f5e..17fa5e38b 100644 --- a/tests/mssql/builder/test_mssql_query_builder.py +++ b/tests/mssql/builder/test_mssql_query_builder.py @@ -9,7 +9,6 @@ class MockConnection: - connection_details = {} def make_connection(self): diff --git a/tests/mssql/builder/test_mssql_query_builder_relationships.py b/tests/mssql/builder/test_mssql_query_builder_relationships.py index 2c50f8fdb..4d36b3f07 100644 --- a/tests/mssql/builder/test_mssql_query_builder_relationships.py +++ b/tests/mssql/builder/test_mssql_query_builder_relationships.py @@ -42,7 +42,6 @@ def profile(self): class BaseTestQueryRelationships(unittest.TestCase): - maxDiff = None def get_builder(self, table="users"): diff --git a/tests/mssql/grammar/test_mssql_insert_grammar.py b/tests/mssql/grammar/test_mssql_insert_grammar.py index dcac9f145..740bff4e8 100644 --- a/tests/mssql/grammar/test_mssql_insert_grammar.py +++ b/tests/mssql/grammar/test_mssql_insert_grammar.py @@ -9,7 +9,6 @@ def setUp(self): self.builder = QueryBuilder(MSSQLGrammar, table="users") def test_can_compile_insert(self): - to_sql = self.builder.create({"name": "Joe"}, query=True).to_sql() sql = "INSERT INTO [users] ([users].[name]) VALUES ('Joe')" diff --git a/tests/mssql/grammar/test_mssql_select_grammar.py b/tests/mssql/grammar/test_mssql_select_grammar.py index a78161512..28615cdf7 100644 --- a/tests/mssql/grammar/test_mssql_select_grammar.py +++ b/tests/mssql/grammar/test_mssql_select_grammar.py @@ -6,7 +6,6 @@ class TestMSSQLGrammar(BaseTestCaseSelectGrammar, unittest.TestCase): - grammar = MSSQLGrammar def can_compile_select(self): diff --git a/tests/mssql/grammar/test_mssql_update_grammar.py b/tests/mssql/grammar/test_mssql_update_grammar.py index d43704a43..49c6e4ab4 100644 --- a/tests/mssql/grammar/test_mssql_update_grammar.py +++ b/tests/mssql/grammar/test_mssql_update_grammar.py @@ -10,7 +10,6 @@ def setUp(self): self.builder = QueryBuilder(MSSQLGrammar, table="users") def test_can_compile_update(self): - to_sql = ( self.builder.where("name", "bob").update({"name": "Joe"}, dry=True).to_sql() ) diff --git a/tests/mssql/schema/test_mssql_schema_builder_alter.py b/tests/mssql/schema/test_mssql_schema_builder_alter.py index ae85faa73..daf2168f3 100644 --- a/tests/mssql/schema/test_mssql_schema_builder_alter.py +++ b/tests/mssql/schema/test_mssql_schema_builder_alter.py @@ -11,7 +11,6 @@ class TestMySQLSchemaBuilderAlter(unittest.TestCase): maxDiff = None def setUp(self): - self.schema = Schema( connection_class=MSSQLConnection, connection="mssql", diff --git a/tests/mysql/builder/test_mysql_builder_transaction.py b/tests/mysql/builder/test_mysql_builder_transaction.py index df6a0f218..7e0fa39bd 100644 --- a/tests/mysql/builder/test_mysql_builder_transaction.py +++ b/tests/mysql/builder/test_mysql_builder_transaction.py @@ -17,7 +17,6 @@ class User(Model): __timestamps__ = False class BaseTestQueryRelationships(unittest.TestCase): - maxDiff = None def get_builder(self, table="users"): diff --git a/tests/mysql/connections/test_mysql_connection_selects.py b/tests/mysql/connections/test_mysql_connection_selects.py index 0db03e3a5..acb09e50e 100644 --- a/tests/mysql/connections/test_mysql_connection_selects.py +++ b/tests/mysql/connections/test_mysql_connection_selects.py @@ -7,7 +7,6 @@ class MockUser(Model): - __table__ = "users" diff --git a/tests/mysql/grammar/test_mysql_delete_grammar.py b/tests/mysql/grammar/test_mysql_delete_grammar.py index 84ee5fea4..c17acee73 100644 --- a/tests/mysql/grammar/test_mysql_delete_grammar.py +++ b/tests/mysql/grammar/test_mysql_delete_grammar.py @@ -41,7 +41,6 @@ def test_can_compile_delete_with_where(self): class TestMySQLDeleteGrammar(BaseDeleteGrammarTest, unittest.TestCase): - grammar = "mysql" def can_compile_delete(self): diff --git a/tests/mysql/grammar/test_mysql_insert_grammar.py b/tests/mysql/grammar/test_mysql_insert_grammar.py index be9c01b4a..79f6a2dc2 100644 --- a/tests/mysql/grammar/test_mysql_insert_grammar.py +++ b/tests/mysql/grammar/test_mysql_insert_grammar.py @@ -68,7 +68,6 @@ def test_can_compile_bulk_create_multiple(self): class TestMySQLUpdateGrammar(BaseInsertGrammarTest, unittest.TestCase): - grammar = "mysql" def can_compile_insert(self): diff --git a/tests/mysql/grammar/test_mysql_qmark.py b/tests/mysql/grammar/test_mysql_qmark.py index 0d411b089..549cc020d 100644 --- a/tests/mysql/grammar/test_mysql_qmark.py +++ b/tests/mysql/grammar/test_mysql_qmark.py @@ -82,7 +82,6 @@ def test_can_compile_where_with_false_value(self): self.assertEqual(mark._bindings, bindings) def test_can_compile_sub_group_bindings(self): - mark = self.builder.where( lambda query: ( query.where("challenger", 1) diff --git a/tests/mysql/grammar/test_mysql_select_grammar.py b/tests/mysql/grammar/test_mysql_select_grammar.py index 3f55cd162..169b6a890 100644 --- a/tests/mysql/grammar/test_mysql_select_grammar.py +++ b/tests/mysql/grammar/test_mysql_select_grammar.py @@ -6,7 +6,6 @@ class TestMySQLGrammar(BaseTestCaseSelectGrammar, unittest.TestCase): - grammar = MySQLGrammar def can_compile_select(self): diff --git a/tests/mysql/grammar/test_mysql_update_grammar.py b/tests/mysql/grammar/test_mysql_update_grammar.py index a427f02e1..0212ec8fe 100644 --- a/tests/mysql/grammar/test_mysql_update_grammar.py +++ b/tests/mysql/grammar/test_mysql_update_grammar.py @@ -70,7 +70,6 @@ def test_raw_expression(self): class TestMySQLUpdateGrammar(BaseTestCaseUpdateGrammar, unittest.TestCase): - grammar = MySQLGrammar def can_compile_update(self): diff --git a/tests/mysql/model/test_accessors_and_mutators.py b/tests/mysql/model/test_accessors_and_mutators.py index 97df526df..6423816da 100644 --- a/tests/mysql/model/test_accessors_and_mutators.py +++ b/tests/mysql/model/test_accessors_and_mutators.py @@ -12,7 +12,6 @@ class User(Model): - __casts__ = {"is_admin": "bool"} def get_name_attribute(self): @@ -23,7 +22,6 @@ def set_name_attribute(self, attribute): class SetUser(Model): - __casts__ = {"is_admin": "bool"} def set_name_attribute(self, attribute): diff --git a/tests/mysql/schema/test_mysql_schema_builder_alter.py b/tests/mysql/schema/test_mysql_schema_builder_alter.py index 4b2befe8c..e31fbc4be 100644 --- a/tests/mysql/schema/test_mysql_schema_builder_alter.py +++ b/tests/mysql/schema/test_mysql_schema_builder_alter.py @@ -12,7 +12,6 @@ class TestMySQLSchemaBuilderAlter(unittest.TestCase): maxDiff = None def setUp(self): - self.schema = Schema( connection_class=MySQLConnection, connection="mysql", diff --git a/tests/mysql/scopes/test_can_use_global_scopes.py b/tests/mysql/scopes/test_can_use_global_scopes.py index e763d0539..0cfe3e537 100644 --- a/tests/mysql/scopes/test_can_use_global_scopes.py +++ b/tests/mysql/scopes/test_can_use_global_scopes.py @@ -15,7 +15,6 @@ class UserSoft(Model, SoftDeletesMixin): class User(Model): - __dry__ = True diff --git a/tests/postgres/builder/test_postgres_query_builder.py b/tests/postgres/builder/test_postgres_query_builder.py index 7571d5e48..86e5b62f8 100644 --- a/tests/postgres/builder/test_postgres_query_builder.py +++ b/tests/postgres/builder/test_postgres_query_builder.py @@ -9,7 +9,6 @@ class MockConnection: - connection_details = {} def make_connection(self): @@ -462,7 +461,6 @@ def test_update_lock(self): class PostgresQueryBuilderTest(BaseTestQueryBuilder, unittest.TestCase): - grammar = PostgresGrammar def sum(self): diff --git a/tests/postgres/builder/test_postgres_transaction.py b/tests/postgres/builder/test_postgres_transaction.py index d0b92c9ce..a07a77f45 100644 --- a/tests/postgres/builder/test_postgres_transaction.py +++ b/tests/postgres/builder/test_postgres_transaction.py @@ -17,7 +17,6 @@ class User(Model): __timestamps__ = False class BaseTestQueryRelationships(unittest.TestCase): - maxDiff = None def get_builder(self, table="users"): diff --git a/tests/postgres/grammar/test_delete_grammar.py b/tests/postgres/grammar/test_delete_grammar.py index 66b3a9532..690e72537 100644 --- a/tests/postgres/grammar/test_delete_grammar.py +++ b/tests/postgres/grammar/test_delete_grammar.py @@ -41,7 +41,6 @@ def test_can_compile_delete_with_where(self): class TestPostgresDeleteGrammar(BaseDeleteGrammarTest, unittest.TestCase): - grammar = "postgres" def can_compile_delete(self): diff --git a/tests/postgres/grammar/test_insert_grammar.py b/tests/postgres/grammar/test_insert_grammar.py index 93f6e2e80..195e85490 100644 --- a/tests/postgres/grammar/test_insert_grammar.py +++ b/tests/postgres/grammar/test_insert_grammar.py @@ -53,7 +53,6 @@ def test_can_compile_bulk_create_qmark(self): class TestPostgresUpdateGrammar(BaseInsertGrammarTest, unittest.TestCase): - grammar = "postgres" def can_compile_insert(self): diff --git a/tests/postgres/grammar/test_select_grammar.py b/tests/postgres/grammar/test_select_grammar.py index 8d71eb1f8..4754114e6 100644 --- a/tests/postgres/grammar/test_select_grammar.py +++ b/tests/postgres/grammar/test_select_grammar.py @@ -6,7 +6,6 @@ class TestPostgresGrammar(BaseTestCaseSelectGrammar, unittest.TestCase): - grammar = PostgresGrammar def can_compile_select(self): diff --git a/tests/postgres/grammar/test_update_grammar.py b/tests/postgres/grammar/test_update_grammar.py index cb6d7fe5c..15a70b624 100644 --- a/tests/postgres/grammar/test_update_grammar.py +++ b/tests/postgres/grammar/test_update_grammar.py @@ -73,7 +73,6 @@ def test_raw_expression(self): class TestPostgresUpdateGrammar(BaseTestCaseUpdateGrammar, unittest.TestCase): - grammar = "postgres" def can_compile_update(self): diff --git a/tests/postgres/relationships/test_postgres_relationships.py b/tests/postgres/relationships/test_postgres_relationships.py index a20477e16..023fd536d 100644 --- a/tests/postgres/relationships/test_postgres_relationships.py +++ b/tests/postgres/relationships/test_postgres_relationships.py @@ -23,7 +23,6 @@ class Logo(Model): __connection__ = "postgres" class User(Model): - __connection__ = "postgres" _eager_loads = () diff --git a/tests/postgres/schema/test_postgres_schema_builder_alter.py b/tests/postgres/schema/test_postgres_schema_builder_alter.py index 0f0519c52..1b98ac210 100644 --- a/tests/postgres/schema/test_postgres_schema_builder_alter.py +++ b/tests/postgres/schema/test_postgres_schema_builder_alter.py @@ -11,7 +11,6 @@ class TestPostgresSchemaBuilderAlter(unittest.TestCase): maxDiff = None def setUp(self): - self.schema = Schema( connection_class=PostgresConnection, connection="postgres", diff --git a/tests/sqlite/builder/test_sqlite_builder_insert.py b/tests/sqlite/builder/test_sqlite_builder_insert.py index df58e73a1..52f6a29fb 100644 --- a/tests/sqlite/builder/test_sqlite_builder_insert.py +++ b/tests/sqlite/builder/test_sqlite_builder_insert.py @@ -17,7 +17,6 @@ class User(Model): class BaseTestQueryRelationships(unittest.TestCase): - maxDiff = None def get_builder(self, table="users"): diff --git a/tests/sqlite/builder/test_sqlite_builder_pagination.py b/tests/sqlite/builder/test_sqlite_builder_pagination.py index 832df6631..2338b6b38 100644 --- a/tests/sqlite/builder/test_sqlite_builder_pagination.py +++ b/tests/sqlite/builder/test_sqlite_builder_pagination.py @@ -15,7 +15,6 @@ class User(Model): class BaseTestQueryRelationships(unittest.TestCase): - maxDiff = None def get_builder(self, table="users", model=User): diff --git a/tests/sqlite/builder/test_sqlite_query_builder.py b/tests/sqlite/builder/test_sqlite_query_builder.py index bf3d4c653..9da9f235c 100644 --- a/tests/sqlite/builder/test_sqlite_query_builder.py +++ b/tests/sqlite/builder/test_sqlite_query_builder.py @@ -392,7 +392,6 @@ def test_between(self): self.assertEqual(builder.to_sql(), sql) def test_between_persisted(self): - builder = QueryBuilder().table("users").on("dev") users = builder.between("age", 1, 2).count() @@ -407,7 +406,6 @@ def test_not_between(self): self.assertEqual(builder.to_sql(), sql) def test_not_between_persisted(self): - builder = QueryBuilder().table("users").on("dev") users = builder.where_not_null("id").not_between("age", 1, 2).count() @@ -583,7 +581,6 @@ def test_truncate_without_foreign_keys(self): class SQLiteQueryBuilderTest(BaseTestQueryBuilder, unittest.TestCase): - grammar = SQLiteGrammar def sum(self): diff --git a/tests/sqlite/builder/test_sqlite_query_builder_eager_loading.py b/tests/sqlite/builder/test_sqlite_query_builder_eager_loading.py index aa2524dde..800e94405 100644 --- a/tests/sqlite/builder/test_sqlite_query_builder_eager_loading.py +++ b/tests/sqlite/builder/test_sqlite_query_builder_eager_loading.py @@ -56,7 +56,6 @@ def profile(self): class BaseTestQueryRelationships(unittest.TestCase): - maxDiff = None def get_builder(self, table="users", model=User): diff --git a/tests/sqlite/builder/test_sqlite_query_builder_relationships.py b/tests/sqlite/builder/test_sqlite_query_builder_relationships.py index 8a35a4e8e..3e4dc03b9 100644 --- a/tests/sqlite/builder/test_sqlite_query_builder_relationships.py +++ b/tests/sqlite/builder/test_sqlite_query_builder_relationships.py @@ -42,7 +42,6 @@ def profile(self): class BaseTestQueryRelationships(unittest.TestCase): - maxDiff = None def get_builder(self, table="users"): diff --git a/tests/sqlite/builder/test_sqlite_transaction.py b/tests/sqlite/builder/test_sqlite_transaction.py index 2cf0238c8..41e87612b 100644 --- a/tests/sqlite/builder/test_sqlite_transaction.py +++ b/tests/sqlite/builder/test_sqlite_transaction.py @@ -18,7 +18,6 @@ class User(Model): class BaseTestQueryRelationships(unittest.TestCase): - maxDiff = None def get_builder(self, table="users"): diff --git a/tests/sqlite/grammar/test_sqlite_delete_grammar.py b/tests/sqlite/grammar/test_sqlite_delete_grammar.py index ee501ffb6..3bd36a872 100644 --- a/tests/sqlite/grammar/test_sqlite_delete_grammar.py +++ b/tests/sqlite/grammar/test_sqlite_delete_grammar.py @@ -40,7 +40,6 @@ def test_can_compile_delete_with_where(self): class TestSqliteDeleteGrammar(BaseDeleteGrammarTest, unittest.TestCase): - grammar = "sqlite" def can_compile_delete(self): diff --git a/tests/sqlite/grammar/test_sqlite_insert_grammar.py b/tests/sqlite/grammar/test_sqlite_insert_grammar.py index cd2b99fad..1df723972 100644 --- a/tests/sqlite/grammar/test_sqlite_insert_grammar.py +++ b/tests/sqlite/grammar/test_sqlite_insert_grammar.py @@ -68,7 +68,6 @@ def test_can_compile_bulk_create_multiple(self): class TestSqliteUpdateGrammar(BaseInsertGrammarTest, unittest.TestCase): - grammar = "sqlite" def can_compile_insert(self): diff --git a/tests/sqlite/grammar/test_sqlite_select_grammar.py b/tests/sqlite/grammar/test_sqlite_select_grammar.py index 22cf12851..015f39675 100644 --- a/tests/sqlite/grammar/test_sqlite_select_grammar.py +++ b/tests/sqlite/grammar/test_sqlite_select_grammar.py @@ -6,7 +6,6 @@ class TestSQLiteGrammar(BaseTestCaseSelectGrammar, unittest.TestCase): - grammar = SQLiteGrammar maxDiff = None diff --git a/tests/sqlite/grammar/test_sqlite_update_grammar.py b/tests/sqlite/grammar/test_sqlite_update_grammar.py index 1bf081b40..4ee265432 100644 --- a/tests/sqlite/grammar/test_sqlite_update_grammar.py +++ b/tests/sqlite/grammar/test_sqlite_update_grammar.py @@ -68,7 +68,6 @@ def test_raw_expression(self): class TestSqliteUpdateGrammar(BaseTestCaseUpdateGrammar, unittest.TestCase): - grammar = "sqlite" def can_compile_update(self): diff --git a/tests/sqlite/models/test_observers.py b/tests/sqlite/models/test_observers.py index a0ce9e50a..178eaee46 100644 --- a/tests/sqlite/models/test_observers.py +++ b/tests/sqlite/models/test_observers.py @@ -63,7 +63,6 @@ class Observer(Model): class BaseTestQueryRelationships(unittest.TestCase): - maxDiff = None def test_created_is_observed(self): diff --git a/tests/sqlite/models/test_sqlite_model.py b/tests/sqlite/models/test_sqlite_model.py index 7e76116a8..91424ef43 100644 --- a/tests/sqlite/models/test_sqlite_model.py +++ b/tests/sqlite/models/test_sqlite_model.py @@ -55,7 +55,6 @@ def team(self): class BaseTestQueryRelationships(unittest.TestCase): - maxDiff = None def test_update_specific_record(self): @@ -163,7 +162,6 @@ class ModelUser(Model): self.assertEqual(count, 0) def test_get_columns(self): - columns = User.get_columns() self.assertEqual( columns, @@ -192,7 +190,6 @@ def test_get_columns(self): ) def test_should_return_relation_applying_hidden_attributes(self): - schema = Schema( connection_details=DATABASES, connection="dev", platform=SQLitePlatform ).on("dev") diff --git a/tests/sqlite/relationships/test_sqlite_polymorphic.py b/tests/sqlite/relationships/test_sqlite_polymorphic.py index 5a69dbca7..e0cd4d392 100644 --- a/tests/sqlite/relationships/test_sqlite_polymorphic.py +++ b/tests/sqlite/relationships/test_sqlite_polymorphic.py @@ -26,7 +26,6 @@ class Logo(Model): class Like(Model): - __connection__ = "dev" @morph_to("record_type", "record_id") @@ -35,7 +34,6 @@ def record(self): class User(Model): - __connection__ = "dev" _eager_loads = () diff --git a/tests/sqlite/relationships/test_sqlite_relationships.py b/tests/sqlite/relationships/test_sqlite_relationships.py index 0349a5a32..b182e8451 100644 --- a/tests/sqlite/relationships/test_sqlite_relationships.py +++ b/tests/sqlite/relationships/test_sqlite_relationships.py @@ -30,7 +30,6 @@ class Logo(Model): class User(Model): - __connection__ = "dev" _eager_loads = () @@ -50,7 +49,6 @@ def get_is_admin(self): class Store(Model): - __connection__ = "dev" @belongs_to_many("store_id", "product_id", "id", "id", with_timestamps=True) @@ -67,12 +65,10 @@ def store_products(self): class Product(Model): - __connection__ = "dev" class UserHasOne(Model): - __table__ = "users" __connection__ = "dev" diff --git a/tests/sqlite/schema/test_table.py b/tests/sqlite/schema/test_table.py index 5f95755eb..2e73a4ab4 100644 --- a/tests/sqlite/schema/test_table.py +++ b/tests/sqlite/schema/test_table.py @@ -7,7 +7,6 @@ class TestTable(unittest.TestCase): - maxDiff = None def setUp(self): From 39c5cd00c239d66d9c2f55242945b859e3b2cfaa Mon Sep 17 00:00:00 2001 From: Eduardo Aguad Date: Sat, 21 Oct 2023 22:30:35 -0300 Subject: [PATCH 104/254] add batch number to status command --- src/masoniteorm/commands/MigrateStatusCommand.py | 11 +++++++---- src/masoniteorm/migrations/Migration.py | 6 ++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/masoniteorm/commands/MigrateStatusCommand.py b/src/masoniteorm/commands/MigrateStatusCommand.py index 8cdbd7068..ea95e7fff 100644 --- a/src/masoniteorm/commands/MigrateStatusCommand.py +++ b/src/masoniteorm/commands/MigrateStatusCommand.py @@ -22,17 +22,20 @@ def handle(self): ) migration.create_table_if_not_exists() table = self.table() - table.set_header_row(["Ran?", "Migration"]) + table.set_header_row(["Ran?", "Migration", "Batch"]) migrations = [] - for migration_file in migration.get_ran_migrations(): + for migration_data in migration.get_ran_migrations(): + migration_file = migration_data["migration_file"] + batch = migration_data["batch"] + migrations.append( - ["Y", f"{migration_file}"] + ["Y", f"{migration_file}", f"{batch}"] ) for migration_file in migration.get_unran_migrations(): migrations.append( - ["N", f"{migration_file}"] + ["N", f"{migration_file}", "-"] ) table.set_rows(migrations) diff --git a/src/masoniteorm/migrations/Migration.py b/src/masoniteorm/migrations/Migration.py index 60a769c65..6d5d5d80d 100644 --- a/src/masoniteorm/migrations/Migration.py +++ b/src/masoniteorm/migrations/Migration.py @@ -118,8 +118,10 @@ def get_ran_migrations(self): database_migrations = self.migration_model.all() for migration in all_migrations: - if migration in database_migrations.pluck("migration"): - ran.append(migration) + if migration := database_migrations.where("migration", migration).first(): + ran.append( + {"migration_file": migration.migration, "batch": migration.batch} + ) return ran def migrate(self, migration="all", output=False): From eb140ef31980c7fb6f9c0b3944afd555c587bedc Mon Sep 17 00:00:00 2001 From: Eduardo Aguad Date: Tue, 24 Oct 2023 22:38:43 -0300 Subject: [PATCH 105/254] replace walrus operator to improve compatibility --- src/masoniteorm/migrations/Migration.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/masoniteorm/migrations/Migration.py b/src/masoniteorm/migrations/Migration.py index 6d5d5d80d..b5481864b 100644 --- a/src/masoniteorm/migrations/Migration.py +++ b/src/masoniteorm/migrations/Migration.py @@ -118,9 +118,15 @@ def get_ran_migrations(self): database_migrations = self.migration_model.all() for migration in all_migrations: - if migration := database_migrations.where("migration", migration).first(): + matched_migration = database_migrations.where( + "migration", migration + ).first() + if matched_migration: ran.append( - {"migration_file": migration.migration, "batch": migration.batch} + { + "migration_file": matched_migration.migration, + "batch": matched_migration.batch, + } ) return ran From 27368348022fdbbefc8224b102fac46d2d09e267 Mon Sep 17 00:00:00 2001 From: Eduardo Aguad Date: Sat, 28 Oct 2023 19:27:44 -0300 Subject: [PATCH 106/254] implement migrate:fresh --- orm | 2 + src/masoniteorm/commands/Entry.py | 2 + .../commands/MigrateFreshCommand.py | 41 +++++++++++++++++++ src/masoniteorm/commands/__init__.py | 1 + src/masoniteorm/migrations/Migration.py | 21 ++++++++++ src/masoniteorm/schema/Schema.py | 19 +++++++++ .../schema/platforms/MSSQLPlatform.py | 3 ++ .../schema/platforms/MySQLPlatform.py | 3 ++ .../schema/platforms/PostgresPlatform.py | 3 ++ .../schema/platforms/SQLitePlatform.py | 3 ++ 10 files changed, 98 insertions(+) create mode 100644 src/masoniteorm/commands/MigrateFreshCommand.py diff --git a/orm b/orm index 588410707..73c273fdc 100644 --- a/orm +++ b/orm @@ -10,6 +10,7 @@ from src.masoniteorm.commands import ( MigrateCommand, MigrateRollbackCommand, MigrateRefreshCommand, + MigrateFreshCommand, MakeMigrationCommand, MakeObserverCommand, MakeModelCommand, @@ -25,6 +26,7 @@ application = Application("ORM Version:", 0.1) application.add(MigrateCommand()) application.add(MigrateRollbackCommand()) application.add(MigrateRefreshCommand()) +application.add(MigrateFreshCommand()) application.add(MakeMigrationCommand()) application.add(MakeModelCommand()) application.add(MakeModelDocstringCommand()) diff --git a/src/masoniteorm/commands/Entry.py b/src/masoniteorm/commands/Entry.py index c704f5f56..c46089048 100644 --- a/src/masoniteorm/commands/Entry.py +++ b/src/masoniteorm/commands/Entry.py @@ -10,6 +10,7 @@ MigrateCommand, MigrateRollbackCommand, MigrateRefreshCommand, + MigrateFreshCommand, MakeMigrationCommand, MakeModelCommand, MakeModelDocstringCommand, @@ -26,6 +27,7 @@ application.add(MigrateCommand()) application.add(MigrateRollbackCommand()) application.add(MigrateRefreshCommand()) +application.add(MigrateFreshCommand()) application.add(MakeMigrationCommand()) application.add(MakeModelCommand()) application.add(MakeModelDocstringCommand()) diff --git a/src/masoniteorm/commands/MigrateFreshCommand.py b/src/masoniteorm/commands/MigrateFreshCommand.py new file mode 100644 index 000000000..03570e666 --- /dev/null +++ b/src/masoniteorm/commands/MigrateFreshCommand.py @@ -0,0 +1,41 @@ +from ..migrations import Migration + +from .Command import Command + + +class MigrateFreshCommand(Command): + """ + Drops all tables and migrates them again. + + migrate:fresh + {--c|connection=default : The connection you want to run migrations on} + {--d|directory=databases/migrations : The location of the migration directory} + {--s|seed=? : Seed database after fresh. The seeder to be ran can be provided in argument} + {--schema=? : Sets the schema to be migrated} + {--D|seed-directory=databases/seeds : The location of the seed directory if seed option is used.} + """ + + def handle(self): + migration = Migration( + command_class=self, + connection=self.option("connection"), + migration_directory=self.option("directory"), + config_path=self.option("config"), + schema=self.option("schema"), + ) + + migration.fresh() + + self.line("") + + if self.option("seed") == "null": + self.call( + "seed:run", + f"None --directory {self.option('seed-directory')} --connection {self.option('connection')}", + ) + + elif self.option("seed"): + self.call( + "seed:run", + f"{self.option('seed')} --directory {self.option('seed-directory')} --connection {self.option('connection')}", + ) diff --git a/src/masoniteorm/commands/__init__.py b/src/masoniteorm/commands/__init__.py index 380b9a442..454ef577d 100644 --- a/src/masoniteorm/commands/__init__.py +++ b/src/masoniteorm/commands/__init__.py @@ -6,6 +6,7 @@ from .MigrateCommand import MigrateCommand from .MigrateRollbackCommand import MigrateRollbackCommand from .MigrateRefreshCommand import MigrateRefreshCommand +from .MigrateFreshCommand import MigrateFreshCommand from .MigrateResetCommand import MigrateResetCommand from .MakeModelCommand import MakeModelCommand from .MakeModelDocstringCommand import MakeModelDocstringCommand diff --git a/src/masoniteorm/migrations/Migration.py b/src/masoniteorm/migrations/Migration.py index 60a769c65..5cd4bd402 100644 --- a/src/masoniteorm/migrations/Migration.py +++ b/src/masoniteorm/migrations/Migration.py @@ -280,3 +280,24 @@ def reset(self, migration="all"): def refresh(self, migration="all"): self.reset(migration) self.migrate(migration) + + def drop_all_tables(self): + if self.command_class: + self.command_class.line("Dropping all tables") + + for table in self.schema.get_all_tables(): + self.schema.drop(table) + + if self.command_class: + self.command_class.line("All tables dropped") + + def fresh(self, migration="all"): + self.drop_all_tables() + self.create_table_if_not_exists() + + if not self.get_unran_migrations(): + if self.command_class: + self.command_class.line("Nothing to migrate") + return + + self.migrate(migration) diff --git a/src/masoniteorm/schema/Schema.py b/src/masoniteorm/schema/Schema.py index 817872e9a..dfbc9f763 100644 --- a/src/masoniteorm/schema/Schema.py +++ b/src/masoniteorm/schema/Schema.py @@ -291,6 +291,25 @@ def get_schema(self): "schema" ) + def get_all_tables(self): + """Gets all tables in the database""" + sql = self.platform().compile_get_all_tables( + database=self.get_connection_information().get("database"), + schema=self.get_schema(), + ) + + if self._dry: + self._sql = sql + return sql + + result = self.new_connection().query(sql, ()) + + return ( + list(map(lambda t: list(t.values())[0], result)) + if result + else [] + ) + def has_table(self, table, query_only=False): """Checks if the a database has a specific table Arguments: diff --git a/src/masoniteorm/schema/platforms/MSSQLPlatform.py b/src/masoniteorm/schema/platforms/MSSQLPlatform.py index c9a362770..9b3b3132b 100644 --- a/src/masoniteorm/schema/platforms/MSSQLPlatform.py +++ b/src/masoniteorm/schema/platforms/MSSQLPlatform.py @@ -334,6 +334,9 @@ def compile_drop_table(self, table): def compile_column_exists(self, table, column): return f"SELECT 1 FROM sys.columns WHERE Name = N'{column}' AND Object_ID = Object_ID(N'{table}')" + def compile_get_all_tables(self, database, schema=None): + return f"SELECT name FROM {database}.sys.tables" + def get_current_schema(self, connection, table_name, schema=None): return Table(table_name) diff --git a/src/masoniteorm/schema/platforms/MySQLPlatform.py b/src/masoniteorm/schema/platforms/MySQLPlatform.py index 9c8de30fb..b37c7ac8f 100644 --- a/src/masoniteorm/schema/platforms/MySQLPlatform.py +++ b/src/masoniteorm/schema/platforms/MySQLPlatform.py @@ -403,6 +403,9 @@ def compile_drop_table(self, table): def compile_column_exists(self, table, column): return f"SELECT column_name FROM information_schema.columns WHERE table_name='{table}' and column_name='{column}'" + def compile_get_all_tables(self, database, schema=None): + return f"SELECT table_name FROM information_schema.tables WHERE table_schema = '{database}'" + def get_current_schema(self, connection, table_name, schema=None): table = Table(table_name) sql = f"DESCRIBE {table_name}" diff --git a/src/masoniteorm/schema/platforms/PostgresPlatform.py b/src/masoniteorm/schema/platforms/PostgresPlatform.py index 4c8fc0978..6aeb67cb6 100644 --- a/src/masoniteorm/schema/platforms/PostgresPlatform.py +++ b/src/masoniteorm/schema/platforms/PostgresPlatform.py @@ -459,6 +459,9 @@ def compile_drop_table(self, table): def compile_column_exists(self, table, column): return f"SELECT column_name FROM information_schema.columns WHERE table_name='{table}' and column_name='{column}'" + def compile_get_all_tables(self, database=None, schema=None): + return f"SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_catalog = '{database}'" + def get_current_schema(self, connection, table_name, schema=None): sql = self.table_information_string().format( table=table_name, schema=schema or "public" diff --git a/src/masoniteorm/schema/platforms/SQLitePlatform.py b/src/masoniteorm/schema/platforms/SQLitePlatform.py index 0b1639948..5e1ca632f 100644 --- a/src/masoniteorm/schema/platforms/SQLitePlatform.py +++ b/src/masoniteorm/schema/platforms/SQLitePlatform.py @@ -401,6 +401,9 @@ def compile_table_exists(self, table, database=None, schema=None): def compile_column_exists(self, table, column): return f"SELECT column_name FROM information_schema.columns WHERE table_name='{table}' and column_name='{column}'" + def compile_get_all_tables(self, database, schema=None): + return "SELECT name FROM sqlite_master WHERE type='table'" + def compile_truncate(self, table, foreign_keys=False): if not foreign_keys: return f"DELETE FROM {self.wrap_table(table)}" From 5a509185ba0960aa3e51abc8314cfabf942363fa Mon Sep 17 00:00:00 2001 From: Eduardo Aguad Date: Sat, 28 Oct 2023 20:37:49 -0300 Subject: [PATCH 107/254] add ignore_fk param --- src/masoniteorm/commands/MigrateFreshCommand.py | 3 ++- src/masoniteorm/migrations/Migration.py | 12 +++++++++--- src/masoniteorm/schema/Schema.py | 6 ++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/masoniteorm/commands/MigrateFreshCommand.py b/src/masoniteorm/commands/MigrateFreshCommand.py index 03570e666..fb90f52ab 100644 --- a/src/masoniteorm/commands/MigrateFreshCommand.py +++ b/src/masoniteorm/commands/MigrateFreshCommand.py @@ -10,6 +10,7 @@ class MigrateFreshCommand(Command): migrate:fresh {--c|connection=default : The connection you want to run migrations on} {--d|directory=databases/migrations : The location of the migration directory} + {--f|ignore-fk=? : The connection you want to run migrations on} {--s|seed=? : Seed database after fresh. The seeder to be ran can be provided in argument} {--schema=? : Sets the schema to be migrated} {--D|seed-directory=databases/seeds : The location of the seed directory if seed option is used.} @@ -24,7 +25,7 @@ def handle(self): schema=self.option("schema"), ) - migration.fresh() + migration.fresh(ignore_fk=self.option("ignore-fk")) self.line("") diff --git a/src/masoniteorm/migrations/Migration.py b/src/masoniteorm/migrations/Migration.py index 5cd4bd402..512899fc8 100644 --- a/src/masoniteorm/migrations/Migration.py +++ b/src/masoniteorm/migrations/Migration.py @@ -281,18 +281,24 @@ def refresh(self, migration="all"): self.reset(migration) self.migrate(migration) - def drop_all_tables(self): + def drop_all_tables(self, ignore_fk=False): if self.command_class: self.command_class.line("Dropping all tables") + if ignore_fk: + self.schema.disable_foreign_key_constraints() + for table in self.schema.get_all_tables(): self.schema.drop(table) + if ignore_fk: + self.schema.enable_foreign_key_constraints() + if self.command_class: self.command_class.line("All tables dropped") - def fresh(self, migration="all"): - self.drop_all_tables() + def fresh(self, ignore_fk=False, migration="all"): + self.drop_all_tables(ignore_fk=ignore_fk) self.create_table_if_not_exists() if not self.get_unran_migrations(): diff --git a/src/masoniteorm/schema/Schema.py b/src/masoniteorm/schema/Schema.py index dfbc9f763..989cb6977 100644 --- a/src/masoniteorm/schema/Schema.py +++ b/src/masoniteorm/schema/Schema.py @@ -332,6 +332,9 @@ def has_table(self, table, query_only=False): def enable_foreign_key_constraints(self): sql = self.platform().enable_foreign_key_constraints() + if not sql: + return True + if self._dry: self._sql = sql return sql @@ -341,6 +344,9 @@ def enable_foreign_key_constraints(self): def disable_foreign_key_constraints(self): sql = self.platform().disable_foreign_key_constraints() + if not sql: + return True + if self._dry: self._sql = sql return sql From dbbdbc7a8451966e48f85bfe4c734dfe8fc10a8d Mon Sep 17 00:00:00 2001 From: Eduardo Aguad Date: Sat, 28 Oct 2023 22:09:32 -0300 Subject: [PATCH 108/254] revert hotfix --- src/masoniteorm/schema/Schema.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/masoniteorm/schema/Schema.py b/src/masoniteorm/schema/Schema.py index 989cb6977..dfbc9f763 100644 --- a/src/masoniteorm/schema/Schema.py +++ b/src/masoniteorm/schema/Schema.py @@ -332,9 +332,6 @@ def has_table(self, table, query_only=False): def enable_foreign_key_constraints(self): sql = self.platform().enable_foreign_key_constraints() - if not sql: - return True - if self._dry: self._sql = sql return sql @@ -344,9 +341,6 @@ def enable_foreign_key_constraints(self): def disable_foreign_key_constraints(self): sql = self.platform().disable_foreign_key_constraints() - if not sql: - return True - if self._dry: self._sql = sql return sql From 5abf3e2905238bc40e90d1809225db9605a3329c Mon Sep 17 00:00:00 2001 From: Benjamin Stout Date: Mon, 30 Oct 2023 12:57:30 -0500 Subject: [PATCH 109/254] Bump setup-python github action versions --- .github/workflows/pythonapp.yml | 4 ++-- .github/workflows/pythonpublish.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index 21ddd8d21..8867ecf58 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -35,7 +35,7 @@ jobs: steps: - uses: actions/checkout@v1 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -63,7 +63,7 @@ jobs: steps: - uses: actions/checkout@v1 - name: Set up Python 3.6 - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: 3.6 - name: Install Flake8 diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml index 662e29f05..bc1301967 100644 --- a/.github/workflows/pythonpublish.yml +++ b/.github/workflows/pythonpublish.yml @@ -37,7 +37,7 @@ jobs: steps: - uses: actions/checkout@v1 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From 879a9048176dcb274099a2fd976fe33881e14841 Mon Sep 17 00:00:00 2001 From: Matthew Altberg Date: Tue, 5 Dec 2023 12:24:36 -0500 Subject: [PATCH 110/254] Add optional config path to ConnectionResolver and ConnectionFactory --- src/masoniteorm/connections/ConnectionFactory.py | 9 +++++---- src/masoniteorm/connections/ConnectionResolver.py | 5 ++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/masoniteorm/connections/ConnectionFactory.py b/src/masoniteorm/connections/ConnectionFactory.py index 9b924a3db..b36292dd4 100644 --- a/src/masoniteorm/connections/ConnectionFactory.py +++ b/src/masoniteorm/connections/ConnectionFactory.py @@ -4,9 +4,10 @@ class ConnectionFactory: """Class for controlling the registration and creation of connection types.""" - _connections = { - # - } + _connections = {} + + def __init__(self, config_path=None): + self.config_path = config_path @classmethod def register(cls, key, connection): @@ -35,7 +36,7 @@ def make(self, key): masoniteorm.connection.BaseConnection -- Returns an instance of a BaseConnection class. """ - DB = load_config().DB + DB = load_config(config_path=self.config_path).DB connections = DB.get_connection_details() diff --git a/src/masoniteorm/connections/ConnectionResolver.py b/src/masoniteorm/connections/ConnectionResolver.py index f62eac302..0266f7516 100644 --- a/src/masoniteorm/connections/ConnectionResolver.py +++ b/src/masoniteorm/connections/ConnectionResolver.py @@ -6,7 +6,7 @@ class ConnectionResolver: _connections = {} _morph_map = {} - def __init__(self): + def __init__(self, config_path=None): from ..connections import ( SQLiteConnection, PostgresConnection, @@ -16,8 +16,7 @@ def __init__(self): from ..connections import ConnectionFactory - self.connection_factory = ConnectionFactory() - + self.connection_factory = ConnectionFactory(config_path=config_path) self.register(SQLiteConnection) self.register(PostgresConnection) self.register(MySQLConnection) From 7a09edd28b4646b4c01a848d3121c49d5a3eef46 Mon Sep 17 00:00:00 2001 From: Matthew Altberg Date: Tue, 5 Dec 2023 12:42:37 -0500 Subject: [PATCH 111/254] Add config_path to Schema and QueryBuilder classes --- src/masoniteorm/connections/ConnectionResolver.py | 1 + src/masoniteorm/query/QueryBuilder.py | 6 ++++-- src/masoniteorm/schema/Schema.py | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/masoniteorm/connections/ConnectionResolver.py b/src/masoniteorm/connections/ConnectionResolver.py index 0266f7516..f408c09c3 100644 --- a/src/masoniteorm/connections/ConnectionResolver.py +++ b/src/masoniteorm/connections/ConnectionResolver.py @@ -14,6 +14,7 @@ def __init__(self, config_path=None): MSSQLConnection, ) + self.config_path = config_path from ..connections import ConnectionFactory self.connection_factory = ConnectionFactory(config_path=config_path) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index fea2c6746..5bd886ec7 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -47,6 +47,7 @@ def __init__( scopes=None, schema=None, dry=False, + config_path=None ): """QueryBuilder initializer @@ -57,6 +58,7 @@ def __init__( connection {masoniteorm.connection.Connection} -- A connection class (default: {None}) table {str} -- the name of the table (default: {""}) """ + self.config_path = config_path self.grammar = grammar self.table(table) self.dry = dry @@ -103,7 +105,7 @@ def __init__( self.set_action("select") if not self._connection_details: - DB = load_config().DB + DB = load_config(config_path=self.config_path).DB self._connection_details = DB.get_connection_details() self.on(connection) @@ -382,7 +384,7 @@ def method(*args, **kwargs): ) def on(self, connection): - DB = load_config().DB + DB = load_config(self.config_path).DB if connection == "default": self.connection = self._connection_details.get("default") diff --git a/src/masoniteorm/schema/Schema.py b/src/masoniteorm/schema/Schema.py index 817872e9a..6d49f8864 100644 --- a/src/masoniteorm/schema/Schema.py +++ b/src/masoniteorm/schema/Schema.py @@ -57,6 +57,7 @@ def __init__( grammar=None, connection_details=None, schema=None, + config_path=None ): self._dry = dry self.connection = connection @@ -68,6 +69,7 @@ def __init__( self._blueprint = None self._sql = None self.schema = schema + self.config_path = config_path if not self.connection_class: self.on(self.connection) @@ -85,7 +87,7 @@ def on(self, connection_key): Returns: cls """ - DB = load_config().DB + DB = load_config(config_path=self.config_path).DB if connection_key == "default": self.connection = self.connection_details.get("default") From 541a8c7e6da3b3e9bcf6cd38043bd4018e41a0a3 Mon Sep 17 00:00:00 2001 From: jrolle Date: Tue, 5 Dec 2023 21:25:25 -0500 Subject: [PATCH 112/254] Added tests --- tests/sqlite/models/test_sqlite_model.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/sqlite/models/test_sqlite_model.py b/tests/sqlite/models/test_sqlite_model.py index 7e76116a8..5ff7619a3 100644 --- a/tests/sqlite/models/test_sqlite_model.py +++ b/tests/sqlite/models/test_sqlite_model.py @@ -83,6 +83,23 @@ def test_can_find_list(self): sql, """SELECT * FROM "users" WHERE "users"."id" IN ('1','2','3')""" ) + def test_find_or_if_record_not_found(self): + # Insane record number so record cannot be found + record_id = 1_000_000_000_000_000 + + result = User.find_or(record_id, lambda: "Record not found.") + self.assertEqual( + result, "Record not found." + ) + + def test_find_or_if_record_found(self): + record_id = 1 + result_id = User.find_or(record_id, lambda: "Record not found.").id + + self.assertEqual( + result_id, record_id + ) + def test_can_set_and_retreive_attribute(self): user = User.hydrate({"id": 1, "name": "joe", "customer_id": 1}) user.customer_id = "CUST1" From b9dd0475db290847e82a8e8e09d26b52d00173eb Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Tue, 5 Dec 2023 22:25:31 -0500 Subject: [PATCH 113/254] reverted #855 --- src/masoniteorm/models/Model.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index 254f4fd2a..bc9b0ff10 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -710,19 +710,9 @@ def update_or_create(cls, wheres, updates): total.update(updates) total.update(wheres) if not record: - # if we don't return fresh, we don't get the primary_key that has been used, - # and we can't call it from outside the function lest we get a QueryBuilder. - # - # Without this we are reduced to performing a DIY update_or_create, e.g.: - # ebay_order = EbayOrder.where({'order_id': d['order_id']}).first() - # if not ebay_order: - # ebay_order = EbayOrder.create(d).fresh() - # else: - # ebay_order.save() return self.create(total, id_key=cls.get_primary_key()).fresh() - rv = self.where(wheres).update(total) - return self.where(wheres).first() + return self.where(wheres).update(total) def relations_to_dict(self): """Converts a models relationships to a dictionary From 22a5c01a7f20d4c17421d8add19ac3f8b41980cb Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Tue, 5 Dec 2023 22:26:20 -0500 Subject: [PATCH 114/254] black formatted --- .../commands/MigrateStatusCommand.py | 12 +++++++-- src/masoniteorm/query/QueryBuilder.py | 5 ++-- src/masoniteorm/schema/Schema.py | 8 ++---- tests/models/test_models.py | 27 +++++-------------- tests/mysql/model/test_model.py | 27 ++++--------------- tests/sqlite/models/test_sqlite_model.py | 8 ++---- 6 files changed, 28 insertions(+), 59 deletions(-) diff --git a/src/masoniteorm/commands/MigrateStatusCommand.py b/src/masoniteorm/commands/MigrateStatusCommand.py index ea95e7fff..7ddbfdd32 100644 --- a/src/masoniteorm/commands/MigrateStatusCommand.py +++ b/src/masoniteorm/commands/MigrateStatusCommand.py @@ -30,12 +30,20 @@ def handle(self): batch = migration_data["batch"] migrations.append( - ["Y", f"{migration_file}", f"{batch}"] + [ + "Y", + f"{migration_file}", + f"{batch}", + ] ) for migration_file in migration.get_unran_migrations(): migrations.append( - ["N", f"{migration_file}", "-"] + [ + "N", + f"{migration_file}", + "-", + ] ) table.set_rows(migrations) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 07acade86..3a45a614a 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -9,7 +9,8 @@ HTTP404, ConnectionNotRegistered, ModelNotFound, - MultipleRecordsFound, InvalidArgument, + MultipleRecordsFound, + InvalidArgument, ) from ..expressions.expressions import ( AggregateExpression, @@ -47,7 +48,7 @@ def __init__( scopes=None, schema=None, dry=False, - config_path=None + config_path=None, ): """QueryBuilder initializer diff --git a/src/masoniteorm/schema/Schema.py b/src/masoniteorm/schema/Schema.py index e7bebc9d6..af1a17c37 100644 --- a/src/masoniteorm/schema/Schema.py +++ b/src/masoniteorm/schema/Schema.py @@ -57,7 +57,7 @@ def __init__( grammar=None, connection_details=None, schema=None, - config_path=None + config_path=None, ): self._dry = dry self.connection = connection @@ -306,11 +306,7 @@ def get_all_tables(self): result = self.new_connection().query(sql, ()) - return ( - list(map(lambda t: list(t.values())[0], result)) - if result - else [] - ) + return list(map(lambda t: list(t.values())[0], result)) if result else [] def has_table(self, table, query_only=False): """Checks if the a database has a specific table diff --git a/tests/models/test_models.py b/tests/models/test_models.py index a473224cf..520244fc2 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -19,30 +19,17 @@ class ModelTest(Model): class FillableModelTest(Model): - __fillable__ = [ - "due_date", - "is_vip", - ] + __fillable__ = ["due_date", "is_vip"] class InvalidFillableGuardedModelTest(Model): - __fillable__ = [ - "due_date", - ] - __guarded__ = [ - "is_vip", - "payload", - ] + __fillable__ = ["due_date"] + __guarded__ = ["is_vip", "payload"] class InvalidFillableGuardedChildModelTest(ModelTest): - __fillable__ = [ - "due_date", - ] - __guarded__ = [ - "is_vip", - "payload", - ] + __fillable__ = ["due_date"] + __guarded__ = ["is_vip", "payload"] class ModelTestForced(Model): @@ -147,9 +134,7 @@ def test_model_can_cast_dict_attributes(self): def test_valid_json_cast(self): model = ModelTest.hydrate( - { - "payload": {"this": "dict", "is": "usable", "as": "json"}, - } + {"payload": {"this": "dict", "is": "usable", "as": "json"}} ) self.assertEqual(type(model.payload), dict) diff --git a/tests/mysql/model/test_model.py b/tests/mysql/model/test_model.py index 6bc7623d1..99fd1ef89 100644 --- a/tests/mysql/model/test_model.py +++ b/tests/mysql/model/test_model.py @@ -113,10 +113,7 @@ def test_create_can_use_guarded_asterisk(self): # An asterisk guarded attribute excludes all fields from mass-assignment. # This would raise a DB error if there are any required fields. - self.assertEqual( - sql, - "INSERT INTO `profiles` (*) VALUES ()", - ) + self.assertEqual(sql, "INSERT INTO `profiles` (*) VALUES ()") def test_bulk_create_can_use_fillable(self): query_builder = ProfileFillable.bulk_create( @@ -173,8 +170,7 @@ def test_bulk_create_can_use_guarded_asterisk(self): # This would obviously raise an invalid SQL syntax error. # TODO: Raise a clearer error? self.assertEqual( - query_builder.to_sql(), - "INSERT INTO `profiles` () VALUES (), ()", + query_builder.to_sql(), "INSERT INTO `profiles` () VALUES (), ()" ) def test_update_can_use_fillable(self): @@ -214,10 +210,7 @@ def test_update_can_use_guarded_asterisk(self): # An asterisk guarded attribute excludes all fields from mass-assignment. # The query builder's sql should not have been altered in any way. - self.assertEqual( - query_builder.to_sql(), - initial_sql, - ) + self.assertEqual(query_builder.to_sql(), initial_sql) def test_table_name(self): table_name = Profile.get_table_name() @@ -250,12 +243,7 @@ def test_serialize_with_hidden(self): def test_serialize_with_visible(self): profile = ProfileSerializeWithVisible.hydrate( - { - "name": "Joe", - "id": 1, - "password": "secret", - "email": "joe@masonite.com", - } + {"name": "Joe", "id": 1, "password": "secret", "email": "joe@masonite.com"} ) self.assertTrue( {"name": "Joe", "email": "joe@masonite.com"}, profile.serialize() @@ -263,12 +251,7 @@ def test_serialize_with_visible(self): def test_serialize_with_visible_and_hidden_raise_error(self): profile = ProfileSerializeWithVisibleAndHidden.hydrate( - { - "name": "Joe", - "id": 1, - "password": "secret", - "email": "joe@masonite.com", - } + {"name": "Joe", "id": 1, "password": "secret", "email": "joe@masonite.com"} ) with self.assertRaises(AttributeError): profile.serialize() diff --git a/tests/sqlite/models/test_sqlite_model.py b/tests/sqlite/models/test_sqlite_model.py index 3a30e4a9e..c833c1137 100644 --- a/tests/sqlite/models/test_sqlite_model.py +++ b/tests/sqlite/models/test_sqlite_model.py @@ -87,17 +87,13 @@ def test_find_or_if_record_not_found(self): record_id = 1_000_000_000_000_000 result = User.find_or(record_id, lambda: "Record not found.") - self.assertEqual( - result, "Record not found." - ) + self.assertEqual(result, "Record not found.") def test_find_or_if_record_found(self): record_id = 1 result_id = User.find_or(record_id, lambda: "Record not found.").id - self.assertEqual( - result_id, record_id - ) + self.assertEqual(result_id, record_id) def test_can_set_and_retreive_attribute(self): user = User.hydrate({"id": 1, "name": "joe", "customer_id": 1}) From e8c6a506aac286269c6b22ca783a27607596b3e0 Mon Sep 17 00:00:00 2001 From: jrolle Date: Sat, 9 Dec 2023 13:10:13 -0500 Subject: [PATCH 115/254] Added method "only" to model class. --- src/masoniteorm/models/Model.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index bc9b0ff10..d9c9e084d 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -810,6 +810,12 @@ def method(*args, **kwargs): return None + def only(self, attributes: list[str]): + results: dict[str, Any] = {} + for attribute in attributes: + results[attribute] = self.get_raw_attribute(attribute) + return results + def __setattr__(self, attribute, value): if hasattr(self, "set_" + attribute + "_attribute"): method = getattr(self, "set_" + attribute + "_attribute") From 5da542390adf50c91635f06776b238d0ed3ba301 Mon Sep 17 00:00:00 2001 From: jrolle Date: Sat, 9 Dec 2023 13:44:59 -0500 Subject: [PATCH 116/254] updated only to support aliasing attribute names. This can be useful when mapping return data into new datasources which expect different naming conventions. This could be done at the database query level but is much easier to do using this method and only adds few extra LOC. --- src/masoniteorm/models/Model.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index d9c9e084d..5967874f6 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -5,7 +5,7 @@ from datetime import datetime from datetime import time as datetimetime from decimal import Decimal -from typing import Any, Dict +from typing import Any, Dict, List import pendulum from inflection import tableize, underscore @@ -810,10 +810,19 @@ def method(*args, **kwargs): return None - def only(self, attributes: list[str]): + def only(self, attributes: List[str]) -> dict: results: dict[str, Any] = {} for attribute in attributes: - results[attribute] = self.get_raw_attribute(attribute) + if " as " in attribute: + attribute, alias = attribute.split(" as ") + alias = alias.strip() + attribute = attribute.strip() + else: + alias = attribute.strip() + attribute = attribute.strip() + + results[alias] = self.get_raw_attribute(attribute) + return results def __setattr__(self, attribute, value): From bf8ff3f017f9bcb173ab1e1c7c3f9bc26604958c Mon Sep 17 00:00:00 2001 From: Jarriq Rolle <36413952+JarriqTheTechie@users.noreply.github.com> Date: Sat, 9 Dec 2023 14:01:11 -0500 Subject: [PATCH 117/254] Update Model.py --- src/masoniteorm/models/Model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index 5967874f6..6d06a9588 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -5,7 +5,7 @@ from datetime import datetime from datetime import time as datetimetime from decimal import Decimal -from typing import Any, Dict, List +from typing import Any, Dict import pendulum from inflection import tableize, underscore @@ -810,7 +810,7 @@ def method(*args, **kwargs): return None - def only(self, attributes: List[str]) -> dict: + def only(self, attributes) -> dict: results: dict[str, Any] = {} for attribute in attributes: if " as " in attribute: From 57aa5cfac1bff5b4db64bdaee672025ed89e2ee2 Mon Sep 17 00:00:00 2001 From: Joseph Mancuso Date: Sun, 10 Dec 2023 12:59:29 -0500 Subject: [PATCH 118/254] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 287a3b137..592f7b167 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version="2.19.2", + version="2.20.0", package_dir={"": "src"}, description="The Official Masonite ORM", long_description=long_description, From 1492f7a32695e1ddeb56bf6aefe64c79cbecb405 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 3 Jan 2024 21:18:03 -0500 Subject: [PATCH 119/254] fixed issue. needs refactoring --- cc.py | 7 +++++-- config/{test-database.py => database.py} | 0 pytest.ini | 2 +- src/masoniteorm/connections/BaseConnection.py | 2 +- src/masoniteorm/query/QueryBuilder.py | 14 ++++++++++++-- src/masoniteorm/scopes/TimeStampsScope.py | 8 +++++++- tests/postgres/grammar/test_update_grammar.py | 8 ++++++++ 7 files changed, 34 insertions(+), 7 deletions(-) rename config/{test-database.py => database.py} (100%) diff --git a/cc.py b/cc.py index 44b2a5e48..5193c8e11 100644 --- a/cc.py +++ b/cc.py @@ -16,8 +16,9 @@ # print(builder.where("id", 1).or_where(lambda q: q.where('id', 2).or_where('id', 3)).get()) class User(Model): - __connection__ = "sqlite" + __connection__ = "mysql" __table__ = "users" + __dates__ = ["verified_at"] @has_many("id", "user_id") def articles(self): @@ -28,7 +29,9 @@ class Article(Model): # user = User.create({"name": "phill", "email": "phill"}) # print(inspect.isclass(User)) -print(User.find(1).with_("articles").first().serialize()) +user = User.first() +user.update({"verified_at": None, "updated_at": None}) +print(user.first().serialize()) # print(user.serialize()) # print(User.first()) \ No newline at end of file diff --git a/config/test-database.py b/config/database.py similarity index 100% rename from config/test-database.py rename to config/database.py diff --git a/pytest.ini b/pytest.ini index fb1fb3add..10ecefaf6 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,3 @@ [pytest] env = - D:DB_CONFIG_PATH=config/test-database \ No newline at end of file + D:DB_CONFIG_PATH=config/database \ No newline at end of file diff --git a/src/masoniteorm/connections/BaseConnection.py b/src/masoniteorm/connections/BaseConnection.py index e6c6f8c6c..ca4cf4076 100644 --- a/src/masoniteorm/connections/BaseConnection.py +++ b/src/masoniteorm/connections/BaseConnection.py @@ -39,7 +39,7 @@ def statement(self, query, bindings=()): raise AttributeError( f"Must set the _cursor attribute on the {self.__class__.__name__} class before calling the 'statement' method." ) - + print('qq', query, bindings) self._cursor.execute(query, bindings) end = "{:.2f}".format(timer() - start) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 3a45a614a..a3830b1f4 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -1441,13 +1441,21 @@ def update( date_fields = model.get_dates() for key, value in updates.items(): if key in date_fields: - updates[key] = model.get_new_datetime_string(value) + if value: + updates[key] = model.get_new_datetime_string(value) + else: + updates[key] = value # Cast value if necessary if cast: - updates[key] = model.cast_value(value) + if value: + updates[key] = model.cast_value(value) + else: + updates[key] = value elif not updates: # Do not perform query if there are no updates return self + + print('ooo', updates) self._updates = (UpdateQueryExpression(updates),) self.set_action("update") @@ -2111,6 +2119,8 @@ def to_qmark(self): sql = grammar.compile(self._action, qmark=True).to_sql() + + self._bindings = grammar._bindings self.reset() diff --git a/src/masoniteorm/scopes/TimeStampsScope.py b/src/masoniteorm/scopes/TimeStampsScope.py index 5e4551cc4..5f0e2c7b7 100644 --- a/src/masoniteorm/scopes/TimeStampsScope.py +++ b/src/masoniteorm/scopes/TimeStampsScope.py @@ -34,7 +34,13 @@ def set_timestamp_create(self, builder): def set_timestamp_update(self, builder): if not builder._model.__timestamps__: return builder - + + print(builder._updates[0]) + for update in builder._updates: + if builder._model.date_updated_at in update.column: + print("not updating this is right") + return + print("still updating. this is wrong") builder._updates += ( UpdateQueryExpression( { diff --git a/tests/postgres/grammar/test_update_grammar.py b/tests/postgres/grammar/test_update_grammar.py index 15a70b624..ba324ea77 100644 --- a/tests/postgres/grammar/test_update_grammar.py +++ b/tests/postgres/grammar/test_update_grammar.py @@ -71,6 +71,14 @@ def test_raw_expression(self): self.assertEqual(to_sql, sql) + def test_update_null(self): + to_sql = self.builder.update({"name": None}, dry=True).to_sql() + print(to_sql) + + sql = "" + + self.assertEqual(to_sql, sql) + class TestPostgresUpdateGrammar(BaseTestCaseUpdateGrammar, unittest.TestCase): grammar = "postgres" From 7297bac95b0b3e7087934e1321b694f4405c28e0 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 3 Jan 2024 21:22:47 -0500 Subject: [PATCH 120/254] removed prints --- src/masoniteorm/connections/BaseConnection.py | 1 - src/masoniteorm/query/QueryBuilder.py | 2 -- src/masoniteorm/scopes/TimeStampsScope.py | 3 --- 3 files changed, 6 deletions(-) diff --git a/src/masoniteorm/connections/BaseConnection.py b/src/masoniteorm/connections/BaseConnection.py index ca4cf4076..c3256c6b7 100644 --- a/src/masoniteorm/connections/BaseConnection.py +++ b/src/masoniteorm/connections/BaseConnection.py @@ -39,7 +39,6 @@ def statement(self, query, bindings=()): raise AttributeError( f"Must set the _cursor attribute on the {self.__class__.__name__} class before calling the 'statement' method." ) - print('qq', query, bindings) self._cursor.execute(query, bindings) end = "{:.2f}".format(timer() - start) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index a3830b1f4..651d50d80 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -1454,8 +1454,6 @@ def update( elif not updates: # Do not perform query if there are no updates return self - - print('ooo', updates) self._updates = (UpdateQueryExpression(updates),) self.set_action("update") diff --git a/src/masoniteorm/scopes/TimeStampsScope.py b/src/masoniteorm/scopes/TimeStampsScope.py index 5f0e2c7b7..fa2920421 100644 --- a/src/masoniteorm/scopes/TimeStampsScope.py +++ b/src/masoniteorm/scopes/TimeStampsScope.py @@ -35,12 +35,9 @@ def set_timestamp_update(self, builder): if not builder._model.__timestamps__: return builder - print(builder._updates[0]) for update in builder._updates: if builder._model.date_updated_at in update.column: - print("not updating this is right") return - print("still updating. this is wrong") builder._updates += ( UpdateQueryExpression( { From b44e74f19e124bbfc360753ab22194599ad76bc6 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 3 Jan 2024 21:26:53 -0500 Subject: [PATCH 121/254] fixed php ini --- config/{database.py => test-database.py} | 0 pytest.ini | 2 +- tests/postgres/grammar/test_update_grammar.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename config/{database.py => test-database.py} (100%) diff --git a/config/database.py b/config/test-database.py similarity index 100% rename from config/database.py rename to config/test-database.py diff --git a/pytest.ini b/pytest.ini index 10ecefaf6..fb1fb3add 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,3 @@ [pytest] env = - D:DB_CONFIG_PATH=config/database \ No newline at end of file + D:DB_CONFIG_PATH=config/test-database \ No newline at end of file diff --git a/tests/postgres/grammar/test_update_grammar.py b/tests/postgres/grammar/test_update_grammar.py index ba324ea77..76d19f7f0 100644 --- a/tests/postgres/grammar/test_update_grammar.py +++ b/tests/postgres/grammar/test_update_grammar.py @@ -75,7 +75,7 @@ def test_update_null(self): to_sql = self.builder.update({"name": None}, dry=True).to_sql() print(to_sql) - sql = "" + sql = """UPDATE "users" SET "name" = \'None\'""" self.assertEqual(to_sql, sql) From 0afd57eb4deedfa7058932ee1de7f21380db840d Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 3 Jan 2024 21:28:39 -0500 Subject: [PATCH 122/254] formatted --- src/masoniteorm/query/QueryBuilder.py | 2 -- src/masoniteorm/scopes/TimeStampsScope.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 651d50d80..52f81b0a0 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -2117,8 +2117,6 @@ def to_qmark(self): sql = grammar.compile(self._action, qmark=True).to_sql() - - self._bindings = grammar._bindings self.reset() diff --git a/src/masoniteorm/scopes/TimeStampsScope.py b/src/masoniteorm/scopes/TimeStampsScope.py index fa2920421..e9da387e9 100644 --- a/src/masoniteorm/scopes/TimeStampsScope.py +++ b/src/masoniteorm/scopes/TimeStampsScope.py @@ -34,7 +34,7 @@ def set_timestamp_create(self, builder): def set_timestamp_update(self, builder): if not builder._model.__timestamps__: return builder - + for update in builder._updates: if builder._model.date_updated_at in update.column: return From ea7f06e85153a060711ff4c12ad9b0ee2cf9f4bb Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 3 Jan 2024 21:35:50 -0500 Subject: [PATCH 123/254] format --- src/masoniteorm/connections/BaseConnection.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/masoniteorm/connections/BaseConnection.py b/src/masoniteorm/connections/BaseConnection.py index c3256c6b7..e6c6f8c6c 100644 --- a/src/masoniteorm/connections/BaseConnection.py +++ b/src/masoniteorm/connections/BaseConnection.py @@ -39,6 +39,7 @@ def statement(self, query, bindings=()): raise AttributeError( f"Must set the _cursor attribute on the {self.__class__.__name__} class before calling the 'statement' method." ) + self._cursor.execute(query, bindings) end = "{:.2f}".format(timer() - start) From b9ba91d57a256d83c613c73b0b574594126d51a8 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 3 Jan 2024 22:21:57 -0500 Subject: [PATCH 124/254] added check for strings --- src/masoniteorm/models/Model.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index 6d06a9588..052f08ee6 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -810,7 +810,9 @@ def method(*args, **kwargs): return None - def only(self, attributes) -> dict: + def only(self, attributes: list) -> dict: + if isinstance(attributes, str): + attributes = [attributes] results: dict[str, Any] = {} for attribute in attributes: if " as " in attribute: From 427fc9deabca6c425a9a86a269acc4da8c5679c9 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 3 Jan 2024 22:27:04 -0500 Subject: [PATCH 125/254] added tests --- tests/models/test_models.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/models/test_models.py b/tests/models/test_models.py index 520244fc2..353884de9 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -186,6 +186,15 @@ def test_force_update_on_model_class(self): self.assertIn("username", sql) self.assertIn("name", sql) + def test_only_method(self): + model = ModelTestForced.hydrate( + {"id": 1, "username": "joe", "name": "Joe", "admin": True} + ) + + + self.assertEquals({"username": "joe"}, model.only("username")) + self.assertEquals({"username": "joe"}, model.only(["username"])) + def test_model_update_without_changes_at_all(self): model = ModelTest.hydrate( {"id": 1, "username": "joe", "name": "Joe", "admin": True} From 1a3b0c46fbc47062ddcdebdc406ac89f242b8078 Mon Sep 17 00:00:00 2001 From: pmn4 Date: Mon, 12 Feb 2024 15:59:32 -0500 Subject: [PATCH 126/254] BaseRelationship: s/self/instance --- CONTRIBUTING.md | 2 +- .../relationships/BaseRelationship.py | 2 +- .../test_mssql_query_builder_relationships.py | 28 +++++++++++++++++++ tests/mysql/model/test_model.py | 2 +- 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 654a27f10..2a118b384 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ If you are interested in the project then it would be a great idea to read the " ## Issues -Everything really should start with opening an issue or finding an issue. If you feel you have an idea for how the project can be improved, no matter how small, you should open an issue so we can have an open dicussion with the maintainers of the project. +Everything really should start with opening an issue or finding an issue. If you feel you have an idea for how the project can be improved, no matter how small, you should open an issue so we can have an open discussion with the maintainers of the project. We can discuss in that issue the solution to the problem or feature you have. If we do not feel it fits within the project then we will close the issue. Feel free to open a new issue if new information comes up. diff --git a/src/masoniteorm/relationships/BaseRelationship.py b/src/masoniteorm/relationships/BaseRelationship.py index 6b9475f62..7767199aa 100644 --- a/src/masoniteorm/relationships/BaseRelationship.py +++ b/src/masoniteorm/relationships/BaseRelationship.py @@ -52,7 +52,7 @@ def __get__(self, instance, owner): object -- Either returns a builder or a hydrated model. """ attribute = self.fn.__name__ - relationship = self.fn(self)() + relationship = self.fn(instance)() self.set_keys(instance, attribute) self._related_builder = relationship.builder diff --git a/tests/mssql/builder/test_mssql_query_builder_relationships.py b/tests/mssql/builder/test_mssql_query_builder_relationships.py index 4d36b3f07..b4fb5566e 100644 --- a/tests/mssql/builder/test_mssql_query_builder_relationships.py +++ b/tests/mssql/builder/test_mssql_query_builder_relationships.py @@ -40,6 +40,14 @@ def articles(self): def profile(self): return Profile + @belongs_to("id", "parent_dynamic_id") + def parent_dynamic(self): + return self.__class__ + + @belongs_to("id", "parent_specified_id") + def parent_specified(self): + return User + class BaseTestQueryRelationships(unittest.TestCase): maxDiff = None @@ -64,6 +72,26 @@ def test_has(self): """)""", ) + def test_has_reference_to_self(self): + builder = self.get_builder() + sql = builder.has("parent_dynamic").to_sql() + self.assertEqual( + sql, + """SELECT * FROM [users] WHERE EXISTS (""" + """SELECT * FROM [users] WHERE [users].[parent_dynamic_id] = [users].[id]""" + """)""", + ) + + def test_has_reference_to_self_using_class(self): + builder = self.get_builder() + sql = builder.has("parent_specified").to_sql() + self.assertEqual( + sql, + """SELECT * FROM [users] WHERE EXISTS (""" + """SELECT * FROM [users] WHERE [users].[parent_specified_id] = [users].[id]""" + """)""", + ) + def test_where_has_query(self): builder = self.get_builder() sql = builder.where_has("articles", lambda q: q.where("active", 1)).to_sql() diff --git a/tests/mysql/model/test_model.py b/tests/mysql/model/test_model.py index 99fd1ef89..06395b0e8 100644 --- a/tests/mysql/model/test_model.py +++ b/tests/mysql/model/test_model.py @@ -316,7 +316,7 @@ def test_attribute_check_with_hasattr(self): self.assertFalse(hasattr(Profile(), "__password__")) -if os.getenv("RUN_MYSQL_DATABASE", None).lower() == "true": +if os.getenv("RUN_MYSQL_DATABASE", "false").lower() == "true": class MysqlTestModel(unittest.TestCase): # TODO: these tests aren't getting run in CI... is that intentional? From 378b8c37c1b8768350795cbfa59f6c29ba6d0861 Mon Sep 17 00:00:00 2001 From: Felipe Hertzer Date: Fri, 23 Feb 2024 19:36:51 +1100 Subject: [PATCH 127/254] Mysql close the connection on rollback --- src/masoniteorm/connections/MySQLConnection.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/masoniteorm/connections/MySQLConnection.py b/src/masoniteorm/connections/MySQLConnection.py index 386145db6..39419650e 100644 --- a/src/masoniteorm/connections/MySQLConnection.py +++ b/src/masoniteorm/connections/MySQLConnection.py @@ -124,6 +124,9 @@ def rollback(self): """Transaction""" self._connection.rollback() self.transaction_level -= 1 + if self.get_transaction_level() <= 0: + self.open = 0 + self._connection.close() def get_transaction_level(self): """Transaction""" From 6f44e9bdde0e762825c52d4a384c945ccf25adaf Mon Sep 17 00:00:00 2001 From: Amir Hosein Salimi Date: Sun, 3 Mar 2024 23:22:43 +0330 Subject: [PATCH 128/254] feat: add `Collection.min` method --- src/masoniteorm/collection/Collection.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/masoniteorm/collection/Collection.py b/src/masoniteorm/collection/Collection.py index 44d84f349..6516c6294 100644 --- a/src/masoniteorm/collection/Collection.py +++ b/src/masoniteorm/collection/Collection.py @@ -109,6 +109,26 @@ def max(self, key=None): except (TypeError, ValueError): pass return result + + def min(self, key=None): + """Returns the min of the items. + + If a key is given it will return the min of all the values of the key. + + Keyword Arguments: + key {string} -- The key to use to find the min of all the values of that key. (default: {None}) + + Returns: + int -- Returns the min. + """ + result = 0 + items = self._get_value(key) or self._items + + try: + return min(items) + except (TypeError, ValueError): + pass + return result def chunk(self, size: int): """Chunks the items. From 61749ccb8be348e44cc62c70c386ec427e09460d Mon Sep 17 00:00:00 2001 From: Amir Hosein Salimi Date: Sun, 3 Mar 2024 23:16:30 +0330 Subject: [PATCH 129/254] fix: fix a typo in docs of `Collection.max` --- src/masoniteorm/collection/Collection.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/masoniteorm/collection/Collection.py b/src/masoniteorm/collection/Collection.py index 44d84f349..8b4083276 100644 --- a/src/masoniteorm/collection/Collection.py +++ b/src/masoniteorm/collection/Collection.py @@ -91,15 +91,15 @@ def avg(self, key=None): return result def max(self, key=None): - """Returns the average of the items. + """Returns the max of the items. - If a key is given it will return the average of all the values of the key. + If a key is given it will return the max of all the values of the key. Keyword Arguments: - key {string} -- The key to use to find the average of all the values of that key. (default: {None}) + key {string} -- The key to use to find the max of all the values of that key. (default: {None}) Returns: - int -- Returns the average. + int -- Returns the max. """ result = 0 items = self._get_value(key) or self._items From 3bc3406378a82dd22d92e0864dd6dd860e97e194 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Mon, 11 Mar 2024 21:09:11 -0400 Subject: [PATCH 130/254] added min test --- tests/collection/test_collection.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/collection/test_collection.py b/tests/collection/test_collection.py index 706666542..02bab4cf1 100644 --- a/tests/collection/test_collection.py +++ b/tests/collection/test_collection.py @@ -185,6 +185,24 @@ def test_max(self): collection = Collection([{"batch": 1}, {"batch": 1}]) self.assertEqual(collection.max("batch"), 1) + def test_min(self): + collection = Collection([1, 1, 2, 4]) + self.assertEqual(collection.min(), 1) + + collection = Collection( + [ + {"name": "Corentin All", "age": 1}, + {"name": "Corentin All", "age": 2}, + {"name": "Corentin All", "age": 3}, + {"name": "Corentin All", "age": 4}, + ] + ) + self.assertEqual(collection.min("age"), 1) + self.assertEqual(collection.min(), 0) + + collection = Collection([{"batch": 1}, {"batch": 1}]) + self.assertEqual(collection.min("batch"), 1) + def test_count(self): collection = Collection([1, 1, 2, 4]) self.assertEqual(collection.count(), 4) From cc30732d7ff7954ed3788a0377fee11f3d80aa33 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Mon, 11 Mar 2024 21:12:08 -0400 Subject: [PATCH 131/254] linted --- src/masoniteorm/collection/Collection.py | 2 +- tests/models/test_models.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/masoniteorm/collection/Collection.py b/src/masoniteorm/collection/Collection.py index 5a064acc5..4b81f69b3 100644 --- a/src/masoniteorm/collection/Collection.py +++ b/src/masoniteorm/collection/Collection.py @@ -109,7 +109,7 @@ def max(self, key=None): except (TypeError, ValueError): pass return result - + def min(self, key=None): """Returns the min of the items. diff --git a/tests/models/test_models.py b/tests/models/test_models.py index 353884de9..a842a2654 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -191,7 +191,6 @@ def test_only_method(self): {"id": 1, "username": "joe", "name": "Joe", "admin": True} ) - self.assertEquals({"username": "joe"}, model.only("username")) self.assertEquals({"username": "joe"}, model.only(["username"])) From f963387565e59d4d58fd0c140883ff57a981beaa Mon Sep 17 00:00:00 2001 From: Joseph Mancuso Date: Mon, 11 Mar 2024 21:17:47 -0400 Subject: [PATCH 132/254] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 592f7b167..dde49590c 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version="2.20.0", + version="2.21.0", package_dir={"": "src"}, description="The Official Masonite ORM", long_description=long_description, From 29426df8c0e53226db5aa9d025bc7818784bae21 Mon Sep 17 00:00:00 2001 From: Eduardo Aguad Date: Tue, 2 Apr 2024 20:03:17 -0300 Subject: [PATCH 133/254] wip: hasmany performance fix --- src/masoniteorm/query/QueryBuilder.py | 3 ++- src/masoniteorm/relationships/HasMany.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 52f81b0a0..839c46137 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -1951,10 +1951,11 @@ def _register_relationships_to_model( Returns: self """ + map_related = related_result.group_by(related.foreign_key) if isinstance(hydrated_model, Collection): for model in hydrated_model: if isinstance(related_result, Collection): - related.register_related(relation_key, model, related_result) + related.register_related(relation_key, model, map_related) else: model.add_relation({relation_key: related_result or None}) else: diff --git a/src/masoniteorm/relationships/HasMany.py b/src/masoniteorm/relationships/HasMany.py index 6ebfef249..8ffb8f2e4 100644 --- a/src/masoniteorm/relationships/HasMany.py +++ b/src/masoniteorm/relationships/HasMany.py @@ -28,5 +28,5 @@ def set_keys(self, owner, attribute): def register_related(self, key, model, collection): model.add_relation( - {key: collection.where(self.foreign_key, getattr(model, self.local_key))} + {key: collection.get(getattr(model, self.local_key))} ) From a6a5eea4169729867e510b23427d62800b8e8f8a Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Tue, 2 Apr 2024 20:51:46 -0400 Subject: [PATCH 134/254] fixed tests and made attempt --- orm.sqlite3 | Bin 135168 -> 147456 bytes src/masoniteorm/collection/Collection.py | 11 +++++++++-- src/masoniteorm/query/QueryBuilder.py | 6 ++++-- src/masoniteorm/relationships/BelongsTo.py | 7 +++---- src/masoniteorm/relationships/HasMany.py | 1 + tests/User.py | 2 +- tests/integrations/config/database.py | 2 +- ...test_sqlite_query_builder_eager_loading.py | 1 + 8 files changed, 20 insertions(+), 10 deletions(-) diff --git a/orm.sqlite3 b/orm.sqlite3 index a0b6a9b115ef09b0eb143509b9eb07f95bcc9f4a..f62e36cb514623c9e553a60d47d10ab6769e3b1d 100644 GIT binary patch delta 1780 zcmbtUTTIhX815;sYrBGICSzoWG7=U+$0nfjI-w8*IxIuN3$?ncQ+G+bVN6WMz-WB( zvWLBBG#C>TjWVjd-OqV3MMnXD>dAT_;S~B0^v@1#`W}WqrKbcPU&iSZlFG^j?8; z^~gX}K}ZFl48ThjTvEY76||HA@+9Zp0(_9()V#!3pd)_6_@ly~dtl53n0c4R8~dbL_SwjcUgyQoLiAj_=g*9XcM; z@$EXkO~?DHc$jzeRt0!RkB)EE@nAJyX0Ln{j zNOX9NQt}k9Sy8u(%%)>&jOY3z^hA^iOU}Sb=~7ZQEyNR?a0ZSu!((A)8jeuYup=Jx zbwcAMYESIZl)%T2CE;L%p`@Nl3(LMpE&Cut4N?q6_fr#)%qh!6Qn6}P?XnT8NUJtg zE6Y)lJsyw6_+(k$m(NAa&ZZ`?Fs*KT^Sb|2Jx zC$43+Z+?PS4)M9TQm?ipEp96`m>O}l*Q%1+rIItLladAM;jf%9NTxYaJe?9^#Wz2J ztLgt*BQ>pQi_3xA;tU3d#f(kD!FU|Lc_c8AlUR~~!+%;S$xvbb1bKK%^-s~t)&(75-E6*-2E6do#JBcxTItvGr gJximABKvghPmGhd-{N43V4R%5$g$mui|H6E0GiB4I{*Lx diff --git a/src/masoniteorm/collection/Collection.py b/src/masoniteorm/collection/Collection.py index 4b81f69b3..e5b3f960e 100644 --- a/src/masoniteorm/collection/Collection.py +++ b/src/masoniteorm/collection/Collection.py @@ -40,6 +40,8 @@ def first(self, callback=None): if callback: filtered = self.filter(callback) response = None + + # print(filtered._items) if filtered: response = filtered[0] return response @@ -343,7 +345,7 @@ def _serialize(item): def add_relation(self, result=None): for model in self._items: - model.add_relations(result or {}) + model.add_relation(result or {}) return self @@ -532,8 +534,13 @@ def __eq__(self, other): def __getitem__(self, item): if isinstance(item, slice): return self.__class__(self._items[item]) + if isinstance(item, dict): + return self._items.get(item, None) - return self._items[item] + try: + return self._items[item] + except KeyError: + return None def __setitem__(self, key, value): self._items[key] = value diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 839c46137..6a9bb5763 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -1951,14 +1951,16 @@ def _register_relationships_to_model( Returns: self """ - map_related = related_result.group_by(related.foreign_key) - if isinstance(hydrated_model, Collection): + if related_result and isinstance(hydrated_model, Collection): + map_related = related_result.group_by(related.foreign_key) for model in hydrated_model: if isinstance(related_result, Collection): + print("aa", related) related.register_related(relation_key, model, map_related) else: model.add_relation({relation_key: related_result or None}) else: + print("heeee", related_result) hydrated_model.add_relation({relation_key: related_result or None}) return self diff --git a/src/masoniteorm/relationships/BelongsTo.py b/src/masoniteorm/relationships/BelongsTo.py index 92c18ed3d..8b54ff6cc 100644 --- a/src/masoniteorm/relationships/BelongsTo.py +++ b/src/masoniteorm/relationships/BelongsTo.py @@ -63,8 +63,7 @@ def get_related(self, query, relation, eagers=(), callback=None): ).first() def register_related(self, key, model, collection): - related = collection.where( - self.foreign_key, getattr(model, self.local_key) - ).first() + print('ooooo', collection._items, "tttt", model.serialize(), model, getattr(model, self.local_key)) + related = collection.get(getattr(model, self.local_key), None) - model.add_relation({key: related or None}) + model.add_relation({key: related[0] if related else None}) diff --git a/src/masoniteorm/relationships/HasMany.py b/src/masoniteorm/relationships/HasMany.py index 8ffb8f2e4..5b25cc31c 100644 --- a/src/masoniteorm/relationships/HasMany.py +++ b/src/masoniteorm/relationships/HasMany.py @@ -27,6 +27,7 @@ def set_keys(self, owner, attribute): return self def register_related(self, key, model, collection): + print("zzzzzz", getattr(model, self.local_key), collection._items) model.add_relation( {key: collection.get(getattr(model, self.local_key))} ) diff --git a/tests/User.py b/tests/User.py index 771817112..452f0b4d6 100644 --- a/tests/User.py +++ b/tests/User.py @@ -8,7 +8,7 @@ class User(Model): __fillable__ = ["name", "email", "password"] - __connection__ = "mysql" + __connection__ = "t" __auth__ = "email" diff --git a/tests/integrations/config/database.py b/tests/integrations/config/database.py index 096918c0f..2b6f057ab 100644 --- a/tests/integrations/config/database.py +++ b/tests/integrations/config/database.py @@ -38,7 +38,7 @@ "log_queries": True, "propagate": False, }, - "t": {"driver": "sqlite", "database": "ormtestreg.sqlite3", "log_queries": True}, + "t": {"driver": "sqlite", "database": "orm.sqlite3", "log_queries": True}, "devprod": { "driver": "mysql", "host": os.getenv("MYSQL_DATABASE_HOST"), diff --git a/tests/sqlite/builder/test_sqlite_query_builder_eager_loading.py b/tests/sqlite/builder/test_sqlite_query_builder_eager_loading.py index 800e94405..0b81a4c93 100644 --- a/tests/sqlite/builder/test_sqlite_query_builder_eager_loading.py +++ b/tests/sqlite/builder/test_sqlite_query_builder_eager_loading.py @@ -96,4 +96,5 @@ def test_with_multiple_per_same_relation(self): builder = self.get_builder() result = User.with_("articles", "articles.logo").where("id", 1).first() self.assertTrue(result.serialize()["articles"]) + print("pppppp", result.serialize()["articles"][0]) self.assertTrue(result.serialize()["articles"][0]["logo"]) From 64585fb8527f3f3cc12381191d8433906a695301 Mon Sep 17 00:00:00 2001 From: Eduardo Aguad Date: Fri, 5 Apr 2024 22:44:01 -0300 Subject: [PATCH 135/254] fix: tests --- src/masoniteorm/collection/Collection.py | 1 - src/masoniteorm/query/QueryBuilder.py | 10 +++++++--- src/masoniteorm/relationships/BelongsTo.py | 1 - src/masoniteorm/relationships/HasMany.py | 3 +-- src/masoniteorm/relationships/MorphTo.py | 2 +- .../sqlite/relationships/test_sqlite_relationships.py | 3 --- 6 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/masoniteorm/collection/Collection.py b/src/masoniteorm/collection/Collection.py index e5b3f960e..e238db8a5 100644 --- a/src/masoniteorm/collection/Collection.py +++ b/src/masoniteorm/collection/Collection.py @@ -41,7 +41,6 @@ def first(self, callback=None): filtered = self.filter(callback) response = None - # print(filtered._items) if filtered: response = filtered[0] return response diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 6a9bb5763..2ce2e93d5 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -1952,18 +1952,22 @@ def _register_relationships_to_model( self """ if related_result and isinstance(hydrated_model, Collection): - map_related = related_result.group_by(related.foreign_key) + map_related = self._map_related(related_result, related) for model in hydrated_model: if isinstance(related_result, Collection): - print("aa", related) related.register_related(relation_key, model, map_related) else: model.add_relation({relation_key: related_result or None}) else: - print("heeee", related_result) hydrated_model.add_relation({relation_key: related_result or None}) return self + def _map_related(self, related_result, related): + if related.__class__.__name__ == 'MorphTo': + return related_result + + return related_result.group_by(related.get_foreign_key()) + def all(self, selects=[], query=False): """Returns all records from the table. diff --git a/src/masoniteorm/relationships/BelongsTo.py b/src/masoniteorm/relationships/BelongsTo.py index 8b54ff6cc..b92af1199 100644 --- a/src/masoniteorm/relationships/BelongsTo.py +++ b/src/masoniteorm/relationships/BelongsTo.py @@ -63,7 +63,6 @@ def get_related(self, query, relation, eagers=(), callback=None): ).first() def register_related(self, key, model, collection): - print('ooooo', collection._items, "tttt", model.serialize(), model, getattr(model, self.local_key)) related = collection.get(getattr(model, self.local_key), None) model.add_relation({key: related[0] if related else None}) diff --git a/src/masoniteorm/relationships/HasMany.py b/src/masoniteorm/relationships/HasMany.py index 5b25cc31c..a940df047 100644 --- a/src/masoniteorm/relationships/HasMany.py +++ b/src/masoniteorm/relationships/HasMany.py @@ -27,7 +27,6 @@ def set_keys(self, owner, attribute): return self def register_related(self, key, model, collection): - print("zzzzzz", getattr(model, self.local_key), collection._items) model.add_relation( - {key: collection.get(getattr(model, self.local_key))} + {key: collection.get(getattr(model, self.local_key)) or Collection()} ) diff --git a/src/masoniteorm/relationships/MorphTo.py b/src/masoniteorm/relationships/MorphTo.py index f0abcf445..84ba8bb75 100644 --- a/src/masoniteorm/relationships/MorphTo.py +++ b/src/masoniteorm/relationships/MorphTo.py @@ -50,7 +50,7 @@ def __get__(self, instance, owner): def __getattr__(self, attribute): relationship = self.fn(self)() - return getattr(relationship.builder, attribute) + return getattr(relationship._related_builder, attribute) def apply_query(self, builder, instance): """Apply the query and return a dictionary to be hydrated diff --git a/tests/sqlite/relationships/test_sqlite_relationships.py b/tests/sqlite/relationships/test_sqlite_relationships.py index b182e8451..d88039ff9 100644 --- a/tests/sqlite/relationships/test_sqlite_relationships.py +++ b/tests/sqlite/relationships/test_sqlite_relationships.py @@ -1,11 +1,8 @@ -import os import unittest - from src.masoniteorm.models import Model from src.masoniteorm.relationships import belongs_to, has_many, has_one, belongs_to_many from tests.integrations.config.database import DB - class Profile(Model): __table__ = "profiles" __connection__ = "dev" From 9393f8e47b765f198b28d8c391808e81c5530f03 Mon Sep 17 00:00:00 2001 From: Eduardo Aguad Date: Fri, 5 Apr 2024 22:46:57 -0300 Subject: [PATCH 136/254] fix: foreign key call --- src/masoniteorm/query/QueryBuilder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 2ce2e93d5..95e40c050 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -1966,7 +1966,7 @@ def _map_related(self, related_result, related): if related.__class__.__name__ == 'MorphTo': return related_result - return related_result.group_by(related.get_foreign_key()) + return related_result.group_by(related.foreign_key) def all(self, selects=[], query=False): """Returns all records from the table. From 41c49c462ad7c9bd91cf527a0d7bd3cb3857b0f3 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sat, 6 Apr 2024 09:18:15 -0400 Subject: [PATCH 137/254] removed print --- tests/sqlite/builder/test_sqlite_query_builder_eager_loading.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/sqlite/builder/test_sqlite_query_builder_eager_loading.py b/tests/sqlite/builder/test_sqlite_query_builder_eager_loading.py index 0b81a4c93..800e94405 100644 --- a/tests/sqlite/builder/test_sqlite_query_builder_eager_loading.py +++ b/tests/sqlite/builder/test_sqlite_query_builder_eager_loading.py @@ -96,5 +96,4 @@ def test_with_multiple_per_same_relation(self): builder = self.get_builder() result = User.with_("articles", "articles.logo").where("id", 1).first() self.assertTrue(result.serialize()["articles"]) - print("pppppp", result.serialize()["articles"][0]) self.assertTrue(result.serialize()["articles"][0]["logo"]) From 6f93468ea3b903611d0583af25c208238a264b45 Mon Sep 17 00:00:00 2001 From: Joseph Mancuso Date: Sat, 6 Apr 2024 09:20:43 -0400 Subject: [PATCH 138/254] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index dde49590c..85d4cfbab 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version="2.21.0", + version="2.22.0", package_dir={"": "src"}, description="The Official Masonite ORM", long_description=long_description, From 2916fc48d21c2ebbb21482944390ac2ca0f9ede5 Mon Sep 17 00:00:00 2001 From: Martins Zemitis Date: Wed, 1 May 2024 14:01:16 +0300 Subject: [PATCH 139/254] add configuration option to enable or disable foreign keys check for all connections --- src/masoniteorm/connections/BaseConnection.py | 10 ++++++++++ src/masoniteorm/connections/MSSQLConnection.py | 2 ++ src/masoniteorm/connections/MySQLConnection.py | 3 +++ src/masoniteorm/connections/PostgresConnection.py | 2 ++ src/masoniteorm/connections/SQLiteConnection.py | 3 +++ tests/integrations/config/database.py | 2 +- 6 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/masoniteorm/connections/BaseConnection.py b/src/masoniteorm/connections/BaseConnection.py index e6c6f8c6c..059fae766 100644 --- a/src/masoniteorm/connections/BaseConnection.py +++ b/src/masoniteorm/connections/BaseConnection.py @@ -76,3 +76,13 @@ def select_many(self, query, bindings, amount): yield result result = self.format_cursor_results(self._cursor.fetchmany(amount)) + + def enable_disable_foreign_keys(self): + foreign_keys = self.full_details.get("foreign_keys") + platform = self.get_default_platform()() + + if foreign_keys: + self._connection.execute(platform.enable_foreign_key_constraints()) + elif foreign_keys is not None: + self._connection.execute(platform.disable_foreign_key_constraints()) + diff --git a/src/masoniteorm/connections/MSSQLConnection.py b/src/masoniteorm/connections/MSSQLConnection.py index 121a2eff7..d4024a6df 100644 --- a/src/masoniteorm/connections/MSSQLConnection.py +++ b/src/masoniteorm/connections/MSSQLConnection.py @@ -70,6 +70,8 @@ def make_connection(self): autocommit=True, ) + self.enable_disable_foreign_keys() + self.open = 1 return self diff --git a/src/masoniteorm/connections/MySQLConnection.py b/src/masoniteorm/connections/MySQLConnection.py index 39419650e..5d0c21754 100644 --- a/src/masoniteorm/connections/MySQLConnection.py +++ b/src/masoniteorm/connections/MySQLConnection.py @@ -78,6 +78,9 @@ def make_connection(self): db=self.database, **self.options ) + + self.enable_disable_foreign_keys() + self.open = 1 return self diff --git a/src/masoniteorm/connections/PostgresConnection.py b/src/masoniteorm/connections/PostgresConnection.py index 3174933ed..19919d95a 100644 --- a/src/masoniteorm/connections/PostgresConnection.py +++ b/src/masoniteorm/connections/PostgresConnection.py @@ -69,6 +69,8 @@ def make_connection(self): self._connection.autocommit = True + self.enable_disable_foreign_keys() + self.open = 1 return self diff --git a/src/masoniteorm/connections/SQLiteConnection.py b/src/masoniteorm/connections/SQLiteConnection.py index 9b8228d41..24bd7f0d8 100644 --- a/src/masoniteorm/connections/SQLiteConnection.py +++ b/src/masoniteorm/connections/SQLiteConnection.py @@ -63,6 +63,9 @@ def make_connection(self): self._connection.create_function("REGEXP", 2, regexp) self._connection.row_factory = sqlite3.Row + + self.enable_disable_foreign_keys() + self.open = 1 return self diff --git a/tests/integrations/config/database.py b/tests/integrations/config/database.py index 2b6f057ab..fe4473fa9 100644 --- a/tests/integrations/config/database.py +++ b/tests/integrations/config/database.py @@ -38,7 +38,7 @@ "log_queries": True, "propagate": False, }, - "t": {"driver": "sqlite", "database": "orm.sqlite3", "log_queries": True}, + "t": {"driver": "sqlite", "database": "orm.sqlite3", "log_queries": True, "foreign_keys": True}, "devprod": { "driver": "mysql", "host": os.getenv("MYSQL_DATABASE_HOST"), From f670552befbcd9b7197daf5274f3fa646016e28f Mon Sep 17 00:00:00 2001 From: Kyrela Date: Wed, 19 Jun 2024 22:38:38 +0200 Subject: [PATCH 140/254] fix: values in compile_alter_sql --- src/masoniteorm/schema/platforms/MySQLPlatform.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/masoniteorm/schema/platforms/MySQLPlatform.py b/src/masoniteorm/schema/platforms/MySQLPlatform.py index a39d9a773..07b3a1742 100644 --- a/src/masoniteorm/schema/platforms/MySQLPlatform.py +++ b/src/masoniteorm/schema/platforms/MySQLPlatform.py @@ -176,11 +176,16 @@ def compile_alter_sql(self, table): else: default = "" + column_constraint = "" + if column.column_type == "enum": + values = ", ".join(f"'{x}'" for x in column.values) + column_constraint = f"({values})" add_columns.append( self.add_column_string() .format( name=self.get_column_string().format(column=column.name), data_type=self.type_map.get(column.column_type, ""), + column_constraint=column_constraint, length=length, constraint="PRIMARY KEY" if column.primary else "", nullable="NULL" if column.is_null else "NOT NULL", @@ -333,14 +338,14 @@ def compile_alter_sql(self, table): def add_column_string(self): return ( - "ADD {name} {data_type}{length}{signed} {nullable}{default}{after}{comment}" + "ADD {name} {data_type}{length}{column_constraint}{signed} {nullable}{default}{after}{comment}" ) def drop_column_string(self): return "DROP COLUMN {name}" def change_column_string(self): - return "MODIFY {name}{data_type}{length} {nullable}{default} {constraint}" + return "MODIFY {name}{data_type}{length}{column_constraint} {nullable}{default} {constraint}" def rename_column_string(self): return "CHANGE {old} {to}" From fff45ac97dd71aa9602939293bd178c214a34c9b Mon Sep 17 00:00:00 2001 From: Kyrela Date: Wed, 19 Jun 2024 23:02:42 +0200 Subject: [PATCH 141/254] lint: remove additional blank line at end of file --- src/masoniteorm/connections/BaseConnection.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/masoniteorm/connections/BaseConnection.py b/src/masoniteorm/connections/BaseConnection.py index 059fae766..eca5dabb2 100644 --- a/src/masoniteorm/connections/BaseConnection.py +++ b/src/masoniteorm/connections/BaseConnection.py @@ -85,4 +85,3 @@ def enable_disable_foreign_keys(self): self._connection.execute(platform.enable_foreign_key_constraints()) elif foreign_keys is not None: self._connection.execute(platform.disable_foreign_key_constraints()) - From 8e6a27b1d1ec886278f4e6147d365ce505811577 Mon Sep 17 00:00:00 2001 From: Kyrela Date: Wed, 19 Jun 2024 23:15:22 +0200 Subject: [PATCH 142/254] tests: `test_can_add_column_enum` and `test_can_change_column_enum` --- .../schema/test_mysql_schema_builder_alter.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/mysql/schema/test_mysql_schema_builder_alter.py b/tests/mysql/schema/test_mysql_schema_builder_alter.py index e31fbc4be..10e345e76 100644 --- a/tests/mysql/schema/test_mysql_schema_builder_alter.py +++ b/tests/mysql/schema/test_mysql_schema_builder_alter.py @@ -294,3 +294,27 @@ def test_can_create_indexes(self): "ALTER TABLE `users` ADD FULLTEXT description_fulltext(description)", ], ) + + def test_can_add_column_enum(self): + with self.schema.table("users") as blueprint: + blueprint.enum("status", ["active", "inactive"]).default("active") + + self.assertEqual(len(blueprint.table.added_columns), 1) + + sql = [ + "ALTER TABLE `users` ADD `status` ENUM('active', 'inactive') NOT NULL DEFAULT 'active'" + ] + + self.assertEqual(blueprint.to_sql(), sql) + + def test_can_change_column_enum(self): + with self.schema.table("users") as blueprint: + blueprint.enum("status", ["active", "inactive"]).default("active").change() + + self.assertEqual(len(blueprint.table.changed_columns), 1) + + sql = [ + "ALTER TABLE `users` MODIFY `status` ENUM('active', 'inactive') NOT NULL DEFAULT 'active'" + ] + + self.assertEqual(blueprint.to_sql(), sql) From ec31a17aea4ac08cc1cb7a23f05aaab24e35b27b Mon Sep 17 00:00:00 2001 From: Kyrela Date: Wed, 19 Jun 2024 23:18:41 +0200 Subject: [PATCH 143/254] tests: `test_can_add_enum` --- tests/mysql/schema/test_mysql_schema_builder.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/mysql/schema/test_mysql_schema_builder.py b/tests/mysql/schema/test_mysql_schema_builder.py index 8d7c751cf..b3aefc033 100644 --- a/tests/mysql/schema/test_mysql_schema_builder.py +++ b/tests/mysql/schema/test_mysql_schema_builder.py @@ -384,3 +384,15 @@ def test_can_truncate_without_foreign_keys(self): "SET FOREIGN_KEY_CHECKS=1", ], ) + + def test_can_add_enum(self): + with self.schema.create("users") as blueprint: + blueprint.enum("status", ["active", "inactive"]).default("active") + + self.assertEqual(len(blueprint.table.added_columns), 1) + self.assertEqual( + blueprint.to_sql(), + [ + "CREATE TABLE `users` (`status` ENUM('active', 'inactive') NOT NULL DEFAULT 'active')" + ], + ) From ab39abfd939c109d0ee7365a221409f83845c7ba Mon Sep 17 00:00:00 2001 From: Kyrela Date: Thu, 20 Jun 2024 19:16:54 +0200 Subject: [PATCH 144/254] fix: values in compile_alter_sql --- .../schema/platforms/PostgresPlatform.py | 18 +++++++++-- .../schema/platforms/SQLitePlatform.py | 13 +++++++- .../mssql/schema/test_mssql_schema_builder.py | 24 +++++++++++++++ .../schema/test_mssql_schema_builder_alter.py | 12 ++++++++ .../schema/test_postgres_schema_builder.py | 12 ++++++++ .../test_postgres_schema_builder_alter.py | 24 +++++++++++++++ .../schema/test_sqlite_schema_builder.py | 12 ++++++++ .../test_sqlite_schema_builder_alter.py | 30 +++++++++++++++++++ 8 files changed, 141 insertions(+), 4 deletions(-) diff --git a/src/masoniteorm/schema/platforms/PostgresPlatform.py b/src/masoniteorm/schema/platforms/PostgresPlatform.py index 2f9f3e65e..c6f45ef7e 100644 --- a/src/masoniteorm/schema/platforms/PostgresPlatform.py +++ b/src/masoniteorm/schema/platforms/PostgresPlatform.py @@ -194,6 +194,11 @@ def compile_alter_sql(self, table): else: default = "" + column_constraint = "" + if column.column_type == "enum": + values = ", ".join(f"'{x}'" for x in column.values) + column_constraint = f" CHECK({column.name} IN ({values}))" + add_columns.append( self.add_column_string() .format( @@ -201,6 +206,7 @@ def compile_alter_sql(self, table): data_type=self.type_map.get(column.column_type, ""), length=length, constraint="PRIMARY KEY" if column.primary else "", + column_constraint=column_constraint, nullable="NULL" if column.is_null else "NOT NULL", default=default, after=(" AFTER " + self.wrap_column(column._after)) @@ -263,12 +269,18 @@ def compile_alter_sql(self, table): changed_sql = [] for name, column in table.changed_columns.items(): + + column_constraint = "" + if column.column_type == "enum": + values = ", ".join(f"'{x}'" for x in column.values) + column_constraint = f" CHECK({column.name} IN ({values}))" changed_sql.append( self.modify_column_string() .format( name=self.wrap_column(name), data_type=self.type_map.get(column.column_type), - nullable="NULL" if column.is_null else "NOT NULL", + column_constraint=column_constraint, + constraint="PRIMARY KEY" if column.primary else "", length="(" + str(column.length) + ")" if column.column_type not in self.types_without_lengths else "", @@ -380,13 +392,13 @@ def alter_format_add_foreign_key(self): return "ALTER TABLE {table} {columns}" def add_column_string(self): - return "ADD COLUMN {name} {data_type}{length} {nullable}{default} {constraint}" + return "ADD COLUMN {name} {data_type}{length}{column_constraint} {nullable}{default} {constraint}" def drop_column_string(self): return "DROP COLUMN {name}" def modify_column_string(self): - return "ALTER COLUMN {name} TYPE {data_type}{length}" + return "ALTER COLUMN {name} TYPE {data_type}{length}{column_constraint} {constraint}" def rename_column_string(self): return "RENAME COLUMN {old} TO {to}" diff --git a/src/masoniteorm/schema/platforms/SQLitePlatform.py b/src/masoniteorm/schema/platforms/SQLitePlatform.py index f4ca6b6d7..93a42bcc3 100644 --- a/src/masoniteorm/schema/platforms/SQLitePlatform.py +++ b/src/masoniteorm/schema/platforms/SQLitePlatform.py @@ -171,15 +171,21 @@ def compile_alter_sql(self, diff): else: default = "" constraint = "" + column_constraint = "" if column.name in diff.added_foreign_keys: foreign_key = diff.added_foreign_keys[column.name] constraint = f" REFERENCES {self.wrap_table(foreign_key.foreign_table)}({self.wrap_column(foreign_key.foreign_column)})" + if column.column_type == "enum": + values = ", ".join(f"'{x}'" for x in column.values) + column_constraint = f" CHECK('{column.name}' IN({values}))" sql.append( - "ALTER TABLE {table} ADD COLUMN {name} {data_type}{signed} {nullable}{default}{constraint}".format( + self.add_column_string() + .format( table=self.wrap_table(diff.name), name=self.wrap_column(column.name), data_type=self.type_map.get(column.column_type, ""), + column_constraint=column_constraint, nullable="NULL" if column.is_null else "NOT NULL", default=default, signed=" " + self.signed.get(column._signed) @@ -291,6 +297,11 @@ def get_table_string(self): def get_column_string(self): return '"{column}"' + def add_column_string(self): + return ( + "ALTER TABLE {table} ADD COLUMN {name} {data_type}{column_constraint}{signed} {nullable}{default}{constraint}" + ) + def create_column_length(self, column_type): if column_type in self.types_without_lengths: return "" diff --git a/tests/mssql/schema/test_mssql_schema_builder.py b/tests/mssql/schema/test_mssql_schema_builder.py index 16813a3ad..4205bc7eb 100644 --- a/tests/mssql/schema/test_mssql_schema_builder.py +++ b/tests/mssql/schema/test_mssql_schema_builder.py @@ -300,3 +300,27 @@ def test_can_truncate_without_foreign_keys(self): "ALTER TABLE [users] WITH CHECK CHECK CONSTRAINT ALL", ], ) + + def test_can_add_enum(self): + with self.schema.create("users") as blueprint: + blueprint.enum("status", ["active", "inactive"]).default("active") + + self.assertEqual(len(blueprint.table.added_columns), 1) + self.assertEqual( + blueprint.to_sql(), + [ + "CREATE TABLE [users] ([status] VARCHAR(255) NOT NULL DEFAULT 'active' CHECK([status] IN ('active', 'inactive')))" + ], + ) + + def test_can_change_column_enum(self): + with self.schema.table("users") as blueprint: + blueprint.enum("status", ["active", "inactive"]).default("active").change() + + self.assertEqual(len(blueprint.table.changed_columns), 1) + self.assertEqual( + blueprint.to_sql(), + [ + "ALTER TABLE [users] ALTER COLUMN [status] VARCHAR(255) NOT NULL DEFAULT 'active' CHECK([status] IN ('active', 'inactive'))" + ], + ) diff --git a/tests/mssql/schema/test_mssql_schema_builder_alter.py b/tests/mssql/schema/test_mssql_schema_builder_alter.py index daf2168f3..f1b323e27 100644 --- a/tests/mssql/schema/test_mssql_schema_builder_alter.py +++ b/tests/mssql/schema/test_mssql_schema_builder_alter.py @@ -268,3 +268,15 @@ def test_timestamp_alter_add_nullable_column(self): sql = ["ALTER TABLE [users] ADD [due_date] DATETIME NULL"] self.assertEqual(blueprint.to_sql(), sql) + + def test_can_add_column_enum(self): + with self.schema.table("users") as blueprint: + blueprint.enum("status", ["active", "inactive"]).default("active") + + self.assertEqual(len(blueprint.table.added_columns), 1) + + sql = [ + "ALTER TABLE [users] ADD [status] VARCHAR(255) NOT NULL DEFAULT 'active' CHECK([status] IN ('active', 'inactive'))" + ] + + self.assertEqual(blueprint.to_sql(), sql) diff --git a/tests/postgres/schema/test_postgres_schema_builder.py b/tests/postgres/schema/test_postgres_schema_builder.py index d5804e811..9e04b3dd5 100644 --- a/tests/postgres/schema/test_postgres_schema_builder.py +++ b/tests/postgres/schema/test_postgres_schema_builder.py @@ -368,3 +368,15 @@ def test_can_truncate_without_foreign_keys(self): 'ALTER TABLE "users" ENABLE TRIGGER ALL', ], ) + + def test_can_add_enum(self): + with self.schema.create("users") as blueprint: + blueprint.enum("status", ["active", "inactive"]).default("active") + + self.assertEqual(len(blueprint.table.added_columns), 1) + self.assertEqual( + blueprint.to_sql(), + [ + 'CREATE TABLE "users" ("status" VARCHAR(255) CHECK(status IN (\'active\', \'inactive\')) NOT NULL ' 'DEFAULT \'active\')' + ], + ) diff --git a/tests/postgres/schema/test_postgres_schema_builder_alter.py b/tests/postgres/schema/test_postgres_schema_builder_alter.py index 1b98ac210..d77d632b6 100644 --- a/tests/postgres/schema/test_postgres_schema_builder_alter.py +++ b/tests/postgres/schema/test_postgres_schema_builder_alter.py @@ -301,3 +301,27 @@ def test_alter_drop_on_table_schema_table(self): with schema.table("table_schema") as blueprint: blueprint.string("name") + + def test_can_add_column_enum(self): + with self.schema.table("users") as blueprint: + blueprint.enum("status", ["active", "inactive"]).default("active") + + self.assertEqual(len(blueprint.table.added_columns), 1) + + sql = [ + 'ALTER TABLE "users" ADD COLUMN "status" VARCHAR(255) CHECK(status IN (\'active\', \'inactive\')) NOT NULL DEFAULT \'active\'', + ] + + self.assertEqual(blueprint.to_sql(), sql) + + def test_can_change_column_enum(self): + with self.schema.table("users") as blueprint: + blueprint.enum("status", ["active", "inactive"]).default("active").change() + + self.assertEqual(len(blueprint.table.changed_columns), 1) + + sql = [ + 'ALTER TABLE "users" ALTER COLUMN "status" TYPE VARCHAR(255) CHECK(status IN (\'active\', \'inactive\')), ALTER COLUMN "status" SET NOT NULL, ALTER COLUMN "status" SET DEFAULT active', + ] + + self.assertEqual(blueprint.to_sql(), sql) diff --git a/tests/sqlite/schema/test_sqlite_schema_builder.py b/tests/sqlite/schema/test_sqlite_schema_builder.py index acaf1bc7b..2259ea7ec 100644 --- a/tests/sqlite/schema/test_sqlite_schema_builder.py +++ b/tests/sqlite/schema/test_sqlite_schema_builder.py @@ -353,3 +353,15 @@ def test_can_truncate_without_foreign_keys(self): "PRAGMA foreign_keys = ON", ], ) + + def test_can_add_enum(self): + with self.schema.create("users") as blueprint: + blueprint.enum("status", ["active", "inactive"]).default("active") + + self.assertEqual(len(blueprint.table.added_columns), 1) + self.assertEqual( + blueprint.to_sql(), + [ + 'CREATE TABLE "users" ("status" VARCHAR(255) CHECK(status IN (\'active\', \'inactive\')) NOT NULL DEFAULT \'active\')' + ], + ) diff --git a/tests/sqlite/schema/test_sqlite_schema_builder_alter.py b/tests/sqlite/schema/test_sqlite_schema_builder_alter.py index 91c5c43d0..8f0ed8a78 100644 --- a/tests/sqlite/schema/test_sqlite_schema_builder_alter.py +++ b/tests/sqlite/schema/test_sqlite_schema_builder_alter.py @@ -209,3 +209,33 @@ def test_alter_add_foreign_key_only(self): ] self.assertEqual(blueprint.to_sql(), sql) + + def test_can_add_column_enum(self): + with self.schema.table("users") as blueprint: + blueprint.enum("status", ["active", "inactive"]).default("active") + + self.assertEqual(len(blueprint.table.added_columns), 1) + + sql = [ + 'ALTER TABLE "users" ADD COLUMN "status" VARCHAR CHECK(\'status\' IN(\'active\', \'inactive\')) NOT NULL DEFAULT \'active\'' + ] + + self.assertEqual(blueprint.to_sql(), sql) + + def test_can_change_column_enum(self): + with self.schema.table("users") as blueprint: + blueprint.enum("status", ["active", "inactive"]).default("active").change() + + blueprint.table.from_table = Table("users") + + self.assertEqual(len(blueprint.table.changed_columns), 1) + + sql = [ + 'CREATE TEMPORARY TABLE __temp__users AS SELECT FROM users', + 'DROP TABLE "users"', + 'CREATE TABLE "users" ("status" VARCHAR(255) CHECK(status IN (\'active\', \'inactive\')) NOT NULL DEFAULT \'active\')', + 'INSERT INTO "users" ("status") SELECT status FROM __temp__users', + 'DROP TABLE __temp__users' + ] + + self.assertEqual(blueprint.to_sql(), sql) From 736eaf85cd1066856e978c3949784e7843029f6d Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 26 Jun 2024 22:11:09 -0400 Subject: [PATCH 145/254] fixed query builder --- src/masoniteorm/query/QueryBuilder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 95e40c050..45262cbd0 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -1955,7 +1955,7 @@ def _register_relationships_to_model( map_related = self._map_related(related_result, related) for model in hydrated_model: if isinstance(related_result, Collection): - related.register_related(relation_key, model, map_related) + related.register_related(relation_key, model, related_result) else: model.add_relation({relation_key: related_result or None}) else: From e4459d6b2b412439b179223958b7446b616c0ea5 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 18 Aug 2024 11:47:28 -0400 Subject: [PATCH 146/254] fixed has many though relationship --- src/masoniteorm/relationships/HasManyThrough.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/masoniteorm/relationships/HasManyThrough.py b/src/masoniteorm/relationships/HasManyThrough.py index ee7396e1d..524603143 100644 --- a/src/masoniteorm/relationships/HasManyThrough.py +++ b/src/masoniteorm/relationships/HasManyThrough.py @@ -76,14 +76,14 @@ def apply_query(self, distant_builder, intermediary_builder, owner): dict -- A dictionary of data which will be hydrated. """ # select * from `countries` inner join `ports` on `ports`.`country_id` = `countries`.`country_id` where `ports`.`port_id` is null and `countries`.`deleted_at` is null and `ports`.`deleted_at` is null - distant_builder.join( + result = distant_builder.join( f"{self.intermediary_builder.get_table_name()}", f"{self.intermediary_builder.get_table_name()}.{self.foreign_key}", "=", f"{distant_builder.get_table_name()}.{self.other_owner_key}", - ) + ).get() - return self + return result def relate(self, related_model): return self.distant_builder.join( From 25a965aaf6c998ed17e69229c2cdc2b1a70712af Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 1 Sep 2024 10:36:10 -0400 Subject: [PATCH 147/254] fixed has many --- src/masoniteorm/relationships/HasMany.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/masoniteorm/relationships/HasMany.py b/src/masoniteorm/relationships/HasMany.py index a940df047..cc73be04b 100644 --- a/src/masoniteorm/relationships/HasMany.py +++ b/src/masoniteorm/relationships/HasMany.py @@ -28,5 +28,5 @@ def set_keys(self, owner, attribute): def register_related(self, key, model, collection): model.add_relation( - {key: collection.get(getattr(model, self.local_key)) or Collection()} + {key: collection.where(self.foreign_key, getattr(model, self.local_key)) or Collection()} ) From 8c9a6bb0b4dd01ac9ed86827c111697f8c4868f9 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 1 Sep 2024 11:09:35 -0400 Subject: [PATCH 148/254] fixed querybuilder --- src/masoniteorm/query/QueryBuilder.py | 6 +++--- src/masoniteorm/relationships/HasMany.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 45262cbd0..4fe68c164 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -1955,11 +1955,11 @@ def _register_relationships_to_model( map_related = self._map_related(related_result, related) for model in hydrated_model: if isinstance(related_result, Collection): - related.register_related(relation_key, model, related_result) + related.register_related(relation_key, model, map_related) else: - model.add_relation({relation_key: related_result or None}) + model.add_relation({relation_key: map_related or None}) else: - hydrated_model.add_relation({relation_key: related_result or None}) + hydrated_model.add_relation({relation_key: map_related or None}) return self def _map_related(self, related_result, related): diff --git a/src/masoniteorm/relationships/HasMany.py b/src/masoniteorm/relationships/HasMany.py index cc73be04b..a940df047 100644 --- a/src/masoniteorm/relationships/HasMany.py +++ b/src/masoniteorm/relationships/HasMany.py @@ -28,5 +28,5 @@ def set_keys(self, owner, attribute): def register_related(self, key, model, collection): model.add_relation( - {key: collection.where(self.foreign_key, getattr(model, self.local_key)) or Collection()} + {key: collection.get(getattr(model, self.local_key)) or Collection()} ) From bff9d8bcaccd9e9652c85f2133fbfe49b26e371c Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 1 Sep 2024 11:34:14 -0400 Subject: [PATCH 149/254] fixed eager load --- src/masoniteorm/query/QueryBuilder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 4fe68c164..4e9a663b9 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -1959,7 +1959,7 @@ def _register_relationships_to_model( else: model.add_relation({relation_key: map_related or None}) else: - hydrated_model.add_relation({relation_key: map_related or None}) + hydrated_model.add_relation({relation_key: related_result or None}) return self def _map_related(self, related_result, related): From 0d0411fae51fde187dec54c2fa6d934983f0507b Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 1 Sep 2024 11:41:04 -0400 Subject: [PATCH 150/254] bumped version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 85d4cfbab..f9fe92760 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version="2.22.0", + version="2.22.2", package_dir={"": "src"}, description="The Official Masonite ORM", long_description=long_description, From e2bb610975972bb2d8685ef2663d970ff259d047 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 1 Sep 2024 14:29:17 -0400 Subject: [PATCH 151/254] bumped version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f9fe92760..8c55fd2bd 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version="2.22.2", + version="2.22.3", package_dir={"": "src"}, description="The Official Masonite ORM", long_description=long_description, From 26fcdba50aef0476cd4a1788781f8f622aaa3534 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 1 Sep 2024 18:00:58 -0400 Subject: [PATCH 152/254] fixed missing where on has many through relationship --- src/masoniteorm/relationships/HasManyThrough.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/masoniteorm/relationships/HasManyThrough.py b/src/masoniteorm/relationships/HasManyThrough.py index 524603143..86174cfb2 100644 --- a/src/masoniteorm/relationships/HasManyThrough.py +++ b/src/masoniteorm/relationships/HasManyThrough.py @@ -81,7 +81,7 @@ def apply_query(self, distant_builder, intermediary_builder, owner): f"{self.intermediary_builder.get_table_name()}.{self.foreign_key}", "=", f"{distant_builder.get_table_name()}.{self.other_owner_key}", - ).get() + ).where(f"{self.intermediary_builder.get_table_name()}.{self.local_owner_key}", getattr(owner, self.other_owner_key)).get() return result From 637ae01467e8df289ebb657a7cf507de19755b34 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 1 Sep 2024 18:28:12 -0400 Subject: [PATCH 153/254] bumped version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8c55fd2bd..b3fc2ab91 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version="2.22.3", + version="2.22.4", package_dir={"": "src"}, description="The Official Masonite ORM", long_description=long_description, From 4671816b3fb236afbbf30adf47941bbf08f01157 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 1 Sep 2024 18:29:24 -0400 Subject: [PATCH 154/254] fixed version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b3fc2ab91..8c55fd2bd 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version="2.22.4", + version="2.22.3", package_dir={"": "src"}, description="The Official Masonite ORM", long_description=long_description, From 2649b5d7575e8a4aaa385573c1cbf41794012ba5 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Tue, 10 Sep 2024 09:11:08 +0800 Subject: [PATCH 155/254] use scopes helper method --- src/masoniteorm/query/QueryBuilder.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 4e9a663b9..6b91bee8e 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -2088,9 +2088,8 @@ def to_sql(self): Returns: self """ - for name, scope in self._global_scopes.get(self._action, {}).items(): - scope(self) + self.run_scopes() grammar = self.get_grammar() sql = grammar.compile(self._action, qmark=False).to_sql() return sql @@ -2117,11 +2116,9 @@ def to_qmark(self): Returns: self """ - for name, scope in self._global_scopes.get(self._action, {}).items(): - scope(self) + self.run_scopes() grammar = self.get_grammar() - sql = grammar.compile(self._action, qmark=True).to_sql() self._bindings = grammar._bindings From c9f4065a15985bdfe242c9e84f3aaf91e52ba480 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Tue, 10 Sep 2024 09:12:39 +0800 Subject: [PATCH 156/254] make QueryBuilder find respect scopes --- src/masoniteorm/query/QueryBuilder.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 6b91bee8e..59f0f374e 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -1792,7 +1792,7 @@ def last(self, column=None, query=False): def _get_eager_load_result(self, related, collection): return related.eager_load_from_collection(collection) - def find(self, record_id): + def find(self, record_id, query=False): """Finds a row by the primary key ID. Requires a model Arguments: @@ -1801,8 +1801,12 @@ def find(self, record_id): Returns: Model|None """ + self.where(self._model.get_primary_key(), record_id) - return self.where(self._model.get_primary_key(), record_id).first() + if query: + return self.to_sql() + + return self.first() def find_or(self, record_id: int, callback: Callable, args=None): """Finds a row by the primary key ID (Requires a model) or raise a ModelNotFound exception. From c4bc92b3d9bf4c773f5e414a8baafed6477155bf Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Tue, 10 Sep 2024 09:46:09 +0800 Subject: [PATCH 157/254] Added modles testts using scope --- tests/mysql/model/test_model_scopes.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 tests/mysql/model/test_model_scopes.py diff --git a/tests/mysql/model/test_model_scopes.py b/tests/mysql/model/test_model_scopes.py new file mode 100644 index 000000000..574424f56 --- /dev/null +++ b/tests/mysql/model/test_model_scopes.py @@ -0,0 +1,25 @@ +import unittest + +from src.masoniteorm.models import Model +from src.masoniteorm.scopes import SoftDeletesMixin + + +class User(Model, SoftDeletesMixin): + pass + + +class TestModelScopes(unittest.TestCase): + def test_find_with_global_scope(self): + user_where = User.where("id", 1).to_sql() + user_find = User.find("1", query=True) + self.assertEqual(user_where, user_find) + + def test_find_with_trashed_scope(self): + user_where = User.with_trashed().where("id", 1).to_sql() + user_find = User.with_trashed().find("1", query=True) + self.assertEqual(user_where, user_find) + + def test_find_with_only_trashed_scope(self): + user_where = User.only_trashed().where("id", 1).to_sql() + user_find = User.only_trashed().find("1", query=True) + self.assertEqual(user_where, user_find) From f4dda5671da90a28ee84052214beda0954867b84 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Tue, 10 Sep 2024 14:03:05 +0800 Subject: [PATCH 158/254] consolidate duplicated calls --- src/masoniteorm/query/QueryBuilder.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 4e9a663b9..fdefc89c3 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -1710,14 +1710,13 @@ def first(self, fields=None, query=False): if not fields: fields = [] - if fields: - self.select(fields) + self.select(fields).limit(1) if query: - return self.limit(1) + return self result = self.new_connection().query( - self.limit(1).to_qmark(), self._bindings, results=1 + self.to_qmark(), self._bindings, results=1 ) return self.prepare_result(result) @@ -1778,11 +1777,13 @@ def last(self, column=None, query=False): dictionary -- Returns a dictionary of results. """ _column = column if column else self._model.get_primary_key() + self.limit(1).order_by(_column, direction="DESC") + if query: - return self.limit(1).order_by(_column, direction="DESC") + return self result = self.new_connection().query( - self.limit(1).order_by(_column, direction="DESC").to_qmark(), + self.to_qmark(), self._bindings, results=1, ) @@ -1868,7 +1869,7 @@ def first_or_fail(self, query=False): """ if query: - return self.limit(1) + return self.first(query=True) result = self.first() From 1ca31b1e1186750db4320f22b221c9690463d5a8 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Tue, 10 Sep 2024 14:04:08 +0800 Subject: [PATCH 159/254] Fixed queryBuilder .all() test --- src/masoniteorm/query/QueryBuilder.py | 4 +++- tests/sqlite/models/test_sqlite_model.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index fdefc89c3..9eb107818 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -1975,9 +1975,11 @@ def all(self, selects=[], query=False): Returns: dictionary -- Returns a dictionary of results. """ + self.select(*selects) + if query: - return self.to_sql() + return self result = self.new_connection().query(self.to_qmark(), self._bindings) or [] diff --git a/tests/sqlite/models/test_sqlite_model.py b/tests/sqlite/models/test_sqlite_model.py index c833c1137..caa38a7a0 100644 --- a/tests/sqlite/models/test_sqlite_model.py +++ b/tests/sqlite/models/test_sqlite_model.py @@ -108,7 +108,7 @@ def test_model_can_use_selects(self): def test_model_can_use_selects_from_methods(self): self.assertEqual( - SelectPass.all(["username"], query=True), + SelectPass.all(["username"], query=True).to_sql(), 'SELECT "select_passes"."username" FROM "select_passes"', ) From 7e07531448693164f20954b42d1e170e49a37ab3 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Tue, 10 Sep 2024 14:41:03 +0800 Subject: [PATCH 160/254] fixed query=True not returning builder Also remove redundant else clause --- src/masoniteorm/models/Model.py | 10 +++++----- src/masoniteorm/query/QueryBuilder.py | 2 +- tests/sqlite/models/test_sqlite_model.py | 6 ++---- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index 052f08ee6..5437935b7 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -426,12 +426,12 @@ def find(cls, record_id, query=False): builder = cls().where(cls.get_primary_key(), record_id) if query: - return builder.to_sql() - else: - if isinstance(record_id, (list, tuple)): - return builder.get() + return builder + + if isinstance(record_id, (list, tuple)): + return builder.get() - return builder.first() + return builder.first() @classmethod def find_or_fail(cls, record_id, query=False): diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 59f0f374e..e6b481291 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -1804,7 +1804,7 @@ def find(self, record_id, query=False): self.where(self._model.get_primary_key(), record_id) if query: - return self.to_sql() + return self return self.first() diff --git a/tests/sqlite/models/test_sqlite_model.py b/tests/sqlite/models/test_sqlite_model.py index c833c1137..8ace14ece 100644 --- a/tests/sqlite/models/test_sqlite_model.py +++ b/tests/sqlite/models/test_sqlite_model.py @@ -72,12 +72,10 @@ def test_update_all_records(self): self.assertEqual(sql, """UPDATE "users" SET "name" = 'joe'""") def test_can_find_list(self): - sql = User.find(1, query=True) - + sql = User.find(1, query=True).to_sql() self.assertEqual(sql, """SELECT * FROM "users" WHERE "users"."id" = '1'""") - sql = User.find([1, 2, 3], query=True) - + sql = User.find([1, 2, 3], query=True).to_sql() self.assertEqual( sql, """SELECT * FROM "users" WHERE "users"."id" IN ('1','2','3')""" ) From 10e6478b513e65e75d0fa6cf7652cb13fe30e8d7 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Tue, 10 Sep 2024 14:55:46 +0800 Subject: [PATCH 161/254] use raw sql for clarity --- tests/mysql/model/test_model_scopes.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/mysql/model/test_model_scopes.py b/tests/mysql/model/test_model_scopes.py index 574424f56..28d37a6b8 100644 --- a/tests/mysql/model/test_model_scopes.py +++ b/tests/mysql/model/test_model_scopes.py @@ -10,16 +10,16 @@ class User(Model, SoftDeletesMixin): class TestModelScopes(unittest.TestCase): def test_find_with_global_scope(self): - user_where = User.where("id", 1).to_sql() - user_find = User.find("1", query=True) - self.assertEqual(user_where, user_find) + find_sql = User.find("1", query=True).to_sql() + raw_sql = """SELECT * FROM `users` WHERE `users`.`id` = '1' AND `users`.`deleted_at` IS NULL""" + self.assertEqual(find_sql, raw_sql) def test_find_with_trashed_scope(self): - user_where = User.with_trashed().where("id", 1).to_sql() - user_find = User.with_trashed().find("1", query=True) - self.assertEqual(user_where, user_find) + find_sql = User.with_trashed().find("1", query=True).to_sql() + raw_sql = """SELECT * FROM `users` WHERE `users`.`id` = '1'""" + self.assertEqual(find_sql, raw_sql) def test_find_with_only_trashed_scope(self): - user_where = User.only_trashed().where("id", 1).to_sql() - user_find = User.only_trashed().find("1", query=True) - self.assertEqual(user_where, user_find) + find_sql = User.only_trashed().find("1", query=True).to_sql() + raw_sql = """SELECT * FROM `users` WHERE `users`.`deleted_at` IS NOT NULL AND `users`.`id` = '1'""" + self.assertEqual(find_sql, raw_sql) From 27760ed3d2cc39c21d3cee4a06e7ed04e395b80d Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Tue, 10 Sep 2024 16:37:53 +0800 Subject: [PATCH 162/254] updated Model find and create --- src/masoniteorm/models/Model.py | 6 +++--- tests/models/test_models.py | 10 +++++----- tests/mysql/model/test_model.py | 10 +++++----- tests/mysql/scopes/test_can_use_global_scopes.py | 2 +- tests/sqlite/models/test_sqlite_model.py | 4 ++-- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index 052f08ee6..632e940ff 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -426,7 +426,7 @@ def find(cls, record_id, query=False): builder = cls().where(cls.get_primary_key(), record_id) if query: - return builder.to_sql() + return builder else: if isinstance(record_id, (list, tuple)): return builder.get() @@ -562,7 +562,7 @@ def create( if query: return cls.builder.create( dictionary, query=True, cast=cast, **kwargs - ).to_sql() + ) return cls.builder.create(dictionary, cast=cast, **kwargs) @@ -897,7 +897,7 @@ def save(self, query=False): if self.is_loaded(): result = builder.update( self.__dirty_attributes__, dry=query, ignore_mass_assignment=True - ).to_sql() + ) else: result = self.create(self.__dirty_attributes__, query=query) diff --git a/tests/models/test_models.py b/tests/models/test_models.py index a842a2654..1ce8e5ee9 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -92,13 +92,13 @@ def test_model_creates_when_new(self): model = ModelTest.hydrate({"id": 1, "username": "joe", "admin": True}) model.name = "Bill" - sql = model.save(query=True) + sql = model.save(query=True).to_sql() self.assertTrue(sql.startswith("UPDATE")) model = ModelTest() model.name = "Bill" - sql = model.save(query=True) + sql = model.save(query=True).to_sql() self.assertTrue(sql.startswith("INSERT")) def test_model_can_cast_attributes(self): @@ -170,7 +170,7 @@ def test_model_update_without_changes(self): model.username = "joe" model.name = "Bill" - sql = model.save(query=True) + sql = model.save(query=True).to_sql() self.assertTrue(sql.startswith("UPDATE")) self.assertNotIn("username", sql) @@ -181,7 +181,7 @@ def test_force_update_on_model_class(self): model.username = "joe" model.name = "Bill" - sql = model.save(query=True) + sql = model.save(query=True).to_sql() self.assertTrue(sql.startswith("UPDATE")) self.assertIn("username", sql) self.assertIn("name", sql) @@ -201,7 +201,7 @@ def test_model_update_without_changes_at_all(self): model.username = "joe" model.name = "Joe" - sql = model.save(query=True) + sql = model.save(query=True).to_sql() self.assertFalse(sql.startswith("UPDATE")) def test_model_using_or_where(self): diff --git a/tests/mysql/model/test_model.py b/tests/mysql/model/test_model.py index 06395b0e8..402da2650 100644 --- a/tests/mysql/model/test_model.py +++ b/tests/mysql/model/test_model.py @@ -81,7 +81,7 @@ class TestModel(unittest.TestCase): def test_create_can_use_fillable(self): sql = ProfileFillable.create( {"name": "Joe", "email": "user@example.com"}, query=True - ) + ).to_sql() self.assertEqual( sql, "INSERT INTO `profiles` (`profiles`.`name`) VALUES ('Joe')" @@ -90,7 +90,7 @@ def test_create_can_use_fillable(self): def test_create_can_use_fillable_asterisk(self): sql = ProfileFillAsterisk.create( {"name": "Joe", "email": "user@example.com"}, query=True - ) + ).to_sql() self.assertEqual( sql, @@ -100,7 +100,7 @@ def test_create_can_use_fillable_asterisk(self): def test_create_can_use_guarded(self): sql = ProfileGuarded.create( {"name": "Joe", "email": "user@example.com"}, query=True - ) + ).to_sql() self.assertEqual( sql, "INSERT INTO `profiles` (`profiles`.`name`) VALUES ('Joe')" @@ -109,7 +109,7 @@ def test_create_can_use_guarded(self): def test_create_can_use_guarded_asterisk(self): sql = ProfileGuardedAsterisk.create( {"name": "Joe", "email": "user@example.com"}, query=True - ) + ).to_sql() # An asterisk guarded attribute excludes all fields from mass-assignment. # This would raise a DB error if there are any required fields. @@ -326,7 +326,7 @@ def test_can_find_first(self): def test_can_touch(self): profile = ProfileFillTimeStamped.hydrate({"name": "Joe", "id": 1}) - sql = profile.touch("now", query=True) + sql = profile.touch("now", query=True).to_sql() self.assertEqual( sql, diff --git a/tests/mysql/scopes/test_can_use_global_scopes.py b/tests/mysql/scopes/test_can_use_global_scopes.py index 0cfe3e537..1f670fcf9 100644 --- a/tests/mysql/scopes/test_can_use_global_scopes.py +++ b/tests/mysql/scopes/test_can_use_global_scopes.py @@ -35,7 +35,7 @@ def test_can_use_global_scopes_on_select(self): def test_can_use_global_scopes_on_time(self): sql = "INSERT INTO `users` (`users`.`name`, `users`.`updated_at`, `users`.`created_at`) VALUES ('Joe'" - self.assertTrue(User.create({"name": "Joe"}, query=True).startswith(sql)) + self.assertTrue(User.create({"name": "Joe"}, query=True).to_sql().startswith(sql)) # def test_can_use_global_scopes_on_inherit(self): # sql = "SELECT * FROM `user_softs` WHERE `user_softs`.`deleted_at` IS NULL" diff --git a/tests/sqlite/models/test_sqlite_model.py b/tests/sqlite/models/test_sqlite_model.py index caa38a7a0..1456e1834 100644 --- a/tests/sqlite/models/test_sqlite_model.py +++ b/tests/sqlite/models/test_sqlite_model.py @@ -72,11 +72,11 @@ def test_update_all_records(self): self.assertEqual(sql, """UPDATE "users" SET "name" = 'joe'""") def test_can_find_list(self): - sql = User.find(1, query=True) + sql = User.find(1, query=True).to_sql() self.assertEqual(sql, """SELECT * FROM "users" WHERE "users"."id" = '1'""") - sql = User.find([1, 2, 3], query=True) + sql = User.find([1, 2, 3], query=True).to_sql() self.assertEqual( sql, """SELECT * FROM "users" WHERE "users"."id" IN ('1','2','3')""" From 43a80e5378cbf6a0d1c478641203537aac6d72f2 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Wed, 11 Sep 2024 09:43:07 +0800 Subject: [PATCH 163/254] moved find scope tests in with other tests --- tests/mysql/model/test_model_scopes.py | 25 ------------------------ tests/mysql/scopes/test_soft_delete.py | 27 +++++++++++++++++++------- 2 files changed, 20 insertions(+), 32 deletions(-) delete mode 100644 tests/mysql/model/test_model_scopes.py diff --git a/tests/mysql/model/test_model_scopes.py b/tests/mysql/model/test_model_scopes.py deleted file mode 100644 index 28d37a6b8..000000000 --- a/tests/mysql/model/test_model_scopes.py +++ /dev/null @@ -1,25 +0,0 @@ -import unittest - -from src.masoniteorm.models import Model -from src.masoniteorm.scopes import SoftDeletesMixin - - -class User(Model, SoftDeletesMixin): - pass - - -class TestModelScopes(unittest.TestCase): - def test_find_with_global_scope(self): - find_sql = User.find("1", query=True).to_sql() - raw_sql = """SELECT * FROM `users` WHERE `users`.`id` = '1' AND `users`.`deleted_at` IS NULL""" - self.assertEqual(find_sql, raw_sql) - - def test_find_with_trashed_scope(self): - find_sql = User.with_trashed().find("1", query=True).to_sql() - raw_sql = """SELECT * FROM `users` WHERE `users`.`id` = '1'""" - self.assertEqual(find_sql, raw_sql) - - def test_find_with_only_trashed_scope(self): - find_sql = User.only_trashed().find("1", query=True).to_sql() - raw_sql = """SELECT * FROM `users` WHERE `users`.`deleted_at` IS NOT NULL AND `users`.`id` = '1'""" - self.assertEqual(find_sql, raw_sql) diff --git a/tests/mysql/scopes/test_soft_delete.py b/tests/mysql/scopes/test_soft_delete.py index b48f829ef..1a0c6c43d 100644 --- a/tests/mysql/scopes/test_soft_delete.py +++ b/tests/mysql/scopes/test_soft_delete.py @@ -1,8 +1,8 @@ -import inspect import unittest +import pendulum + from tests.integrations.config.database import DATABASES -from src.masoniteorm.models import Model from src.masoniteorm.query import QueryBuilder from src.masoniteorm.query.grammars import MySQLGrammar from src.masoniteorm.scopes import SoftDeleteScope @@ -10,16 +10,14 @@ from src.masoniteorm.models import Model from src.masoniteorm.scopes import SoftDeletesMixin -from tests.User import User class UserSoft(Model, SoftDeletesMixin): __dry__ = True - + __table__ = "users" class UserSoftArchived(Model, SoftDeletesMixin): __dry__ = True - __deleted_at__ = "archived_at" __table__ = "users" @@ -52,7 +50,7 @@ def test_restore(self): self.assertEqual(sql, builder.restore().to_sql()) def test_force_delete_with_wheres(self): - sql = "DELETE FROM `user_softs` WHERE `user_softs`.`active` = '1'" + sql = "DELETE FROM `users` WHERE `users`.`active` = '1'" builder = self.get_builder().set_global_scope(SoftDeleteScope()) self.assertEqual( sql, UserSoft.where("active", 1).force_delete(query=True).to_sql() @@ -69,9 +67,24 @@ def test_only_trashed(self): self.assertEqual(sql, builder.only_trashed().to_sql()) def test_only_trashed_on_model(self): - sql = "SELECT * FROM `user_softs` WHERE `user_softs`.`deleted_at` IS NOT NULL" + sql = "SELECT * FROM `users` WHERE `users`.`deleted_at` IS NOT NULL" self.assertEqual(sql, UserSoft.only_trashed().to_sql()) def test_can_change_column(self): sql = "SELECT * FROM `users` WHERE `users`.`archived_at` IS NOT NULL" self.assertEqual(sql, UserSoftArchived.only_trashed().to_sql()) + + def test_find_with_global_scope(self): + find_sql = UserSoft.find("1", query=True).to_sql() + raw_sql = """SELECT * FROM `users` WHERE `users`.`id` = '1' AND `users`.`deleted_at` IS NULL""" + self.assertEqual(find_sql, raw_sql) + + def test_find_with_trashed_scope(self): + find_sql = UserSoft.with_trashed().find("1", query=True).to_sql() + raw_sql = """SELECT * FROM `users` WHERE `users`.`id` = '1'""" + self.assertEqual(find_sql, raw_sql) + + def test_find_with_only_trashed_scope(self): + find_sql = UserSoft.only_trashed().find("1", query=True).to_sql() + raw_sql = """SELECT * FROM `users` WHERE `users`.`deleted_at` IS NOT NULL AND `users`.`id` = '1'""" + self.assertEqual(find_sql, raw_sql) From 7e2e551c40e8a6c54623386c4ba7d314006e093d Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Thu, 17 Oct 2024 19:16:38 +0800 Subject: [PATCH 164/254] Fixed HasOneThrough not loading distant model Fixed eager and on demand loading of distant table use vars for tablenames to increase readability. --- src/masoniteorm/query/QueryBuilder.py | 2 + .../relationships/HasOneThrough.py | 187 ++++++++++++------ 2 files changed, 127 insertions(+), 62 deletions(-) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index 4e9a663b9..963017a36 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -1965,6 +1965,8 @@ def _register_relationships_to_model( def _map_related(self, related_result, related): if related.__class__.__name__ == 'MorphTo': return related_result + elif related.__class__.__name__ == 'HasOneThrough': + return related_result.group_by(related.local_key) return related_result.group_by(related.foreign_key) diff --git a/src/masoniteorm/relationships/HasOneThrough.py b/src/masoniteorm/relationships/HasOneThrough.py index ae7c587b4..f6ccf2c50 100644 --- a/src/masoniteorm/relationships/HasOneThrough.py +++ b/src/masoniteorm/relationships/HasOneThrough.py @@ -1,5 +1,4 @@ from .BaseRelationship import BaseRelationship -from ..collection import Collection class HasOneThrough(BaseRelationship): @@ -26,6 +25,10 @@ def __init__( self.local_owner_key = local_owner_key or "id" self.other_owner_key = other_owner_key or "id" + def __getattr__(self, attribute): + relationship = self.fn(self)[1]() + return getattr(relationship.builder, attribute) + def set_keys(self, distant_builder, intermediary_builder, attribute): self.local_key = self.local_key or "id" self.foreign_key = self.foreign_key or f"{attribute}_id" @@ -34,17 +37,18 @@ def set_keys(self, distant_builder, intermediary_builder, attribute): return self def __get__(self, instance, owner): - """This method is called when the decorated method is accessed. + """ + This method is called when the decorated method is accessed. - Arguments: - instance {object|None} -- The instance we called. + Arguments + instance (object|None): The instance we called. If we didn't call the attribute and only accessed it then this will be None. + owner (object): The current model that the property was accessed on. - owner {object} -- The current model that the property was accessed on. - - Returns: - object -- Either returns a builder or a hydrated model. + Returns + QueryBuilder|Model: Either returns a builder or a hydrated model. """ + attribute = self.fn.__name__ self.attribute = attribute relationship1 = self.fn(self)[0]() @@ -57,43 +61,60 @@ def __get__(self, instance, owner): if attribute in instance._relationships: return instance._relationships[attribute] - result = self.apply_query( + return self.apply_query( self.distant_builder, self.intermediary_builder, instance ) - return result else: return self def apply_query(self, distant_builder, intermediary_builder, owner): - """Apply the query and return a dictionary to be hydrated. - Used during accessing a relationship on a model + """ + Apply the query and return a dict of data for the distant model to be hydrated with. - Arguments: - query {oject} -- The relationship object - owner {object} -- The current model oject. + Method is used when accessing a relationship on a model if its not + already eager loaded - Returns: - dict -- A dictionary of data which will be hydrated. + Arguments + distant_builder (QueryBuilder): QueryVuilder attached to the distant table + intermediate_builder (QueryBuilder): QueryVuilder attached to the intermesiate (linking) table + owner (Any): the model this relationship is starting from + + Returns + dict: A dictionary of data which will be hydrated. """ - # select * from `countries` inner join `ports` on `ports`.`country_id` = `countries`.`country_id` where `ports`.`port_id` is null and `countries`.`deleted_at` is null and `ports`.`deleted_at` is null - distant_builder.join( - f"{self.intermediary_builder.get_table_name()}", - f"{self.intermediary_builder.get_table_name()}.{self.foreign_key}", - "=", - f"{distant_builder.get_table_name()}.{self.other_owner_key}", - ) - return self + dist_table = distant_builder.get_table_name() + int_table = intermediary_builder.get_table_name() + + return ( + distant_builder.select( + f"{dist_table}.*, {int_table}.{self.local_owner_key} as {self.local_key}" + ) + .join( + f"{int_table}", + f"{int_table}.{self.foreign_key}", + "=", + f"{dist_table}.{self.other_owner_key}", + ) + .where( + f"{int_table}.{self.local_owner_key}", + getattr(owner, self.local_key), + ) + .first() + ) def relate(self, related_model): + dist_table = self.distant_builder.get_table_name() + int_table = self.intermediary_builder.get_table_name() + return self.distant_builder.join( - f"{self.intermediary_builder.get_table_name()}", - f"{self.intermediary_builder.get_table_name()}.{self.foreign_key}", + f"{int_table}", + f"{int_table}.{self.foreign_key}", "=", - f"{self.distant_builder.get_table_name()}.{self.other_owner_key}", - ).where( - f"{self.intermediary_builder.get_table_name()}.{self.local_key}", - getattr(related_model, self.local_owner_key), + f"{dist_table}.{self.other_owner_key}", + ).where_column( + f"{int_table}.{self.local_owner_key}", + f"{query.get_table_name()}.{self.local_key}", ) def get_builder(self): @@ -104,42 +125,83 @@ def make_builder(self, eagers=None): return builder + def register_related(self, key, model, collection): + """ + Attach the related model to source models attribute + + Arguments + key (str): The attribute name + model (Any): The model instance + collection (Collection): The data for the related models + + Returns + None + """ + + related = collection.get(getattr(model, self.local_key), None) + model.add_relation({key: related[0] if related else None}) + def get_related(self, query, relation, eagers=None, callback=None): - builder = self.distant_builder + """ + Get the data to htdrate the model for the distant table with + Used when eager loading the model attribute + + Arguments + query (QueryBuilder): The source models QueryBuilder object + relation (HasOneThrough): this relationship object + eagers (Any): + callback (Any): + + Returns + dict: the dict to hydrate the distant model with + """ + + dist_table = self.distant_builder.get_table_name() + int_table = self.intermediary_builder.get_table_name() if callback: callback(builder) - if isinstance(relation, Collection): - return builder.where_in( - f"{builder.get_table_name()}.{self.foreign_key}", - Collection(relation._get_value(self.local_key)).unique(), - ).get() - else: - return builder.where( - f"{builder.get_table_name()}.{self.foreign_key}", - getattr(relation, self.local_owner_key), - ).first() + return ( + self.distant_builder.select( + f"{dist_table}.*, {int_table}.{self.local_owner_key} as {self.local_key}" + ) + .join( + f"{int_table}", + f"{int_table}.{self.foreign_key}", + "=", + f"{dist_table}.{self.other_owner_key}", + ) + .where( + f"{int_table}.{self.local_owner_key}", + relation._get_value(self.local_key), + ) + .get() + ) def query_where_exists( self, current_query_builder, callback, method="where_exists" ): query = self.distant_builder + dist_table = self.distant_builder.get_table_name() + int_table = self.intermediary_builder.get_table_name() getattr(current_query_builder, method)( query.join( - f"{self.intermediary_builder.get_table_name()}", - f"{self.intermediary_builder.get_table_name()}.{self.foreign_key}", + f"{int_table}", + f"{int_table}.{self.foreign_key}", "=", - f"{query.get_table_name()}.{self.other_owner_key}", + f"{dist_table}.{self.other_owner_key}", ).where_column( - f"{current_query_builder.get_table_name()}.{self.local_owner_key}", - f"{self.intermediary_builder.get_table_name()}.{self.local_key}", + f"{int_table}.{self.local_owner_key}", + f"{query.get_table_name()}.{self.local_key}", ) ).when(callback, lambda q: (callback(q))) def get_with_count_query(self, builder, callback): query = self.distant_builder + dist_table = self.distant_builder.get_table_name() + int_table = self.intermediary_builder.get_table_name() if not builder._columns: builder = builder.select("*") @@ -150,16 +212,16 @@ def get_with_count_query(self, builder, callback): ( q.count("*") .join( - f"{self.intermediary_builder.get_table_name()}", - f"{self.intermediary_builder.get_table_name()}.{self.foreign_key}", + f"{int_table}", + f"{int_table}.{self.foreign_key}", "=", - f"{query.get_table_name()}.{self.other_owner_key}", + f"{dist_table}.{self.other_owner_key}", ) .where_column( - f"{builder.get_table_name()}.{self.local_owner_key}", - f"{self.intermediary_builder.get_table_name()}.{self.local_key}", + f"{int_table}.{self.local_owner_key}", + f"{query.get_table_name()}.{self.local_key}", ) - .table(query.get_table_name()) + .table(dist_table) .when( callback, lambda q: ( @@ -186,18 +248,19 @@ def attach_related(self, current_model, related_record): ) def query_has(self, current_query_builder, method="where_exists"): - related_builder = self.get_builder() + dist_table = self.distant_builder.get_table_name() + int_table = self.intermediary_builder.get_table_name() getattr(current_query_builder, method)( - self.distant_builder.where_column( - f"{current_query_builder.get_table_name()}.{self.local_owner_key}", - f"{self.intermediary_builder.get_table_name()}.{self.local_key}", - ).join( - f"{self.intermediary_builder.get_table_name()}", - f"{self.intermediary_builder.get_table_name()}.{self.foreign_key}", + self.distant_builder.join( + f"{int_table}", + f"{int_table}.{self.foreign_key}", "=", - f"{self.distant_builder.get_table_name()}.{self.other_owner_key}", + f"{dist_table}.{self.other_owner_key}", + ).where_column( + f"{current_query_builder.get_table_name()}.{self.local_key}", + f"{int_table}.{self.local_owner_key}", ) ) - return related_builder + return self.distant_builder From 1ac4d2938f332f74c3d0aaf89d47218d03a9202e Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Fri, 18 Oct 2024 13:49:25 +0800 Subject: [PATCH 165/254] Fixed HasOneThrough with additional queries --- .../relationships/HasOneThrough.py | 94 +++++++++---------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/src/masoniteorm/relationships/HasOneThrough.py b/src/masoniteorm/relationships/HasOneThrough.py index f6ccf2c50..1a87debb3 100644 --- a/src/masoniteorm/relationships/HasOneThrough.py +++ b/src/masoniteorm/relationships/HasOneThrough.py @@ -61,13 +61,13 @@ def __get__(self, instance, owner): if attribute in instance._relationships: return instance._relationships[attribute] - return self.apply_query( + return self.apply_relation_query( self.distant_builder, self.intermediary_builder, instance ) else: return self - def apply_query(self, distant_builder, intermediary_builder, owner): + def apply_relation_query(self, distant_builder, intermediary_builder, owner): """ Apply the query and return a dict of data for the distant model to be hydrated with. @@ -114,7 +114,7 @@ def relate(self, related_model): f"{dist_table}.{self.other_owner_key}", ).where_column( f"{int_table}.{self.local_owner_key}", - f"{query.get_table_name()}.{self.local_key}", + getattr(related_model, self.local_key), ) def get_builder(self): @@ -160,7 +160,7 @@ def get_related(self, query, relation, eagers=None, callback=None): int_table = self.intermediary_builder.get_table_name() if callback: - callback(builder) + callback(query) return ( self.distant_builder.select( @@ -179,34 +179,60 @@ def get_related(self, query, relation, eagers=None, callback=None): .get() ) - def query_where_exists( - self, current_query_builder, callback, method="where_exists" - ): - query = self.distant_builder + def attach(self, current_model, related_record): + raise NotImplementedError( + "HasOneThrough relationship does not implement the attach method" + ) + + def attach_related(self, current_model, related_record): + raise NotImplementedError( + "HasOneThrough relationship does not implement the attach_related method" + ) + + def query_has(self, current_builder, method="where_exists"): dist_table = self.distant_builder.get_table_name() int_table = self.intermediary_builder.get_table_name() - getattr(current_query_builder, method)( - query.join( + getattr(current_builder, method)( + self.distant_builder.join( f"{int_table}", f"{int_table}.{self.foreign_key}", "=", f"{dist_table}.{self.other_owner_key}", ).where_column( f"{int_table}.{self.local_owner_key}", - f"{query.get_table_name()}.{self.local_key}", + f"{current_builder.get_table_name()}.{self.local_key}", ) - ).when(callback, lambda q: (callback(q))) + ) - def get_with_count_query(self, builder, callback): - query = self.distant_builder + return self.distant_builder + + def query_where_exists(self, current_builder, callback, method="where_exists"): dist_table = self.distant_builder.get_table_name() int_table = self.intermediary_builder.get_table_name() - if not builder._columns: - builder = builder.select("*") + getattr(current_builder, method)( + self.distant_builder.join( + f"{int_table}", + f"{int_table}.{self.foreign_key}", + "=", + f"{dist_table}.{self.other_owner_key}", + ) + .where_column( + f"{int_table}.{self.local_owner_key}", + f"{current_builder.get_table_name()}.{self.local_key}", + ) + .when(callback, lambda q: (callback(q))) + ) + + def get_with_count_query(self, current_builder, callback): + dist_table = self.distant_builder.get_table_name() + int_table = self.intermediary_builder.get_table_name() - return_query = builder.add_select( + if not current_builder._columns: + current_builder.select("*") + + return_query = current_builder.add_select( f"{self.attribute}_count", lambda q: ( ( @@ -219,7 +245,7 @@ def get_with_count_query(self, builder, callback): ) .where_column( f"{int_table}.{self.local_owner_key}", - f"{query.get_table_name()}.{self.local_key}", + f"{current_builder.get_table_name()}.{self.local_key}", ) .table(dist_table) .when( @@ -227,7 +253,9 @@ def get_with_count_query(self, builder, callback): lambda q: ( q.where_in( self.foreign_key, - callback(query.select(self.other_owner_key)), + callback( + self.distant_builder.select(self.other_owner_key) + ), ) ), ) @@ -236,31 +264,3 @@ def get_with_count_query(self, builder, callback): ) return return_query - - def attach(self, current_model, related_record): - raise NotImplementedError( - "HasOneThrough relationship does not implement the attach method" - ) - - def attach_related(self, current_model, related_record): - raise NotImplementedError( - "HasOneThrough relationship does not implement the attach_related method" - ) - - def query_has(self, current_query_builder, method="where_exists"): - dist_table = self.distant_builder.get_table_name() - int_table = self.intermediary_builder.get_table_name() - - getattr(current_query_builder, method)( - self.distant_builder.join( - f"{int_table}", - f"{int_table}.{self.foreign_key}", - "=", - f"{dist_table}.{self.other_owner_key}", - ).where_column( - f"{current_query_builder.get_table_name()}.{self.local_key}", - f"{int_table}.{self.local_owner_key}", - ) - ) - - return self.distant_builder From 4f41fc9ea98ed824864d1c3726c61dd0a0a6e6d9 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Fri, 18 Oct 2024 13:49:48 +0800 Subject: [PATCH 166/254] Fixed tests --- .../mysql/relationships/test_has_one_through.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/mysql/relationships/test_has_one_through.py b/tests/mysql/relationships/test_has_one_through.py index f0ae66a11..355a748c5 100644 --- a/tests/mysql/relationships/test_has_one_through.py +++ b/tests/mysql/relationships/test_has_one_through.py @@ -13,7 +13,7 @@ class InboundShipment(Model): - @has_one_through("port_id", "country_id", "from_port_id", "country_id") + @has_one_through(None, "from_port_id", "country_id", "port_id", "country_id") def from_country(self): return Country, Port @@ -34,7 +34,7 @@ def test_has_query(self): self.assertEqual( sql, - """SELECT * FROM `inbound_shipments` WHERE EXISTS (SELECT * FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `inbound_shipments`.`from_port_id` = `ports`.`port_id`)""", + """SELECT * FROM `inbound_shipments` WHERE EXISTS (SELECT * FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `ports`.`port_id` = `inbound_shipments`.`from_port_id`)""", ) def test_or_has(self): @@ -42,7 +42,7 @@ def test_or_has(self): self.assertEqual( sql, - """SELECT * FROM `inbound_shipments` WHERE `inbound_shipments`.`name` = 'Joe' OR EXISTS (SELECT * FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `inbound_shipments`.`from_port_id` = `ports`.`port_id`)""", + """SELECT * FROM `inbound_shipments` WHERE `inbound_shipments`.`name` = 'Joe' OR EXISTS (SELECT * FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `ports`.`port_id` = `inbound_shipments`.`from_port_id`)""", ) def test_where_has_query(self): @@ -52,7 +52,7 @@ def test_where_has_query(self): self.assertEqual( sql, - """SELECT * FROM `inbound_shipments` WHERE EXISTS (SELECT * FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `inbound_shipments`.`from_port_id` = `ports`.`port_id`) AND `inbound_shipments`.`name` = 'USA'""", + """SELECT * FROM `inbound_shipments` WHERE EXISTS (SELECT * FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `ports`.`port_id` = `inbound_shipments`.`from_port_id` AND `countries`.`name` = 'USA')""", ) def test_or_where_has(self): @@ -64,7 +64,7 @@ def test_or_where_has(self): self.assertEqual( sql, - """SELECT * FROM `inbound_shipments` WHERE `inbound_shipments`.`name` = 'Joe' OR EXISTS (SELECT * FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `inbound_shipments`.`from_port_id` = `ports`.`port_id`) AND `inbound_shipments`.`name` = 'USA'""", + """SELECT * FROM `inbound_shipments` WHERE `inbound_shipments`.`name` = 'Joe' OR EXISTS (SELECT * FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `ports`.`port_id` = `inbound_shipments`.`from_port_id` AND `countries`.`name` = 'USA')""", ) def test_doesnt_have(self): @@ -72,7 +72,7 @@ def test_doesnt_have(self): self.assertEqual( sql, - """SELECT * FROM `inbound_shipments` WHERE NOT EXISTS (SELECT * FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `inbound_shipments`.`from_port_id` = `ports`.`port_id`)""", + """SELECT * FROM `inbound_shipments` WHERE NOT EXISTS (SELECT * FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `ports`.`port_id` = `inbound_shipments`.`from_port_id`)""", ) def test_or_where_doesnt_have(self): @@ -86,7 +86,7 @@ def test_or_where_doesnt_have(self): self.assertEqual( sql, - """SELECT * FROM `inbound_shipments` WHERE `inbound_shipments`.`name` = 'Joe' OR NOT EXISTS (SELECT * FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `inbound_shipments`.`from_port_id` = `ports`.`port_id`) AND `inbound_shipments`.`name` = 'USA'""", + """SELECT * FROM `inbound_shipments` WHERE `inbound_shipments`.`name` = 'Joe' OR NOT EXISTS (SELECT * FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `ports`.`port_id` = `inbound_shipments`.`from_port_id` AND `countries`.`name` = 'USA')""", ) def test_has_one_through_with_count(self): @@ -94,5 +94,5 @@ def test_has_one_through_with_count(self): self.assertEqual( sql, - """SELECT `inbound_shipments`.*, (SELECT COUNT(*) AS m_count_reserved FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `inbound_shipments`.`from_port_id` = `ports`.`port_id`) AS from_country_count FROM `inbound_shipments`""", + """SELECT `inbound_shipments`.*, (SELECT COUNT(*) AS m_count_reserved FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `ports`.`port_id` = `inbound_shipments`.`from_port_id`) AS from_country_count FROM `inbound_shipments`""", ) From 9cdf5600f309c38e9de5085accf8437a4d094de8 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Fri, 18 Oct 2024 18:38:20 -0400 Subject: [PATCH 167/254] Update pendulum version requirement --- requirements.txt | 2 +- setup.py | 2 +- tests/models/test_models.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index a2188f679..98830ccf4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ inflection==0.3.1 psycopg2-binary pyodbc -pendulum>=2.1,<2.2 +pendulum>=3.0,<=3.1 cleo>=0.8.0,<0.9 python-dotenv==0.14.0 \ No newline at end of file diff --git a/setup.py b/setup.py index 8c55fd2bd..ed2d73a52 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ # https://packaging.python.org/en/latest/requirements.html install_requires=[ "inflection>=0.3,<0.6", - "pendulum>=2.1,<2.2", + "ppendulum>=3.0,<=3.1", "faker>=4.1.0,<14.0", "cleo>=0.8.0,<0.9", ], diff --git a/tests/models/test_models.py b/tests/models/test_models.py index a842a2654..c2586b241 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -191,8 +191,8 @@ def test_only_method(self): {"id": 1, "username": "joe", "name": "Joe", "admin": True} ) - self.assertEquals({"username": "joe"}, model.only("username")) - self.assertEquals({"username": "joe"}, model.only(["username"])) + self.assertEqual({"username": "joe"}, model.only("username")) + self.assertEqual({"username": "joe"}, model.only(["username"])) def test_model_update_without_changes_at_all(self): model = ModelTest.hydrate( From 511e20c3d58d3eb688d07a270796e2b0746b02e2 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Fri, 18 Oct 2024 18:41:51 -0400 Subject: [PATCH 168/254] Update pendulum version requirement --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 98830ccf4..34de478e8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ inflection==0.3.1 psycopg2-binary pyodbc -pendulum>=3.0,<=3.1 +pendulum>=2.1,<=3.1 cleo>=0.8.0,<0.9 python-dotenv==0.14.0 \ No newline at end of file diff --git a/setup.py b/setup.py index ed2d73a52..cc5f8ffb8 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ # https://packaging.python.org/en/latest/requirements.html install_requires=[ "inflection>=0.3,<0.6", - "ppendulum>=3.0,<=3.1", + "ppendulum>=2.1,<=3.1", "faker>=4.1.0,<14.0", "cleo>=0.8.0,<0.9", ], From d534e28f5113a60ad55dc4110b8cb70f00080630 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Fri, 18 Oct 2024 18:44:59 -0400 Subject: [PATCH 169/254] Update pendulum version requirement --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 34de478e8..aba4fd5b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ inflection==0.3.1 psycopg2-binary pyodbc -pendulum>=2.1,<=3.1 +pendulum>=2.1,<3.1 cleo>=0.8.0,<0.9 python-dotenv==0.14.0 \ No newline at end of file diff --git a/setup.py b/setup.py index cc5f8ffb8..f13347819 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ # https://packaging.python.org/en/latest/requirements.html install_requires=[ "inflection>=0.3,<0.6", - "ppendulum>=2.1,<=3.1", + "ppendulum>=2.1,<3.1", "faker>=4.1.0,<14.0", "cleo>=0.8.0,<0.9", ], From d5433afd88c250adc17ad902a893d92e611d5fc1 Mon Sep 17 00:00:00 2001 From: Joseph Mancuso Date: Fri, 18 Oct 2024 18:49:31 -0400 Subject: [PATCH 170/254] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f13347819..c5e4083ab 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version="2.22.3", + version="2.23.0", package_dir={"": "src"}, description="The Official Masonite ORM", long_description=long_description, From 817e44eb95c983cbd371274c1f9ba54166118b7a Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Sun, 20 Oct 2024 15:59:14 +0800 Subject: [PATCH 171/254] removed unused importsa and duplicate test --- tests/mysql/relationships/test_has_many_through.py | 11 ----------- tests/mysql/relationships/test_has_one_through.py | 5 +---- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/tests/mysql/relationships/test_has_many_through.py b/tests/mysql/relationships/test_has_many_through.py index 310a3b3c4..61830c2b8 100644 --- a/tests/mysql/relationships/test_has_many_through.py +++ b/tests/mysql/relationships/test_has_many_through.py @@ -2,10 +2,7 @@ from src.masoniteorm.models import Model from src.masoniteorm.relationships import ( - has_one, - belongs_to_many, has_many_through, - has_many, ) from dotenv import load_dotenv @@ -88,11 +85,3 @@ def test_or_where_doesnt_have(self): sql, """SELECT * FROM `inbound_shipments` WHERE `inbound_shipments`.`name` = 'Joe' OR NOT EXISTS (SELECT * FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `inbound_shipments`.`from_port_id` = `ports`.`port_id`) AND `inbound_shipments`.`name` = 'USA'""", ) - - def test_has_one_through_with_count(self): - sql = InboundShipment.with_count("from_country").to_sql() - - self.assertEqual( - sql, - """SELECT `inbound_shipments`.*, (SELECT COUNT(*) AS m_count_reserved FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `inbound_shipments`.`from_port_id` = `ports`.`port_id`) AS from_country_count FROM `inbound_shipments`""", - ) diff --git a/tests/mysql/relationships/test_has_one_through.py b/tests/mysql/relationships/test_has_one_through.py index 355a748c5..4337dc837 100644 --- a/tests/mysql/relationships/test_has_one_through.py +++ b/tests/mysql/relationships/test_has_one_through.py @@ -2,10 +2,7 @@ from src.masoniteorm.models import Model from src.masoniteorm.relationships import ( - has_one, - belongs_to_many, has_one_through, - has_many, ) from dotenv import load_dotenv @@ -26,7 +23,7 @@ class Port(Model): pass -class MySQLRelationships(unittest.TestCase): +class MySQLHasOneThroughRelationship(unittest.TestCase): maxDiff = None def test_has_query(self): From 79fad5f46a6e380552d0df2c8e60a20ee0e9b94b Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Sun, 20 Oct 2024 16:08:02 +0800 Subject: [PATCH 172/254] Fixed linting --- tests/sqlite/relationships/test_sqlite_relationships.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/sqlite/relationships/test_sqlite_relationships.py b/tests/sqlite/relationships/test_sqlite_relationships.py index d88039ff9..a3be52469 100644 --- a/tests/sqlite/relationships/test_sqlite_relationships.py +++ b/tests/sqlite/relationships/test_sqlite_relationships.py @@ -3,6 +3,7 @@ from src.masoniteorm.relationships import belongs_to, has_many, has_one, belongs_to_many from tests.integrations.config.database import DB + class Profile(Model): __table__ = "profiles" __connection__ = "dev" From 59e6d1b58bd818ba33c18e873918c51f3f4524e8 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Sun, 20 Oct 2024 16:26:48 +0800 Subject: [PATCH 173/254] Fixed typos --- src/masoniteorm/relationships/HasOneThrough.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/masoniteorm/relationships/HasOneThrough.py b/src/masoniteorm/relationships/HasOneThrough.py index 1a87debb3..a323f1f0b 100644 --- a/src/masoniteorm/relationships/HasOneThrough.py +++ b/src/masoniteorm/relationships/HasOneThrough.py @@ -1,4 +1,6 @@ +from build.lib.masoniteorm.query import QueryBuilder from .BaseRelationship import BaseRelationship +from ..collection import Collection class HasOneThrough(BaseRelationship): @@ -75,8 +77,8 @@ def apply_relation_query(self, distant_builder, intermediary_builder, owner): already eager loaded Arguments - distant_builder (QueryBuilder): QueryVuilder attached to the distant table - intermediate_builder (QueryBuilder): QueryVuilder attached to the intermesiate (linking) table + distant_builder (QueryBuilder): QueryBuilder attached to the distant table + intermediate_builder (QueryBuilder): QueryBuilder attached to the intermediate (linking) table owner (Any): the model this relationship is starting from Returns @@ -143,7 +145,7 @@ def register_related(self, key, model, collection): def get_related(self, query, relation, eagers=None, callback=None): """ - Get the data to htdrate the model for the distant table with + Get the data to hydrate the model for the distant table with Used when eager loading the model attribute Arguments From 7d303573894d13b493694066e9101ddfcb96905c Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Sun, 20 Oct 2024 16:27:38 +0800 Subject: [PATCH 174/254] Fixed eager loading --- .../relationships/HasOneThrough.py | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/src/masoniteorm/relationships/HasOneThrough.py b/src/masoniteorm/relationships/HasOneThrough.py index a323f1f0b..316378e7f 100644 --- a/src/masoniteorm/relationships/HasOneThrough.py +++ b/src/masoniteorm/relationships/HasOneThrough.py @@ -140,10 +140,13 @@ def register_related(self, key, model, collection): None """ - related = collection.get(getattr(model, self.local_key), None) - model.add_relation({key: related[0] if related else None}) + related_id = getattr(model, self.local_key) + for id, item in collection.items(): + if id == related_id: + model.add_relation({key: item[0]}) + break - def get_related(self, query, relation, eagers=None, callback=None): + def get_related(self, current_builder, relation, eagers=None, callback=None): """ Get the data to hydrate the model for the distant table with Used when eager loading the model attribute @@ -162,24 +165,28 @@ def get_related(self, query, relation, eagers=None, callback=None): int_table = self.intermediary_builder.get_table_name() if callback: - callback(query) + callback(current_builder) - return ( - self.distant_builder.select( - f"{dist_table}.*, {int_table}.{self.local_owner_key} as {self.local_key}" - ) - .join( - f"{int_table}", - f"{int_table}.{self.foreign_key}", - "=", - f"{dist_table}.{self.other_owner_key}", + (self.distant_builder.select(f"{dist_table}.*, {int_table}.{self.local_owner_key} as {self.local_key}") + .join( + f"{int_table}", + f"{int_table}.{self.foreign_key}", + "=", + f"{dist_table}.{self.other_owner_key}", + )) + + if isinstance(relation, Collection): + self.distant_builder.where_in( + f"{int_table}.{self.local_owner_key}", + Collection(relation._get_value(self.local_key)).unique(), ) - .where( + else: + self.distant_builder.where( f"{int_table}.{self.local_owner_key}", - relation._get_value(self.local_key), + getattr(relation, self.local_key), ) - .get() - ) + + return self.distant_builder.get() def attach(self, current_model, related_record): raise NotImplementedError( From 80aeb848272697e917f0405af33f5afb27fa4a63 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Sun, 20 Oct 2024 16:29:55 +0800 Subject: [PATCH 175/254] Added tests for HasOneThrough eager loading --- .../test_sqlite_has_through_relationships.py | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 tests/sqlite/relationships/test_sqlite_has_through_relationships.py diff --git a/tests/sqlite/relationships/test_sqlite_has_through_relationships.py b/tests/sqlite/relationships/test_sqlite_has_through_relationships.py new file mode 100644 index 000000000..e02af9bfb --- /dev/null +++ b/tests/sqlite/relationships/test_sqlite_has_through_relationships.py @@ -0,0 +1,119 @@ +import unittest + +from build.lib.masoniteorm.collection import Collection +from build.lib.masoniteorm.scopes import scope +from src.masoniteorm.models import Model +from src.masoniteorm.relationships import has_one_through +from tests.integrations.config.database import DB +from tests.integrations.config.database import DATABASES +from src.masoniteorm.schema import Schema +from src.masoniteorm.schema.platforms import SQLitePlatform + + +class Port(Model): + __table__ = "ports" + __connection__ = "dev" + __fillable__ = ["port_id", "name", "port_country_id"] + + +class Country(Model): + __table__ = "countries" + __connection__ = "dev" + __fillable__ = ["country_id", "name"] + + +class IncomingShipment(Model): + __table__ = "incoming_shipments" + __connection__ = "dev" + __fillable__ = ["shipment_id", "name", "from_port_id"] + + @has_one_through(None, "from_port_id", "port_country_id", "port_id", "country_id") + def from_country(self): + return [Country, Port] + + +class TestRelationships(unittest.TestCase): + def setUp(self): + self.schema = Schema( + connection="dev", + connection_details=DATABASES, + platform=SQLitePlatform, + ).on("dev") + + with self.schema.create_table_if_not_exists("incoming_shipments") as table: + table.integer("shipment_id").primary() + table.string("name") + table.integer("from_port_id") + + with self.schema.create_table_if_not_exists("ports") as table: + table.integer("port_id").primary() + table.string("name") + table.integer("port_country_id") + + with self.schema.create_table_if_not_exists("countries") as table: + table.integer("country_id").primary() + table.string("name") + + if not Country.count(): + Country.builder.new().bulk_create( + [ + {"country_id": 10, "name": "Australia"}, + {"country_id": 20, "name": "USA"}, + {"country_id": 30, "name": "Canada"}, + {"country_id": 40, "name": "United Kingdom"}, + ] + ) + + if not Port.count(): + Port.builder.new().bulk_create( + [ + {"port_id": 100, "name": "Melbourne", "port_country_id": 10}, + {"port_id": 200, "name": "Darwin", "port_country_id": 10}, + {"port_id": 300, "name": "South Louisiana", "port_country_id": 20}, + {"port_id": 400, "name": "Houston", "port_country_id": 20}, + {"port_id": 500, "name": "Montreal", "port_country_id": 30}, + {"port_id": 600, "name": "Vancouver", "port_country_id": 30}, + {"port_id": 700, "name": "Southampton", "port_country_id": 40}, + {"port_id": 800, "name": "London Gateway", "port_country_id": 40}, + ] + ) + + if not IncomingShipment.count(): + IncomingShipment.builder.new().bulk_create( + [ + {"name": "Bread", "from_port_id": 300}, + {"name": "Milk", "from_port_id": 100}, + {"name": "Tractor Parts", "from_port_id": 100}, + {"name": "Fridges", "from_port_id": 700}, + {"name": "Wheat", "from_port_id": 600}, + {"name": "Kettles", "from_port_id": 400}, + {"name": "Bread", "from_port_id": 700}, + ] + ) + + def test_has_one_through_can_eager_load(self): + shipments = IncomingShipment.where("name", "Bread").with_("from_country").get() + self.assertEqual(shipments.count(), 2) + + shipment1 = shipments.shift() + self.assertIsInstance(shipment1.from_country, Country) + self.assertEqual(shipment1.from_country.country_id, 20) + + shipment2 = shipments.shift() + self.assertIsInstance(shipment2.from_country, Country) + self.assertEqual(shipment2.from_country.country_id, 40) + + def test_has_one_through_eager_load_can_be_empty(self): + shipments = ( + IncomingShipment.where("name", "Bread") + .with_( + "from_country", + ) + .get() + ) + self.assertEqual(shipments.count(), 2) + + def test_has_one_through_can_get_related(self): + shipment = IncomingShipment.where("name", "Milk").first() + self.assertIsInstance(shipment.from_country, Country) + self.assertEqual(shipment.from_country.country_id, 10) From cd40bf9d491d7a996b2dd514e67d7926b6e66365 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Sun, 20 Oct 2024 16:40:01 +0800 Subject: [PATCH 176/254] removed unused import --- src/masoniteorm/relationships/HasOneThrough.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/masoniteorm/relationships/HasOneThrough.py b/src/masoniteorm/relationships/HasOneThrough.py index 316378e7f..910914ccd 100644 --- a/src/masoniteorm/relationships/HasOneThrough.py +++ b/src/masoniteorm/relationships/HasOneThrough.py @@ -1,4 +1,3 @@ -from build.lib.masoniteorm.query import QueryBuilder from .BaseRelationship import BaseRelationship from ..collection import Collection From ad33effc96b70c12112705655421d6af2e46eea3 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Sun, 20 Oct 2024 16:46:57 +0800 Subject: [PATCH 177/254] removed unused imports --- .../relationships/test_sqlite_has_through_relationships.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/sqlite/relationships/test_sqlite_has_through_relationships.py b/tests/sqlite/relationships/test_sqlite_has_through_relationships.py index e02af9bfb..46a0f913a 100644 --- a/tests/sqlite/relationships/test_sqlite_has_through_relationships.py +++ b/tests/sqlite/relationships/test_sqlite_has_through_relationships.py @@ -1,10 +1,7 @@ import unittest -from build.lib.masoniteorm.collection import Collection -from build.lib.masoniteorm.scopes import scope from src.masoniteorm.models import Model from src.masoniteorm.relationships import has_one_through -from tests.integrations.config.database import DB from tests.integrations.config.database import DATABASES from src.masoniteorm.schema import Schema from src.masoniteorm.schema.platforms import SQLitePlatform From d377d3722774fc60e72699d1e2f6538430117b3d Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Mon, 21 Oct 2024 09:25:28 +0800 Subject: [PATCH 178/254] Fixed using .first() fixed model attribute containing a list instead of a model when using .first() added additional tests checking .first() usage --- .../relationships/HasOneThrough.py | 17 +++++-------- .../test_sqlite_has_through_relationships.py | 24 ++++++++++++++++++- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/masoniteorm/relationships/HasOneThrough.py b/src/masoniteorm/relationships/HasOneThrough.py index 910914ccd..0f9eb0878 100644 --- a/src/masoniteorm/relationships/HasOneThrough.py +++ b/src/masoniteorm/relationships/HasOneThrough.py @@ -139,11 +139,8 @@ def register_related(self, key, model, collection): None """ - related_id = getattr(model, self.local_key) - for id, item in collection.items(): - if id == related_id: - model.add_relation({key: item[0]}) - break + related = collection.get(getattr(model, self.local_key), None) + model.add_relation({key: related[0] if related else None}) def get_related(self, current_builder, relation, eagers=None, callback=None): """ @@ -175,17 +172,15 @@ def get_related(self, current_builder, relation, eagers=None, callback=None): )) if isinstance(relation, Collection): - self.distant_builder.where_in( + return self.distant_builder.where_in( f"{int_table}.{self.local_owner_key}", Collection(relation._get_value(self.local_key)).unique(), - ) + ).get() else: - self.distant_builder.where( + return self.distant_builder.where( f"{int_table}.{self.local_owner_key}", getattr(relation, self.local_key), - ) - - return self.distant_builder.get() + ).first() def attach(self, current_model, related_record): raise NotImplementedError( diff --git a/tests/sqlite/relationships/test_sqlite_has_through_relationships.py b/tests/sqlite/relationships/test_sqlite_has_through_relationships.py index 46a0f913a..d23f4847a 100644 --- a/tests/sqlite/relationships/test_sqlite_has_through_relationships.py +++ b/tests/sqlite/relationships/test_sqlite_has_through_relationships.py @@ -100,17 +100,39 @@ def test_has_one_through_can_eager_load(self): self.assertIsInstance(shipment2.from_country, Country) self.assertEqual(shipment2.from_country.country_id, 40) + # check .first() and .get() produce the same result + single = ( + IncomingShipment.where("name", "Tractor Parts") + .with_("from_country") + .first() + ) + single_get = ( + IncomingShipment.where("name", "Tractor Parts").with_("from_country").get() + ) + self.assertEqual(single.from_country.country_id, 10) + self.assertEqual(single_get.count(), 1) + self.assertEqual( + single.from_country.country_id, single_get.first().from_country.country_id + ) + def test_has_one_through_eager_load_can_be_empty(self): shipments = ( IncomingShipment.where("name", "Bread") + .where_has("from_country", lambda query: query.where("name", "Ueaguay")) .with_( "from_country", ) .get() ) - self.assertEqual(shipments.count(), 2) + self.assertEqual(shipments.count(), 0) def test_has_one_through_can_get_related(self): shipment = IncomingShipment.where("name", "Milk").first() self.assertIsInstance(shipment.from_country, Country) self.assertEqual(shipment.from_country.country_id, 10) + + def test_has_one_through_has_query(self): + shipments = IncomingShipment.where_has( + "from_country", lambda query: query.where("name", "USA") + ) + self.assertEqual(shipments.count(), 2) From 19eda095f3340b52c61317e8d94f28bf85290704 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Mon, 21 Oct 2024 15:01:49 -0400 Subject: [PATCH 179/254] Refactor MySQLConnection to use connection pooling --- .../connections/MySQLConnection.py | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/masoniteorm/connections/MySQLConnection.py b/src/masoniteorm/connections/MySQLConnection.py index 5d0c21754..7ff44d9b6 100644 --- a/src/masoniteorm/connections/MySQLConnection.py +++ b/src/masoniteorm/connections/MySQLConnection.py @@ -68,16 +68,30 @@ def make_connection(self): if self.has_global_connection(): return self.get_global_connection() - self._connection = pymysql.connect( - cursorclass=pymysql.cursors.DictCursor, - autocommit=True, - host=self.host, - user=self.user, - password=self.password, - port=self.port, - db=self.database, - **self.options - ) + # Check if there is an available connection in the pool + if CONNECTION_POOL: + self._connection = CONNECTION_POOL.pop() + else: + if len(CONNECTION_POOL) < self.options.get("pool_size", 5): # Default pool size is 5 + self._connection = pymysql.connect( + cursorclass=pymysql.cursors.DictCursor, + autocommit=True, + host=self.host, + user=self.user, + password=self.password, + port=self.port, + database=self.database, + **self.options + ) + else: + raise ConnectionError("Connection pool limit reached") + + # Add the connection back to the pool when it's closed + def close_connection(): + CONNECTION_POOL.append(self._connection) + self._connection = None + + self._connection.close = close_connection self.enable_disable_foreign_keys() From 8a66a0719f3cb4c062c4d2e045593a569fcd4af7 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Mon, 21 Oct 2024 15:03:29 -0400 Subject: [PATCH 180/254] Update version and dependencies in setup.py --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index c5e4083ab..f62fc8e81 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version="2.23.0", + version="2.23.1", package_dir={"": "src"}, description="The Official Masonite ORM", long_description=long_description, @@ -30,7 +30,7 @@ # https://packaging.python.org/en/latest/requirements.html install_requires=[ "inflection>=0.3,<0.6", - "ppendulum>=2.1,<3.1", + "pendulum>=2.1,<3.1", "faker>=4.1.0,<14.0", "cleo>=0.8.0,<0.9", ], From fd7d20088244ac512df093abc9496dd0b384687b Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Mon, 21 Oct 2024 20:44:43 -0400 Subject: [PATCH 181/254] Refactor PostgresConnection to use connection pooling --- .../connections/PostgresConnection.py | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/masoniteorm/connections/PostgresConnection.py b/src/masoniteorm/connections/PostgresConnection.py index 19919d95a..5304365a8 100644 --- a/src/masoniteorm/connections/PostgresConnection.py +++ b/src/masoniteorm/connections/PostgresConnection.py @@ -4,6 +4,7 @@ from ..schema.platforms import PostgresPlatform from ..query.processors import PostgresPostProcessor from ..exceptions import QueryException +from psycopg2 import pool CONNECTION_POOL = [] @@ -58,14 +59,22 @@ def make_connection(self): schema = self.schema or self.full_details.get("schema") - self._connection = psycopg2.connect( - database=self.database, - user=self.user, - password=self.password, - host=self.host, - port=self.port, - options=f"-c search_path={schema}" if schema else "", - ) + # if connection pool is empty, create a new connection pool + if not CONNECTION_POOL: + CONNECTION_POOL.append( + pool.SimpleConnectionPool( + 1, 20, # minconn, maxconn + database=self.database, + user=self.user, + password=self.password, + host=self.host, + port=self.port, + options=f"-c search_path={schema}" if schema else "", + ) + ) + + # get a connection from the pool + self._connection = CONNECTION_POOL[0].getconn() self._connection.autocommit = True @@ -122,6 +131,7 @@ def get_transaction_level(self): def set_cursor(self): from psycopg2.extras import RealDictCursor + self._cursor = self._connection.cursor(cursor_factory=RealDictCursor) return self._cursor From e033bfdf3db007d2b4cd24cc804445729be0cd73 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Thu, 24 Oct 2024 13:54:43 +0800 Subject: [PATCH 182/254] fixed .on_null and .on_not_null had to be last criteria --- src/masoniteorm/query/grammars/BaseGrammar.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/masoniteorm/query/grammars/BaseGrammar.py b/src/masoniteorm/query/grammars/BaseGrammar.py index 5a0535765..f08cf6601 100644 --- a/src/masoniteorm/query/grammars/BaseGrammar.py +++ b/src/masoniteorm/query/grammars/BaseGrammar.py @@ -251,13 +251,13 @@ def process_joins(self, qmark=False): on_string += f"{keyword} {self._table_column_string(clause.column1)} {clause.equality} {self._table_column_string(clause.column2)} " else: if clause.value_type == "NULL": - sql_string = self.where_null_string() + sql_string = f"{self.where_null_string()} " on_string += sql_string.format( keyword=keyword, column=self.process_column(clause.column), ) elif clause.value_type == "NOT NULL": - sql_string = self.where_not_null_string() + sql_string = f"{self.where_not_null_string()} " on_string += sql_string.format( keyword=keyword, column=self.process_column(clause.column), From afb140bf877d921c8ee9a775f717053d74fecf9b Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Sat, 26 Oct 2024 07:58:50 +0800 Subject: [PATCH 183/254] updated tests fo .on_null and .on_not_null --- .../testing/BaseTestCaseSelectGrammar.py | 15 +++++++++++++++ tests/mssql/grammar/test_mssql_select_grammar.py | 16 +++++++++++++++- tests/mysql/grammar/test_mysql_select_grammar.py | 16 +++++++++++++++- tests/postgres/grammar/test_select_grammar.py | 16 +++++++++++++++- .../sqlite/grammar/test_sqlite_select_grammar.py | 16 +++++++++++++++- 5 files changed, 75 insertions(+), 4 deletions(-) diff --git a/src/masoniteorm/testing/BaseTestCaseSelectGrammar.py b/src/masoniteorm/testing/BaseTestCaseSelectGrammar.py index 0182e3c08..f1aaa119a 100644 --- a/src/masoniteorm/testing/BaseTestCaseSelectGrammar.py +++ b/src/masoniteorm/testing/BaseTestCaseSelectGrammar.py @@ -340,7 +340,22 @@ def test_can_compile_join_clause_with_null(self): clause = ( JoinClause("report_groups as rg") .on_null("bgt.acct") + .or_on_null("bgt.dept") + .on_value("rg.abc", 10) + ) + to_sql = self.builder.join(clause).to_sql() + + sql = getattr( + self, inspect.currentframe().f_code.co_name.replace("test_", "") + )() + self.assertEqual(to_sql, sql) + + def test_can_compile_join_clause_with_not_null(self): + clause = ( + JoinClause("report_groups as rg") + .on_not_null("bgt.acct") .or_on_not_null("bgt.dept") + .on_value("rg.abc", 10) ) to_sql = self.builder.join(clause).to_sql() diff --git a/tests/mssql/grammar/test_mssql_select_grammar.py b/tests/mssql/grammar/test_mssql_select_grammar.py index 28615cdf7..383a0f43c 100644 --- a/tests/mssql/grammar/test_mssql_select_grammar.py +++ b/tests/mssql/grammar/test_mssql_select_grammar.py @@ -405,11 +405,25 @@ def can_compile_join_clause_with_null(self): clause = ( JoinClause("report_groups as rg") .on_null("bgt.acct") + .or_on_null("bgt.dept") + .on_value("rg.abc", 10) + ) + builder.join(clause).to_sql() + """ + return "SELECT * FROM [users] INNER JOIN [report_groups] AS [rg] ON [acct] IS NULL OR [dept] IS NULL AND [rg].[abc] = '10'" + + def can_compile_join_clause_with_not_null(self): + """ + builder = self.get_builder() + clause = ( + JoinClause("report_groups as rg") + .on_not_null("bgt.acct") .or_on_not_null("bgt.dept") + .on_value("rg.abc", 10) ) builder.join(clause).to_sql() """ - return "SELECT * FROM [users] INNER JOIN [report_groups] AS [rg] ON [acct] IS NULL OR [dept] IS NOT NULL" + return "SELECT * FROM [users] INNER JOIN [report_groups] AS [rg] ON [acct] IS NOT NULL OR [dept] IS NOT NULL AND [rg].[abc] = '10'" def can_compile_join_clause_with_lambda(self): """ diff --git a/tests/mysql/grammar/test_mysql_select_grammar.py b/tests/mysql/grammar/test_mysql_select_grammar.py index 169b6a890..8d2d80e22 100644 --- a/tests/mysql/grammar/test_mysql_select_grammar.py +++ b/tests/mysql/grammar/test_mysql_select_grammar.py @@ -395,11 +395,25 @@ def can_compile_join_clause_with_null(self): clause = ( JoinClause("report_groups as rg") .on_null("bgt.acct") + .or_on_null("bgt.dept") + .on_value("rg.abc", 10) + ) + builder.join(clause).to_sql() + """ + return "SELECT * FROM `users` INNER JOIN `report_groups` AS `rg` ON `acct` IS NULL OR `dept` IS NULL AND `rg`.`abc` = '10'" + + def can_compile_join_clause_with_not_null(self): + """ + builder = self.get_builder() + clause = ( + JoinClause("report_groups as rg") + .on_not_null("bgt.acct") .or_on_not_null("bgt.dept") + .on_value("rg.abc", 10) ) builder.join(clause).to_sql() """ - return "SELECT * FROM `users` INNER JOIN `report_groups` AS `rg` ON `acct` IS NULL OR `dept` IS NOT NULL" + return "SELECT * FROM `users` INNER JOIN `report_groups` AS `rg` ON `acct` IS NOT NULL OR `dept` IS NOT NULL AND `rg`.`abc` = '10'" def can_compile_join_clause_with_lambda(self): """ diff --git a/tests/postgres/grammar/test_select_grammar.py b/tests/postgres/grammar/test_select_grammar.py index 4754114e6..d4a7915e9 100644 --- a/tests/postgres/grammar/test_select_grammar.py +++ b/tests/postgres/grammar/test_select_grammar.py @@ -410,11 +410,25 @@ def can_compile_join_clause_with_null(self): clause = ( JoinClause("report_groups as rg") .on_null("bgt.acct") + .or_on_null("bgt.dept") + .on_value("rg.abc", 10) + ) + builder.join(clause).to_sql() + """ + return """SELECT * FROM "users" INNER JOIN "report_groups" AS "rg" ON "acct" IS NULL OR "dept" IS NULL AND "rg"."abc" = '10'""" + + def can_compile_join_clause_with_not_null(self): + """ + builder = self.get_builder() + clause = ( + JoinClause("report_groups as rg") + .on_not_null("bgt.acct") .or_on_not_null("bgt.dept") + .on_value("rg.abc", 10) ) builder.join(clause).to_sql() """ - return """SELECT * FROM "users" INNER JOIN "report_groups" AS "rg" ON "acct" IS NULL OR "dept" IS NOT NULL""" + return """SELECT * FROM "users" INNER JOIN "report_groups" AS "rg" ON "acct" IS NOT NULL OR "dept" IS NOT NULL AND "rg"."abc" = '10'""" def can_compile_join_clause_with_lambda(self): """ diff --git a/tests/sqlite/grammar/test_sqlite_select_grammar.py b/tests/sqlite/grammar/test_sqlite_select_grammar.py index 015f39675..cc0bc6e5a 100644 --- a/tests/sqlite/grammar/test_sqlite_select_grammar.py +++ b/tests/sqlite/grammar/test_sqlite_select_grammar.py @@ -380,11 +380,25 @@ def can_compile_join_clause_with_null(self): clause = ( JoinClause("report_groups as rg") .on_null("bgt.acct") + .or_on_null("bgt.dept") + .on_value("rg.abc", 10) + ) + builder.join(clause).to_sql() + """ + return """SELECT * FROM "users" INNER JOIN "report_groups" AS "rg" ON "acct" IS NULL OR "dept" IS NULL AND "rg"."abc" = '10'""" + + def can_compile_join_clause_with_not_null(self): + """ + builder = self.get_builder() + clause = ( + JoinClause("report_groups as rg") + .on_not_null("bgt.acct") .or_on_not_null("bgt.dept") + .on_value("rg.abc", 10) ) builder.join(clause).to_sql() """ - return """SELECT * FROM "users" INNER JOIN "report_groups" AS "rg" ON "acct" IS NULL OR "dept" IS NOT NULL""" + return """SELECT * FROM "users" INNER JOIN "report_groups" AS "rg" ON "acct" IS NOT NULL OR "dept" IS NOT NULL AND "rg"."abc" = '10'""" def can_compile_join_clause_with_lambda(self): """ From 3ff0a839a35b173aad5151c4ffa3bc8ffeec55fb Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sat, 26 Oct 2024 11:32:18 -0400 Subject: [PATCH 184/254] Refactor MySQLConnection to use connection pooling --- .../connections/MySQLConnection.py | 78 ++++++++++++------- tests/integrations/config/database.py | 6 +- 2 files changed, 54 insertions(+), 30 deletions(-) diff --git a/src/masoniteorm/connections/MySQLConnection.py b/src/masoniteorm/connections/MySQLConnection.py index 7ff44d9b6..578cac4cd 100644 --- a/src/masoniteorm/connections/MySQLConnection.py +++ b/src/masoniteorm/connections/MySQLConnection.py @@ -55,27 +55,45 @@ def make_connection(self): "You must have the 'pymysql' package installed to make a connection to MySQL. Please install it using 'pip install pymysql'" ) - try: - import pendulum - import pymysql.converters - - pymysql.converters.conversions[ - pendulum.DateTime - ] = pymysql.converters.escape_datetime - except ImportError: - pass if self.has_global_connection(): return self.get_global_connection() # Check if there is an available connection in the pool - if CONNECTION_POOL: - self._connection = CONNECTION_POOL.pop() + self._connection = self.create_connection() + + self._connection.close = self.close_connection + self.enable_disable_foreign_keys() + + # self._connection._open = 1 + self.open = 1 + + return self + + # Add the connection back to the pool when it's closed + def close_connection(self): + if self.full_details.get("connection_pooling_enabled") and len(CONNECTION_POOL) < self.full_details.get("connection_pooling_size", 10): + print("connection closing. pool append", self._connection, CONNECTION_POOL, len(CONNECTION_POOL)) + CONNECTION_POOL.append(self._connection) + self._connection = None + + def create_connection(self, autocommit=True): + import pymysql + import pendulum + import pymysql.converters + pymysql.converters.conversions[ + pendulum.DateTime + ] = pymysql.converters.escape_datetime + + print("STARTING POOL", CONNECTION_POOL, len(CONNECTION_POOL)) + + if self.full_details.get("connection_pooling_enabled") and CONNECTION_POOL: + connection = CONNECTION_POOL.pop() + print("pool popped", connection, "remaining:", CONNECTION_POOL, len(CONNECTION_POOL)) else: - if len(CONNECTION_POOL) < self.options.get("pool_size", 5): # Default pool size is 5 - self._connection = pymysql.connect( + connection = pymysql.connect( cursorclass=pymysql.cursors.DictCursor, - autocommit=True, + autocommit=autocommit, host=self.host, user=self.user, password=self.password, @@ -83,21 +101,18 @@ def make_connection(self): database=self.database, **self.options ) - else: - raise ConnectionError("Connection pool limit reached") + + # Add the connection to the pool if pooling is enabled and the pool size is not exceeded + if self.full_details.get("connection_pooling_enabled"): + connection_pooling_size = self.full_details.get("connection_pooling_size", 10) + if len(CONNECTION_POOL) < connection_pooling_size: + CONNECTION_POOL.append(connection) + + return connection + + + return - # Add the connection back to the pool when it's closed - def close_connection(): - CONNECTION_POOL.append(self._connection) - self._connection = None - - self._connection.close = close_connection - - self.enable_disable_foreign_keys() - - self.open = 1 - - return self def reconnect(self): self._connection.connect() @@ -170,10 +185,15 @@ def query(self, query, bindings=(), results="*"): if self._dry: return {} - if not self._connection.open: + if not self.open: + if self._connection is None: + self._connection = self.create_connection() + self._connection.connect() self._cursor = self._connection.cursor() + + print("pool", self._connection, CONNECTION_POOL) try: with self._cursor as cursor: diff --git a/tests/integrations/config/database.py b/tests/integrations/config/database.py index fe4473fa9..452dda0e6 100644 --- a/tests/integrations/config/database.py +++ b/tests/integrations/config/database.py @@ -31,12 +31,14 @@ "host": os.getenv("MYSQL_DATABASE_HOST"), "user": os.getenv("MYSQL_DATABASE_USER"), "password": os.getenv("MYSQL_DATABASE_PASSWORD"), - "database": os.getenv("MYSQL_DATABASE_DATABASE"), + "database": "rothco_ll_preproduction", "port": os.getenv("MYSQL_DATABASE_PORT"), "prefix": "", "options": {"charset": "utf8mb4"}, "log_queries": True, "propagate": False, + "connection_pooling_enabled": True, + "connection_pooling_size": 2, }, "t": {"driver": "sqlite", "database": "orm.sqlite3", "log_queries": True, "foreign_keys": True}, "devprod": { @@ -101,6 +103,8 @@ "authentication": "ActiveDirectoryPassword", "driver": "ODBC Driver 17 for SQL Server", "connection_timeout": 15, + "connection_pooling": False, + "connection_pooling_size": 100, }, }, } From bc08c93823494b667c9cafc9591a1f6d5bcf6a3b Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 27 Oct 2024 11:47:13 -0400 Subject: [PATCH 185/254] Refactor MySQLConnection to use connection pooling and add connection_pool_size parameter --- .../connections/MySQLConnection.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/masoniteorm/connections/MySQLConnection.py b/src/masoniteorm/connections/MySQLConnection.py index 578cac4cd..613e6e690 100644 --- a/src/masoniteorm/connections/MySQLConnection.py +++ b/src/masoniteorm/connections/MySQLConnection.py @@ -31,10 +31,12 @@ def __init__( if str(port).isdigit(): self.port = int(self.port) self.database = database + self.user = user self.password = password self.prefix = prefix self.full_details = full_details or {} + self.connection_pool_size = full_details.get("connection_pooling_size", 100) self.options = options or {} self._cursor = None self.open = 0 @@ -72,9 +74,10 @@ def make_connection(self): # Add the connection back to the pool when it's closed def close_connection(self): - if self.full_details.get("connection_pooling_enabled") and len(CONNECTION_POOL) < self.full_details.get("connection_pooling_size", 10): - print("connection closing. pool append", self._connection, CONNECTION_POOL, len(CONNECTION_POOL)) + if self.full_details.get("connection_pooling_enabled") and len(CONNECTION_POOL) < self.connection_pool_size: CONNECTION_POOL.append(self._connection) + print("connection closing. pool append", self._connection, CONNECTION_POOL, len(CONNECTION_POOL)) + self._connection = None def create_connection(self, autocommit=True): @@ -87,7 +90,7 @@ def create_connection(self, autocommit=True): print("STARTING POOL", CONNECTION_POOL, len(CONNECTION_POOL)) - if self.full_details.get("connection_pooling_enabled") and CONNECTION_POOL: + if self.full_details.get("connection_pooling_enabled") and CONNECTION_POOL and len(CONNECTION_POOL) > 0: connection = CONNECTION_POOL.pop() print("pool popped", connection, "remaining:", CONNECTION_POOL, len(CONNECTION_POOL)) else: @@ -102,17 +105,7 @@ def create_connection(self, autocommit=True): **self.options ) - # Add the connection to the pool if pooling is enabled and the pool size is not exceeded - if self.full_details.get("connection_pooling_enabled"): - connection_pooling_size = self.full_details.get("connection_pooling_size", 10) - if len(CONNECTION_POOL) < connection_pooling_size: - CONNECTION_POOL.append(connection) - return connection - - - return - def reconnect(self): self._connection.connect() From 28418eb792bc8c32ba4515201b18276d019d03d8 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 27 Oct 2024 11:54:26 -0400 Subject: [PATCH 186/254] Refactor MySQLConnection to use connection pooling and initialize connection pool with specified size --- src/masoniteorm/connections/MySQLConnection.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/masoniteorm/connections/MySQLConnection.py b/src/masoniteorm/connections/MySQLConnection.py index 613e6e690..122a80df1 100644 --- a/src/masoniteorm/connections/MySQLConnection.py +++ b/src/masoniteorm/connections/MySQLConnection.py @@ -90,6 +90,23 @@ def create_connection(self, autocommit=True): print("STARTING POOL", CONNECTION_POOL, len(CONNECTION_POOL)) + # Initialize the connection pool if the option is set + initialize_size = self.full_details.get("connection_pool_initialize_size") + if initialize_size and len(CONNECTION_POOL) < initialize_size: + for _ in range(initialize_size - len(CONNECTION_POOL)): + connection = pymysql.connect( + cursorclass=pymysql.cursors.DictCursor, + autocommit=autocommit, + host=self.host, + user=self.user, + password=self.password, + port=self.port, + database=self.database, + **self.options + ) + CONNECTION_POOL.append(connection) + print(f"Initialized connection pool with {initialize_size} connections") + if self.full_details.get("connection_pooling_enabled") and CONNECTION_POOL and len(CONNECTION_POOL) > 0: connection = CONNECTION_POOL.pop() print("pool popped", connection, "remaining:", CONNECTION_POOL, len(CONNECTION_POOL)) From 341db08a2a3580aae42255bd86b498e35062e96a Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 27 Oct 2024 11:55:23 -0400 Subject: [PATCH 187/254] Refactor MySQLConnection to use connection pooling and remove debug print statements --- src/masoniteorm/connections/MySQLConnection.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/masoniteorm/connections/MySQLConnection.py b/src/masoniteorm/connections/MySQLConnection.py index 122a80df1..938813c17 100644 --- a/src/masoniteorm/connections/MySQLConnection.py +++ b/src/masoniteorm/connections/MySQLConnection.py @@ -76,7 +76,6 @@ def make_connection(self): def close_connection(self): if self.full_details.get("connection_pooling_enabled") and len(CONNECTION_POOL) < self.connection_pool_size: CONNECTION_POOL.append(self._connection) - print("connection closing. pool append", self._connection, CONNECTION_POOL, len(CONNECTION_POOL)) self._connection = None @@ -88,7 +87,6 @@ def create_connection(self, autocommit=True): pendulum.DateTime ] = pymysql.converters.escape_datetime - print("STARTING POOL", CONNECTION_POOL, len(CONNECTION_POOL)) # Initialize the connection pool if the option is set initialize_size = self.full_details.get("connection_pool_initialize_size") @@ -105,11 +103,9 @@ def create_connection(self, autocommit=True): **self.options ) CONNECTION_POOL.append(connection) - print(f"Initialized connection pool with {initialize_size} connections") if self.full_details.get("connection_pooling_enabled") and CONNECTION_POOL and len(CONNECTION_POOL) > 0: connection = CONNECTION_POOL.pop() - print("pool popped", connection, "remaining:", CONNECTION_POOL, len(CONNECTION_POOL)) else: connection = pymysql.connect( cursorclass=pymysql.cursors.DictCursor, @@ -203,7 +199,6 @@ def query(self, query, bindings=(), results="*"): self._cursor = self._connection.cursor() - print("pool", self._connection, CONNECTION_POOL) try: with self._cursor as cursor: From 1ad5ad74e6423bd4abe59596231aad7a500f61d8 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 27 Oct 2024 11:59:06 -0400 Subject: [PATCH 188/254] Refactor MySQLConnection to use connection pooling and update connection pool size parameter --- src/masoniteorm/connections/MySQLConnection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/masoniteorm/connections/MySQLConnection.py b/src/masoniteorm/connections/MySQLConnection.py index 938813c17..9eb13beab 100644 --- a/src/masoniteorm/connections/MySQLConnection.py +++ b/src/masoniteorm/connections/MySQLConnection.py @@ -36,7 +36,7 @@ def __init__( self.password = password self.prefix = prefix self.full_details = full_details or {} - self.connection_pool_size = full_details.get("connection_pooling_size", 100) + self.connection_pool_size = full_details.get("connection_pooling_max_size", 100) self.options = options or {} self._cursor = None self.open = 0 @@ -89,7 +89,7 @@ def create_connection(self, autocommit=True): # Initialize the connection pool if the option is set - initialize_size = self.full_details.get("connection_pool_initialize_size") + initialize_size = self.full_details.get("connection_pool_min_size") if initialize_size and len(CONNECTION_POOL) < initialize_size: for _ in range(initialize_size - len(CONNECTION_POOL)): connection = pymysql.connect( From ba7372f2dbd1ea3a36a5b688c73a6df229c65673 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 27 Oct 2024 12:04:00 -0400 Subject: [PATCH 189/254] Refactor MySQLConnection to use connection pooling and update connection pool size parameter --- tests/integrations/config/database.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/integrations/config/database.py b/tests/integrations/config/database.py index 452dda0e6..4cf7b3a7a 100644 --- a/tests/integrations/config/database.py +++ b/tests/integrations/config/database.py @@ -24,6 +24,7 @@ They can be named whatever you want. """ + DATABASES = { "default": "mysql", "mysql": { @@ -31,14 +32,15 @@ "host": os.getenv("MYSQL_DATABASE_HOST"), "user": os.getenv("MYSQL_DATABASE_USER"), "password": os.getenv("MYSQL_DATABASE_PASSWORD"), - "database": "rothco_ll_preproduction", + "database": os.getenv("MYSQL_DATABASE_DATABASE"), "port": os.getenv("MYSQL_DATABASE_PORT"), "prefix": "", "options": {"charset": "utf8mb4"}, "log_queries": True, "propagate": False, "connection_pooling_enabled": True, - "connection_pooling_size": 2, + "connection_pooling_max_size": 10, + "connection_pool_min_size": None, }, "t": {"driver": "sqlite", "database": "orm.sqlite3", "log_queries": True, "foreign_keys": True}, "devprod": { From cf9bc847966e200656cca8d9b816d4bc79c7fb3c Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 27 Oct 2024 12:04:37 -0400 Subject: [PATCH 190/254] Refactor MySQLConnection to use connection pooling and update connection pool size parameter --- src/masoniteorm/connections/MySQLConnection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/masoniteorm/connections/MySQLConnection.py b/src/masoniteorm/connections/MySQLConnection.py index 9eb13beab..5045f59c5 100644 --- a/src/masoniteorm/connections/MySQLConnection.py +++ b/src/masoniteorm/connections/MySQLConnection.py @@ -89,7 +89,7 @@ def create_connection(self, autocommit=True): # Initialize the connection pool if the option is set - initialize_size = self.full_details.get("connection_pool_min_size") + initialize_size = self.full_details.get("connection_pooling_min_size") if initialize_size and len(CONNECTION_POOL) < initialize_size: for _ in range(initialize_size - len(CONNECTION_POOL)): connection = pymysql.connect( From 3010894ff7e710d6c9aee74577dc8ae2c0ef756a Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 27 Oct 2024 12:09:44 -0400 Subject: [PATCH 191/254] Refactor MySQLConnection to use connection pooling and update connection pool size parameter --- .../connections/MySQLConnection.py | 34 +++++++++++-------- tests/integrations/config/database.py | 2 +- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/masoniteorm/connections/MySQLConnection.py b/src/masoniteorm/connections/MySQLConnection.py index 5045f59c5..1875414d7 100644 --- a/src/masoniteorm/connections/MySQLConnection.py +++ b/src/masoniteorm/connections/MySQLConnection.py @@ -31,7 +31,7 @@ def __init__( if str(port).isdigit(): self.port = int(self.port) self.database = database - + self.user = user self.password = password self.prefix = prefix @@ -57,13 +57,12 @@ def make_connection(self): "You must have the 'pymysql' package installed to make a connection to MySQL. Please install it using 'pip install pymysql'" ) - if self.has_global_connection(): return self.get_global_connection() # Check if there is an available connection in the pool self._connection = self.create_connection() - + self._connection.close = self.close_connection self.enable_disable_foreign_keys() @@ -73,21 +72,25 @@ def make_connection(self): return self # Add the connection back to the pool when it's closed + def close_connection(self): - if self.full_details.get("connection_pooling_enabled") and len(CONNECTION_POOL) < self.connection_pool_size: + if ( + self.full_details.get("connection_pooling_enabled") + and len(CONNECTION_POOL) < self.connection_pool_size + ): CONNECTION_POOL.append(self._connection) self._connection = None - + def create_connection(self, autocommit=True): import pymysql import pendulum import pymysql.converters - pymysql.converters.conversions[ - pendulum.DateTime - ] = pymysql.converters.escape_datetime - - + + pymysql.converters.conversions[pendulum.DateTime] = ( + pymysql.converters.escape_datetime + ) + # Initialize the connection pool if the option is set initialize_size = self.full_details.get("connection_pooling_min_size") if initialize_size and len(CONNECTION_POOL) < initialize_size: @@ -104,7 +107,11 @@ def create_connection(self, autocommit=True): ) CONNECTION_POOL.append(connection) - if self.full_details.get("connection_pooling_enabled") and CONNECTION_POOL and len(CONNECTION_POOL) > 0: + if ( + self.full_details.get("connection_pooling_enabled") + and CONNECTION_POOL + and len(CONNECTION_POOL) > 0 + ): connection = CONNECTION_POOL.pop() else: connection = pymysql.connect( @@ -117,7 +124,7 @@ def create_connection(self, autocommit=True): database=self.database, **self.options ) - + return connection def reconnect(self): @@ -194,11 +201,10 @@ def query(self, query, bindings=(), results="*"): if not self.open: if self._connection is None: self._connection = self.create_connection() - + self._connection.connect() self._cursor = self._connection.cursor() - try: with self._cursor as cursor: diff --git a/tests/integrations/config/database.py b/tests/integrations/config/database.py index 4cf7b3a7a..48edb0b14 100644 --- a/tests/integrations/config/database.py +++ b/tests/integrations/config/database.py @@ -40,7 +40,7 @@ "propagate": False, "connection_pooling_enabled": True, "connection_pooling_max_size": 10, - "connection_pool_min_size": None, + "connection_pooling_min_size": None, }, "t": {"driver": "sqlite", "database": "orm.sqlite3", "log_queries": True, "foreign_keys": True}, "devprod": { From db112cade634e072321cd3fc4e7509d025b85985 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 27 Oct 2024 12:10:55 -0400 Subject: [PATCH 192/254] format --- src/masoniteorm/connections/PostgresConnection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/masoniteorm/connections/PostgresConnection.py b/src/masoniteorm/connections/PostgresConnection.py index 5304365a8..8864411e5 100644 --- a/src/masoniteorm/connections/PostgresConnection.py +++ b/src/masoniteorm/connections/PostgresConnection.py @@ -63,7 +63,8 @@ def make_connection(self): if not CONNECTION_POOL: CONNECTION_POOL.append( pool.SimpleConnectionPool( - 1, 20, # minconn, maxconn + 1, + 20, # minconn, maxconn database=self.database, user=self.user, password=self.password, @@ -131,7 +132,6 @@ def get_transaction_level(self): def set_cursor(self): from psycopg2.extras import RealDictCursor - self._cursor = self._connection.cursor(cursor_factory=RealDictCursor) return self._cursor From 8291bc708884803a91c37d9ccbb9772545a6e061 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 27 Oct 2024 12:36:32 -0400 Subject: [PATCH 193/254] Refactor PostgresConnection to use connection pooling and update connection pool size parameter --- .../connections/PostgresConnection.py | 71 ++++++++++++++----- 1 file changed, 52 insertions(+), 19 deletions(-) diff --git a/src/masoniteorm/connections/PostgresConnection.py b/src/masoniteorm/connections/PostgresConnection.py index 8864411e5..abaf8d859 100644 --- a/src/masoniteorm/connections/PostgresConnection.py +++ b/src/masoniteorm/connections/PostgresConnection.py @@ -4,7 +4,6 @@ from ..schema.platforms import PostgresPlatform from ..query.processors import PostgresPostProcessor from ..exceptions import QueryException -from psycopg2 import pool CONNECTION_POOL = [] @@ -35,8 +34,10 @@ def __init__( self.database = database self.user = user self.password = password + self.prefix = prefix self.full_details = full_details or {} + self.connection_pool_size = full_details.get("connection_pooling_max_size", 100) self.options = options or {} self._cursor = None self.transaction_level = 0 @@ -59,23 +60,7 @@ def make_connection(self): schema = self.schema or self.full_details.get("schema") - # if connection pool is empty, create a new connection pool - if not CONNECTION_POOL: - CONNECTION_POOL.append( - pool.SimpleConnectionPool( - 1, - 20, # minconn, maxconn - database=self.database, - user=self.user, - password=self.password, - host=self.host, - port=self.port, - options=f"-c search_path={schema}" if schema else "", - ) - ) - - # get a connection from the pool - self._connection = CONNECTION_POOL[0].getconn() + self._connection = self.create_connection() self._connection.autocommit = True @@ -84,7 +69,43 @@ def make_connection(self): self.open = 1 return self + + def create_connection(self): + import psycopg2 + + # Initialize the connection pool if the option is set + initialize_size = self.full_details.get("connection_pooling_min_size") + if self.full_details.get("connection_pooling_enabled") and initialize_size and len(CONNECTION_POOL) < initialize_size: + for _ in range(initialize_size - len(CONNECTION_POOL)): + connection = psycopg2.connect( + database=self.database, + user=self.user, + password=self.password, + host=self.host, + port=self.port, + options=f"-c search_path={self.schema or self.full_details.get('schema')}" if self.schema or self.full_details.get('schema') else "", + ) + CONNECTION_POOL.append(connection) + + + if ( + self.full_details.get("connection_pooling_enabled") + and CONNECTION_POOL + and len(CONNECTION_POOL) > 0 + ): + connection = CONNECTION_POOL.pop() + else: + connection = psycopg2.connect( + database=self.database, + user=self.user, + password=self.password, + host=self.host, + port=self.port, + options=f"-c search_path={self.schema or self.full_details.get('schema')}" if self.schema or self.full_details.get('schema') else "", + ) + return connection + def get_database_name(self): return self.database @@ -102,6 +123,17 @@ def get_default_post_processor(cls): def reconnect(self): pass + + def close_connection(self): + if ( + self.full_details.get("connection_pooling_enabled") + and len(CONNECTION_POOL) < self.connection_pool_size + ): + CONNECTION_POOL.append(self._connection) + else: + self._connection.close() + + self._connection = None def commit(self): """Transaction""" @@ -174,4 +206,5 @@ def query(self, query, bindings=(), results="*"): finally: if self.get_transaction_level() <= 0: self.open = 0 - self._connection.close() + self.close_connection() + # self._connection.close() From 4cbd0e40b3bc186c223eba2b1ac70ab84326552d Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 27 Oct 2024 12:38:00 -0400 Subject: [PATCH 194/254] Refactor PostgresConnection to remove unused variable --- src/masoniteorm/connections/PostgresConnection.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/masoniteorm/connections/PostgresConnection.py b/src/masoniteorm/connections/PostgresConnection.py index abaf8d859..deb3075c9 100644 --- a/src/masoniteorm/connections/PostgresConnection.py +++ b/src/masoniteorm/connections/PostgresConnection.py @@ -58,8 +58,6 @@ def make_connection(self): if self.has_global_connection(): return self.get_global_connection() - schema = self.schema or self.full_details.get("schema") - self._connection = self.create_connection() self._connection.autocommit = True From a4656b90928fa15c907dbb285c32430814e286e4 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 27 Oct 2024 12:40:33 -0400 Subject: [PATCH 195/254] Refactor PostgresConnection to enable connection pooling with a maximum size of 10 --- tests/integrations/config/database.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/integrations/config/database.py b/tests/integrations/config/database.py index 48edb0b14..d7a64acb7 100644 --- a/tests/integrations/config/database.py +++ b/tests/integrations/config/database.py @@ -73,6 +73,9 @@ "password": os.getenv("POSTGRES_DATABASE_PASSWORD"), "database": os.getenv("POSTGRES_DATABASE_DATABASE"), "port": os.getenv("POSTGRES_DATABASE_PORT"), + "connection_pooling_enabled": True, + "connection_pooling_max_size": 10, + "connection_pooling_min_size": None, "prefix": "", "log_queries": True, "propagate": False, From 6a237fa58128c3644f269eea62c1d6a143c894df Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 27 Oct 2024 12:45:18 -0400 Subject: [PATCH 196/254] Refactor database configuration to update connection pool minimum size --- tests/integrations/config/database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/config/database.py b/tests/integrations/config/database.py index d7a64acb7..ed1fd02e0 100644 --- a/tests/integrations/config/database.py +++ b/tests/integrations/config/database.py @@ -75,7 +75,7 @@ "port": os.getenv("POSTGRES_DATABASE_PORT"), "connection_pooling_enabled": True, "connection_pooling_max_size": 10, - "connection_pooling_min_size": None, + "connection_pooling_min_size": 2, "prefix": "", "log_queries": True, "propagate": False, From 6c4ff02617c8ff0e164e1e5748682d42da8793ac Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 27 Oct 2024 13:03:54 -0400 Subject: [PATCH 197/254] Refactor SQLiteConnection to use a separate method for creating the connection --- src/masoniteorm/connections/SQLiteConnection.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/masoniteorm/connections/SQLiteConnection.py b/src/masoniteorm/connections/SQLiteConnection.py index 24bd7f0d8..0bbd2d876 100644 --- a/src/masoniteorm/connections/SQLiteConnection.py +++ b/src/masoniteorm/connections/SQLiteConnection.py @@ -59,16 +59,20 @@ def make_connection(self): if self.has_global_connection(): return self.get_global_connection() - self._connection = sqlite3.connect(self.database, isolation_level=None) - self._connection.create_function("REGEXP", 2, regexp) - - self._connection.row_factory = sqlite3.Row + self._connection = self.create_connection() self.enable_disable_foreign_keys() self.open = 1 return self + + def create_connection(self): + import sqlite3 + connection = sqlite3.connect(self.database, isolation_level=None) + connection.create_function("REGEXP", 2, regexp) + connection.row_factory = sqlite3.Row + return connection @classmethod def get_default_query_grammar(cls): From d14c826ad1ffbc1f0208a52915cc86c453446e28 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 27 Oct 2024 14:44:17 -0400 Subject: [PATCH 198/254] Refactor SQLiteConnection to use a separate method for creating the connection --- src/masoniteorm/connections/SQLiteConnection.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/masoniteorm/connections/SQLiteConnection.py b/src/masoniteorm/connections/SQLiteConnection.py index 0bbd2d876..24bd7f0d8 100644 --- a/src/masoniteorm/connections/SQLiteConnection.py +++ b/src/masoniteorm/connections/SQLiteConnection.py @@ -59,20 +59,16 @@ def make_connection(self): if self.has_global_connection(): return self.get_global_connection() - self._connection = self.create_connection() + self._connection = sqlite3.connect(self.database, isolation_level=None) + self._connection.create_function("REGEXP", 2, regexp) + + self._connection.row_factory = sqlite3.Row self.enable_disable_foreign_keys() self.open = 1 return self - - def create_connection(self): - import sqlite3 - connection = sqlite3.connect(self.database, isolation_level=None) - connection.create_function("REGEXP", 2, regexp) - connection.row_factory = sqlite3.Row - return connection @classmethod def get_default_query_grammar(cls): From 76590f84423e40c90ef9108d95142f9fd1d4ded4 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 27 Oct 2024 14:45:29 -0400 Subject: [PATCH 199/254] linted --- .../connections/PostgresConnection.py | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/masoniteorm/connections/PostgresConnection.py b/src/masoniteorm/connections/PostgresConnection.py index deb3075c9..20a96b4b5 100644 --- a/src/masoniteorm/connections/PostgresConnection.py +++ b/src/masoniteorm/connections/PostgresConnection.py @@ -34,7 +34,7 @@ def __init__( self.database = database self.user = user self.password = password - + self.prefix = prefix self.full_details = full_details or {} self.connection_pool_size = full_details.get("connection_pooling_max_size", 100) @@ -67,13 +67,17 @@ def make_connection(self): self.open = 1 return self - + def create_connection(self): import psycopg2 # Initialize the connection pool if the option is set initialize_size = self.full_details.get("connection_pooling_min_size") - if self.full_details.get("connection_pooling_enabled") and initialize_size and len(CONNECTION_POOL) < initialize_size: + if ( + self.full_details.get("connection_pooling_enabled") + and initialize_size + and len(CONNECTION_POOL) < initialize_size + ): for _ in range(initialize_size - len(CONNECTION_POOL)): connection = psycopg2.connect( database=self.database, @@ -81,10 +85,13 @@ def create_connection(self): password=self.password, host=self.host, port=self.port, - options=f"-c search_path={self.schema or self.full_details.get('schema')}" if self.schema or self.full_details.get('schema') else "", + options=( + f"-c search_path={self.schema or self.full_details.get('schema')}" + if self.schema or self.full_details.get("schema") + else "" + ), ) CONNECTION_POOL.append(connection) - if ( self.full_details.get("connection_pooling_enabled") @@ -99,11 +106,15 @@ def create_connection(self): password=self.password, host=self.host, port=self.port, - options=f"-c search_path={self.schema or self.full_details.get('schema')}" if self.schema or self.full_details.get('schema') else "", + options=( + f"-c search_path={self.schema or self.full_details.get('schema')}" + if self.schema or self.full_details.get("schema") + else "" + ), ) return connection - + def get_database_name(self): return self.database @@ -121,7 +132,7 @@ def get_default_post_processor(cls): def reconnect(self): pass - + def close_connection(self): if ( self.full_details.get("connection_pooling_enabled") From d28435b6ee5bededa3b16748bb13a14e0a2752b5 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 27 Oct 2024 14:50:08 -0400 Subject: [PATCH 200/254] Refactor PostgresConnection to handle closed connections during query execution --- src/masoniteorm/connections/PostgresConnection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/masoniteorm/connections/PostgresConnection.py b/src/masoniteorm/connections/PostgresConnection.py index 20a96b4b5..b5c6253ea 100644 --- a/src/masoniteorm/connections/PostgresConnection.py +++ b/src/masoniteorm/connections/PostgresConnection.py @@ -191,7 +191,7 @@ def query(self, query, bindings=(), results="*"): dict|None -- Returns a dictionary of results or None """ try: - if self._connection.closed: + if not hasattr(self, '_connection') or self._connection.closed: self.make_connection() self.set_cursor() From 9475b9eee179a5b70aba81098355d98eec3963d7 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 27 Oct 2024 14:50:37 -0400 Subject: [PATCH 201/254] Refactor PostgresConnection to handle closed connections during query execution --- src/masoniteorm/connections/PostgresConnection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/masoniteorm/connections/PostgresConnection.py b/src/masoniteorm/connections/PostgresConnection.py index b5c6253ea..0bbfe172f 100644 --- a/src/masoniteorm/connections/PostgresConnection.py +++ b/src/masoniteorm/connections/PostgresConnection.py @@ -191,7 +191,7 @@ def query(self, query, bindings=(), results="*"): dict|None -- Returns a dictionary of results or None """ try: - if not hasattr(self, '_connection') or self._connection.closed: + if not self._connection or self._connection.closed: self.make_connection() self.set_cursor() From 09d082b1c4e2f489c92155e9c97e78bf8032a810 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 27 Oct 2024 15:01:58 -0400 Subject: [PATCH 202/254] Refactor MySQLConnection to handle closed connections during query execution --- .../connections/MySQLConnection.py | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/src/masoniteorm/connections/MySQLConnection.py b/src/masoniteorm/connections/MySQLConnection.py index 1875414d7..fa3c25483 100644 --- a/src/masoniteorm/connections/MySQLConnection.py +++ b/src/masoniteorm/connections/MySQLConnection.py @@ -31,7 +31,7 @@ def __init__( if str(port).isdigit(): self.port = int(self.port) self.database = database - + self.user = user self.password = password self.prefix = prefix @@ -57,40 +57,35 @@ def make_connection(self): "You must have the 'pymysql' package installed to make a connection to MySQL. Please install it using 'pip install pymysql'" ) + if self.has_global_connection(): return self.get_global_connection() # Check if there is an available connection in the pool self._connection = self.create_connection() - self._connection.close = self.close_connection self.enable_disable_foreign_keys() - # self._connection._open = 1 self.open = 1 return self # Add the connection back to the pool when it's closed - def close_connection(self): - if ( - self.full_details.get("connection_pooling_enabled") - and len(CONNECTION_POOL) < self.connection_pool_size - ): + if self.full_details.get("connection_pooling_enabled") and len(CONNECTION_POOL) < self.connection_pool_size: CONNECTION_POOL.append(self._connection) self._connection = None - + def create_connection(self, autocommit=True): import pymysql import pendulum import pymysql.converters - - pymysql.converters.conversions[pendulum.DateTime] = ( - pymysql.converters.escape_datetime - ) - + pymysql.converters.conversions[ + pendulum.DateTime + ] = pymysql.converters.escape_datetime + + # Initialize the connection pool if the option is set initialize_size = self.full_details.get("connection_pooling_min_size") if initialize_size and len(CONNECTION_POOL) < initialize_size: @@ -107,11 +102,7 @@ def create_connection(self, autocommit=True): ) CONNECTION_POOL.append(connection) - if ( - self.full_details.get("connection_pooling_enabled") - and CONNECTION_POOL - and len(CONNECTION_POOL) > 0 - ): + if self.full_details.get("connection_pooling_enabled") and CONNECTION_POOL and len(CONNECTION_POOL) > 0: connection = CONNECTION_POOL.pop() else: connection = pymysql.connect( @@ -124,7 +115,7 @@ def create_connection(self, autocommit=True): database=self.database, **self.options ) - + return connection def reconnect(self): @@ -201,10 +192,11 @@ def query(self, query, bindings=(), results="*"): if not self.open: if self._connection is None: self._connection = self.create_connection() - + self._connection.connect() self._cursor = self._connection.cursor() + try: with self._cursor as cursor: From e4662828aa8819577a3011362c04b96611fd672c Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 27 Oct 2024 15:04:57 -0400 Subject: [PATCH 203/254] Refactor MySQLConnection to handle closed connections during query execution --- src/masoniteorm/connections/MySQLConnection.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/masoniteorm/connections/MySQLConnection.py b/src/masoniteorm/connections/MySQLConnection.py index fa3c25483..f972238a2 100644 --- a/src/masoniteorm/connections/MySQLConnection.py +++ b/src/masoniteorm/connections/MySQLConnection.py @@ -63,7 +63,6 @@ def make_connection(self): # Check if there is an available connection in the pool self._connection = self.create_connection() - self._connection.close = self.close_connection self.enable_disable_foreign_keys() self.open = 1 @@ -115,6 +114,8 @@ def create_connection(self, autocommit=True): database=self.database, **self.options ) + + connection.close = self.close_connection return connection From 94b1347981a1cc67e0343b0aa8e3bad182238794 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 27 Oct 2024 15:07:45 -0400 Subject: [PATCH 204/254] Refactor MySQLConnection to handle closed connections during query execution --- .../connections/MySQLConnection.py | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/masoniteorm/connections/MySQLConnection.py b/src/masoniteorm/connections/MySQLConnection.py index f972238a2..fba0777aa 100644 --- a/src/masoniteorm/connections/MySQLConnection.py +++ b/src/masoniteorm/connections/MySQLConnection.py @@ -31,7 +31,7 @@ def __init__( if str(port).isdigit(): self.port = int(self.port) self.database = database - + self.user = user self.password = password self.prefix = prefix @@ -57,7 +57,6 @@ def make_connection(self): "You must have the 'pymysql' package installed to make a connection to MySQL. Please install it using 'pip install pymysql'" ) - if self.has_global_connection(): return self.get_global_connection() @@ -70,21 +69,25 @@ def make_connection(self): return self # Add the connection back to the pool when it's closed + def close_connection(self): - if self.full_details.get("connection_pooling_enabled") and len(CONNECTION_POOL) < self.connection_pool_size: + if ( + self.full_details.get("connection_pooling_enabled") + and len(CONNECTION_POOL) < self.connection_pool_size + ): CONNECTION_POOL.append(self._connection) self._connection = None - + def create_connection(self, autocommit=True): import pymysql import pendulum import pymysql.converters - pymysql.converters.conversions[ - pendulum.DateTime - ] = pymysql.converters.escape_datetime - - + + pymysql.converters.conversions[pendulum.DateTime] = ( + pymysql.converters.escape_datetime + ) + # Initialize the connection pool if the option is set initialize_size = self.full_details.get("connection_pooling_min_size") if initialize_size and len(CONNECTION_POOL) < initialize_size: @@ -101,7 +104,11 @@ def create_connection(self, autocommit=True): ) CONNECTION_POOL.append(connection) - if self.full_details.get("connection_pooling_enabled") and CONNECTION_POOL and len(CONNECTION_POOL) > 0: + if ( + self.full_details.get("connection_pooling_enabled") + and CONNECTION_POOL + and len(CONNECTION_POOL) > 0 + ): connection = CONNECTION_POOL.pop() else: connection = pymysql.connect( @@ -114,9 +121,9 @@ def create_connection(self, autocommit=True): database=self.database, **self.options ) - + connection.close = self.close_connection - + return connection def reconnect(self): @@ -193,11 +200,10 @@ def query(self, query, bindings=(), results="*"): if not self.open: if self._connection is None: self._connection = self.create_connection() - + self._connection.connect() self._cursor = self._connection.cursor() - try: with self._cursor as cursor: From fd6785fd36091c9db4483552af29a0b0fe62f8bb Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 27 Oct 2024 15:43:02 -0400 Subject: [PATCH 205/254] linted --- .../connections/MySQLConnection.py | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/masoniteorm/connections/MySQLConnection.py b/src/masoniteorm/connections/MySQLConnection.py index fba0777aa..c96ac8e91 100644 --- a/src/masoniteorm/connections/MySQLConnection.py +++ b/src/masoniteorm/connections/MySQLConnection.py @@ -36,7 +36,11 @@ def __init__( self.password = password self.prefix = prefix self.full_details = full_details or {} - self.connection_pool_size = full_details.get("connection_pooling_max_size", 100) + self.connection_pool_size = ( + full_details.get( + "connection_pooling_max_size", 100 + ) + ) self.options = options or {} self._cursor = None self.open = 0 @@ -50,13 +54,6 @@ def make_connection(self): if self._dry: return - try: - import pymysql - except ModuleNotFoundError: - raise DriverNotFound( - "You must have the 'pymysql' package installed to make a connection to MySQL. Please install it using 'pip install pymysql'" - ) - if self.has_global_connection(): return self.get_global_connection() @@ -80,7 +77,15 @@ def close_connection(self): self._connection = None def create_connection(self, autocommit=True): - import pymysql + + try: + import pymysql + except ModuleNotFoundError: + raise DriverNotFound( + "You must have the 'pymysql' package " + "installed to make a connection to MySQL. " + "Please install it using 'pip install pymysql'" + ) import pendulum import pymysql.converters @@ -180,15 +185,19 @@ def get_cursor(self): return self._cursor def query(self, query, bindings=(), results="*"): - """Make the actual query that will reach the database and come back with a result. + """Make the actual query that + will reach the database and come back with a result. Arguments: - query {string} -- A string query. This could be a qmarked string or a regular query. + query {string} -- A string query. + This could be a qmarked string or a regular query. bindings {tuple} -- A tuple of bindings Keyword Arguments: - results {str|1} -- If the results is equal to an asterisks it will call 'fetchAll' - else it will return 'fetchOne' and return a single record. (default: {"*"}) + results {str|1} -- If the results is equal to an + asterisks it will call 'fetchAll' + else it will return 'fetchOne' and + return a single record. (default: {"*"}) Returns: dict|None -- Returns a dictionary of results or None From 0468c45a44fb47ccd6a1b5e29b1838ef97d20e81 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Sun, 27 Oct 2024 18:22:26 -0400 Subject: [PATCH 206/254] Refactor MySQLConnection to handle closed connections during query execution --- src/masoniteorm/connections/MySQLConnection.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/masoniteorm/connections/MySQLConnection.py b/src/masoniteorm/connections/MySQLConnection.py index c96ac8e91..27d1a204e 100644 --- a/src/masoniteorm/connections/MySQLConnection.py +++ b/src/masoniteorm/connections/MySQLConnection.py @@ -61,7 +61,7 @@ def make_connection(self): self._connection = self.create_connection() self.enable_disable_foreign_keys() - self.open = 1 + return self @@ -73,7 +73,7 @@ def close_connection(self): and len(CONNECTION_POOL) < self.connection_pool_size ): CONNECTION_POOL.append(self._connection) - + self.open = 0 self._connection = None def create_connection(self, autocommit=True): @@ -128,6 +128,8 @@ def create_connection(self, autocommit=True): ) connection.close = self.close_connection + + self.open = 1 return connection From 7a6e7a8b19650d14cfabe879ccfc21d85ac612ee Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Mon, 28 Oct 2024 15:07:15 -0400 Subject: [PATCH 207/254] linted --- src/masoniteorm/connections/MySQLConnection.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/masoniteorm/connections/MySQLConnection.py b/src/masoniteorm/connections/MySQLConnection.py index 27d1a204e..355f2da89 100644 --- a/src/masoniteorm/connections/MySQLConnection.py +++ b/src/masoniteorm/connections/MySQLConnection.py @@ -61,12 +61,8 @@ def make_connection(self): self._connection = self.create_connection() self.enable_disable_foreign_keys() - - return self - # Add the connection back to the pool when it's closed - def close_connection(self): if ( self.full_details.get("connection_pooling_enabled") @@ -128,7 +124,7 @@ def create_connection(self, autocommit=True): ) connection.close = self.close_connection - + self.open = 1 return connection From 9cb1d47f800da9cc82c0e66e6a0d6bcff1dcacc5 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:01:10 +0800 Subject: [PATCH 208/254] fixed HasManyThrough eager loading --- src/masoniteorm/query/QueryBuilder.py | 2 +- .../relationships/HasManyThrough.py | 74 +++++++++++++++---- 2 files changed, 62 insertions(+), 14 deletions(-) diff --git a/src/masoniteorm/query/QueryBuilder.py b/src/masoniteorm/query/QueryBuilder.py index d07ec5145..169fa5633 100644 --- a/src/masoniteorm/query/QueryBuilder.py +++ b/src/masoniteorm/query/QueryBuilder.py @@ -1969,7 +1969,7 @@ def _register_relationships_to_model( def _map_related(self, related_result, related): if related.__class__.__name__ == 'MorphTo': return related_result - elif related.__class__.__name__ == 'HasOneThrough': + elif related.__class__.__name__ in ['HasOneThrough', 'HasManyThrough']: return related_result.group_by(related.local_key) return related_result.group_by(related.foreign_key) diff --git a/src/masoniteorm/relationships/HasManyThrough.py b/src/masoniteorm/relationships/HasManyThrough.py index 86174cfb2..622fbef3a 100644 --- a/src/masoniteorm/relationships/HasManyThrough.py +++ b/src/masoniteorm/relationships/HasManyThrough.py @@ -76,12 +76,19 @@ def apply_query(self, distant_builder, intermediary_builder, owner): dict -- A dictionary of data which will be hydrated. """ # select * from `countries` inner join `ports` on `ports`.`country_id` = `countries`.`country_id` where `ports`.`port_id` is null and `countries`.`deleted_at` is null and `ports`.`deleted_at` is null - result = distant_builder.join( - f"{self.intermediary_builder.get_table_name()}", - f"{self.intermediary_builder.get_table_name()}.{self.foreign_key}", - "=", - f"{distant_builder.get_table_name()}.{self.other_owner_key}", - ).where(f"{self.intermediary_builder.get_table_name()}.{self.local_owner_key}", getattr(owner, self.other_owner_key)).get() + result = ( + distant_builder.join( + f"{self.intermediary_builder.get_table_name()}", + f"{self.intermediary_builder.get_table_name()}.{self.foreign_key}", + "=", + f"{distant_builder.get_table_name()}.{self.other_owner_key}", + ) + .where( + f"{self.intermediary_builder.get_table_name()}.{self.local_owner_key}", + getattr(owner, self.other_owner_key), + ) + .get() + ) return result @@ -104,20 +111,61 @@ def make_builder(self, eagers=None): return builder + def register_related(self, key, model, collection): + """ + Attach the related model to source models attribute + + Arguments + key (str): The attribute name + model (Any): The model instance + collection (Collection): The data for the related models + + Returns + None + """ + related = collection.get(getattr(model, self.local_owner_key), None) + model.add_relation({key: related if related else None}) + def get_related(self, query, relation, eagers=None, callback=None): - builder = self.distant_builder + """ + Get a Collection to hydrate the models for the distant table with + Used when eager loading the model attribute + + Arguments + query (QueryBuilder): The source models QueryBuilder object + relation (HasManyThrough): this relationship object + eagers (Any): + callback (Any): + + Returns + Collection the collection of dicts to hydrate the distant models with + """ + + dist_table = self.distant_builder.get_table_name() + int_table = self.intermediary_builder.get_table_name() if callback: - callback(builder) + callback(current_builder) + + ( + self.distant_builder.select( + f"{dist_table}.*, {int_table}.{self.local_key}" + ).join( + f"{int_table}", + f"{int_table}.{self.foreign_key}", + "=", + f"{dist_table}.{self.other_owner_key}", + ) + ) if isinstance(relation, Collection): - return builder.where_in( - f"{builder.get_table_name()}.{self.foreign_key}", - Collection(relation._get_value(self.local_key)).unique(), + return self.distant_builder.where_in( + f"{int_table}.{self.local_key}", + Collection(relation._get_value(self.local_owner_key)).unique(), ).get() else: - return builder.where( - f"{builder.get_table_name()}.{self.foreign_key}", + return self.distant_builder.where( + f"{int_table}.{self.local_key}", getattr(relation, self.local_owner_key), ).get() From bd10fce86f92172b50fba4e2ccabee679de0d250 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:35:21 +0800 Subject: [PATCH 209/254] fixed HasManyThrough related model load --- .../relationships/HasManyThrough.py | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/src/masoniteorm/relationships/HasManyThrough.py b/src/masoniteorm/relationships/HasManyThrough.py index 622fbef3a..49849021f 100644 --- a/src/masoniteorm/relationships/HasManyThrough.py +++ b/src/masoniteorm/relationships/HasManyThrough.py @@ -57,41 +57,47 @@ def __get__(self, instance, owner): if attribute in instance._relationships: return instance._relationships[attribute] - result = self.apply_query( + result = self.apply_related_query( self.distant_builder, self.intermediary_builder, instance ) return result else: return self - def apply_query(self, distant_builder, intermediary_builder, owner): - """Apply the query and return a dictionary to be hydrated. - Used during accessing a relationship on a model + def apply_related_query(self, distant_builder, intermediary_builder, owner): + """ + Apply the query to return a Collection of data for the distant models to be hydrated with. - Arguments: - query {oject} -- The relationship object - owner {object} -- The current model oject. + Method is used when accessing a relationship on a model if its not + already eager loaded - Returns: - dict -- A dictionary of data which will be hydrated. + Arguments + distant_builder (QueryBuilder): QueryBuilder attached to the distant table + intermediate_builder (QueryBuilder): QueryBuilder attached to the intermediate (linking) table + owner (Any): the model this relationship is starting from + + Returns + Collection: Collection of dicts which will be used for hydrating models. """ - # select * from `countries` inner join `ports` on `ports`.`country_id` = `countries`.`country_id` where `ports`.`port_id` is null and `countries`.`deleted_at` is null and `ports`.`deleted_at` is null - result = ( - distant_builder.join( - f"{self.intermediary_builder.get_table_name()}", - f"{self.intermediary_builder.get_table_name()}.{self.foreign_key}", + + dist_table = self.distant_builder.get_table_name() + int_table = self.intermediary_builder.get_table_name() + + return ( + self.distant_builder.select(f"{dist_table}.*, {int_table}.{self.local_key}") + .join( + f"{int_table}", + f"{int_table}.{self.foreign_key}", "=", - f"{distant_builder.get_table_name()}.{self.other_owner_key}", + f"{dist_table}.{self.other_owner_key}", ) .where( - f"{self.intermediary_builder.get_table_name()}.{self.local_owner_key}", - getattr(owner, self.other_owner_key), + f"{int_table}.{self.local_key}", + getattr(owner, self.local_owner_key), ) .get() ) - return result - def relate(self, related_model): return self.distant_builder.join( f"{self.intermediary_builder.get_table_name()}", From 928bbb7631808f9dce908b3628e5d9bdc08c31e1 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Wed, 30 Oct 2024 06:24:46 +0800 Subject: [PATCH 210/254] cleaned up and fixed where clauses for HasManyThrough --- .../relationships/HasManyThrough.py | 115 +++++++++--------- 1 file changed, 59 insertions(+), 56 deletions(-) diff --git a/src/masoniteorm/relationships/HasManyThrough.py b/src/masoniteorm/relationships/HasManyThrough.py index 49849021f..f652c520d 100644 --- a/src/masoniteorm/relationships/HasManyThrough.py +++ b/src/masoniteorm/relationships/HasManyThrough.py @@ -175,34 +175,81 @@ def get_related(self, query, relation, eagers=None, callback=None): getattr(relation, self.local_owner_key), ).get() - def get_with_count_query(self, builder, callback): - query = self.distant_builder + def attach(self, current_model, related_record): + raise NotImplementedError( + "HasOneThrough relationship does not implement the attach method" + ) + + def attach_related(self, current_model, related_record): + raise NotImplementedError( + "HasOneThrough relationship does not implement the attach_related method" + ) + + def query_has(self, current_builder, method="where_exists"): + dist_table = self.distant_builder.get_table_name() + int_table = self.intermediary_builder.get_table_name() + + getattr(current_builder, method)( + self.distant_builder.join( + f"{int_table}", + f"{int_table}.{self.foreign_key}", + "=", + f"{dist_table}.{self.other_owner_key}", + ).where_column( + f"{int_table}.{self.local_key}", + f"{current_builder.get_table_name()}.{self.local_owner_key}", + ) + ) + + return self.distant_builder - if not builder._columns: - builder = builder.select("*") + def query_where_exists( + self, current_builder, callback, method="where_exists" + ): + dist_table = self.distant_builder.get_table_name() + int_table = self.intermediary_builder.get_table_name() - return_query = builder.add_select( + getattr(current_builder, method)( + self.distant_builder.join( + f"{int_table}", + f"{int_table}.{self.foreign_key}", + "=", + f"{dist_table}.{self.other_owner_key}", + ).where_column( + f"{int_table}.{self.local_key}", + f"{current_builder.get_table_name()}.{self.local_owner_key}", + ).when(callback, lambda q: (callback(q))) + ) + + def get_with_count_query(self, current_builder, callback): + dist_table = self.distant_builder.get_table_name() + int_table = self.intermediary_builder.get_table_name() + + if not current_builder._columns: + current_builder.select("*") + + return_query = current_builder.add_select( f"{self.attribute}_count", lambda q: ( ( q.count("*") .join( - f"{self.intermediary_builder.get_table_name()}", - f"{self.intermediary_builder.get_table_name()}.{self.foreign_key}", + f"{int_table}", + f"{int_table}.{self.foreign_key}", "=", - f"{query.get_table_name()}.{self.other_owner_key}", + f"{dist_table}.{self.other_owner_key}", ) .where_column( - f"{builder.get_table_name()}.{self.local_owner_key}", - f"{self.intermediary_builder.get_table_name()}.{self.local_key}", + f"{int_table}.{self.local_key}", + f"{current_builder.get_table_name()}.{self.local_owner_key}", ) - .table(query.get_table_name()) + .table(dist_table) .when( callback, lambda q: ( q.where_in( self.foreign_key, - callback(query.select(self.other_owner_key)), + callback(self.distant_builder.select(self.other_owner_key)), ) ), ) @@ -211,47 +258,3 @@ def get_with_count_query(self, builder, callback): ) return return_query - - def attach(self, current_model, related_record): - raise NotImplementedError( - "HasOneThrough relationship does not implement the attach method" - ) - - def attach_related(self, current_model, related_record): - raise NotImplementedError( - "HasOneThrough relationship does not implement the attach_related method" - ) - - def query_has(self, current_query_builder, method="where_exists"): - related_builder = self.get_builder() - - getattr(current_query_builder, method)( - self.distant_builder.where_column( - f"{current_query_builder.get_table_name()}.{self.local_owner_key}", - f"{self.intermediary_builder.get_table_name()}.{self.local_key}", - ).join( - f"{self.intermediary_builder.get_table_name()}", - f"{self.intermediary_builder.get_table_name()}.{self.foreign_key}", - "=", - f"{self.distant_builder.get_table_name()}.{self.other_owner_key}", - ) - ) - - return related_builder - - def query_where_exists( - self, current_query_builder, callback, method="where_exists" - ): - query = self.distant_builder - - getattr(current_query_builder, method)( - query.join( - f"{self.intermediary_builder.get_table_name()}", - f"{self.intermediary_builder.get_table_name()}.{self.foreign_key}", - "=", - f"{query.get_table_name()}.{self.other_owner_key}", - ).where_column( - f"{current_query_builder.get_table_name()}.{self.local_owner_key}", - f"{self.intermediary_builder.get_table_name()}.{self.local_key}", - ) - ).when(callback, lambda q: (callback(q))) From c13750e06aee60cb3158fe782db828b49a7ce13d Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Wed, 30 Oct 2024 06:26:08 +0800 Subject: [PATCH 211/254] separated HasOneThrough and HasManyThrough tests --- ...st_sqlite_has_many_through_relationship.py | 136 ++++++++++++++++++ ...st_sqlite_has_one_through_relationship.py} | 3 +- 2 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 tests/sqlite/relationships/test_sqlite_has_many_through_relationship.py rename tests/sqlite/relationships/{test_sqlite_has_through_relationships.py => test_sqlite_has_one_through_relationship.py} (98%) diff --git a/tests/sqlite/relationships/test_sqlite_has_many_through_relationship.py b/tests/sqlite/relationships/test_sqlite_has_many_through_relationship.py new file mode 100644 index 000000000..56d30f307 --- /dev/null +++ b/tests/sqlite/relationships/test_sqlite_has_many_through_relationship.py @@ -0,0 +1,136 @@ +import unittest + +from src.masoniteorm.models import Model +from src.masoniteorm.relationships import has_many_through +from tests.integrations.config.database import DATABASES +from src.masoniteorm.schema import Schema +from src.masoniteorm.schema.platforms import SQLitePlatform + + +class Enrolment(Model): + __table__ = "enrolment" + __connection__ = "dev" + __fillable__ = ["active_student_id", "in_course_id"] + + +class Student(Model): + __table__ = "student" + __connection__ = "dev" + __fillable__ = ["student_id", "name"] + + +class Course(Model): + __table__ = "course" + __connection__ = "dev" + __fillable__ = ["course_id", "name"] + + @has_many_through( + None, + "in_course_id", + "active_student_id", + "course_id", + "student_id" + ) + def students(self): + return [Student, Enrolment] + + +class TestHasManyThroughRelationship(unittest.TestCase): + def setUp(self): + self.schema = Schema( + connection="dev", + connection_details=DATABASES, + platform=SQLitePlatform, + ).on("dev") + + with self.schema.create_table_if_not_exists("student") as table: + table.integer("student_id").primary() + table.string("name") + + with self.schema.create_table_if_not_exists("course") as table: + table.integer("course_id").primary() + table.string("name") + + with self.schema.create_table_if_not_exists("enrolment") as table: + table.integer("enrolment_id").primary() + table.integer("active_student_id") + table.integer("in_course_id") + + if not Course.count(): + Course.builder.new().bulk_create( + [ + {"course_id": 10, "name": "Math 101"}, + {"course_id": 20, "name": "History 101"}, + {"course_id": 30, "name": "Math 302"}, + {"course_id": 40, "name": "Biology 302"}, + ] + ) + + if not Student.count(): + Student.builder.new().bulk_create( + [ + {"student_id": 100, "name": "Bob"}, + {"student_id": 200, "name": "Alice"}, + {"student_id": 300, "name": "Steve"}, + {"student_id": 400, "name": "Megan"}, + ] + ) + + if not Enrolment.count(): + Enrolment.builder.new().bulk_create( + [ + {"active_student_id": 100, "in_course_id": 30}, + {"active_student_id": 200, "in_course_id": 10}, + {"active_student_id": 100, "in_course_id": 10}, + {"active_student_id": 400, "in_course_id": 20}, + ] + ) + + def test_has_many_through_can_eager_load(self): + courses = Course.where("name", "Math 101").with_("students").get() + students = courses.first().students + + self.assertEqual(len(students), 2) + + student1 = students[0] + self.assertIsInstance(student1, Student) + self.assertEqual(student1.name, "Alice") + + student2 = students[1] + self.assertIsInstance(student2, Student) + self.assertEqual(student2.name, "Bob") + + # check .first() and .get() produce the same result + single = ( + Course.where("name", "History 101") + .with_("students") + .first() + ) + single_get = ( + Course.where("name", "History 101").with_("students").get() + ) + self.assertEqual(single.students.count(), 1) + self.assertEqual(single_get.first().students.count(), 1) + + single_name = single.students.first().name + single_get_name = single_get.first().students.first().name + self.assertEqual(single_name, single_get_name) + + def test_has_many_through_eager_load_can_be_empty(self): + courses = ( + Course.where("name", "Biology 302") + .with_("students") + .get() + ) + self.assertIsNone(courses.first().students) + + def test_has_many_through_can_get_related(self): + course = Course.where("name", "Math 101").first() + self.assertIsInstance(course.students.first(), Student) + self.assertEqual(course.students.count(), 2) + + def test_has_many_through_has_query(self): + courses = Course.where_has( + "students", lambda query: query.where("name", "Bob") + ) + self.assertEqual(courses.count(), 2) diff --git a/tests/sqlite/relationships/test_sqlite_has_through_relationships.py b/tests/sqlite/relationships/test_sqlite_has_one_through_relationship.py similarity index 98% rename from tests/sqlite/relationships/test_sqlite_has_through_relationships.py rename to tests/sqlite/relationships/test_sqlite_has_one_through_relationship.py index d23f4847a..dee1bff9d 100644 --- a/tests/sqlite/relationships/test_sqlite_has_through_relationships.py +++ b/tests/sqlite/relationships/test_sqlite_has_one_through_relationship.py @@ -29,7 +29,8 @@ def from_country(self): return [Country, Port] -class TestRelationships(unittest.TestCase): + +class TestHasOneThroughRelationship(unittest.TestCase): def setUp(self): self.schema = Schema( connection="dev", From a3e775b3e434c27ccdf838745f08ba10f91b9b46 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Wed, 30 Oct 2024 07:12:33 +0800 Subject: [PATCH 212/254] fixed queriy tests --- tests/mysql/relationships/test_has_many_through.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/mysql/relationships/test_has_many_through.py b/tests/mysql/relationships/test_has_many_through.py index 61830c2b8..3c6d5b7ef 100644 --- a/tests/mysql/relationships/test_has_many_through.py +++ b/tests/mysql/relationships/test_has_many_through.py @@ -31,7 +31,7 @@ def test_has_query(self): self.assertEqual( sql, - """SELECT * FROM `inbound_shipments` WHERE EXISTS (SELECT * FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `inbound_shipments`.`from_port_id` = `ports`.`port_id`)""", + """SELECT * FROM `inbound_shipments` WHERE EXISTS (SELECT * FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `ports`.`port_id` = `inbound_shipments`.`from_port_id`)""", ) def test_or_has(self): @@ -39,7 +39,7 @@ def test_or_has(self): self.assertEqual( sql, - """SELECT * FROM `inbound_shipments` WHERE `inbound_shipments`.`name` = 'Joe' OR EXISTS (SELECT * FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `inbound_shipments`.`from_port_id` = `ports`.`port_id`)""", + """SELECT * FROM `inbound_shipments` WHERE `inbound_shipments`.`name` = 'Joe' OR EXISTS (SELECT * FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `ports`.`port_id` = `inbound_shipments`.`from_port_id`)""", ) def test_where_has_query(self): @@ -49,7 +49,7 @@ def test_where_has_query(self): self.assertEqual( sql, - """SELECT * FROM `inbound_shipments` WHERE EXISTS (SELECT * FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `inbound_shipments`.`from_port_id` = `ports`.`port_id`) AND `inbound_shipments`.`name` = 'USA'""", + """SELECT * FROM `inbound_shipments` WHERE EXISTS (SELECT * FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `ports`.`port_id` = `inbound_shipments`.`from_port_id` AND `countries`.`name` = 'USA')""", ) def test_or_where_has(self): @@ -61,7 +61,7 @@ def test_or_where_has(self): self.assertEqual( sql, - """SELECT * FROM `inbound_shipments` WHERE `inbound_shipments`.`name` = 'Joe' OR EXISTS (SELECT * FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `inbound_shipments`.`from_port_id` = `ports`.`port_id`) AND `inbound_shipments`.`name` = 'USA'""", + """SELECT * FROM `inbound_shipments` WHERE `inbound_shipments`.`name` = 'Joe' OR EXISTS (SELECT * FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `ports`.`port_id` = `inbound_shipments`.`from_port_id` AND `countries`.`name` = 'USA')""", ) def test_doesnt_have(self): @@ -69,7 +69,7 @@ def test_doesnt_have(self): self.assertEqual( sql, - """SELECT * FROM `inbound_shipments` WHERE NOT EXISTS (SELECT * FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `inbound_shipments`.`from_port_id` = `ports`.`port_id`)""", + """SELECT * FROM `inbound_shipments` WHERE NOT EXISTS (SELECT * FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `ports`.`port_id` = `inbound_shipments`.`from_port_id`)""", ) def test_or_where_doesnt_have(self): @@ -83,5 +83,5 @@ def test_or_where_doesnt_have(self): self.assertEqual( sql, - """SELECT * FROM `inbound_shipments` WHERE `inbound_shipments`.`name` = 'Joe' OR NOT EXISTS (SELECT * FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `inbound_shipments`.`from_port_id` = `ports`.`port_id`) AND `inbound_shipments`.`name` = 'USA'""", + """SELECT * FROM `inbound_shipments` WHERE `inbound_shipments`.`name` = 'Joe' OR NOT EXISTS (SELECT * FROM `countries` INNER JOIN `ports` ON `ports`.`country_id` = `countries`.`country_id` WHERE `ports`.`port_id` = `inbound_shipments`.`from_port_id` AND `countries`.`name` = 'USA')""", ) From ab9308a08e882be01f957e3e17ce067699f14c13 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Wed, 30 Oct 2024 07:33:46 +0800 Subject: [PATCH 213/254] make sure related attribute always contains a Collection --- .../relationships/HasManyThrough.py | 19 ++++++++++++------- ...st_sqlite_has_many_through_relationship.py | 14 +++++++++++--- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/masoniteorm/relationships/HasManyThrough.py b/src/masoniteorm/relationships/HasManyThrough.py index f652c520d..46828e098 100644 --- a/src/masoniteorm/relationships/HasManyThrough.py +++ b/src/masoniteorm/relationships/HasManyThrough.py @@ -1,5 +1,5 @@ -from .BaseRelationship import BaseRelationship from ..collection import Collection +from .BaseRelationship import BaseRelationship class HasManyThrough(BaseRelationship): @@ -130,6 +130,9 @@ def register_related(self, key, model, collection): None """ related = collection.get(getattr(model, self.local_owner_key), None) + if related and not isinstance(related, Collection): + related = Collection(related) + model.add_relation({key: related if related else None}) def get_related(self, query, relation, eagers=None, callback=None): @@ -203,9 +206,7 @@ def query_has(self, current_builder, method="where_exists"): return self.distant_builder - def query_where_exists( - self, current_builder, callback, method="where_exists" - ): + def query_where_exists(self, current_builder, callback, method="where_exists"): dist_table = self.distant_builder.get_table_name() int_table = self.intermediary_builder.get_table_name() @@ -215,10 +216,12 @@ def query_where_exists( f"{int_table}.{self.foreign_key}", "=", f"{dist_table}.{self.other_owner_key}", - ).where_column( + ) + .where_column( f"{int_table}.{self.local_key}", f"{current_builder.get_table_name()}.{self.local_owner_key}", - ).when(callback, lambda q: (callback(q))) + ) + .when(callback, lambda q: (callback(q))) ) def get_with_count_query(self, current_builder, callback): @@ -249,7 +252,9 @@ def get_with_count_query(self, current_builder, callback): lambda q: ( q.where_in( self.foreign_key, - callback(self.distant_builder.select(self.other_owner_key)), + callback( + self.distant_builder.select(self.other_owner_key) + ), ) ), ) diff --git a/tests/sqlite/relationships/test_sqlite_has_many_through_relationship.py b/tests/sqlite/relationships/test_sqlite_has_many_through_relationship.py index 56d30f307..baf68eae8 100644 --- a/tests/sqlite/relationships/test_sqlite_has_many_through_relationship.py +++ b/tests/sqlite/relationships/test_sqlite_has_many_through_relationship.py @@ -1,5 +1,6 @@ import unittest +from src.masoniteorm.collection import Collection from src.masoniteorm.models import Model from src.masoniteorm.relationships import has_many_through from tests.integrations.config.database import DATABASES @@ -90,13 +91,14 @@ def test_has_many_through_can_eager_load(self): courses = Course.where("name", "Math 101").with_("students").get() students = courses.first().students - self.assertEqual(len(students), 2) + self.assertIsInstance(students, Collection) + self.assertEqual(students.count(), 2) - student1 = students[0] + student1 = students.shift() self.assertIsInstance(student1, Student) self.assertEqual(student1.name, "Alice") - student2 = students[1] + student2 = students.shift() self.assertIsInstance(student2, Student) self.assertEqual(student2.name, "Bob") @@ -106,9 +108,14 @@ def test_has_many_through_can_eager_load(self): .with_("students") .first() ) + self.assertIsInstance(single.students, Collection) + single_get = ( Course.where("name", "History 101").with_("students").get() ) + + print(single.students) + print(single_get.first().students) self.assertEqual(single.students.count(), 1) self.assertEqual(single_get.first().students.count(), 1) @@ -126,6 +133,7 @@ def test_has_many_through_eager_load_can_be_empty(self): def test_has_many_through_can_get_related(self): course = Course.where("name", "Math 101").first() + self.assertIsInstance(course.students, Collection) self.assertIsInstance(course.students.first(), Student) self.assertEqual(course.students.count(), 2) From f0c23593826b6a7ce83800312aa2f3ff78305957 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Wed, 30 Oct 2024 10:18:20 +0800 Subject: [PATCH 214/254] Added ability so specify default select criteria for Models Specify default select criteriaa for the Model by overriding get_selects() in the subclass --- src/masoniteorm/models/Model.py | 5 ++++- tests/models/test_models.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/masoniteorm/models/Model.py b/src/masoniteorm/models/Model.py index 28a8056c3..35967eeb1 100644 --- a/src/masoniteorm/models/Model.py +++ b/src/masoniteorm/models/Model.py @@ -354,7 +354,10 @@ def get_builder(self): dry=self.__dry__, ) - return self.builder.select(*self.__selects__) + return self.builder.select(*self.get_selects()) + + def get_selects(self): + return self.__selects__ @classmethod def get_columns(cls): diff --git a/tests/models/test_models.py b/tests/models/test_models.py index 319d71e0c..9c28c8a18 100644 --- a/tests/models/test_models.py +++ b/tests/models/test_models.py @@ -36,6 +36,12 @@ class ModelTestForced(Model): __table__ = "users" __force_update__ = True +class BaseModel(Model): + def get_selects(self): + return [f"{self.get_table_name()}.*"] + +class ModelWithBaseModel(BaseModel): + __table__ = "users" class TestModels(unittest.TestCase): def test_model_can_access_str_dates_as_pendulum(self): @@ -253,3 +259,17 @@ def test_both_fillable_and_guarded_attributes_raise(self): # Removing one of the props allows us to instantiate delattr(InvalidFillableGuardedModelTest, "__guarded__") InvalidFillableGuardedModelTest() + + def test_model_can_provide_default_select(self): + sql = ModelWithBaseModel.to_sql() + self.assertEqual( + sql, + """SELECT `users`.* FROM `users`""", + ) + + def test_model_can_add_to_default_select(self): + sql = ModelWithBaseModel.select(["products.name", "products.id", "store.name"]).to_sql() + self.assertEqual( + sql, + """SELECT `users`.*, `products`.`name`, `products`.`id`, `store`.`name` FROM `users`""", + ) From bd6e473e1d4b8672eba5c206760250891ab1925c Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 30 Oct 2024 13:00:12 -0400 Subject: [PATCH 215/254] Refactor PostgresConnection to support SSL options --- src/masoniteorm/connections/PostgresConnection.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/masoniteorm/connections/PostgresConnection.py b/src/masoniteorm/connections/PostgresConnection.py index 0bbfe172f..2823c2992 100644 --- a/src/masoniteorm/connections/PostgresConnection.py +++ b/src/masoniteorm/connections/PostgresConnection.py @@ -85,6 +85,10 @@ def create_connection(self): password=self.password, host=self.host, port=self.port, + sslmode=self.options.get("sslmode"), + sslcert=self.options.get("sslcert"), + sslkey=self.options.get("sslkey"), + sslrootcert=self.options.get("sslrootcert"), options=( f"-c search_path={self.schema or self.full_details.get('schema')}" if self.schema or self.full_details.get("schema") @@ -106,6 +110,10 @@ def create_connection(self): password=self.password, host=self.host, port=self.port, + sslmode=self.options.get("sslmode"), + sslcert=self.options.get("sslcert"), + sslkey=self.options.get("sslkey"), + sslrootcert=self.options.get("sslrootcert"), options=( f"-c search_path={self.schema or self.full_details.get('schema')}" if self.schema or self.full_details.get("schema") From 7e86c3363cc6a6595da7720c5e77019a62588c52 Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Thu, 31 Oct 2024 07:08:48 +0800 Subject: [PATCH 216/254] use passed in parameters --- src/masoniteorm/relationships/HasManyThrough.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/masoniteorm/relationships/HasManyThrough.py b/src/masoniteorm/relationships/HasManyThrough.py index 46828e098..1f5d5eea1 100644 --- a/src/masoniteorm/relationships/HasManyThrough.py +++ b/src/masoniteorm/relationships/HasManyThrough.py @@ -80,8 +80,8 @@ def apply_related_query(self, distant_builder, intermediary_builder, owner): Collection: Collection of dicts which will be used for hydrating models. """ - dist_table = self.distant_builder.get_table_name() - int_table = self.intermediary_builder.get_table_name() + dist_table = distant_builder.get_table_name() + int_table = intermediary_builder.get_table_name() return ( self.distant_builder.select(f"{dist_table}.*, {int_table}.{self.local_key}") From 97eaa5755aa3b7d3ad0f696f01ac1a5460e17c2f Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Thu, 31 Oct 2024 07:10:50 +0800 Subject: [PATCH 217/254] fixed parameter name --- src/masoniteorm/relationships/HasManyThrough.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/masoniteorm/relationships/HasManyThrough.py b/src/masoniteorm/relationships/HasManyThrough.py index 1f5d5eea1..1ec613011 100644 --- a/src/masoniteorm/relationships/HasManyThrough.py +++ b/src/masoniteorm/relationships/HasManyThrough.py @@ -135,13 +135,13 @@ def register_related(self, key, model, collection): model.add_relation({key: related if related else None}) - def get_related(self, query, relation, eagers=None, callback=None): + def get_related(self, current_builder, relation, eagers=None, callback=None): """ Get a Collection to hydrate the models for the distant table with Used when eager loading the model attribute Arguments - query (QueryBuilder): The source models QueryBuilder object + current_builder (QueryBuilder): The source models QueryBuilder object relation (HasManyThrough): this relationship object eagers (Any): callback (Any): From 58edcded6f8b6a4ef298bd4ba4216e0640e5287f Mon Sep 17 00:00:00 2001 From: Kieren Eaton <499977+circulon@users.noreply.github.com> Date: Thu, 31 Oct 2024 07:30:01 +0800 Subject: [PATCH 218/254] renamed table variables --- .../relationships/HasManyThrough.py | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/src/masoniteorm/relationships/HasManyThrough.py b/src/masoniteorm/relationships/HasManyThrough.py index 1ec613011..e044f9a7e 100644 --- a/src/masoniteorm/relationships/HasManyThrough.py +++ b/src/masoniteorm/relationships/HasManyThrough.py @@ -80,19 +80,19 @@ def apply_related_query(self, distant_builder, intermediary_builder, owner): Collection: Collection of dicts which will be used for hydrating models. """ - dist_table = distant_builder.get_table_name() - int_table = intermediary_builder.get_table_name() + distant_table = distant_builder.get_table_name() + intermediate_table = intermediary_builder.get_table_name() return ( - self.distant_builder.select(f"{dist_table}.*, {int_table}.{self.local_key}") + self.distant_builder.select(f"{distant_table}.*, {intermediate_table}.{self.local_key}") .join( - f"{int_table}", - f"{int_table}.{self.foreign_key}", + f"{intermediate_table}", + f"{intermediate_table}.{self.foreign_key}", "=", - f"{dist_table}.{self.other_owner_key}", + f"{distant_table}.{self.other_owner_key}", ) .where( - f"{int_table}.{self.local_key}", + f"{intermediate_table}.{self.local_key}", getattr(owner, self.local_owner_key), ) .get() @@ -150,31 +150,31 @@ def get_related(self, current_builder, relation, eagers=None, callback=None): Collection the collection of dicts to hydrate the distant models with """ - dist_table = self.distant_builder.get_table_name() - int_table = self.intermediary_builder.get_table_name() + distant_table = self.distant_builder.get_table_name() + intermediate_table = self.intermediary_builder.get_table_name() if callback: callback(current_builder) ( self.distant_builder.select( - f"{dist_table}.*, {int_table}.{self.local_key}" + f"{distant_table}.*, {intermediate_table}.{self.local_key}" ).join( - f"{int_table}", - f"{int_table}.{self.foreign_key}", + f"{intermediate_table}", + f"{intermediate_table}.{self.foreign_key}", "=", - f"{dist_table}.{self.other_owner_key}", + f"{distant_table}.{self.other_owner_key}", ) ) if isinstance(relation, Collection): return self.distant_builder.where_in( - f"{int_table}.{self.local_key}", + f"{intermediate_table}.{self.local_key}", Collection(relation._get_value(self.local_owner_key)).unique(), ).get() else: return self.distant_builder.where( - f"{int_table}.{self.local_key}", + f"{intermediate_table}.{self.local_key}", getattr(relation, self.local_owner_key), ).get() @@ -189,17 +189,17 @@ def attach_related(self, current_model, related_record): ) def query_has(self, current_builder, method="where_exists"): - dist_table = self.distant_builder.get_table_name() - int_table = self.intermediary_builder.get_table_name() + distant_table = self.distant_builder.get_table_name() + intermediate_table = self.intermediary_builder.get_table_name() getattr(current_builder, method)( self.distant_builder.join( - f"{int_table}", - f"{int_table}.{self.foreign_key}", + f"{intermediate_table}", + f"{intermediate_table}.{self.foreign_key}", "=", - f"{dist_table}.{self.other_owner_key}", + f"{distant_table}.{self.other_owner_key}", ).where_column( - f"{int_table}.{self.local_key}", + f"{intermediate_table}.{self.local_key}", f"{current_builder.get_table_name()}.{self.local_owner_key}", ) ) @@ -207,26 +207,26 @@ def query_has(self, current_builder, method="where_exists"): return self.distant_builder def query_where_exists(self, current_builder, callback, method="where_exists"): - dist_table = self.distant_builder.get_table_name() - int_table = self.intermediary_builder.get_table_name() + distant_table = self.distant_builder.get_table_name() + intermediate_table = self.intermediary_builder.get_table_name() getattr(current_builder, method)( self.distant_builder.join( - f"{int_table}", - f"{int_table}.{self.foreign_key}", + f"{intermediate_table}", + f"{intermediate_table}.{self.foreign_key}", "=", - f"{dist_table}.{self.other_owner_key}", + f"{distant_table}.{self.other_owner_key}", ) .where_column( - f"{int_table}.{self.local_key}", + f"{intermediate_table}.{self.local_key}", f"{current_builder.get_table_name()}.{self.local_owner_key}", ) .when(callback, lambda q: (callback(q))) ) def get_with_count_query(self, current_builder, callback): - dist_table = self.distant_builder.get_table_name() - int_table = self.intermediary_builder.get_table_name() + distant_table = self.distant_builder.get_table_name() + intermediate_table = self.intermediary_builder.get_table_name() if not current_builder._columns: current_builder.select("*") @@ -237,16 +237,16 @@ def get_with_count_query(self, current_builder, callback): ( q.count("*") .join( - f"{int_table}", - f"{int_table}.{self.foreign_key}", + f"{intermediate_table}", + f"{intermediate_table}.{self.foreign_key}", "=", - f"{dist_table}.{self.other_owner_key}", + f"{distant_table}.{self.other_owner_key}", ) .where_column( - f"{int_table}.{self.local_key}", + f"{intermediate_table}.{self.local_key}", f"{current_builder.get_table_name()}.{self.local_owner_key}", ) - .table(dist_table) + .table(distant_table) .when( callback, lambda q: ( From 93c7a38187115436dc752827d87ca03c3ff1272b Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 18:13:04 -0500 Subject: [PATCH 219/254] Add Radon analysis workflow for cyclomatic complexity checks --- .github/workflows/radon-analysis.yml | 39 ++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/radon-analysis.yml diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml new file mode 100644 index 000000000..6caa92731 --- /dev/null +++ b/.github/workflows/radon-analysis.yml @@ -0,0 +1,39 @@ +name: Radon Cyclomatic Complexity + +on: + push: + branches: + - main + pull_request: + +jobs: + radon-check: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.x + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install radon + + - name: Run Radon Cyclomatic Complexity + run: radon cc -s -a src/ + + - name: Comment on Pull Request + if: github.event_name == 'pull_request' + uses: marocchino/sticky-pull-request-comment@v2 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + header: Radon Cyclomatic Complexity Report + message: | + ```console + $(radon cc -s -a src/) + ``` \ No newline at end of file From 54f561ca96b61740d87c5bfcc36b677fc76947cb Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 18:16:02 -0500 Subject: [PATCH 220/254] Enhance Radon analysis workflow to capture and output cyclomatic complexity results --- .github/workflows/radon-analysis.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml index 6caa92731..3ad4087ba 100644 --- a/.github/workflows/radon-analysis.yml +++ b/.github/workflows/radon-analysis.yml @@ -25,7 +25,11 @@ jobs: pip install radon - name: Run Radon Cyclomatic Complexity - run: radon cc -s -a src/ + id: radon + run: | + radon_output=$(radon cc -s -a src/) + echo "$radon_output" + echo "::set-output name=radon_output::$radon_output" - name: Comment on Pull Request if: github.event_name == 'pull_request' @@ -35,5 +39,5 @@ jobs: header: Radon Cyclomatic Complexity Report message: | ```console - $(radon cc -s -a src/) + ${{ steps.radon.outputs.radon_output }} ``` \ No newline at end of file From 72e3347094f8032e24044cdd3e1d42a977528052 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 18:17:54 -0500 Subject: [PATCH 221/254] Update Radon analysis workflow to save cyclomatic complexity output to a file and read from it --- .github/workflows/radon-analysis.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml index 3ad4087ba..a578455a6 100644 --- a/.github/workflows/radon-analysis.yml +++ b/.github/workflows/radon-analysis.yml @@ -25,11 +25,14 @@ jobs: pip install radon - name: Run Radon Cyclomatic Complexity + run: | + radon cc -s -a src/ > radon_output.txt + + - name: Read Radon Output id: radon run: | - radon_output=$(radon cc -s -a src/) - echo "$radon_output" - echo "::set-output name=radon_output::$radon_output" + output=$(cat radon_output.txt) + echo "::set-output name=radon_output::$output" - name: Comment on Pull Request if: github.event_name == 'pull_request' From 29551f8b354c54eaacfa88f79ccd3d9b084a706b Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 18:20:55 -0500 Subject: [PATCH 222/254] test --- .github/workflows/radon-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml index a578455a6..e67c7aa1b 100644 --- a/.github/workflows/radon-analysis.yml +++ b/.github/workflows/radon-analysis.yml @@ -26,7 +26,7 @@ jobs: - name: Run Radon Cyclomatic Complexity run: | - radon cc -s -a src/ > radon_output.txt + radon cc -s -a src/masoniteorm/collection > radon_output.txt - name: Read Radon Output id: radon From efc95677712805f83a70d1834fcd81ea1b0e06d2 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 18:22:51 -0500 Subject: [PATCH 223/254] Update Radon analysis workflow to set output using GitHub environment variable --- .github/workflows/radon-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml index e67c7aa1b..2e3512656 100644 --- a/.github/workflows/radon-analysis.yml +++ b/.github/workflows/radon-analysis.yml @@ -32,7 +32,7 @@ jobs: id: radon run: | output=$(cat radon_output.txt) - echo "::set-output name=radon_output::$output" + echo "radon_output=$output" >> $GITHUB_ENV - name: Comment on Pull Request if: github.event_name == 'pull_request' From 162fced99093ddea1dd3a1d32a713cef888b7f3c Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 18:23:52 -0500 Subject: [PATCH 224/254] Update Radon analysis workflow to use set-output for environment variable --- .github/workflows/radon-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml index 2e3512656..e67c7aa1b 100644 --- a/.github/workflows/radon-analysis.yml +++ b/.github/workflows/radon-analysis.yml @@ -32,7 +32,7 @@ jobs: id: radon run: | output=$(cat radon_output.txt) - echo "radon_output=$output" >> $GITHUB_ENV + echo "::set-output name=radon_output::$output" - name: Comment on Pull Request if: github.event_name == 'pull_request' From e364693bd8952c5c1a588f9f51e402dabdf5f13e Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 18:26:51 -0500 Subject: [PATCH 225/254] Refactor Radon analysis workflow to streamline output handling and remove redundant step --- .github/workflows/radon-analysis.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml index e67c7aa1b..7d6ec474f 100644 --- a/.github/workflows/radon-analysis.yml +++ b/.github/workflows/radon-analysis.yml @@ -25,13 +25,11 @@ jobs: pip install radon - name: Run Radon Cyclomatic Complexity - run: | - radon cc -s -a src/masoniteorm/collection > radon_output.txt - - - name: Read Radon Output id: radon run: | + radon cc -s -a src/masoniteorm/collection > radon_output.txt output=$(cat radon_output.txt) + echo "$output" echo "::set-output name=radon_output::$output" - name: Comment on Pull Request @@ -43,4 +41,4 @@ jobs: message: | ```console ${{ steps.radon.outputs.radon_output }} - ``` \ No newline at end of file + ``` From fe5a784b574bd86feb253698c37a576c7d026104 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 18:29:19 -0500 Subject: [PATCH 226/254] Update Radon analysis workflow to set output using GitHub environment variable --- .github/workflows/radon-analysis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml index 7d6ec474f..a347026fc 100644 --- a/.github/workflows/radon-analysis.yml +++ b/.github/workflows/radon-analysis.yml @@ -30,7 +30,7 @@ jobs: radon cc -s -a src/masoniteorm/collection > radon_output.txt output=$(cat radon_output.txt) echo "$output" - echo "::set-output name=radon_output::$output" + echo "radon_output=$output" >> $GITHUB_ENV - name: Comment on Pull Request if: github.event_name == 'pull_request' @@ -40,5 +40,5 @@ jobs: header: Radon Cyclomatic Complexity Report message: | ```console - ${{ steps.radon.outputs.radon_output }} + ${{ env.radon_output }} ``` From 0d34132e0cf0e1992a15996979ecaa9787216bb8 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 18:33:15 -0500 Subject: [PATCH 227/254] Update Radon analysis workflow to enhance reporting and streamline comments on pull requests --- .github/workflows/radon-analysis.yml | 55 +++++++++------------------- 1 file changed, 17 insertions(+), 38 deletions(-) diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml index a347026fc..cef437b18 100644 --- a/.github/workflows/radon-analysis.yml +++ b/.github/workflows/radon-analysis.yml @@ -1,44 +1,23 @@ -name: Radon Cyclomatic Complexity - +name: Radon Analysis on: - push: - branches: - - main pull_request: + branches: ["2.0"] jobs: - radon-check: + comment: + permissions: + pull-requests: write runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.x - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install radon - - - name: Run Radon Cyclomatic Complexity - id: radon - run: | - radon cc -s -a src/masoniteorm/collection > radon_output.txt - output=$(cat radon_output.txt) - echo "$output" - echo "radon_output=$output" >> $GITHUB_ENV - - - name: Comment on Pull Request - if: github.event_name == 'pull_request' - uses: marocchino/sticky-pull-request-comment@v2 - with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - header: Radon Cyclomatic Complexity Report - message: | - ```console - ${{ env.radon_output }} - ``` + - uses: actions/checkout@v3 + - name: Create Radon's reports + run: | + radon cc src/ -j >cc.json + radon mi src/ -j >mi.json + radon hal src/ -j >hal.json + - name: Comment the results on the PR + uses: Libra-foundation/radon-comment@V1.0 + with: + cc: "cc.json" + mi: "mi.json" + hal: "hal.json" \ No newline at end of file From 84b168ea0bd6be3b7d2338af607b82238364ecb1 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 18:34:13 -0500 Subject: [PATCH 228/254] Update Radon comment action to use version 0.1 --- .github/workflows/radon-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml index cef437b18..e1ec1c4ab 100644 --- a/.github/workflows/radon-analysis.yml +++ b/.github/workflows/radon-analysis.yml @@ -16,7 +16,7 @@ jobs: radon mi src/ -j >mi.json radon hal src/ -j >hal.json - name: Comment the results on the PR - uses: Libra-foundation/radon-comment@V1.0 + uses: Libra-foundation/radon-comment@V0.1 with: cc: "cc.json" mi: "mi.json" From a0534d8308d8e2fec4d0bb8d1f1f11d0f323d4f6 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 18:36:18 -0500 Subject: [PATCH 229/254] Fix version tag for Radon comment action in workflow --- .github/workflows/radon-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml index e1ec1c4ab..c83c687e7 100644 --- a/.github/workflows/radon-analysis.yml +++ b/.github/workflows/radon-analysis.yml @@ -16,7 +16,7 @@ jobs: radon mi src/ -j >mi.json radon hal src/ -j >hal.json - name: Comment the results on the PR - uses: Libra-foundation/radon-comment@V0.1 + uses: Libra-foundation/radon-comment@v0.1 with: cc: "cc.json" mi: "mi.json" From c50c9dc2322dfd36d9c0b7674d92f1fe143f6de0 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 18:37:38 -0500 Subject: [PATCH 230/254] Add installation step for Radon in analysis workflow --- .github/workflows/radon-analysis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml index c83c687e7..5e9cc433e 100644 --- a/.github/workflows/radon-analysis.yml +++ b/.github/workflows/radon-analysis.yml @@ -10,6 +10,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: Install Radon + run: pip install radon - name: Create Radon's reports run: | radon cc src/ -j >cc.json From 49a2427222ae88877a8201795d6a3a4855c06a70 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 18:41:10 -0500 Subject: [PATCH 231/254] Update Radon analysis workflow to report on changed files only --- .github/workflows/radon-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml index 5e9cc433e..908b06b1e 100644 --- a/.github/workflows/radon-analysis.yml +++ b/.github/workflows/radon-analysis.yml @@ -14,9 +14,9 @@ jobs: run: pip install radon - name: Create Radon's reports run: | - radon cc src/ -j >cc.json - radon mi src/ -j >mi.json - radon hal src/ -j >hal.json + radon cc $(git diff --name-only origin/2.0) -j >cc.json + radon mi $(git diff --name-only origin/2.0) -j >mi.json + radon hal $(git diff --name-only origin/2.0) -j >hal.json - name: Comment the results on the PR uses: Libra-foundation/radon-comment@v0.1 with: From 12a213252f63dfb3dd4e441068d2599c4b5f922f Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 18:42:12 -0500 Subject: [PATCH 232/254] Update Radon analysis workflow to use the latest commit for reporting --- .github/workflows/radon-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml index 908b06b1e..e8adf3e59 100644 --- a/.github/workflows/radon-analysis.yml +++ b/.github/workflows/radon-analysis.yml @@ -14,9 +14,9 @@ jobs: run: pip install radon - name: Create Radon's reports run: | - radon cc $(git diff --name-only origin/2.0) -j >cc.json - radon mi $(git diff --name-only origin/2.0) -j >mi.json - radon hal $(git diff --name-only origin/2.0) -j >hal.json + radon cc $(git diff --name-only origin/HEAD) -j >cc.json + radon mi $(git diff --name-only origin/HEAD) -j >mi.json + radon hal $(git diff --name-only origin/HEAD) -j >hal.json - name: Comment the results on the PR uses: Libra-foundation/radon-comment@v0.1 with: From c3e3ce734b0f78ac86d2ac1cda8faa026d919c03 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 18:46:10 -0500 Subject: [PATCH 233/254] Update Radon analysis workflow to use the correct base branch for reporting --- .github/workflows/radon-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml index e8adf3e59..908b06b1e 100644 --- a/.github/workflows/radon-analysis.yml +++ b/.github/workflows/radon-analysis.yml @@ -14,9 +14,9 @@ jobs: run: pip install radon - name: Create Radon's reports run: | - radon cc $(git diff --name-only origin/HEAD) -j >cc.json - radon mi $(git diff --name-only origin/HEAD) -j >mi.json - radon hal $(git diff --name-only origin/HEAD) -j >hal.json + radon cc $(git diff --name-only origin/2.0) -j >cc.json + radon mi $(git diff --name-only origin/2.0) -j >mi.json + radon hal $(git diff --name-only origin/2.0) -j >hal.json - name: Comment the results on the PR uses: Libra-foundation/radon-comment@v0.1 with: From 6e716cdf79cd968880bc3e949a8f56599bbfa4f6 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 18:47:09 -0500 Subject: [PATCH 234/254] Add test comment in __setitem__ method of Collection class --- src/masoniteorm/collection/Collection.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/masoniteorm/collection/Collection.py b/src/masoniteorm/collection/Collection.py index e238db8a5..a24e98af7 100644 --- a/src/masoniteorm/collection/Collection.py +++ b/src/masoniteorm/collection/Collection.py @@ -542,6 +542,7 @@ def __getitem__(self, item): return None def __setitem__(self, key, value): + # test self._items[key] = value def __delitem__(self, key): From d4bd4253a03649ade14c44731739df15a1d84b02 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 18:48:18 -0500 Subject: [PATCH 235/254] Fix Radon analysis workflow to use the correct branch reference for reporting --- .github/workflows/radon-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml index 908b06b1e..d2c23126f 100644 --- a/.github/workflows/radon-analysis.yml +++ b/.github/workflows/radon-analysis.yml @@ -14,9 +14,9 @@ jobs: run: pip install radon - name: Create Radon's reports run: | - radon cc $(git diff --name-only origin/2.0) -j >cc.json - radon mi $(git diff --name-only origin/2.0) -j >mi.json - radon hal $(git diff --name-only origin/2.0) -j >hal.json + radon cc $(git diff --name-only 2.0) -j >cc.json + radon mi $(git diff --name-only 2.0) -j >mi.json + radon hal $(git diff --name-only 2.0) -j >hal.json - name: Comment the results on the PR uses: Libra-foundation/radon-comment@v0.1 with: From a01f769c68953ec09c14ce53547065324e636df2 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 18:51:20 -0500 Subject: [PATCH 236/254] Update Radon analysis workflow to use the correct range for changed files --- .github/workflows/radon-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml index d2c23126f..91278b34b 100644 --- a/.github/workflows/radon-analysis.yml +++ b/.github/workflows/radon-analysis.yml @@ -14,9 +14,9 @@ jobs: run: pip install radon - name: Create Radon's reports run: | - radon cc $(git diff --name-only 2.0) -j >cc.json - radon mi $(git diff --name-only 2.0) -j >mi.json - radon hal $(git diff --name-only 2.0) -j >hal.json + radon cc $(git diff --name-only 2.0...HEAD) -j >cc.json + radon mi $(git diff --name-only 2.0...HEAD) -j >mi.json + radon hal $(git diff --name-only 2.0...HEAD) -j >hal.json - name: Comment the results on the PR uses: Libra-foundation/radon-comment@v0.1 with: From 86daadff859d0edef973436697e0fbc963adde02 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 18:52:51 -0500 Subject: [PATCH 237/254] Handle empty file lists in Radon analysis workflow to prevent errors --- .github/workflows/radon-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml index 91278b34b..ad1b8113d 100644 --- a/.github/workflows/radon-analysis.yml +++ b/.github/workflows/radon-analysis.yml @@ -14,9 +14,9 @@ jobs: run: pip install radon - name: Create Radon's reports run: | - radon cc $(git diff --name-only 2.0...HEAD) -j >cc.json - radon mi $(git diff --name-only 2.0...HEAD) -j >mi.json - radon hal $(git diff --name-only 2.0...HEAD) -j >hal.json + radon cc $(git diff --name-only 2.0...HEAD || echo "no_files") -j >cc.json + radon mi $(git diff --name-only 2.0...HEAD || echo "no_files") -j >mi.json + radon hal $(git diff --name-only 2.0...HEAD || echo "no_files") -j >hal.json - name: Comment the results on the PR uses: Libra-foundation/radon-comment@v0.1 with: From f62592e1208eab408de0dbfb6d8ee8e899413f99 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 18:56:18 -0500 Subject: [PATCH 238/254] Refactor Radon analysis workflow to support both push and pull_request events and ensure full git history is available --- .github/workflows/radon-analysis.yml | 41 ++++++++++++++-------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml index ad1b8113d..1e9ee5e4d 100644 --- a/.github/workflows/radon-analysis.yml +++ b/.github/workflows/radon-analysis.yml @@ -1,25 +1,26 @@ name: Radon Analysis -on: - pull_request: - branches: ["2.0"] + +on: [push, pull_request] jobs: - comment: - permissions: - pull-requests: write + radon-analysis: runs-on: ubuntu-latest + steps: - - uses: actions/checkout@v3 - - name: Install Radon - run: pip install radon - - name: Create Radon's reports - run: | - radon cc $(git diff --name-only 2.0...HEAD || echo "no_files") -j >cc.json - radon mi $(git diff --name-only 2.0...HEAD || echo "no_files") -j >mi.json - radon hal $(git diff --name-only 2.0...HEAD || echo "no_files") -j >hal.json - - name: Comment the results on the PR - uses: Libra-foundation/radon-comment@v0.1 - with: - cc: "cc.json" - mi: "mi.json" - hal: "hal.json" \ No newline at end of file + - name: Checkout repository + uses: actions/checkout@v2 + with: + fetch-depth: 0 # Fetch all history for all branches and tags + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + + - name: Install Radon + run: pip install radon + + - name: Run Radon Analysis + run: | + git fetch --unshallow # Ensure full history is available + radon cc $(git diff --name-only 2.0...HEAD || echo "no_files") -j >cc.json \ No newline at end of file From 07b3a0b8e5a9bf9667457d3308436f7cf6dd124c Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 18:57:34 -0500 Subject: [PATCH 239/254] Simplify git fetch command in Radon analysis workflow to ensure full history is available --- .github/workflows/radon-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml index 1e9ee5e4d..f15278aef 100644 --- a/.github/workflows/radon-analysis.yml +++ b/.github/workflows/radon-analysis.yml @@ -22,5 +22,5 @@ jobs: - name: Run Radon Analysis run: | - git fetch --unshallow # Ensure full history is available + git fetch # Ensure full history is available radon cc $(git diff --name-only 2.0...HEAD || echo "no_files") -j >cc.json \ No newline at end of file From 1f823786a041d0e78194dcc674b0e8778db972fa Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 19:00:06 -0500 Subject: [PATCH 240/254] Update Radon analysis workflow to use the correct base branch for changed files --- .github/workflows/radon-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml index f15278aef..4a8276810 100644 --- a/.github/workflows/radon-analysis.yml +++ b/.github/workflows/radon-analysis.yml @@ -23,4 +23,4 @@ jobs: - name: Run Radon Analysis run: | git fetch # Ensure full history is available - radon cc $(git diff --name-only 2.0...HEAD || echo "no_files") -j >cc.json \ No newline at end of file + radon cc $(git diff --name-only master...HEAD || echo "no_files") -j >cc.json \ No newline at end of file From af80dd2ef8646880c132435938317ce788a3984c Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 19:03:58 -0500 Subject: [PATCH 241/254] Refactor Radon analysis workflow to trigger on pull requests and streamline report generation --- .github/workflows/radon-analysis.yml | 41 +++++++++++++--------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml index 4a8276810..55ee2dda9 100644 --- a/.github/workflows/radon-analysis.yml +++ b/.github/workflows/radon-analysis.yml @@ -1,26 +1,23 @@ -name: Radon Analysis - -on: [push, pull_request] +name: Radon +on: + pull_request: + branches: ["main"] jobs: - radon-analysis: + comment: + permissions: + pull-requests: write runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v2 - with: - fetch-depth: 0 # Fetch all history for all branches and tags - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.x' - - - name: Install Radon - run: pip install radon - - - name: Run Radon Analysis - run: | - git fetch # Ensure full history is available - radon cc $(git diff --name-only master...HEAD || echo "no_files") -j >cc.json \ No newline at end of file + - uses: actions/checkout@v3 + - name: Create Radon's reports + run: | + radon cc src/ -j >cc.json + radon mi src/ -j >mi.json + radon hal src/ -j >hal.json + - name: Comment the results on the PR + uses: Libra-foundation/radon-comment@v0.1 + with: + cc: "cc.json" + mi: "mi.json" + hal: "hal.json" \ No newline at end of file From b28e152019e31e86a1cb576688f4bf26ac24bffd Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 19:05:01 -0500 Subject: [PATCH 242/254] Update Radon analysis workflow to trigger on pull requests for the 2.0 branch --- .github/workflows/radon-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml index 55ee2dda9..6d5b31b66 100644 --- a/.github/workflows/radon-analysis.yml +++ b/.github/workflows/radon-analysis.yml @@ -1,7 +1,7 @@ name: Radon on: pull_request: - branches: ["main"] + branches: ["2.0"] jobs: comment: From 583a32fafd6e6a1c6f01a30d05db5a3e234f52c8 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 19:05:57 -0500 Subject: [PATCH 243/254] Add Radon installation step to analysis workflow --- .github/workflows/radon-analysis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml index 6d5b31b66..17787d8d4 100644 --- a/.github/workflows/radon-analysis.yml +++ b/.github/workflows/radon-analysis.yml @@ -10,6 +10,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: Install Radon + run: pip install radon - name: Create Radon's reports run: | radon cc src/ -j >cc.json From 97a853eec7df07800d0cb69fa34a13d48a48e5ba Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 19:09:03 -0500 Subject: [PATCH 244/254] Add printing of Radon reports in analysis workflow --- .github/workflows/radon-analysis.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml index 17787d8d4..d608ff40c 100644 --- a/.github/workflows/radon-analysis.yml +++ b/.github/workflows/radon-analysis.yml @@ -17,9 +17,18 @@ jobs: radon cc src/ -j >cc.json radon mi src/ -j >mi.json radon hal src/ -j >hal.json + - name: Print Radon reports + run: | + echo "Cyclomatic Complexity Report:" + cat cc.json + echo "Maintainability Index Report:" + cat mi.json + echo "Halstead Metrics Report:" + cat hal.json - name: Comment the results on the PR uses: Libra-foundation/radon-comment@v0.1 with: cc: "cc.json" mi: "mi.json" - hal: "hal.json" \ No newline at end of file + hal: "hal.json" + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From 1742970f4735834fd4d858a56c664d81d0c33688 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 19:16:30 -0500 Subject: [PATCH 245/254] Enhance Radon analysis workflow to trigger on Python file changes and streamline report generation --- .github/workflows/radon-analysis.yml | 75 +++++++++++++++++----------- 1 file changed, 47 insertions(+), 28 deletions(-) diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml index d608ff40c..e09b63fab 100644 --- a/.github/workflows/radon-analysis.yml +++ b/.github/workflows/radon-analysis.yml @@ -1,34 +1,53 @@ -name: Radon +name: Radon Analysis + on: pull_request: - branches: ["2.0"] + paths: + - '**/*.py' # Only trigger for Python files jobs: - comment: - permissions: - pull-requests: write + radon_analysis: runs-on: ubuntu-latest + steps: - - uses: actions/checkout@v3 - - name: Install Radon - run: pip install radon - - name: Create Radon's reports - run: | - radon cc src/ -j >cc.json - radon mi src/ -j >mi.json - radon hal src/ -j >hal.json - - name: Print Radon reports - run: | - echo "Cyclomatic Complexity Report:" - cat cc.json - echo "Maintainability Index Report:" - cat mi.json - echo "Halstead Metrics Report:" - cat hal.json - - name: Comment the results on the PR - uses: Libra-foundation/radon-comment@v0.1 - with: - cc: "cc.json" - mi: "mi.json" - hal: "hal.json" - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + + - name: Install Radon + run: pip install radon + + - name: Get list of changed files + id: get_changed_files + run: | + echo "CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} | grep '.py$')" >> $GITHUB_ENV + + - name: Run Radon analysis + id: radon_analysis + run: | + if [ -z "${{ env.CHANGED_FILES }}" ]; then + echo "No Python files changed." + echo "RESULTS=None" >> $GITHUB_ENV + else + RESULTS=$(echo "${{ env.CHANGED_FILES }}" | xargs radon cc -s -n A) + echo "RESULTS=$RESULTS" >> $GITHUB_ENV + fi + + - name: Comment on PR + if: env.RESULTS != 'None' + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: Radon Complexity Analysis + message: | + **Radon Analysis Results:** + ``` + ${{ env.RESULTS }} + ``` + + - name: Comment no Python files found + if: env.RESULTS == 'None' + run: echo "No Python files changed in this PR." From dc4a53d577307110b4a0aefb847970862e304493 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 19:21:06 -0500 Subject: [PATCH 246/254] Enhance Radon analysis workflow to fetch full git history and debug changed Python files --- .github/workflows/radon-analysis.yml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml index e09b63fab..5652908f3 100644 --- a/.github/workflows/radon-analysis.yml +++ b/.github/workflows/radon-analysis.yml @@ -12,7 +12,9 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v3 - + with: + fetch-depth: 0 # Ensure full history is fetched for git diff to work + - name: Set up Python uses: actions/setup-python@v4 with: @@ -24,7 +26,15 @@ jobs: - name: Get list of changed files id: get_changed_files run: | - echo "CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} | grep '.py$')" >> $GITHUB_ENV + git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} + CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} | grep '.py$' || true) + echo "Changed files: $CHANGED_FILES" + echo "CHANGED_FILES=$CHANGED_FILES" >> $GITHUB_ENV + + - name: Debug changed files + run: | + echo "Changed files from env: ${{ env.CHANGED_FILES }}" + shell: bash - name: Run Radon analysis id: radon_analysis @@ -48,6 +58,6 @@ jobs: ${{ env.RESULTS }} ``` - - name: Comment no Python files found + - name: Log if no Python files found if: env.RESULTS == 'None' run: echo "No Python files changed in this PR." From 3a97cb341276969a822c383d7081f1d08ca6da24 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 19:23:08 -0500 Subject: [PATCH 247/254] Refine Radon analysis workflow to improve environment variable handling and ensure proper output formatting --- .github/workflows/radon-analysis.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml index 5652908f3..609bb2bd0 100644 --- a/.github/workflows/radon-analysis.yml +++ b/.github/workflows/radon-analysis.yml @@ -13,7 +13,7 @@ jobs: - name: Checkout code uses: actions/checkout@v3 with: - fetch-depth: 0 # Ensure full history is fetched for git diff to work + fetch-depth: 0 # Ensure full history is fetched for git diff to work properly - name: Set up Python uses: actions/setup-python@v4 @@ -26,10 +26,11 @@ jobs: - name: Get list of changed files id: get_changed_files run: | - git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} | grep '.py$' || true) echo "Changed files: $CHANGED_FILES" - echo "CHANGED_FILES=$CHANGED_FILES" >> $GITHUB_ENV + echo "CHANGED_FILES<> $GITHUB_ENV + echo "$CHANGED_FILES" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV - name: Debug changed files run: | @@ -43,8 +44,10 @@ jobs: echo "No Python files changed." echo "RESULTS=None" >> $GITHUB_ENV else - RESULTS=$(echo "${{ env.CHANGED_FILES }}" | xargs radon cc -s -n A) - echo "RESULTS=$RESULTS" >> $GITHUB_ENV + RESULTS=$(echo "${{ env.CHANGED_FILES }}" | xargs radon cc -s -n A || true) + echo "RESULTS<> $GITHUB_ENV + echo "$RESULTS" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV fi - name: Comment on PR From 59fd976b8c45401087bcc802205fc0003e383bab Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 19:25:58 -0500 Subject: [PATCH 248/254] Add granular Radon analysis workflow for method-level change detection --- .github/workflows/radon-anylsis-granular.yml | 79 ++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 .github/workflows/radon-anylsis-granular.yml diff --git a/.github/workflows/radon-anylsis-granular.yml b/.github/workflows/radon-anylsis-granular.yml new file mode 100644 index 000000000..c5ba2d6eb --- /dev/null +++ b/.github/workflows/radon-anylsis-granular.yml @@ -0,0 +1,79 @@ +name: Radon Analysis (Granular) + +on: + pull_request: + paths: + - '**/*.py' # Only trigger for Python files + +jobs: + radon_analysis: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 # Ensure full history is fetched for git diff to work properly + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + + - name: Install dependencies + run: pip install radon + + - name: Get changed lines + id: get_changed_lines + run: | + git diff -U0 ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} | grep -E '^\+\+\+|^\+\d' > changed_lines.diff + echo "Changed lines:" + cat changed_lines.diff + + - name: Parse changed methods + id: parse_methods + run: | + CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} | grep '.py$' || true) + echo "Analyzing methods in changed files: $CHANGED_FILES" + + RESULTS="" + for FILE in $CHANGED_FILES; do + echo "Processing file: $FILE" + METHODS=$(radon cc $FILE -s | awk '/^ / {print $NF}') + echo "Methods found: $METHODS" + + # Map changes to specific methods + while IFS= read -r LINE; do + if [[ $LINE == *"$FILE"* ]]; then + LINE_NUMBER=$(echo $LINE | grep -oP '(?<=\+)\d+') + MATCHING_METHOD=$(radon cc $FILE -s | awk -v line=$LINE_NUMBER '{ + if ($1 ~ /^[0-9]+:/ && $1 <= line) method=$NF; + if ($1 > line) { print method; exit } + }') + RESULTS+="$MATCHING_METHOD (in $FILE)\n" + fi + done <<< "$(cat changed_lines.diff)" + done + echo -e "$RESULTS" > results.txt + echo "RESULTS<> $GITHUB_ENV + cat results.txt >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + + - name: Debug results + run: | + echo "Granular results:" + cat results.txt + + - name: Comment on PR + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: Radon Method-Level Analysis + message: | + **Radon Analysis Results:** + ``` + ${{ env.RESULTS }} + ``` + + - name: Log if no methods found + if: env.RESULTS == '' + run: echo "No methods found with changes in this PR." From 85b0643a9f4ab240fa0b0f550d52737efebe5598 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 19:27:51 -0500 Subject: [PATCH 249/254] Refine Radon analysis workflow to accurately match methods with changed lines and avoid duplicates --- .github/workflows/radon-anylsis-granular.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/radon-anylsis-granular.yml b/.github/workflows/radon-anylsis-granular.yml index c5ba2d6eb..e14560be7 100644 --- a/.github/workflows/radon-anylsis-granular.yml +++ b/.github/workflows/radon-anylsis-granular.yml @@ -39,18 +39,26 @@ jobs: RESULTS="" for FILE in $CHANGED_FILES; do echo "Processing file: $FILE" - METHODS=$(radon cc $FILE -s | awk '/^ / {print $NF}') + + # Get method names and their line numbers + METHODS=$(radon cc $FILE -s | grep -oP "^\s*\d+.*\K[^\(]+" || true) echo "Methods found: $METHODS" - # Map changes to specific methods + # Extract the lines that are changed in this file while IFS= read -r LINE; do if [[ $LINE == *"$FILE"* ]]; then LINE_NUMBER=$(echo $LINE | grep -oP '(?<=\+)\d+') + + # Match the method based on line number MATCHING_METHOD=$(radon cc $FILE -s | awk -v line=$LINE_NUMBER '{ if ($1 ~ /^[0-9]+:/ && $1 <= line) method=$NF; if ($1 > line) { print method; exit } }') - RESULTS+="$MATCHING_METHOD (in $FILE)\n" + + # Append method results + if [[ ! " ${RESULTS[@]} " =~ " ${MATCHING_METHOD} " ]]; then + RESULTS+="$MATCHING_METHOD\n" + fi fi done <<< "$(cat changed_lines.diff)" done From 7657fa46a9e4d12f3e49ff73105e98d7708389cc Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 19:32:25 -0500 Subject: [PATCH 250/254] Refine Radon analysis workflow to capture raw output and improve method detection --- .github/workflows/radon-anylsis-granular.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/radon-anylsis-granular.yml b/.github/workflows/radon-anylsis-granular.yml index e14560be7..7324cd8b4 100644 --- a/.github/workflows/radon-anylsis-granular.yml +++ b/.github/workflows/radon-anylsis-granular.yml @@ -40,8 +40,13 @@ jobs: for FILE in $CHANGED_FILES; do echo "Processing file: $FILE" - # Get method names and their line numbers - METHODS=$(radon cc $FILE -s | grep -oP "^\s*\d+.*\K[^\(]+" || true) + # Run radon cc and check the raw output + RADON_OUTPUT=$(radon cc $FILE -s || true) + echo "Raw Radon Output for $FILE:" + echo "$RADON_OUTPUT" + + # Check if there are methods in the output + METHODS=$(echo "$RADON_OUTPUT" | grep -oP "^\s*\d+.*\K[^\(]+" || true) echo "Methods found: $METHODS" # Extract the lines that are changed in this file @@ -50,7 +55,7 @@ jobs: LINE_NUMBER=$(echo $LINE | grep -oP '(?<=\+)\d+') # Match the method based on line number - MATCHING_METHOD=$(radon cc $FILE -s | awk -v line=$LINE_NUMBER '{ + MATCHING_METHOD=$(echo "$RADON_OUTPUT" | awk -v line=$LINE_NUMBER '{ if ($1 ~ /^[0-9]+:/ && $1 <= line) method=$NF; if ($1 > line) { print method; exit } }') From a820a13bcfa134c9c477cfe4743f995012f47d4b Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 19:33:51 -0500 Subject: [PATCH 251/254] Remove granular Radon analysis workflow configuration --- .github/workflows/radon-anylsis-granular.yml | 92 -------------------- 1 file changed, 92 deletions(-) delete mode 100644 .github/workflows/radon-anylsis-granular.yml diff --git a/.github/workflows/radon-anylsis-granular.yml b/.github/workflows/radon-anylsis-granular.yml deleted file mode 100644 index 7324cd8b4..000000000 --- a/.github/workflows/radon-anylsis-granular.yml +++ /dev/null @@ -1,92 +0,0 @@ -name: Radon Analysis (Granular) - -on: - pull_request: - paths: - - '**/*.py' # Only trigger for Python files - -jobs: - radon_analysis: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - fetch-depth: 0 # Ensure full history is fetched for git diff to work properly - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.9' - - - name: Install dependencies - run: pip install radon - - - name: Get changed lines - id: get_changed_lines - run: | - git diff -U0 ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} | grep -E '^\+\+\+|^\+\d' > changed_lines.diff - echo "Changed lines:" - cat changed_lines.diff - - - name: Parse changed methods - id: parse_methods - run: | - CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} | grep '.py$' || true) - echo "Analyzing methods in changed files: $CHANGED_FILES" - - RESULTS="" - for FILE in $CHANGED_FILES; do - echo "Processing file: $FILE" - - # Run radon cc and check the raw output - RADON_OUTPUT=$(radon cc $FILE -s || true) - echo "Raw Radon Output for $FILE:" - echo "$RADON_OUTPUT" - - # Check if there are methods in the output - METHODS=$(echo "$RADON_OUTPUT" | grep -oP "^\s*\d+.*\K[^\(]+" || true) - echo "Methods found: $METHODS" - - # Extract the lines that are changed in this file - while IFS= read -r LINE; do - if [[ $LINE == *"$FILE"* ]]; then - LINE_NUMBER=$(echo $LINE | grep -oP '(?<=\+)\d+') - - # Match the method based on line number - MATCHING_METHOD=$(echo "$RADON_OUTPUT" | awk -v line=$LINE_NUMBER '{ - if ($1 ~ /^[0-9]+:/ && $1 <= line) method=$NF; - if ($1 > line) { print method; exit } - }') - - # Append method results - if [[ ! " ${RESULTS[@]} " =~ " ${MATCHING_METHOD} " ]]; then - RESULTS+="$MATCHING_METHOD\n" - fi - fi - done <<< "$(cat changed_lines.diff)" - done - echo -e "$RESULTS" > results.txt - echo "RESULTS<> $GITHUB_ENV - cat results.txt >> $GITHUB_ENV - echo "EOF" >> $GITHUB_ENV - - - name: Debug results - run: | - echo "Granular results:" - cat results.txt - - - name: Comment on PR - uses: marocchino/sticky-pull-request-comment@v2 - with: - header: Radon Method-Level Analysis - message: | - **Radon Analysis Results:** - ``` - ${{ env.RESULTS }} - ``` - - - name: Log if no methods found - if: env.RESULTS == '' - run: echo "No methods found with changes in this PR." From be07de784c74a2d226cd93d15d8f916805c38993 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 19:36:15 -0500 Subject: [PATCH 252/254] wip --- .github/workflows/radon-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/radon-analysis.yml b/.github/workflows/radon-analysis.yml index 609bb2bd0..f81c58e4d 100644 --- a/.github/workflows/radon-analysis.yml +++ b/.github/workflows/radon-analysis.yml @@ -38,7 +38,7 @@ jobs: shell: bash - name: Run Radon analysis - id: radon_analysis + id: radon_analysis run: | if [ -z "${{ env.CHANGED_FILES }}" ]; then echo "No Python files changed." From bb2317b5d9e1f6becc074d4c1a639c8f3d67bf31 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 19:37:52 -0500 Subject: [PATCH 253/254] test --- src/masoniteorm/commands/MakeMigrationCommand.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/masoniteorm/commands/MakeMigrationCommand.py b/src/masoniteorm/commands/MakeMigrationCommand.py index f62aea426..bcebaf2dd 100644 --- a/src/masoniteorm/commands/MakeMigrationCommand.py +++ b/src/masoniteorm/commands/MakeMigrationCommand.py @@ -12,7 +12,7 @@ class MakeMigrationCommand(Command): Creates a new migration file. migration - {name : The name of the migration} + {name : The name of the migration} {--c|create=None : The table to create} {--t|table=None : The table to alter} {--d|directory=databases/migrations : The location of the migration directory} From 0fd07449501ee619fb3691bab6985c4e647165c7 Mon Sep 17 00:00:00 2001 From: Joe Mancuso Date: Wed, 20 Nov 2024 19:47:48 -0500 Subject: [PATCH 254/254] Remove commented test line and clean up migration command help text formatting --- src/masoniteorm/collection/Collection.py | 1 - src/masoniteorm/commands/MakeMigrationCommand.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/masoniteorm/collection/Collection.py b/src/masoniteorm/collection/Collection.py index a24e98af7..e238db8a5 100644 --- a/src/masoniteorm/collection/Collection.py +++ b/src/masoniteorm/collection/Collection.py @@ -542,7 +542,6 @@ def __getitem__(self, item): return None def __setitem__(self, key, value): - # test self._items[key] = value def __delitem__(self, key): diff --git a/src/masoniteorm/commands/MakeMigrationCommand.py b/src/masoniteorm/commands/MakeMigrationCommand.py index bcebaf2dd..f62aea426 100644 --- a/src/masoniteorm/commands/MakeMigrationCommand.py +++ b/src/masoniteorm/commands/MakeMigrationCommand.py @@ -12,7 +12,7 @@ class MakeMigrationCommand(Command): Creates a new migration file. migration - {name : The name of the migration} + {name : The name of the migration} {--c|create=None : The table to create} {--t|table=None : The table to alter} {--d|directory=databases/migrations : The location of the migration directory}