From 55f23610dbb4782dfbd4c6572754dcf27abe64b4 Mon Sep 17 00:00:00 2001 From: Kieren Eaton Date: Fri, 22 Jul 2022 12:17:29 +0800 Subject: [PATCH 001/105] 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 47179b85..9ccc8fa8 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 002/105] 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 a6147acb..e08cbdf2 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 d246beff..daf6f008 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 68859fb3..4c3f7ef6 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 8b701777..9e0c0c35 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 003/105] 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 9ccc8fa8..49c427fd 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 004/105] 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 e08cbdf2..3919f8dc 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 daf6f008..21996ec3 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 4c3f7ef6..43fa784d 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 9e0c0c35..cd587dd6 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 005/105] 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 a9922c0b..34037a01 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 006/105] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bb3caf5d..ad96aa92 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 007/105] 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 34c1315b..90af2a23 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 2f5d8942..fd461b69 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 eab71ca1..6c42d0ea 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 f3d87ce5..fe82d56d 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 8591882f..cfbd4acd 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 16f6f59b..6b6635fe 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 8df5492f..aca8edb8 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 b1196975..7e064812 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 008/105] 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 36c7ec61..33aa177e 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 009/105] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ad96aa92..3d937e21 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 010/105] 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 34037a01..6b9475f6 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 1339124a..92c18ed3 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 33aa177e..1d55cafc 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 9a256fe6..ee7396e1 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 a1a76c40..c6005640 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 3840da32..ae7c587b 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 011/105] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3d937e21..fbd45579 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 012/105] 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 235e1bcf..c7750f6d 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 013/105] 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 b5595e30..62e98a1d 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 014/105] 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 235e1bcf..ce3d8fc9 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 482a5e28..79c2cb81 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 015/105] 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 efadd444..0c3cb9c5 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 016/105] 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 c7750f6d..235e1bcf 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 017/105] 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 49c427fd..9ac9ddf1 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 018/105] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index fbd45579..0d1c6c1c 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 019/105] 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 ce3d8fc9..b3fd6cc8 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 9ac9ddf1..52b35844 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 020/105] 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 6e37ef0c..441025ca 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 021/105] 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 b56e5d37..71a15aa4 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 022/105] 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 441025ca..7895dc95 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 c9d21274..ebc48c2b 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 3b20464d..3c9c0b1b 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 6140c8c2..812a48fb 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 023/105] 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 ce3d8fc9..73f969da 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 024/105] 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 73f969da..a6af4122 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 025/105] 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 72d71354..8ff862c8 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 026/105] 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 a6af4122..c25bd008 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 027/105] .. --- 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 8ff862c8..165df4f1 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 c25bd008..2e839ff6 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 028/105] 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 2e839ff6..102c46da 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 029/105] 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 d55eab38..399c4a79 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 030/105] 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 ce3d8fc9..83e6df7c 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 031/105] 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 83e6df7c..0ffdac8c 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 032/105] 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 165df4f1..7a9591c5 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 033/105] 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 7a9591c5..91c7ae88 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 102c46da..2e839ff6 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 399c4a79..a38b3317 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 034/105] 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 a38b3317..d97706dd 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 035/105] 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 d97706dd..007072a2 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 036/105] 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 2e839ff6..721bb970 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 007072a2..9bc64200 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 037/105] 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 91c7ae88..72d71354 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 721bb970..2dc08241 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 038/105] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0d1c6c1c..2c79ba54 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 039/105] 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 52b35844..a1e127d6 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 040/105] 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 a3b45079..3fbf671f 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 8ce5df4e..cd188787 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 d905e0d8..46a98ffb 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 7fa4a80d..0449e000 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 041/105] 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 c7a6abc8..294f6654 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 042/105] 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 eb71bfc1..4c466d80 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 8174227d..80212992 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 ea18e22f..ddcde460 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 9e659a1e..5b905b70 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 043/105] 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 f2019e9b..33771240 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 044/105] 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 3bc9af5e..c794ed20 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 045/105] 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 3bc9af5e..e1aa7c99 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 046/105] 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 c794ed20..8848ce33 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 ebc48c2b..3e2cb040 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 047/105] 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 563fdf6f..d34f9571 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 048/105] 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 294f6654..4273dd17 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 049/105] 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 4273dd17..affc45fe 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 f405defb..1615a68c 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 050/105] 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 d34f9571..e3f43232 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 affc45fe..ac2b4788 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 1615a68c..b48f829e 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 051/105] 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 ac2b4788..ee79fcf5 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 052/105] 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 e3f43232..67ec544e 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 053/105] 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 1d55cafc..897142e2 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 054/105] 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 d73684fb..cd8d3e3f 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 055/105] 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 4c466d80..4bb670b4 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 056/105] 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 f10a24f0..d19a3617 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 d80c2d3b..771e2fe8 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 a1e127d6..7bb0ee02 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 9bc64200..4a45cae3 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 cd188787..a280dfd3 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 057/105] 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 39826405..dd6a013d 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 d19a3617..03d51a13 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 771e2fe8..14efa3f3 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 7bb0ee02..24fde28c 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 4a45cae3..27d6c9c2 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 3fbf671f..895f28f5 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 3919f8dc..dcac9f14 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 469e55a0..a7816151 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 a280dfd3..88a7382b 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 21996ec3..be9c01b4 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 7155c3a6..3f55cd16 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 33771240..d4f460e6 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 46a98ffb..7571d5e4 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 43fa784d..93f6e2e8 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 14e300d8..8d71eb1f 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 0449e000..bf3d4c65 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 cd587dd6..cd2b99fa 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 058/105] 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 683335f7..fc04f0f9 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 00000000..1e73c5a6 --- /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 dd6a013d..2e33f1d4 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 059/105] 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 24fde28c..f9489941 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 060/105] Restore accidental removal of dotenv --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2e33f1d4..a2188f67 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 061/105] 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 03d51a13..ba529ebd 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 062/105] 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 f9489941..e8c936cb 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 063/105] 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 f593b05c..3be9e134 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 00000000..a05058cb --- /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 fc04f0f9..0cd57152 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 00000000..e8604534 --- /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 1e73c5a6..15b09344 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 7895dc95..663a9399 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 064/105] 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 ba529ebd..542fb775 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 065/105] 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 3be9e134..e226ea16 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 0cd57152..2d5e5c24 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 663a9399..b4180824 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 066/105] 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 542fb775..743c2483 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 27d6c9c2..f46c88c9 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 067/105] 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 14efa3f3..1cb20363 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 e8c936cb..2e98c5dc 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 f46c88c9..a473224c 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 6711f98a..694b545a 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 068/105] Combine black formatting commands --- makefile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/makefile b/makefile index 2d5e5c24..4676fea9 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 069/105] combine isort commands, add sort to `make check` --- makefile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/makefile b/makefile index 4676fea9..13752433 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 070/105] 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 694b545a..4adfd010 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 071/105] 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 e8604534..fb1fb3ad 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 b4180824..52ce4eb3 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 072/105] 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 00000000..118c1901 --- /dev/null +++ b/.envrc @@ -0,0 +1,2 @@ +use asdf +layout python diff --git a/.gitignore b/.gitignore index e226ea16..3dee2e72 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 00000000..8b869bd7 --- /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 073/105] 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 232f1e0d..21ddd8d2 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 dc4afe8e..662e29f0 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 074/105] 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 d7dd237b..280b7fc0 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 075/105] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2c79ba54..5b59446c 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 076/105] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5b59446c..d26156d9 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 077/105] 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 e2aed15f..9d4594db 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 2e98c5dc..5e09a294 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 078/105] 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 743c2483..14f7b829 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 5e09a294..ef767b61 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 079/105] 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 ef767b61..274e98cc 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 080/105] 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 743c2483..78c8f3d4 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 2e98c5dc..7ae5ec88 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 081/105] 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 7ae5ec88..1ac3d4ab 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 082/105] 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 274e98cc..4d5cba32 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 083/105] 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 d24ed957..5e4551cc 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 4adfd010..6bc7623d 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 a12b53f0..f890be6f 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 084/105] 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 743c2483..323beeff 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 085/105] 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 743c2483..c72bb7c5 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 2e98c5dc..0454b644 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 086/105] 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 c72bb7c5..f1155b78 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 0454b644..0c496bae 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 087/105] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d26156d9..4320978f 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 088/105] 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 ef091de7..00f814ad 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 089/105] 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 822af6ec..f231b2bb 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 090/105] 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 762e85e0..386145db 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 091/105] 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 822af6ec..bf34e066 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 092/105] 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 bf34e066..deff4ff8 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 093/105] 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 deff4ff8..4e397ee4 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 094/105] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4320978f..287a3b13 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 095/105] 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 f81aecc1..a5fb7e4b 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 096/105] 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 0c3cb9c5..44d84f34 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 71a15aa4..c6573564 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 3ae6b585..89e90359 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 ce8e8361..e6c6f8c6 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 d518c36a..f62eac30 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 a5fb7e4b..121a2eff 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 dccada02..3174933e 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 d3f7741a..0aef9ba1 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 280b7fc0..60a769c6 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 d7677db4..d915c559 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 279482f6..0f0e0587 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 7e8bd81a..fea2c674 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 4c25b7de..5a053576 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 812a48fb..817872e9 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 7f3ba591..c9a36277 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 25e5a486..4d69187e 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 cd8d3e3f..0b163994 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 e2dd2e48..0182e3c0 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 7711af16..96fc2e3c 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 97894f48..482f2160 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 895f28f5..17fa5e38 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 2c50f8fd..4d36b3f0 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 dcac9f14..740bff4e 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 a7816151..28615cdf 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 d43704a4..49c6e4ab 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 ae85faa7..daf2168f 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 df6a0f21..7e0fa39b 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 0db03e3a..acb09e50 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 84ee5fea..c17acee7 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 be9c01b4..79f6a2dc 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 0d411b08..549cc020 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 3f55cd16..169b6a89 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 a427f02e..0212ec8f 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 97df526d..6423816d 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 4b2befe8..e31fbc4b 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 e763d053..0cfe3e53 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 7571d5e4..86e5b62f 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 d0b92c9c..a07a77f4 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 66b3a953..690e7253 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 93f6e2e8..195e8549 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 8d71eb1f..4754114e 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 cb6d7fe5..15a70b62 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 a20477e1..023fd536 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 0f0519c5..1b98ac21 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 df58e73a..52f6a29f 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 832df663..2338b6b3 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 bf3d4c65..9da9f235 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 aa2524dd..800e9440 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 8a35a4e8..3e4dc03b 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 2cf0238c..41e87612 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 ee501ffb..3bd36a87 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 cd2b99fa..1df72397 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 22cf1285..015f3967 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 1bf081b4..4ee26543 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 a0ce9e50..178eaee4 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 7e76116a..91424ef4 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 5a69dbca..e0cd4d39 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 0349a5a3..b182e845 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 5f95755e..2e73a4ab 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 097/105] 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 8cdbd706..ea95e7ff 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 60a769c6..6d5d5d80 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 098/105] 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 6d5d5d80..b5481864 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 099/105] 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 58841070..73c273fd 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 c704f5f5..c4608904 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 00000000..03570e66 --- /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 380b9a44..454ef577 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 60a769c6..5cd4bd40 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 817872e9..dfbc9f76 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 c9a36277..9b3b3132 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 9c8de30f..b37c7ac8 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 4c8fc097..6aeb67cb 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 0b163994..5e1ca632 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 100/105] 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 03570e66..fb90f52a 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 5cd4bd40..512899fc 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 dfbc9f76..989cb697 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 101/105] 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 989cb697..dfbc9f76 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 102/105] 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 21ddd8d2..8867ecf5 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 662e29f0..bc130196 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 103/105] 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 9b924a3d..b36292dd 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 f62eac30..0266f751 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 104/105] 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 0266f751..f408c09c 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 fea2c674..5bd886ec 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 817872e9..6d49f886 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 105/105] 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 7e76116a..5ff7619a 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"